├── .gitignore ├── example ├── project │ └── build.properties ├── build.sbt └── src │ └── main │ └── scala │ ├── playground.sc │ └── type.scala ├── part3 ├── ch11.md ├── ch8.md ├── ch12.md └── ch9.md ├── functional_design_patterns.md ├── functional_domain_logic.md ├── README.md ├── part2 └── ch5.md └── part1 └── ch1.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | -------------------------------------------------------------------------------- /example/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.2.8 -------------------------------------------------------------------------------- /example/build.sbt: -------------------------------------------------------------------------------- 1 | name := "example" 2 | 3 | version := "0.1" 4 | 5 | scalaVersion := "2.12.8" -------------------------------------------------------------------------------- /example/src/main/scala/playground.sc: -------------------------------------------------------------------------------- 1 | import example.contact.simple.{Contact => SimpleContact} 2 | 3 | new SimpleContact("Eunmin", null, "Kim", "eunmin@eunmin.com", false) 4 | 5 | import example.example.cardgame.CardGame._ 6 | 7 | Card (Club (), Two () ) match { 8 | case Card ((_, y) ) => println (y) 9 | } 10 | 11 | import example.temp._ 12 | 13 | def printTemp(temp: Temp) { 14 | temp match { 15 | case F(value) => println(s"F: $value") 16 | case C(value) => println(s"C: $value") 17 | } 18 | } 19 | 20 | printTemp(F(10)) 21 | printTemp(C(1.0F)) 22 | 23 | import example.payment.{Card => CreditCard, _} 24 | 25 | def printPaymentMethod(paymentMethod: PaymentMethod): Unit = { 26 | paymentMethod match { 27 | case Cash() => println("Cash") 28 | case Cheque(ChequeNumber(number)) => println(s"Cheque: $number") 29 | case CreditCard((cardType, CardNumber(number))) => println(s"Card: $cardType, $number") 30 | } 31 | } 32 | 33 | printPaymentMethod(Cash()) 34 | printPaymentMethod(Cheque(ChequeNumber(1234))) 35 | printPaymentMethod(CreditCard((Master(), CardNumber(4321)))) 36 | 37 | import example.contact.typed._ 38 | 39 | EmailAddress("valid@emailaddress.com") 40 | EmailAddress("invalidEmailAddress") 41 | 42 | String50("abcd") 43 | String50("012345678901234567890123456789012345678901234567890123456789") -------------------------------------------------------------------------------- /part3/ch11.md: -------------------------------------------------------------------------------- 1 | # 직렬화 2 | 3 | 워크플로우는 커맨드를 받아서 이벤트를 리턴한다. 커맨드와 이벤트는 메시지 큐나 웹 요청 같은 외부 인프라와 4 | 연결된다. 외부 인프라는 도메인에 대해 모르기 때문에 JSON, XML, 바이너리, protobuf 같은 데이터를 5 | 도메인 데이터로 변환해줘야 한다. 6 | 7 | ## 직렬화 설계 8 | 9 | 도메인 데이터는 특별한 타입도 많고 복잡해서 직렬화하기 어렵다. 그래서 도메인 타입을 직렬화하기 쉬운 타입으로 10 | 변환해야하는데 이 타입을 DTO라고 한다. 11 | 12 | ``` 13 | [Domain Type -> Domain Type to DTO -> DTO] Type -> Serialize -> JSON/XML 14 | 도메인 바운더리 15 | ``` 16 | 17 | ``` 18 | JSON/XML -> Deserialize -> DTO [Type -> DTO to Domain Type -> Domain Type] 19 | 도메인 바운더리 20 | ``` 21 | 22 | ## 워크플로우에 직렬화 코드 연결하기 23 | 24 | Serialize는 워크플로우 뒤에 추가하고 Deserialize 워크플로우 앞에 추가한다. 25 | 26 | ```f# 27 | type MyInputType = ... 28 | type MyOutputType = ... 29 | 30 | type Workflow = MyInputType -> MyOutputType 31 | 32 | let workflowWithSerialization jsonString = 33 | jsonString 34 | |> deserializeInputDto // JSON to DTO 35 | |> inputDtoToDomain // DTO to Domain Object 36 | |> workflow // workflow 37 | |> outputDtoFromDomain // Domain Object to DTO 38 | |> serializeOutputDto // DTO to JSON​ 39 | ``` 40 | 41 | ## 직렬화 예제 42 | 43 | `Person` 도메인 타입이 아래와 같다고 하자. 44 | 45 | ```f# 46 | module​ Domain = ​// our domain-driven types​ 47 | ​  48 | ​/// constrained to be not null and at most 50 chars​ 49 | ​type​ String50 = String50 ​of​ ​string​ 50 | ​  51 | ​/// constrained to be bigger than 1/1/1900 and less than today's date​ 52 | ​type​ Birthdate = Birthdate ​of​ DateTime 53 | ​  54 | ​/// Domain type​ 55 | ​type​ Person = { 56 | First: String50 57 | Last: String50 58 | Birthdate : Birthdate 59 | } 60 | ``` 61 | 62 | 아래는 Person DTO인데 타입이 일반적인 타입으로 되어 있다. 63 | 64 | ```f# 65 | module​ Dto = 66 | 67 | type​ Person = { 68 | First: ​string​ 69 | Last: ​string​ 70 | Birthdate : DateTime 71 | } 72 | 73 | module​ Person = 74 | ​let​ fromDomain (person:Domain.Person) :Dto.Person = 75 | ... 76 | 77 | ​let​ toDomain (dto:Dto.Person) :Result = 78 | ... 79 | ``` 80 | 81 | ... 82 | 83 | ## 도메인 타입을 어떻게 DTO 타입으로 변환 할까? 84 | -------------------------------------------------------------------------------- /functional_design_patterns.md: -------------------------------------------------------------------------------- 1 | # Functional Design Patterns 2 | 3 | Scott Wlaschin 4 | 5 | ## 함수형 프로그래밍의 기본 원리 6 | 7 | - 함수는 값이다 (Functions are things) 8 | - 함수는 조합 가능하다 9 | - 타입은 클래스가 아니다 10 | 11 | ### 함수는 값이다 (Functions are things) 12 | 13 | - 사과를 넣으면 바나나가 나온다 (apple -> banana) 14 | 15 | ```f# 16 | let z = 1 17 | 18 | let add x y = x + y 19 | ``` 20 | 21 | - z, add는 값은 성질의 것이다 22 | - 함수는 리턴 값으로 쓸 수 있다 23 | 24 | ```f# 25 | let add x = (fun y -> x + y) 26 | ``` 27 | 28 | - 함수는 인자로 받을 수 있다. (Function as parameter) 29 | 30 | ```f# 31 | let useFn f = (f 1) + 2 32 | ``` 33 | 34 | - 함수를 인자로 받아 내부 동작을 바꿀 수 있다. 복잡한 시스템도 이런 식으로 쓸 수 있다. 35 | 36 | ```f# 37 | let transformInt f x = (f x) + 1 38 | ``` 39 | 40 | ### 함수는 조합 가능하다 41 | 42 | - apple -> banana 함수와 banana -> cherry 함수를 조합하면 apple -> cherry 함수를 만들 수 43 | 있다. 44 | - 저수준의 동작을 조합해 서비스를 만들로 서비스를 조합해 유즈케이스를 만들고 유즈케이스를 조합해 웹 어플리케이션 45 | 을 만든다 46 | 47 | ### 타입은 클래스가 아니다 48 | 49 | - 타입은 셑의 이름이다 50 | - 함수는 입력 셑을 받아서 결과 셑을 출력한다 51 | - 가능한 숫자를 입력 받아 가능한 문자를 출력한다 52 | - 사람 이름을 출력 받아 가능한 과일을 출력한다 53 | - 타입도 조합이 가능하다 54 | - AND 타입은 모든 타입을 다 가지고 있는 새로운 타입이다 55 | 56 | ```f# 57 | type FruitSalad = { 58 | Apple: AppleVariety 59 | Banana: BananaVariety 60 | Cherry: CherryVariety 61 | } 62 | ``` 63 | 64 | - OR 타입은 여러 타입 중 하나를 선택할 수 있는 타입이다 65 | 66 | ```f# 67 | type Snack = { 68 | | Apple of AppleVariety 69 | | Banana of BananaVariety 70 | | Cherry of CherryVariety 71 | } 72 | ``` 73 | 74 | - 예제 75 | 76 | ```java 77 | interface IPaymentMethod { 78 | ... 79 | } 80 | 81 | class Cash implements IPaymentMethod { 82 | ... 83 | } 84 | 85 | class Check implements IPaymentMethod { 86 | public Check(int checkNo) { ... } 87 | } 88 | 89 | class Card implements IPaymentMethod { 90 | public Card(String cardType, String cardNo) { ... } 91 | } 92 | ``` 93 | 94 | ```f# 95 | type CheckNumber = int 96 | type CardNumber = string 97 | type CardType = Visa | Mastercard 98 | type CreditCardInfo = CardType * CardNumber 99 | 100 | type PaymentMethod = 101 | | Cash 102 | | Check of CheckNumber 103 | | Card of CreditCardInfo 104 | 105 | type PaymentAmount = decimal 106 | type Currency = EUR | USD 107 | 108 | type Payment = { 109 | Amount: PaymentMethod 110 | Currency: Currency 111 | Method: PaymentMethod 112 | } 113 | ``` 114 | 115 | ## 디자인 원리 116 | 117 | - 0으로 나눌 가능성이 있는 함수에서 에러 처리는 0을 허용하지 않는 입력 타입을 쓰거나 옵셔널 한 출력 값을 118 | 리턴하는 것이다. 예외를 던지면 안된다. 119 | -------------------------------------------------------------------------------- /functional_domain_logic.md: -------------------------------------------------------------------------------- 1 | # 도메인 논리 패턴과 함수형 프로그래밍 2 | 3 | 마틴 파울러의 엔터프라이즈 애플리케이션 아키텍처 패턴에 도메인 논리을 위한 패턴 몇 개를 소개 했다. 4 | 그 중 저자가 반대의 성격이라고 이야기한 트랜잭션 스크립트와 도메인 모델에 대해 설명하고 함수형 언어를 5 | 사용한다면 도메인 논리를 어떻게 구성하면 좋을 지 생각해 봤다. 6 | 7 | ## 트랜잭션 스크립트 8 | 9 | https://martinfowler.com/eaaCatalog/transactionScript.html 10 | 11 | ```java 12 | ResultSet contracts = db.findContract(contractNumber); 13 | contracts.next(); 14 | Money totalRevenue = Money.dollars(contracts.getBigDecimal("revenue")); 15 | MfDate recognitionDate = new MfDate(contracts.getDate("dateSigned")); 16 | String type = contracts.getString("type"); 17 | 18 | if (type.equals("S")) { 19 | Money[] alloction = totalRevenue.allocate(3); 20 | db.insertRecognition(contractNumber, alloction[0], recognitionDate); 21 | db.insertRecognition(contractNumber, alloction[1], recognitionDate.addDays(60)); 22 | db.insertRecognition(contractNumber, alloction[2], recognitionDate.addDays(90)); 23 | } else if (type.equals("W")) { 24 | db.insertRecognition(contractNumber, totalRevenue, recognitionDate); 25 | } else if (type.equals("D")) { 26 | Money[] allocation = totalRevenue.allocate(3); 27 | db.insertRecognition(contractNumber, allocation[0], recognitionDate); 28 | db.insertRecognition(contractNumber, allocation[1], recognitionDate.addDays(30)); 29 | db.insertRecognition(contractNumber, allocation[2], recognitionDate.addDays(60)); 30 | } 31 | ``` 32 | 33 | ## 도메인 모델 34 | 35 | https://martinfowler.com/eaaCatalog/domainModel.html 36 | 37 | ```java 38 | 39 | ``` 40 | 41 | ## 트랜잭션 스크립트 vs 도메인 모델 42 | 43 | 저자는 도메인 논리를 선택하는 기준을 도메인 논리의 복잡도와 데이터베이스 연결 난이도에 있다고 한다. 44 | 45 | 트랜잭션 스크립트를 사용하면 행 데이터 게이트웨이나 테이블 게이트웨이 패턴으로 데이터소스를 사용하는 것이 46 | 적합하다고 한다. 물론 데이터 매퍼를 사용하지 못하는 것은 아니다. 하지만 트랜잭션 스크립트에서는 테이블 구조와 47 | 가까운 형태로 데이터를 다루기 때문에 데이터 매퍼가 하는 일이 거의 없고 가치가 없다. 트랜잭션 스크립트를 48 | 사용하면 테이블 데이터 구조와 비슷한 데이터를 다루기 때문에 데이터베이스 연결 난이도가 낮다. 하지만 49 | 트랜잭션 스크립트는 도메인이 복잡해지는 경우 코드의 복잡성을 잘 다루지 못하기 때문에 유지 보수에 어려운 50 | 코드를 갖게 된다. 가장 쉬운 예가 많은 조건 문으로 인한 복잡성이다. 객체 지향 언어에서는 이러한 복잡성을 51 | 다형성으로 풀 수 있다. 52 | 53 | 도메인 모델은 객체 지향 패러다임을 이용해 복잡한 도메인을 표현하는 방식의 도메인 논리 패턴이다. 객체 지향은 54 | 데이터와 연산으로 구성된 객체로 데이터 분리해서 표현할 수 있다. 객체 지향에서 사용하는 다양한 기술로 55 | 복잡한 코드를 잘 다룰 수 있다. 책에 나온 예로는 반복되는 조건문으로 구성된 수익 인식 모델을 전략 패턴으로 56 | 반복되는 코드를 줄이고 도메인 논리도 더 명확하게 표현하고 있다. 도메인 모델의 단점으로는 데이터베이스 연결 57 | 난이도가 높다는 것이다. 잘 만들어진 도메인 모델은 거대한 데이터를 객체로 쪼개고 객체간 협력으로 문제를 58 | 풀어간다. 하지만 이 쪼개진 데이터는 데이터베이스 테이블 구조와 많은 차이를 만들게 된다. 이 차이로 인해 59 | 도메인 모델을 데이터베이스와 연결하는 일이 어려워진다. 데이터 매퍼 패턴을 사용해 도메인 모델과 테이블간 60 | 차이를 해소한다. 요즘은 데이터 매퍼 라이브러리들이 잘 나와 있어 객체/관계 매핑에 어려움을 많이 해결해 61 | 주지만 트랜잭션 스크립트에 비해 상대적으로 데이터베이스 연결 난이도가 높은 것은 사실이다. 책에는 객체 62 | 데이터베이스와 같은 기술을 언급했지만 당시 기술의 발달이 덜 된 이유로 많이 사용하지 않는다고 했다. 63 | 요즘에 다시 생각해볼 수 있는 것은 MongoDB와 같은 문서 기반 데이터베이스로 도메인 모델을 있는 그대로 영속화 64 | 해보는 것은 다시 생각해볼 수 있을 것 같다. 65 | 66 | ## 함수형 프로그래밍 67 | 68 | 언어의 유연성과 표현력은 구성 요소의 조합에서 나온다. 객체 지향 패러다임은 데이터와 연산을 갖고 있는 객체를 69 | 많이 만들고 객체간 조합으로 유연성과 표현력을 갖는다. 함수형 패러다임은 함수를 조합해 유연성과 표현력을 갖는다. 70 | 두 방식은 어느 패러다임이 더 좋다라고 말할 수 없다. 이 문제를 표현 문제(Expression Problem)이라고 71 | 부른다. 여기서는 함수형 패러다임을 사용하는 경우 도메인 모델을 어떻게 구성하면 좋을지에 대해 생각해보자. 72 | 73 | 함수형 패러다임을 사용하게되면 보통 데이터와 연산을 묶어 표현하지 않기 때문에 데이터를 쪼갤 필요는 없다. 74 | 따라서 트랜잭션 스크립트에서 테이블 형태와 비슷한 데이터 구조를 다루는 것 처럼 함수형 패러다임에서도 75 | 테이블 형태와 비슷한 데이터 구조를 다루는 것이 데이터베이스 연결 난이도를 낮출 수 있는 방법이다. 76 | 그럼 트랜잭션 스크립트에서 문제가 되는 도메인 복잡성을 어떻게 처리할 수 있을까? 77 | 78 | 많은 사례를 생각해 보진 못했지만 이 책에서 나온 트랜잭션 스크립트의 문제인 조건문 반복을 예를 들어보면 79 | 80 | 정리중... 81 | 82 | Scott Wlaschin이 타입 시스템으로 도메인 논리를 표현한 자료 83 | https://www.youtube.com/watch?v=1pSH8kElmM4 84 | 85 | Peter Norvig의 객체 지향 언어가 아닌 언어로 Design Pattern을 설명한 자료 86 | https://www.researchgate.net/publication/242609494_Design_patterns_in_dynamic_programming 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Domain Modeling Made Functional - Scott Wlaschin 2 | 3 | https://www.youtube.com/watch?v=1pSH8kElmM4 4 | 5 | 시스템에 쓰레기를 넣으면 쓰레기가 나온다. 6 | F# 타입 시스템으로 도메인 모델링을 해보자. 7 | 8 | ## 뭐가 잘 못 된 건가? 9 | 10 | ```f# 11 | type Contact = { 12 | FirstName: string 13 | MiddleInitial: string 14 | LastName: string 15 | 16 | EmailAddress: string 17 | IsEmailVerified: bool 18 | } 19 | ``` 20 | 21 | - 옵셔널 타입에 대한 정의가 없다 22 | - MiddleInitial 은 옵셔널 23 | - FirstName,LastName,EmailAddress에 대한 제약 조건이 없다 24 | - 이름과 이메일 영역은 함께 변경 되기 때문에 묶어야 한다. 25 | 26 | ## DDD 27 | 28 | - 코드와 개발자와 도메인 전문가가 공유 맨탈 모델을 공유한다. 29 | - 같은 용어도 다른 컨텍스트(바운디드 컨텍스트)에서는 다르게 해석된다. 30 | - 바운디드 컨텍스트 안에서 사용하는 용어가 유비쿼터스 언어다. 31 | 32 | ## 코드 예제 33 | 34 | ```f# 35 | module CardGame = 36 | type Suit = Club | Diamond | Spade | Heart 37 | 38 | type Rank = Two | Three | Four | Five | Six | Seven | Eight 39 | | Nine | Ten | Jack | Queen | King | Ace 40 | 41 | type Card = Suit * Rank 42 | 43 | type Hand = Card list 44 | type Deck = Card list 45 | 46 | type Player = {Name:string; Hand:Hand} 47 | type Game = {Deck:Deck; Players:Player list} 48 | 49 | type Deal = Deck -> (Deck * Card) 50 | 51 | type PickupCard = (Hand * Card) -> Hand 52 | ``` 53 | 54 | - module인 CardGame은 바운디드 컨텍스트 55 | - 그 안에 type은 유비쿼터스 언어 56 | - `|` 표시는 하나를 선택 57 | - `*` 표시는 페어 58 | - `->` 함수의 입력과 출력 59 | - 개발자가 아니라도 이해할 수 있다. 60 | - 기술적인 내용이 없다. 61 | - UML 같은 다른 문서는 필요 없다. 62 | 63 | ```clojure 64 | 65 | ``` 66 | 67 | ## F# 타입 시스템 68 | 69 | - 컴포저블 타입 시스템 70 | - 곱하기(타입1 `x` 타입2) : 타입1, 타입2 조합 가능한 모든 페어 71 | - 더하기(타입1 `+` 타입2) : 타이1, 타입2 둘 중 하나 72 | 73 | ```f# 74 | type Temp = F of int | C of float 75 | ``` 76 | 77 | ```f# 78 | type PaymentMethod = 79 | | Cash 80 | | Cheque of ChequeNumber 81 | | Card of CardType * CardNumber 82 | ``` 83 | 84 | - 타입 매칭을 쓸 수 있다. 85 | - 컴파일 타임 체크는 타입 시스템의 장점 86 | 87 | ## 타입 설계 88 | 89 | ### 옵셔널 값 90 | 91 | - null은 해롭다. 92 | - String 타입에 null을 허용하지 않고 더하기 타입으로 표현할 수 있다. 93 | 94 | ```f# 95 | type OptionalString = 96 | | SomString of string 97 | | Nothing 98 | ``` 99 | 100 | - 타입별로 다 만들면 중복이기 때문에 제너릭 타입으로 만든다. 101 | 102 | ```f# 103 | type Option<'T> = 104 | | Some of 'T 105 | | None 106 | ``` 107 | 108 | - 첫번째 예제를 바꿔보자. 109 | 110 | ``` 111 | type PersonalName = 112 | { 113 | FirstName: string 114 | MiddleInitial: Option ## string option을 f#에서 제공 115 | LastName: string 116 | } 117 | ``` 118 | 119 | ### Single choice 타입 120 | 121 | - EmailAddress는 그냥 string이 아니다. 122 | - CustomerId는 그냥 int가 아니다. 123 | 124 | ```f# 125 | type EmailAddress = EmailAddress of string 126 | type PhoneNumber = PhoneNumber of string 127 | ``` 128 | 129 | - EmailAddress, PhoneNumber 둘다 string 타입이지만 다른 의미를 갖기 때문에 Single choice 130 | 타입으로 만들어 주는 것이 좋다. (예를 들어 PhoneNumber 받을 곳에 EmailAddress를 받으면 안된다) 131 | - f#은 변경 불가능한 데이터 타입을 가지고 있기 때문에 EmailAddress 생성자 함수에서 Validation 체크를 132 | 하면 된다. 133 | 134 | ```f# 135 | let createEmailAddress(s:string) = 136 | if Regex.IsMatch(s, @"^\S+@\S+\.\S+$") 137 | then Some (EmailAddress s) 138 | else None 139 | 140 | createEmailAddress: 141 | string -> EmailAddress option 142 | ``` 143 | 144 | ```f# 145 | type String50 = String50 of String 146 | 147 | let createString50 (s:string) = 148 | if s.Length <= 50 149 | then Some (String50 s) 150 | else None 151 | 152 | createString50: 153 | string -> String50 option 154 | ``` 155 | 156 | - 첫번째 예제를 다시 변경해 보자. 157 | 158 | ```f# 159 | type PersonalName = { 160 | FirstName: String50 161 | MiddleInitial: StringI option 162 | LastName: String50 } 163 | 164 | type EmailContactInfo = { 165 | EmailAddress: EmailAddress 166 | IsEmailVerified: bool } 167 | 168 | type Contact = { 169 | Name: PersonalName 170 | Email: EmailContactInfo } 171 | ``` 172 | 173 | - 이제 IsEmailVerified 타입을 생각해보자 174 | - 규칙1 EmailAddress 변경 되면 false가 된다. 175 | - 규칙2 특별한 서비스로 flag가 설정된다. 176 | 177 | - 아래 처럼 VerifiedEmail 타입을 만들면 규칙을 타입으로 표현할 수 있다. 178 | 179 | ```f# 180 | type VerifiedEmail = VerifiedEmail of EmailAddress 181 | 182 | type VerificationService = 183 | (EmailAddress * VerificationHash) -> VerifiedEmail option 184 | 185 | type EmailContactInfo = 186 | | Unverified of EmailAddress 187 | | Verified of VerifiedEmail 188 | ``` 189 | - 최종 버전 190 | 191 | ```f# 192 | type PersonalName = { 193 | FirstName: String50 194 | MiddleInitial: StringI option 195 | LastName: String50 } 196 | 197 | type VerifiedEmail = ... 198 | 199 | 200 | type EmailContactInfo = 201 | | Unverified of EmailAddress 202 | | Verified of VerifiedEmail 203 | 204 | type Contact = { 205 | Name: PersonalName 206 | Email: EmailContactInfo } 207 | ``` 208 | 209 | ### 보너스 210 | 211 | - 만약 아래 모델에서 email 또는 postal address 둘 중 하나는 반드시 있어야 한다는 것을 표현하려면? 212 | 213 | ```f# 214 | type Contact = { 215 | Name: Name 216 | Email: EmailContactInfo 217 | Address: PostalContactInfo 218 | } 219 | ``` 220 | 221 | - 둘다 option 으로 할 수는 없다. 이럴 때는 아래와 같이 표현 할 수 있다. 222 | 223 | ```f# 224 | type ContactInfo = 225 | | EmailOnly of EmailContactInfo 226 | | AddrOnly of PostalContactInfo 227 | | EmailAndAddr of EmailContactInfo * PostalContactInfo 228 | 229 | type Contact = { 230 | Name: Name 231 | ContactInfo: ContactInfo 232 | } 233 | ``` 234 | 235 | - 연락 가능한 방법이 하나는 꼭 있어야 한다는 도메인은 아래 처럼 표현 할 수 있다. 236 | 237 | ```f# 238 | type ContactInfo = 239 | | Email of EmailContactInfo 240 | | Addr of PostalContactInfo 241 | 242 | type Contact = { 243 | Name: Name 244 | PrimaryContactInfo: ContactInfo 245 | SecondaryContactInfo: ContactInfo option 246 | } 247 | ``` 248 | -------------------------------------------------------------------------------- /part3/ch8.md: -------------------------------------------------------------------------------- 1 | # 함수 이해하기 2 | 3 | ## Functions, Functions, Everywhere 4 | 5 | - 함수형 프로그래밍의 정의가 다양해서 객체지향 하는 사람들이 혼란스러워한다. 그래서 하나로 요약하면 6 | - 함수형 프로그래밍은 함수를 정말 중하게 생각하는 프로그래밍이다. 7 | - 객체지향에서는 큰 프로그램을 클래스로 나누고 함수형 패러다임에서는 큰 프로그램을 함수로 나눈다. 8 | - 객체지향에서 컴포넌트 의존성을 줄이기 위해 인터페이스와 의존성 주입을 사용하고 함수형 패러다임에서는 9 | 함수 파라미터를 사용한다. (함수 안에서 함수와 함수의 결합은 강한 의존성, 함수를 인자로 받아 의존성을 낮춘다) 10 | - 코드의 재사용을 높이기 위해 객체지향은 상속이나 데코레터 패턴을 쓰고 함수형 패러다임은 재사용 코드를 함수에 11 | 넣고 함수를 컴포지션하는 방법을 쓴다. 12 | 13 | ## Functions Are Things 14 | - 함수는 인풋 값이나 아웃풋 값으로 쓸수 있고 함수의 행동을 조절하기 위해 파라미터로 전달될 수 있다? 15 | 16 | ## F#에서 함수 17 | 18 | ```f# 19 | let​ plus3 x = x + 3 ​// plus3 : x:int -> int​ 20 | ​let​ times2 x = x * 2 ​// times2 : x:int -> int​ 21 | ​let​ square = (​fun​ x -> x * x) ​// square : x:int -> int​ 22 | let​ addThree = plus3 ​// addThree : (int -> int)​ 23 | ``` 24 | 25 | ```f# 26 | // listOfFunctions : (int -> int) list​ 27 | let​ listOfFunctions = 28 | [addThree; times2; square] 29 | ``` 30 | 31 | ```f# 32 | for​ fn ​in​ listOfFunctions ​do​ 33 | ​let​ result = fn 100 ​// call the function​ 34 | printfn ​"If 100 is the input, the output is %i"​ result 35 | 36 | ​// Result =>​ 37 | ​// If 100 is the input, the output is 103​ 38 | ​// If 100 is the input, the output is 200​ 39 | ​// If 100 is the input, the output is 10000​ 40 | ``` 41 | 42 | ### 입력 값으로 함수 43 | 44 | - 입력값으로 사용할 수 있는 예제 (특별한 내용 없음) 45 | 46 | ### 출력 값으로 함수 47 | 48 | ```f# 49 | let​ add1 x = x + 1 50 | ​let​ add2 x = x + 2 51 | let​ add3 x = x + 3 52 | ``` 53 | 54 | - 위 함수는 중복 코드가 있다. 아래 처럼 바꿀 수 있음 55 | 56 | ```f# 57 | ​let​ adderGenerator numberToAdd = 58 | ​// return a lambda​ 59 | ​ fun​ x -> numberToAdd + x 60 | 61 | ​// val adderGenerator :​ 62 | ​// int -> (int -> int)​ 63 | ``` 64 | 65 | - 익명 함수 대신 이름 있는 함수로 리턴할 수 도 있음 66 | 67 | ```f# 68 | let​ adderGenerator numberToAdd = 69 | ​// define a nested inner function​ 70 | ​let​ innerFn x = 71 | numberToAdd + x 72 | ​  73 | ​// return the inner function​ 74 | innerFn 75 | ``` 76 | 77 | - 사용법은 아래와 같음 78 | 79 | ``` 80 | // test​ 81 | let​ add1 = adderGenerator 1 82 | add1 2 ​// result => 3​ 83 | ​  84 | let​ add100 = adderGenerator 100 85 | add100 2 ​// result => 102​ 86 | ``` 87 | 88 | ### 커링 89 | 90 | - F#도 하스켈처럼 일부 파라미터만 적용하면 나머지 파리미터를 받는 함수를 리턴한다. 91 | 92 | ### 부분 적용 93 | 94 | ```f# 95 | // sayGreeting: string -> string -> unit​ 96 | let​ sayGreeting greeting name = 97 | printfn ​"%s %s"​ greeting name 98 | ``` 99 | 100 | 위 함수가 있을 때 첫번째 인자만 적용된 각기 다른 함수를 만들 수 있다. 101 | 102 | ```f# 103 | // sayHello: string -> unit​ 104 | let​ sayHello = sayGreeting ​"Hello"​ 105 |   106 | // sayGoodbye: string -> unit​ 107 | ​let​ sayGoodbye = sayGreeting ​"Goodbye"​ 108 | 109 | sayHello ​"Alex"​ 110 | ​// output: "Hello Alex" 111 | ​  112 | sayGoodbye ​"Alex"​ 113 | ​// output: "Goodbye Alex"​ 114 | ``` 115 | 116 | ## Total Functions 117 | 118 | 아래 함수는 0인 경우 int 대신 예외가 발생하기 때문에 `int -> int` 함수 시그네처를 어긴다. 119 | 120 | ```f# 121 | let​ twelveDividedBy n = 122 | ​match​ n ​with​ 123 | | 6 -> 2 124 | | 5 -> 2 125 | | 4 -> 3 126 | | 3 -> 4 127 | | 2 -> 6 128 | | 1 -> 12 129 | | 0 -> failwith ​"Can't divide by zero 130 | ``` 131 | 132 | 첫번째 해결 방법은 입력 값을 제한 하는 방법이다. 133 | 134 | ```f# 135 | ​type​ NonZeroInteger = 136 | // Defined to be constrained to non-zero ints.​ 137 | ​// Add smart constructor, etc​ 138 | ​private​ NonZeroInteger ​of​ ​int​ 139 | 140 | ​/// Uses restricted input​ 141 | let​ twelveDividedBy (NonZeroInteger n) = 142 | match​ n ​with​ 143 | | 6 -> 2 144 | ... 145 | // 0 can't be in the input​ 146 | ​// so doesn't need to be handled​ 147 | 148 | twelveDividedBy : NonZeroInteger -> int 149 | ``` 150 | 151 | 두번째 방법은 출력 타입을 올바른 값과 잘못된 값을 가질 수 있는 타입(Option)으로 정의 하는 방법이다. 152 | 153 | ```f# 154 | /// Uses extended output​ 155 | let​ twelveDividedBy n = 156 | ​match​ n ​with​ 157 | | 6 -> Some 2 ​// valid​ 158 | | 5 -> Some 2 ​// valid​ 159 | | 4 -> Some 3 ​// valid​ 160 | ... 161 | | 0 -> None ​// undefined​ 162 | 163 | twelveDividedBy : int -> int option 164 | ``` 165 | 166 | ## Composition 167 | 168 | - `사과 -> 바나나` 함수와 `바나나 -> 체리` 함수를 결합해서 `사과 -> 체리` 함수를 만들 수 있다. 이런 169 | 결합을 바나나가 감춰졌다고 해서 `information hiding`이라고 한다. 170 | 171 | ### F#에서 Composition 172 | 173 | - 파이핑(piping)으로 함수 결합을 할 수 있다. 유닉스 파이프와 비슷하다. 174 | 175 | ```f# 176 | let​ add1 x = x + 1 ​// an int -> int function​ 177 | ​let​ square x = x * x ​// an int -> int function​ 178 | ​  179 | let​ add1ThenSquare x = 180 | x |> add1 |> square 181 | 182 | ​// test​ 183 | add1ThenSquare 5 ​// result is 36​ 184 | ``` 185 | 186 | ### Building an Entire Application from Functions 187 | 188 | - `Low Level Operation`을 컴포지션 해서 `Service`를 만들고 `Service`를 컴포지션 해서 189 | `Workflow`를 만들 수 있다. 190 | - 그리고 `Workflow`를 병렬(parallel)로 컴포지션해서 `Application`을 만들 수 있다. 191 | 192 | ### Challenges in Composing Functions 193 | 194 | - 함수 타입이 맞지 않는 경우 조합이 어렵다. 195 | - `... -> Option` 함수와 `int -> ...` 함수는 연결할 수 없다. 196 | - `... -> int` 함수와 `Option -> ...` 함수는 연결할 수 없다. 197 | - 가장 일반적인 방법은 양측의 동일한 유형, 즉 최소 공배수(lowest common multiple)로 연결하는 방법 198 | 이다. 199 | - `... -> int` 함수와 `Option -> ...`의 연결은 두 타입의 lowest common multiple은 200 | `Option`이기 때문에 `int`를 `Some`으로 `Option` 타입으로 변환해 두번째 함수로 넘길 수 있다. 201 | 202 | ```f# 203 | ​// a function that has an int as output​ 204 | let​ add1 x = x + 1 205 | 206 | // a function that has an Option as input​ 207 | let​ printOption x = 208 | ​match​ x ​with​ 209 | | Some i -> printfn ​"The int is %i"​ i 210 | | None -> printfn ​"No value"​ 211 | ``` 212 | 213 | 두 함수를 아래처럼 조합할 수 있다. 214 | 215 | ```f# 216 | 5 |> add1 |> Some |> printOption 217 | ``` 218 | -------------------------------------------------------------------------------- /part2/ch5.md: -------------------------------------------------------------------------------- 1 | # 타입으로 도메인 모델링 하기 2 | 3 | 첫번째 장에서 공유 멘탈 모델의 중요성에 대해 이야기 했다. 그리고 코드가 공유 모델을 반영해야 한다고 강조하고 4 | 개발자는 도메인 모델을 코드로 나타내는 것을 개을리 하면 안된다고 했다. 이상적으로 코드는 문서가 되는 것이 5 | 좋다. 이것의 의미는 개발자와 도메인 전문가가 코드를 함께 보면서 설계를 확인할 수 있다는 뜻이다. 6 | 7 | F# 타입 시스템은 도메인 모델을 코드로 나타내기에 충분하고 또한 도메인 전문가나 다른 개발자가 읽고 이해할 수 8 | 있다. 앞으로 타입이 대부분의 문서를 대체할 수 있다는 것을 알게 될거다. 이것은 굉장한 이점이 있는데 설계가 9 | 곧 코드 자체라서 구현과 설계가 달라지지 않는다는 장점이 있다. 10 | 11 | ## 도메인 모델 다시보기 12 | 13 | 만들었던 도메인 모델을 다시보자. 14 | 15 | ```f# 16 | context: Order-Taking 17 | 18 | // ---------------------- 19 | // Simple types 20 | // ---------------------- 21 | 22 | // Product codes 23 | data ProductCode = WidgetCode OR GizmoCode 24 | data WidgetCode = string starting with "W" then 4 digits 25 | data GizmoCode = ... 26 | // Order Quantity 27 | data OrderQuantity = UnitQuantity OR KilogramQuantity 28 | data UnitQuantity = ... 29 | data KilogramQuantity = ... 30 | 31 | // ---------------------- 32 | // Order life cycle 33 | // ---------------------- 34 | 35 | // ----- unvalidated state ----- 36 | data UnvalidatedOrder = 37 | UnvalidatedCustomerInfo 38 | AND UnvalidatedShippingAddress 39 | AND UnvalidatedBillingAddress 40 | AND list of UnvalidatedOrderLine 41 | 42 | data UnvalidatedOrderLine = 43 | UnvalidatedProductCode 44 | AND UnvalidatedOrderQuantity 45 | 46 | // ----- validated state ----- 47 | data ValidatedOrder = ... 48 | data ValidatedOrderLine = ... 49 | 50 | // ----- priced state ----- 51 | data PricedOrder = ... 52 | data PricedOrderLine = ... 53 | 54 | // ----- output events ----- 55 | ​data OrderAcknowledgmentSent = ... 56 | ​data OrderPlaced = ... 57 | ​data BillableOrderPlaced = ... 58 | ​ 59 | ​// ---------------------- 60 | ​// Workflows 61 | ​// ---------------------- 62 | ​ 63 | ​workflow "Place Order" = 64 | ​ input: UnvalidatedOrder 65 | ​ output (on success): 66 | ​ OrderAcknowledgmentSent 67 | ​ AND OrderPlaced (to send to shipping) 68 | ​ AND BillableOrderPlaced (to send to billing) 69 | ​ output (on error): 70 | ​ InvalidOrder 71 | ​ 72 | ​// etc 73 | ``` 74 | 75 | 이번 장의 목표는 이 모델을 코드로 옮기는 것이다. 76 | 77 | ## 도메인 모델의 패턴을 살펴보기 78 | 79 | 도메인 모델이 달라도 반복되는 많은 패턴이 있다. 몇가지 전형적인 도메인 패턴을 살펴보고 모델 컴포넌트와 80 | 어떻게 연관되는지 알아보자. 81 | 82 | - 단순 값. 문자열이나 숫자같은 기본 타입으로 표현할 수 있는 것들이 있다. 하지만 이것은 실제 숫자나 문자열 83 | 이 아니다. 도메인 전문가는 이걸 `int`나 `string`이라고 보지 않고 `OrderId`나 `ProductCode`라고 84 | 생각한다. - 유비쿼터스 언어 85 | - `AND`로 표현되는 값 조합. 관련된 데이터끼리 연결된 그룹이 있다. 종이로 치면 문서나 문서에 있는 부분들 86 | 이다.: 이름, 주소, 주문 등 87 | - `OR`로 표현되는 선택적인 값. 도메인에서 선택할 수 있는 것을 나타낸다. `Order` 또는 `Quote`, 88 | `UnitQuantity` 또는 `KilogramQuantity` 89 | - 워크플로우. 마지막으로 입력과 출력이 있는 비지니스 프로세스가 있다. 90 | 91 | 남은 장에서 F# 타입으로 이런 패턴들을 어떻게 표현하는지 알아본다. 92 | 93 | ## 단순 값을 모델링 하기 94 | 95 | 도메인 전문가는 `int`나 `string`이라고 보지 않고 `OrderId`나 `ProductCode`라고 생각한다고 앞에서 96 | 말했다. `OrderId`나 `ProductCode`는 섞어쓰지 않는 것이 중요하다. 기본 값을 나타내는 래퍼 타입을 97 | 만들거다. 98 | 99 | - F#에서 한가지 경우만 있는 타입을 아래 처럼 표현할 수 있다. 100 | 101 | ```f# 102 | type CustomerId = 103 | | Customer of int 104 | ``` 105 | 106 | - 하지만 더 간단하게 아래 처럼 표현할 수 있다. 107 | 108 | ```f# 109 | type CustomerId = Customer of int 110 | ``` 111 | 112 | - 레코드 타입 같은 복합 타입과 구분하기 위해 위 타입을 심플 타입이라고 부르자. 이 타입은 `int`나 `string` 113 | 같은 기본 값을 포함하고 있다. 114 | 115 | - 우리 도메인에서 심플 타입은 아래 처럼 표현할 수 있다. 116 | 117 | ```f# 118 | type​ WidgetCode = WidgetCode ​of​ ​string​ 119 | ​type​ UnitQuantity = UnitQuantity ​of​ ​int​ 120 | ​type​ KilogramQuantity = KilogramQuantity ​of​ decimal 121 | ``` 122 | 123 | 124 | 125 | ## 복합 데이터를 모델링 하기 126 | 127 | ## 함수로 워크플우를 모델링 하기 128 | 129 | ## A Question of Identity: 값 객체 130 | 131 | ## A Question of Identity: 엔티티 132 | 133 | ## Aggregates 134 | 135 | ## 모두 합치기 136 | 137 | ```f# 138 | ​namespace​ OrderTaking.Domain 139 | 140 | // Product code related​ 141 | ​type​ WidgetCode = WidgetCode ​of​ ​string​ 142 | ​// constraint: starting with "W" then 4 digits​ 143 | ​type​ GizmoCode = GizmoCode ​of​ ​string​ 144 | ​// constraint: starting with "G" then 3 digits​ 145 | ​type​ ProductCode = 146 | | Widget ​of​ WidgetCode 147 | | Gizmo ​of​ GizmoCode 148 | 149 | ​// Order Quantity related​ 150 | ​type​ UnitQuantity = UnitQuantity ​of​ ​int​ 151 | ​type​ KilogramQuantity = KilogramQuantity ​of​ decimal 152 | ​type​ OrderQuantity = 153 | | Unit ​of​ UnitQuantity 154 | | Kilos ​of​ KilogramQuantity 155 | 156 | type​ OrderId = Undefined 157 | type​ OrderLineId = Undefined 158 | type​ CustomerId = Undefined 159 | 160 | type​ CustomerInfo = Undefined 161 | ​type​ ShippingAddress = Undefined 162 | ​type​ BillingAddress = Undefined 163 | ​type​ Price = Undefined 164 | ​type​ BillingAmount = Undefined 165 | 166 | ​type​ Order = { 167 | Id : OrderId ​// id for entity​ 168 | CustomerId : CustomerId ​// customer reference​ 169 | ShippingAddress : ShippingAddress 170 | BillingAddress : BillingAddress 171 | OrderLines : OrderLine ​list​ 172 | AmountToBill : BillingAmount 173 | } 174 | 175 | ​and​ OrderLine = { 176 | Id : OrderLineId ​// id for entity​ 177 | OrderId : OrderId 178 | ProductCode : ProductCode 179 | OrderQuantity : OrderQuantity 180 | Price : Price 181 | } 182 | 183 | type​ UnvalidatedOrder = { 184 | OrderId : ​string​ 185 | CustomerInfo : ... 186 | ShippingAddress : ... 187 | ... 188 | } 189 | 190 | type​ PlaceOrderEvents = { 191 |   AcknowledgmentSent : ... 192 |   OrderPlaced : ... 193 |   BillableOrderPlaced : ... 194 |   } 195 | 196 | type​ PlaceOrderError = 197 | | ValidationError ​of​ ValidationError ​list​ 198 | | ... ​// other errors​ 199 | 200 | and​ ValidationError = { 201 | FieldName : ​string​ 202 | ErrorDescription : ​string​ 203 | } 204 | 205 | /// The "Place Order" process​ 206 | ​type​ PlaceOrder = 207 | ​  UnvalidatedOrder -> Result 208 | ``` 209 | ## 마무리 210 | -------------------------------------------------------------------------------- /example/src/main/scala/type.scala: -------------------------------------------------------------------------------- 1 | package example { 2 | package contact { 3 | /* 4 | type Contact = { 5 | FirstName: string 6 | MiddleInitial: string 7 | LastName: string 8 | 9 | EmailAddress: string 10 | IsEmailVerified: bool 11 | } 12 | */ 13 | package simple { 14 | 15 | class Contact( 16 | val firstName: String, 17 | val middleInitial: String, 18 | val lastName: String, 19 | val emailAddress: String, 20 | val isEmailVerified: Boolean 21 | ) 22 | 23 | } 24 | package typed { 25 | /* 26 | let createEmailAddress(s:string) = 27 | if Regex.IsMatch(s, @"^\S+@\S+\.\S+$") 28 | then Some (EmailAddress s) 29 | else None 30 | 31 | createEmailAddress: 32 | string -> EmailAddress option 33 | */ 34 | case class EmailAddress(emailAddress: String) 35 | 36 | object EmailAddress { 37 | private val emailRegx = """^[a-zA-Z0-9\.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r 38 | 39 | def apply(emailAddress: String): Option[EmailAddress] = 40 | emailAddress match { 41 | case s if emailRegx.findFirstIn(s).isDefined => Some(new EmailAddress(emailAddress.toUpperCase)) 42 | case _ => None 43 | } 44 | } 45 | 46 | /* 47 | type String50 = String50 of String 48 | 49 | let createString50 (s:string) = 50 | if s.Length <= 50 51 | then Some (String50 s) 52 | else None 53 | 54 | createString50: 55 | string -> String50 option 56 | */ 57 | case class String50(value: String) 58 | 59 | object String50 { 60 | def apply(value: String): Option[String50] = 61 | if (value.length() < 50) { 62 | Some(new String50(value)) 63 | } 64 | else { 65 | None 66 | } 67 | } 68 | 69 | case class PersonalName(firstName: String50, middleInitial: Option[String50], lastName: String50) 70 | 71 | case class VerifiedEmail(verifiedEmail: EmailAddress) 72 | 73 | sealed abstract class EmailContactInfo; 74 | case class Unverified(emailAddress: EmailAddress) extends EmailContactInfo 75 | case class Verified(emailAddress: VerifiedEmail) extends EmailContactInfo 76 | 77 | case class PostalContactInfo() 78 | 79 | /* 80 | type ContactInfo = 81 | | EmailOnly of EmailContactInfo 82 | | AddrOnly of PostalContactInfo 83 | | EmailAndAddr of EmailContactInfo * PostalContactInfo 84 | 85 | type Contact = { 86 | Name: Name 87 | ContactInfo: ContactInfo 88 | } 89 | */ 90 | abstract class ContactInfo 91 | case class EmailOnly(emailContactInfo: EmailContactInfo) extends ContactInfo 92 | case class AddrOnly(postalContactInfo: PostalContactInfo) extends ContactInfo 93 | case class EmailAndAddr(pair: (EmailContactInfo, PostalContactInfo)) 94 | 95 | case class Contact(name: PersonalName, contactInfo: ContactInfo) 96 | 97 | /* 98 | type ContactInfo = 99 | | Email of EmailContactInfo 100 | | Addr of PostalContactInfo 101 | 102 | type Contact = { 103 | Name: Name 104 | PrimaryContactInfo: ContactInfo 105 | SecondaryContactInfo: ContactInfo option 106 | } 107 | */ 108 | case class MultiContact(name: PersonalName, primaryContactInfo: ContactInfo, secondContactInfo: Option[ContactInfo]) 109 | } 110 | } 111 | 112 | /* 113 | module CardGame = 114 | type Suit = Club | Diamond | Spade | Heart 115 | 116 | type Rank = Two | Three | Four | Five | Six | Seven | Eight 117 | | Nine | Ten | Jack | Queen | King | Ace 118 | 119 | type Card = Suit * Rank 120 | 121 | type Hand = Card list 122 | type Deck = Card list 123 | 124 | type Player = {Name:string; Hand:Hand} 125 | type Game = {Deck:Deck; Players:Player list} 126 | 127 | type Deal = Deck -> (Deck * Card) 128 | 129 | type PickupCard = (Hand * Card) -> Hand 130 | */ 131 | 132 | package example.cardgame { 133 | 134 | object CardGame { 135 | 136 | sealed abstract class Suit 137 | 138 | case class Club() extends Suit 139 | 140 | case class Diamond() extends Suit 141 | 142 | case class Spade() extends Suit 143 | 144 | case class Heart() extends Suit 145 | 146 | sealed abstract class Rank 147 | 148 | case class Two() extends Rank 149 | 150 | case class Three() extends Rank 151 | 152 | case class Four() extends Rank 153 | 154 | case class Five() extends Rank 155 | 156 | case class Six() extends Rank 157 | 158 | case class Seven() extends Rank 159 | 160 | case class Eight() extends Rank 161 | 162 | case class Nine() extends Rank 163 | 164 | case class Ten() extends Rank 165 | 166 | case class Jack() extends Rank 167 | 168 | case class Queen() extends Rank 169 | 170 | case class King() extends Rank 171 | 172 | case class Ace() extends Rank 173 | 174 | case class Card(pair: (Suit, Rank)) 175 | 176 | case class Hand(cards: List[Card]) 177 | 178 | case class Deck(cards: List[Card]) 179 | 180 | case class Player(name: String, hand: Hand) 181 | 182 | case class Game(deck: Deck, players: List[Player]) 183 | 184 | } 185 | 186 | import CardGame._ 187 | 188 | trait CardGame { 189 | def deal(deck: Deck): (Deck, Card) 190 | 191 | def pickupCard(pair: (Hand, Card)): Hand 192 | } 193 | } 194 | 195 | package temp { 196 | // type Temp = F of int | C of float 197 | sealed abstract class Temp 198 | case class F(value: Int) extends Temp 199 | case class C(value: Float) extends Temp 200 | } 201 | 202 | package payment { 203 | /* 204 | type PaymentMethod = 205 | | Cash 206 | | Cheque of ChequeNumber 207 | | Card of CardType * CardNumber 208 | */ 209 | sealed abstract class CardType 210 | case class Visa() extends CardType 211 | case class Master() extends CardType 212 | 213 | case class CardNumber(cardNumber: Long) 214 | case class ChequeNumber(chequeNumber: Long) 215 | 216 | sealed abstract class PaymentMethod 217 | case class Cash() extends PaymentMethod 218 | case class Cheque(chequeNumber: ChequeNumber) extends PaymentMethod 219 | case class Card(pair: (CardType, CardNumber)) extends PaymentMethod 220 | } 221 | } 222 | 223 | 224 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /part1/ch1.md: -------------------------------------------------------------------------------- 1 | # Domain Modeling Made Functional 2 | 3 | ## Domain-Driven Design 소개 4 | 5 | - 코드만 잘 짜면 되는 것이 아님 6 | - 좋은 설계가 좋은 결과를 줄 것임 7 | - DDD는 Data Driven, Object Oriented와는 다름 8 | - DDD 접근 방식이 항상 좋은 것은 아니지만 비개발자와 협업하는 엔터프라이즈 개발에는 도움이 됨 9 | 10 | ### 공유 모델의 중요성 11 | 12 | - 문제를 풀기전에 문제를 정확히 이해하는 것이 중요함 13 | - 개발자가 도메인을 잘 이해하는 것이 중요함 14 | - 도메인 전문가가 비즈니스 분석가에게 도메인에 대해 설명한 후 분석가는 요구사항 명세를 만들고 아키텍트에게 15 | 전달하고 다음 개발팀으로 전달되면 도메인에 대한 내용이 왜곡 될 가능성이 커짐 16 | - 도메인 전문가가 개발팀에게 직접 도메인에 대한 설명을 하고 개발팀이 구현 후 도메인 전문가에게 피드백을 17 | 주는 애자일 방식이 그나마 나음 18 | - 하지만 애자일 방식에서 피드백이 빠르긴 하지만 도메인 내용이 왜곡될 수 있음 19 | - 만약 소스 코드가 개발팀과 도메인 전문가와 이해관계자가 함께 볼 수 있다면 더 빠른 피드백으로 도메인 내용을 20 | 빨리 반영할 수 있음. 이것이 DDD의 목표. 21 | - 소프트웨어 모델과 도메인 모델을 일치시키면 다음과 같은 이점이 있다 22 | - 더 빨리 시장에 출시 할 수 있다 23 | - 문제를 정확히 해결하는 솔루션은 고객에게 더 큰 가치를 준다 24 | - 명확한 명세는 오해로 인한 재작업을 시간 낭비를 줄여준다 25 | - 코드가 모델을 반영할 때 유지 보수하기 쉽다 26 | - 공유 모델을 만들기 위한 가이드 27 | - 데이터 구조 보다는 비지니스 이벤트와 워크플로우에 집중 28 | - 도메인을 더 작은 서브 도메인 단위로 나눈다 29 | - 서브 도메인으로 도메인을 모델로 만든다. 30 | - 프로젝트에 참여하는 사람이 공유할 수 있는 일반적인 언어(유비쿼터스 언어)로 만든다. 31 | 32 | ### 비지니스 이벤트를 통해 도메인 이해하기 33 | 34 | - 도메인을 이해하기 위해서 비지니스 이벤트 부터 시작하는 것이 좋다. 35 | - 비지니스는 데이터 변환 과정에 가치가 있기 때문에 이 변환 과정이 어떻게 되는지 이해하는 것이 중요하다. 36 | - 아무 것도 하지 않는 정적인 데이터는 쓸모가 없다. 37 | - 특정 이벤트로 데이터 변환이 시작된다. (이메일, 전화를 받거나, 정해진 시간이 되거나...) 38 | - 뭐가 되든 이것이 중요하고 이것을 도메인 이벤트라고 부른다. 39 | - 도메인 이벤트는 비지니스 프로세스의 시작점이 된다. 예를 들어 '새로운 주문을 받았다'라는 이벤트는 주문 40 | 프로세스가 시작됨을 말한다. 41 | - 도메인 이벤트는 변할 수 없기 때문에 항상 과거 시점을 작성된다. 42 | 43 | #### 도메인을 발견하기 위해 이벤트 스토밍 사용하기 44 | 45 | - 프로젝트가 성공하기 바라는 모든 사람들이 모여 도메인 이벤트를 나열한다. 46 | - 긴 벽이 있는 회의실에 포스트잇으로 이벤트 스토밍을 한다 47 | - 워크샵 동안 참여자는 포스트잇에 도메인 비지니스 이벤트를 적어 붙인다 48 | - 이벤트는 시간 순으로 나열된다 49 | - 누구나 질문하고 누구나 대답한다 50 | 51 | #### 도메인 발견하기: 주문 추적 시스템 52 | 53 | * 이 책에서는 주문 추적 시스템이라는 실제 비즈니스 문제를 다룰 것이다 54 | * 이 문제를 가지고 설계하는 것과 도메인 모델링 구현을 해볼 것이다 55 | 56 | - Widget Inc 라는 회사는 자동화된 주문 추적을 온라인 시스템으로 만들기로 하였다. 57 | 58 | - 맥스라는 매니저가 상황 설명을 한다 59 | 60 | ``` 61 | 저희 회사는 다른 회사를 위해 위젯이나 기즈모 같은 부품을 만드는 작은 회사입니다. 회사가 빠르게 성장했고 현재 프로세스를 유지하기 어렵게 되었습니다. 지금은 모든 것을 수기로 하고 있는데 전산화를 해서 직원들이 더 큰 주문을 처리할 수 있었으면 합니다. 특히 고객들이 웹사이트를 통해 주문하기나 주문 상태 확인 같이 고객이 할 수 있는 것들은 고객이 직접 할 수 있었으면 좋겠습니다. 62 | ``` 63 | 64 | - 그럼 어디서 시작해볼까요? 65 | 66 | * 첫번째 가이드라인인 "비즈니스 이벤트"에 집중 하기를 생각하면서 이벤트 스토밍 세션을 해보자. 67 | 68 | ``` 69 | 당신: 비즈니스 이벤트 붙이는 것을 시작해보실 분! 70 | 올리: 주문 추적 부서에 올리입니다. 들어오는 주문과 견적을 처리하는 일이 대부분 입니다. 71 | 당신: 이 일은 어떤 것에 의해 생기나요? 72 | 올리: 고객이 이메일로 양식을 우리에게 보낼 때 생기는 일입니다. 73 | 당신: 그러면 이벤트는 "Order form received"나 "Quote form received" 같은 것이 되면 될까요? 74 | 올리: 네! 제가 써서 벽에 붙일께요. 75 | 샘: 저는 배송 부서에 샘입니다. 우리는 그 주문이 승인되면 처리합니다. 76 | 당신: 승인되었다는 것은 어떻게 아나요? 77 | 샘: 주문이 배송 추적 부서에서 넘어올 때 압니다. 78 | 당신: 이 이벤트는 뭐라고 부를까요? 79 | 샘: "Order available"이 어떨까요? 80 | 올리: 우리는 배송 준비가 완료된 주문을 "Placed order"라고 부르는데요. 다들 이 용어를 쓰는 것에 동의하시는지요? 81 | 샘: 그러면 "Order placed"라는 것이 우리가 다루고 있는 이벤트가 될 수 있겠네요. 82 | ``` 83 | 84 | * 시간이 흐르고 아래와 같은 이벤트가 나왔다 85 | 86 | * Order form received 87 | * Order placed 88 | * Order shipped 89 | * Order change requested 90 | * Order cancellation requested 91 | * Return requested 92 | * Quote form received 93 | * Quote provided 94 | * New customer request received 95 | * New customer registered 96 | 97 | - 어떤 비즈니스 이벤트는 앞에 "Place order"이나 "Ship order" 같은 포스트잇이 붙어 있다. 98 | 99 | - 이벤트 스토밍 세션 전체를 자세히 다루진 않겠지만 몇 가지는 살펴보자 100 | 101 | - 비즈니스에 대한 공유 모델 102 | 103 | - 이벤트 스토밍의 중요한 이점은 이벤트를 도출하는 것 뿐만 아니라 참여자 모두가 벽에 있는 같은 것을 보기 때문에 비즈니스에 대한 이해를 공유하고 발전 시킨다는 점에 있다 104 | - 이벤트 스토밍도 DDD 처럼 당신과 나에 대한 생각 대신 공유 모델과 커뮤니케이션을 강조한다 105 | - 참석자들은 생소한 도메인에 대한 지식을 배우기도 하지만 다른 팀에 대한 가정이 잘 못되었다는 것을 알거나 비즈니스 개선에 도움이 되는 인사이트도 발전시켜 나갈 수 있다 106 | 107 | - 모든 용어의 인지 108 | 109 | - 가끔 자신에게 관련 있는 비즈니스 한 측면에만 집중하고 자신이 만드는 데이터를 사용하는 다른 팀이 관련되어 있다는 사실을 잊을 때 도 있다 110 | 111 | - 모든 이해 관계자가 한 방에 있다면 보고 있는 사람 중 누구든지 이야기할 수 있다. 112 | 113 | ``` 114 | 빌링 부서에 블래이크입니다. 우리가 있다는 것을 잊지 마세요. 우리도 주문 절차에 대해 완벽하게 알아야합니다. 그래야 고객이 지불하고 회사가 돈을 벌 수 있습니다. 그래서 우리도 마찬가지로 "Order placed" 이벤트가 필요합니다. 115 | ``` 116 | 117 | - 요구 사항의 간극을 발견하기 118 | 119 | - 이벤트가 시간 순으로 벽에 나열되 있을 때 가끔 빠진 요구 사항이 명확해진다. 120 | 121 | ``` 122 | 맥스: 올리, 주문 준비가 끝났을 때 고객에게 알려주나요? 벽에는 표시되있지 않아서요. 123 | 올리: 아! 맞아요! 깜빡했네요. 주문이 들어오면 우리가 잘 받았고 배송될 것이라는 것을 고객에게 이메일로 보냅니다. 이건 또 다른 이벤트인데 "Order acknowledgment sent to customer"가 될 것 같아요. 124 | ``` 125 | 126 | - 질문에 대한 명확한 답이 없다면 나중에 다시 이야기해보기 위해 포스트잇으로 붙여논다 127 | 128 | - 만약 특정한 프로세스에 대한 논쟁이나 일치되지 않은 의견이 있다면 문제라고 생각하지 말고 기회라고 생각하는 것이 좋다 129 | 130 | - 이런 부분에서 많은 것을 배울 수 있다. 131 | 132 | - 프로젝트가 시작할 때 요구 사항이 모호한 것은 당연한 것이라서 질문이나 논쟁을 시각적으로 문서화하면 더 많은 일을 해야하고 프로젝트를 시작하지 못하게 만든다. 133 | 134 | - 용어 간 연결 135 | 136 | - 타임 라인에 있는 이벤트는 그룹화 할 수 있다. 어떤 팀이 결과물이 다른 팀의 입력 값이 될 수 있다는 것이 명확해진다. 137 | - 예를 들어 배송 추적 팀에서 주문이 완료되었을 때 새로운 주문이 들어왔다는 신호가 필요하다. 이 "Order placed" 이벤트는 배송 팀과 결제 팀에 입력이 된다. 138 | - 기술적으로 구체적으로 어떻게 연결되는지는 나타내지 않는다 139 | - 메시지 큐가 좋은 지 데이터베이스가 좋은지는 중요하지 않고 도메인에 집중하는 것이 필요하다 140 | 141 | - 리포팅 요구 사항에 대한 인지 142 | 143 | - 도메인을 이해할 때 프로세스와 트랜젝션에 집중하기는 쉽다 144 | - 어떤 비즈니스는 과거에 어떤 일이 있었는지 이해하는 것이 중요하다 - 리포팅은 항상 도메인의 일부다 145 | - 리포팅이나 다른 읽기 전용 모델(UI에서 뷰 모델)도 이벤트 스토밍에 포함시키도록 한다 146 | 147 | - 워크플로우, 시나리오, 유즈 케이스, 프로세스 용어에 대해서 148 | 149 | * 많은 곳에서 위 용어를 혼용해서 쓰지만 여기서는 조금 더 명확하게 정의하고 쓰겠다 150 | * 시나리오는 주문 입력과 같이 고객이 달성하고자 하는 목표를 기술 한다. 애자일에 사용자 스토리와 비슷하다. 151 | * 유즈케이스는 시나리오에 구체적인 버전이다. 일반적인 용어로 고객이 목표를 달성하기 위해 사용자 액션이나 다른 단계들을 기술 한다. 152 | * 시나리오나 유즈케이스 모두 사용자 관점에서 어떻게 상호작용 할 것인지 중점을 두는 사용자 중심 컨셉이다. 153 | * 비즈니스 프로세스는 개인 고객이 아닌 비즈니스 관점에서 달성하고자 하는 것을 기술 한다. 시나리오와 비슷하지만 사용자 중심보다 비즈니스 중심 적이다. 154 | * 워크플로우는 비즈니스 프로세스의 한 부분을 기술 한다. 비즈니스 목표를 달성하기 위한 정확한 단계를 나타내는 목록이다. 155 | * 워크플로우는 한 사람이나 한팀이 할 수 있는 일로 제한되고 비즈니스 프로세스가 여러 팀에 걸쳐서 분산될 때(주문 프로세스 처럼)는 전체 비즈니스 프로세스를 작은 워크플로우로 나눌 수 있다. 156 | 157 | #### 커맨드을 문서화 158 | 159 | * 벽에 이벤트가 몇 개 생겼을 때 무엇이 이벤트를 만드는지 질문해 볼 수 있다. 160 | * 누군가 또는 무언가가 이벤트가 발생되길 원했다. 예를 들어 고객은 우리가 주문서를 받길 원했거나 상사가 당신이 뭔가 하길 원했다. 161 | * 이런 것을 DDD 용어로 커맨드라고 부른다. (객체지향 패턴에 커맨드 패턴과는 다른 것이다) 162 | * 커맨드는 항상 명령형으로 표현한다. "Do this for me" 163 | * 물론 모든 커맨드가 성공하진 않는다 - 메일에 있는 주문서는 잃어버릴 수도 있다. 또 상사를 돕는 일 때문에 뭔가 더 중요한 일로 바쁠 수도 있다. 164 | * 하지만 커맨드가 성공하면 워크플로우가 동작하고 해당하는 도메인 이벤트가 생긴다. 예를 들면: 165 | * 커맨드가 "Make X happen" 였고 워크플로우가 X가 되게 했다면 도메인 이벤트는 "X happend"일 것이다. 166 | * 커맨드가 "Send an order form to Widgets Inc,"였고 워크플로우가 주문을 보냈다면 해당하는 도메인 이벤트는 "Order form sent"가 될것이다. 167 | * 커맨드: "Place an order"; 도메인 이벤트: "Order placed" 168 | * 커맨드: "Send a shipment to customer ABC"; 도메인 이벤트: "Shipment sent" 169 | * 사실 비즈니스 프로세스는 이런식으로 모델링 할 것이다. 170 | * 이벤트 어떤 비즈니스 워크플로우를 시작시키는 커맨드를 실행시킬 수 있다. 171 | * 워크플로우의 결과는 몇 개의 이벤트다. 그리고 이벤트들은 여러 커맨드를 실행시킬 수 있다. 172 | * 이런 식으로 입력과 출력을 가진 비즈니스 프로세스(파이프라인)로 생각하는 것은 함수형 프로그래밍과 잘 맞는다. 앞으로 살펴볼 것이다. 173 | * 이 접근 방법으로 배송 추적 프로세를 보면 다음과 같다: 174 | * [Event] Order form received -> triggers -> [Command] Place Order -> Input: 주문에 필요한 데이터 -> [Workflow] Place Order -> Output: 이벤트 목록 [Event1] Order placed (for shipping), [Event 2] Order placed (for biling) 175 | * 지금은 커맨드가 성공한다고 가정했지만 실패할 수도 있다. 커맨드가 실패하는 경우는 10장에서 다룬다. 176 | * 모든 이벤트가 커맨드랑 연결될 필요는 없다. 어떤 이벤트는 스케쥴러나 모니터링 시스템에 의해 만들어 지기도한다. (계정 시스템에 MonthEndClose 나 재고 시스템에 OutOfStock) 177 | 178 | ### 도메인을 서브 도메인으로 나누기 179 | 180 | ### 경계(Bounded Context)로 문제 해결하기 181 | 182 | - 183 | 184 | ### 유비쿼터스 언어 만들기 185 | 186 | ### 요약 187 | -------------------------------------------------------------------------------- /part3/ch12.md: -------------------------------------------------------------------------------- 1 | # Persistent 2 | 3 | ## 영속화를 뒤로 미루기 4 | 5 | - invoce 지불하기 예제 6 | - 데이터베이스 로드 -> 지불 -> 지불 상태에 따라 디비에 다른 형태로 저장 7 | 8 | ```f# 9 | ​// workflow mixes domain logic and I/O​ 10 | let​ payInvoice invoiceId payment = 11 | // load from DB​ 12 | ​let​ invoice = loadInvoiceFromDatabase(invoiceId) 13 | 14 | // apply payment​ 15 | invoice.ApplyPayment(payment) 16 | 17 | // handle different outcomes​ 18 | if​ invoice.IsFullyPaid ​then​ 19 | markAsFullyPaidInDb(invoiceId) 20 | postInvoicePaidEvent(invoiceId) 21 | ​else​ 22 | markAsPartiallyPaidInDb(invoiceId) 23 | ``` 24 | 25 | - 문제는 함수가 순수하지 않기 때문에 테스트가 어렵다. 26 | - 순수한 비지니스 로직을 뽑아보자. 27 | 28 | ```f# 29 | type​ InvoicePaymentResult = 30 | | FullyPaid 31 | | PartiallyPaid ​of​ ... 32 | 33 | // domain workflow: pure function​ 34 | let​ applyPayment unpaidInvoice payment :InvoicePaymentResult = 35 | ​// apply payment​ 36 | ​let​ updatedInvoice = unpaidInvoice |> applyPayment payment 37 | 38 | ​// handle different outcomes​ 39 | ​if​ isFullyPaid updatedInvoice ​then​ 40 | FullyPaid 41 | ​else​ 42 | PartiallyPaid updatedInvoice 43 | ​// return PartiallyPaid or FullyPaid​ 44 | ``` 45 | 46 | - 위 함수는 데이터를 로드하거나 저장하지 않고 파라미터로 받기 때문에 순수하다. 47 | - I/O와 관련된 코드는 다음과 같이 분리할 수 있다. 48 | 49 | ```f# 50 | type​ PayInvoiceCommand = { 51 | InvoiceId : ... 52 | Payment : ... 53 | } 54 | 55 | // command handler at the edge of the bounded context​ 56 | let​ payInvoice payInvoiceCommand = 57 | // load from DB​ 58 | ​let​ invoiceId = payInvoiceCommand.InvoiceId 59 | ​let​ unpaidInvoice = 60 | loadInvoiceFromDatabase invoiceId ​// I/O​ 61 | 62 | ​// call into pure domain​ 63 | ​let​ payment = 64 | payInvoiceCommand.Payment ​// pure​ 65 | ​let​ paymentResult = 66 | applyPayment unpaidInvoice payment ​// pure​ 67 | 68 | ​// handle result​ 69 | ​match​ paymentResult ​with​ 70 | | FullyPaid -> 71 | markAsFullyPaidInDb invoiceId ​// I/O​ 72 | postInvoicePaidEvent invoiceId ​// I/O​ 73 | | PartiallyPaid updatedInvoice -> 74 | updateInvoiceInDb updatedInvoice ​// I/O​ 75 | ``` 76 | 77 | - 위 함수에는 판단 로직 같은 것은 없다 78 | - 위 함수도 테스트하기 쉬도록 만드려면 I/O관련 함수를 인자로 받으면 된다. 79 | 80 | ```f# 81 | // command handler at the edge of the bounded context​ 82 | let​ payInvoice 83 | loadUnpaidInvoiceFromDatabase ​// dependency​ 84 | markAsFullyPaidInDb ​// dependency​ 85 | updateInvoiceInDb ​// dependency​ 86 | payInvoiceCommand = 87 | 88 | ​// load from DB​ 89 | ​let​ invoiceId = payInvoiceCommand.InvoiceId 90 | ​let​ unpaidInvoice = 91 | loadUnpaidInvoiceFromDatabase invoiceId 92 | 93 | ​// call into pure domain​ 94 | let​ payment = 95 | payInvoiceCommand.Payment 96 | ​let​ paymentResult = 97 | applyPayment unpaidInvoice payment 98 | ​// handle result​ 99 | ​match​ paymentResult ​with​ 100 | | FullyPaid -> 101 | markAsFullyPaidInDb(invoiceId) 102 | postInvoicePaidEvent(invoiceId) 103 | | PartiallyPaid updatedInvoice -> 104 | updateInvoiceInDb updatedInvoice 105 | ``` 106 | 107 | ### 쿼리에 의존하는 결정 108 | 109 | - 만약 계산이 쿼리 결과에 의존한다면 어떻게 해야할까? 110 | 111 | ```f# 112 | --- I/O--- 113 | Load invoice from DB 114 | 115 | --- Pure --- 116 | Do payment logic 117 | 118 | --- I/O --- 119 | Pattern match on output choice type: 120 | if "FullyPaid" -> Mark invoice as paid in DB 121 | if "PartiallyPaid" -> Save updated invoice to DB 122 | 123 | --- I/O --- 124 | Load all amounts from unpaid invoices in DB 125 | 126 | --- Pure --- 127 | Add the amounts up and decide if amount is too large 128 | 129 | --- I/O --- 130 | Pattern match on output choice type: 131 | If "OverdueWarningNeeded" -> Send message to customer 132 | If "NoActionNeeded" -> do nothing 133 | ``` 134 | 135 | - 위와 같이 I/O와 순수함수가 반복될때는 워크플로우를 더 작게 나누는 것이 좋다. 136 | 137 | ### 리포지토리 패턴은? 138 | 139 | - 리포지토리 패턴은 OO에서 영속화를 감추는 방법인데 함수 끝으로 영속화를 미루면 필요없다? 140 | 141 | ## 명령과 조회 분리하기 142 | 143 | - command-query 분리에 대해 알아보자. 144 | - 함수형 프로그래밍에서 데이터는 모두 불변형이다. 데이터베이스도 불변형이라고 생각해보자. 145 | - insert 동작을 예로 들어보면 새 모델을 insert 하려면 새 모델 데이터와 원래 상태를 함께 insert 하고 146 | 리턴 값으로 insert 된 데이터를 받는다. 코드로 표현하면 아래와 같다. 147 | 148 | ```f# 149 | type InsertData = DataStoreState -> Data -> NewDataStoreState 150 | ``` 151 | 152 | - Create, Read, Update, Delete 동작 중에서 Read와 나머지 동작으로 분리할 수 있다. 153 | - 두 동작을 별도의 모듈로 분리할 수 도 있다. command-query responsibility segregation or CQRS 154 | 155 | ```f# 156 | type​ SaveCustomer = WriteModel.Customer -> DbResult 157 | type​ LoadCustomer = CustomerId -> DbResult 158 | ``` 159 | 160 | - 물리적으로 쓰기 데이터베이스와 읽기 데이터베이스를 분리할 수 도 있다. 161 | - 쓰기 쪽에서 읽기 쪽으로는 복제가 이뤄저야하고 시간이 걸리지만 `eventually consistent`를 보장한다. 162 | 163 | ## 관계형 데이터베이스 사용하기 164 | 165 | - 관계형 데이터베이스는 코드와 매우 다르게 생겼기 때문에 사용하기 어렵다. 166 | - 오래전 부터 객체와 데이터베이스간 임피던스 미스매칭이라고 불렀다. 167 | - 데이터와 연산이 함께 있지 않기 때문에 함수형으로 개발된 모델은 관계형 데이터베이스를 쓰기 더 쉽다. 168 | - 그래도 문제는 있다. 169 | - 테이블은 함수형 모델의 컬렉션과 잘 맞는다. select, where 같은 연산은 map, filter등과 비슷하다. 170 | - 쉬운 방법은 Serialize를 사용해서 테이블에 직접 매핑할 수 있는 모델을 만드는 것이다. 171 | 172 | ```f# 173 | ​type​ CustomerId = CustomerId ​of​ ​int​ 174 | type​ String50 = String50 ​of​ ​string​ 175 | type​ Birthdate = Birthdate ​of​ DateTime 176 | ​  177 | type​ Customer = { 178 | CustomerId : CustomerId 179 | Name : String50 180 | Birthdate : Birthdate option 181 | } 182 | ``` 183 | 184 | - 위 모델은 아래 테이블과 일치한다. 185 | 186 | ```sql 187 | CREATE​ ​TABLE​ Customer ( 188 | CustomerId ​int​ ​NOT​ ​NULL​, 189 | ​Name​ ​NVARCHAR​(50) ​NOT​ ​NULL​, 190 | Birthdate ​DATETIME​ ​NULL​, 191 | ​CONSTRAINT​ PK_Customer ​PRIMARY​ ​KEY​ (CustomerId) 192 | ) 193 | ``` 194 | 195 | - 테이블은 int나 string 같은 타입만 저장할 수 있기 때문에 ProductCode 나 OrderId 같은 도메인 196 | 타입을 풀어야 한다. 197 | - 더 안좋은 것은 choice 타입은 관계형 데이터베이스에 잘 맞지 않는 다는 점이다. 198 | 199 | ### Choice 타입을 매핑하기 200 | 201 | - Chocie 타입은 단계가 하나 있는 상속 객체와 같다. 202 | - 매핑 방법은 각 case를 테이블로 나누는 방법과 하나의 테이블에 모두 넣는 방법이 있다. 203 | - 예를 들어 Contract Choice 타입이 있고 이것을 영속화 해본다고 하자 204 | 205 | ```f# 206 | type​ Contact = { 207 | ContactId : ContactId 208 | Info : ContactInfo 209 | } 210 | 211 | and​ ContactInfo = 212 | | Email ​of​ EmailAddress 213 | | Phone ​of​ PhoneNumber 214 | ​  215 | and​ EmailAddress = EmailAddress ​of​ ​string​ 216 | and​ PhoneNumber = PhoneNumber ​of​ ​string​ 217 | and​ ContactId = ContactId ​of​ ​int​ 218 | ``` 219 | 220 | - 먼저 하나의 테이블에 다 넣을 수 있도록 해보자. 221 | 222 | ```sql 223 | ​CREATE​ ​TABLE​ ContactInfo ( 224 | ​-- shared data​ 225 | ContactId ​int​ ​NOT​ ​NULL​, 226 | ​-- case flags​ 227 | IsEmail ​bit​ ​NOT​ ​NULL​, 228 | IsPhone ​bit​ ​NOT​ ​NULL​, 229 | ​-- data for the "Email" case​ 230 | EmailAddress ​NVARCHAR​(100), ​-- Nullable​ 231 | ​-- data for the "Phone" case​ 232 | PhoneNumber ​NVARCHAR​(25), ​-- Nullable​ 233 | ​-- primary key constraint​ 234 | ​CONSTRAINT​ PK_ContactInfo ​PRIMARY​ ​KEY​ (ContactId) 235 | ) 236 | ``` 237 | 238 | - case에 따른 필드를 만들고 EmailAddress, PhoneNumber을 Nullable로 만들어 쓴다. 239 | 240 | - 두번째 방법은 각각 테이블로 나누는 방법이다. 241 | 242 | ```sql 243 | -- Main table​ 244 | ​CREATE​ ​TABLE​ ContactInfo ( 245 | ​-- shared data​ 246 | ContactId ​int​ ​NOT​ ​NULL​, 247 | -- case flags​ 248 | IsEmail ​bit​ ​NOT​ ​NULL​, 249 | IsPhone ​bit​ ​NOT​ ​NULL​, 250 | ​CONSTRAINT​ PK_ContactInfo ​PRIMARY​ ​KEY​ (ContactId) 251 | ) 252 | ​  253 | -- Child table for "Email" case​ 254 | ​CREATE​ ​TABLE​ ContactEmail ( 255 | ContactId ​int​ ​NOT​ ​NULL​, 256 | ​-- case-specific data​ 257 | EmailAddress ​NVARCHAR​(100) ​NOT​ ​NULL​, 258 | ​CONSTRAINT​ PK_ContactEmail ​PRIMARY​ ​KEY​ (ContactId) 259 | ) 260 | ​  261 | -- Child table for "Phone" case​ 262 | ​CREATE​ ​TABLE​ ContactPhone ( 263 | ContactId ​int​ ​NOT​ ​NULL​, 264 | -- case-specific data​ 265 | PhoneNumber ​NVARCHAR​(25) ​NOT​ ​NULL​, 266 | ​CONSTRAINT​ PK_ContactPhone ​PRIMARY​ ​KEY​ (ContactId) 267 | ) 268 | ``` 269 | 270 | - 두번째 방법은 데이터가 매우 크고 공통점이 적을 때 좋을 수 있지만 기본적으로 한 테이블로 표현하는 방법을 271 | 쓰자. 272 | 273 | ### 다른 타입을 포함 하는 경우 매핑 274 | 275 | - 타입이 다른 타입을 포함하는 경우 엔티티 타입이면 별도의 테이블로 값 타입이면 그 타입 테이블에 넣는다. 276 | - Order 타입이 OrderLine 타입을 여러개 포함하고 있고 OderLine 타입이 엔티티기 때문에 테이블을 분리한다. 277 | 278 | ```sql 279 | CREATE​ ​TABLE​ ​Order​ ( 280 | OrderId ​int​ ​NOT​ ​NULL​, 281 | -- and other columns​ 282 | ) 283 | ​  284 | CREATE​ ​TABLE​ OrderLine ( 285 | OrderLineId ​int​ ​NOT​ ​NULL​, 286 | OrderId ​int​ ​NOT​ ​NULL​, 287 | -- and other columns​ 288 | ) 289 | ``` 290 | 291 | - Order 타입이 Address 값을 가지고 있는데 Address는 값 타입이기 때문에 원래 테이블에 넣는다. 292 | 293 | ```sql 294 | ​CREATE​ ​TABLE​ ​Order​ ( 295 | OrderId ​int​ ​NOT​ ​NULL​, 296 | 297 | ​-- inline the shipping address Value Object​ 298 | ShippingAddress1 ​varchar​(50) 299 | ShippingAddress2 ​varchar​(50) 300 | ShippingAddressCity ​varchar​(50) 301 | ​-- and so on​ 302 | 303 | ​-- inline the billing address Value Object​ 304 | BillingAddress1 ​varchar​(50) 305 | BillingAddress2 ​varchar​(50) 306 | BillingAddressCity ​varchar​(50) 307 | -- and so on​ 308 | ​ 309 | ​ -- other columns​ 310 | ) 311 | ``` 312 | -------------------------------------------------------------------------------- /part3/ch9.md: -------------------------------------------------------------------------------- 1 | # 구현: Composing a pipeline 2 | 3 | ```f# 4 | let​ placeOrder unvalidatedOrder = 5 | unvalidatedOrder 6 | |> validateOrder 7 | |> priceOrder 8 | |> acknowledgeOrder 9 | |> createEvents 10 | ``` 11 | 12 | - 위 주문 과정은 두단계로 나눠서 설명할 예정, 1 개별 함수의 구현, 2 조합하기 13 | - 조합하기는 2가지 이유로 어려움 14 | - 어떤 함수는 추가 파라미터가 필요함 (디팬던시) 15 | - 에러 핸들링을 위한 Result 같은 타입이 input/output 타입의 불일치를 만듬 16 | 17 | ## Working with Simple Type 18 | 19 | - OrderId 타입을 String 타입 값으로 생성하는 create 핼퍼함수와 값을 리턴하는 value 핼퍼함수를 만듬 20 | 21 | ```f# 22 | module​ Domain = 23 | ​type​ OrderId = ​private​ OrderId ​of​ ​string​ 24 | 25 | ​module​ OrderId = 26 | ​/// Define a "Smart constructor" for OrderId​ 27 | ​/// string -> OrderId​ 28 | ​let​ create str = 29 | if​ String.IsNullOrEmpty(str) ​then​ 30 | // use exceptions rather than Result for now​ 31 | failwith ​"OrderId must not be null or empty"​ 32 | ​elif​ str.Length > 50 ​then​ 33 | failwith ​"OrderId must not be more than 50 chars"​ 34 | ​else​ 35 | OrderId str 36 | 37 | ​/// Extract the inner value from an OrderId​ 38 | ​/// OrderId -> string​ 39 | ​let​ value (OrderId str) = ​// unwrap in the parameter!​ 40 | str ​// return the inner value’ 41 | ``` 42 | 43 | ## Using Function Types to Guide the Implementation 44 | 45 | - validateOrder 함수 46 | 47 | ```f# 48 | let​ validateOrder 49 | checkProductCodeExists ​// dependency​ 50 | checkAddressExists ​// dependency​ 51 | unvalidatedOrder = ​// input​ 52 | ... 53 | ``` 54 | 55 | - 함수 시그네처를 따로 정의하고 람다 함수로 리턴하기 56 | 57 | ```f# 58 | // define a function signature​ 59 | type​ MyFunctionSignature = Param1 -> Param2 -> Result 60 | ​  61 | // define a function that implements that signature​ 62 | let​ myFunc: MyFunctionSignature = 63 | ​fun​ param1 param2 -> 64 | ``` 65 | 66 | - validateOrder 함수 시그네처를 따로 정의 하고 람다 함수로 리턴하기 67 | 68 | ```f# 69 | let​ validateOrder : ValidateOrder = 70 | ​fun​ checkProductCodeExists checkAddressExists unvalidatedOrder -> 71 | // ^dependency ^dependency ^input​ 72 | ... 73 | ``` 74 | 75 | - 아래는 타입 체크의 예, checkProductCodeExists 함수에 ProductCode 타입이어야 하는데 int 타입이 76 | 넘어감 77 | 78 | ```f# 79 | let​ validateOrder : ValidateOrder = 80 | ​fun​ checkProductCodeExists checkAddressExists unvalidatedOrder -> 81 | if​ checkProductCodeExists 42 ​then​ 82 | // compiler error ^​ 83 | ​// This expression was expected to have type ProductCode​ 84 | // but here has type int​ 85 | ... 86 | ... 87 | ``` 88 | 89 | ## Implementation the Validation Step 90 | 91 | - 원래 함수 시그네처 92 | 93 | ```f# 94 | type​ CheckAddressExists = 95 | UnvalidatedAddress -> AsyncResult 96 | ​  97 | ​type​ ValidateOrder = 98 | CheckProductCodeExists ​// dependency​ 99 | -> CheckAddressExists ​// AsyncResult dependency​ 100 | -> UnvalidatedOrder ​// input​ 101 | -> AsyncResult ​// output​ 102 | ``` 103 | 104 | - 일단 이 장에서는 effect(Result)를 제거하기 위해 아래 처럼 시그네처를 줄임 105 | 106 | ```f# 107 | type​ CheckAddressExists = 108 | UnvalidatedAddress -> CheckedAddress 109 | 110 | type​ ValidateOrder = 111 | CheckProductCodeExists ​// dependency​ 112 | -> CheckAddressExists ​// dependency​ 113 | -> UnvalidatedOrder ​// input​ 114 | -> ValidatedOrder ​// output​ 115 | ``` 116 | 117 | - 이제 validateOrder 함수를 구현해보자. 118 | 119 | ```f# 120 | let​ validateOrder : ValidateOrder = 121 | fun​ checkProductCodeExists checkAddressExists unvalidatedOrder -> 122 | ​  123 | ​let​ orderId = 124 | unvalidatedOrder.OrderId 125 | |> OrderId.create 126 | 127 | ​let​ customerInfo = 128 | unvalidatedOrder.CustomerInfo 129 | |> toCustomerInfo ​// helper function​ 130 | 131 | ​let​ shippingAddress = 132 | unvalidatedOrder.ShippingAddress 133 | |> toAddress ​// helper function​ 134 | ​  135 | ​// and so on, for each property of the unvalidatedOrder​ 136 | ​  137 | ​// when all the fields are ready, use them to​ 138 | ​// create and return a new "ValidatedOrder" record​ 139 | { 140 | OrderId = orderId 141 | CustomerInfo = customerInfo 142 | ShippingAddress = shippingAddress 143 | BillingAddress = ... 144 | Lines = ... 145 | } 146 | ``` 147 | 148 | - 서브 컴포넌트인 toCustomerInfo 함수는 아래 처럼 만들 수 있다. 149 | 150 | ```f# 151 | let​ toCustomerInfo (customer:UnvalidatedCustomerInfo) : CustomerInfo = 152 | // create the various CustomerInfo properties​ 153 | ​// and throw exceptions if invalid​ 154 | let​ firstName = customer.FirstName |> String50.create 155 | ​let​ lastName = customer.LastName |> String50.create 156 | ​let​ emailAddress = customer.EmailAddress |> EmailAddress.create 157 | 158 | ​// create a PersonalName​ 159 | ​let​ name : PersonalName = { 160 | FirstName = firstName 161 | LastName = lastName 162 | } 163 | 164 | // create a CustomerInfo​ 165 | ​let​ customerInfo : CustomerInfo = { 166 | Name = name 167 | EmailAddress = emailAddress 168 | } 169 | // ... and return it​ 170 | customerInfo 171 | ``` 172 | - toAddress 함수는 checkAddressExists 디팬던시를 사용하기 때문에 조금 복잡하다 173 | 174 | ```f# 175 | let​ toAddress (checkAddressExists:CheckAddressExists) unvalidatedAddress = 176 | ​// call the remote service​ 177 | ​let​ checkedAddress = checkAddressExists unvalidatedAddress 178 | ​// extract the inner value using pattern matching​ 179 | ​let​ (CheckedAddress checkedAddress) = checkedAddress 180 | 181 | ​let​ addressLine1 = 182 | checkedAddress.AddressLine1 |> String50.create 183 | ​let​ addressLine2 = 184 | checkedAddress.AddressLine2 |> String50.createOption 185 | ​let​ addressLine3 = 186 | checkedAddress.AddressLine3 |> String50.createOption 187 | ​let​ addressLine4 = 188 | checkedAddress.AddressLine4 |> String50.createOption 189 | ​let​ city = 190 | checkedAddress.City |> String50.create 191 | ​let​ zipCode = 192 | checkedAddress.ZipCode |> ZipCode.create 193 | ​// create the address​ 194 | ​let​ address : Address = { 195 | AddressLine1 = addressLine1 196 | AddressLine2 = addressLine2 197 | AddressLine3 = addressLine3 198 | AddressLine4 = addressLine4 199 | City = city 200 | ZipCode = zipCode 201 | } 202 | // return the address​ 203 | address 204 | ``` 205 | 206 | - CheckAddressExists 디팬던시는 toAddress를 사용하는 validateOrder에 가지고 있기 때문에 207 | 전달해줄 수 있다. 208 | 209 | ```f# 210 | let​ validateOrder : ValidateOrder = 211 | ​fun​ checkProductCodeExists checkAddressExists unvalidatedOrder -> 212 | ​let​ orderId = ... 213 | ​let​ customerInfo = ... 214 | ​let​ shippingAddress = 215 | unvalidatedOrder.ShippingAddress 216 | |> toAddress checkAddressExists ​// new parameter​ 217 | ​  218 | ... 219 | ``` 220 | 221 | - 위에 보면 toAddress는 인자가 두개 인데 하나만 전달해 주는 이유는 나머지 인자는 파이프닝으로 전달되기 222 | 때문이다. 223 | 224 | ### Creating the Order Lines 225 | 226 | - 먼저 UnvalidatedOrderLine 하나를 ValidatedOrderLine으로 변환하는 함수를 만들어보자 227 | 228 | ```f# 229 | ​let​ toValidatedOrderLine checkProductCodeExists 230 | (unvalidatedOrderLine:UnvalidatedOrderLine) = 231 | let​ orderLineId = 232 | unvalidatedOrderLine.OrderLineId 233 | |> OrderLineId.create 234 | ​let​ productCode = 235 | unvalidatedOrderLine.ProductCode 236 | |> toProductCode checkProductCodeExists ​// helper function​ 237 | ​let​ quantity = 238 | unvalidatedOrderLine.Quantity 239 | |> toOrderQuantity productCode ​// helper function​ 240 | ​let​ validatedOrderLine = { 241 | OrderLineId = orderLineId 242 | ProductCode = productCode 243 | Quantity = quantity 244 | } 245 | validatedOrderLine 246 | ``` 247 | 248 | - 이제 리스트 형식의 OrderLine을 변환하는 코드를 만들어보자. 249 | 250 | ```f# 251 | ​let​ validateOrder : ValidateOrder = 252 | fun​ checkProductCodeExists checkAddressExists unvalidatedOrder -> 253 | 254 | ​let​ orderId = ... 255 | ​let​ customerInfo = ... 256 | ​let​ shippingAddress = ... 257 | ​  258 | ​let​ orderLines = 259 | unvalidatedOrder.Lines 260 | ​// convert each line using `toValidatedOrderLine`​ 261 | |> List.map (toValidatedOrderLine checkProductCodeExists) 262 | ... 263 | ``` 264 | 265 | - 다음은 toOrderQuantity 헬퍼 함수다. 266 | 267 | ```f# 268 | let​ toOrderQuantity productCode quantity = 269 | match​ productCode ​with​ 270 | | Widget _ -> 271 | quantity 272 | |> ​int​ ​// convert decimal to int​ 273 | |> UnitQuantity.create ​// to UnitQuantity​ 274 | |> OrderQuantity.Unit ​// lift to OrderQuantity type​ 275 | | Gizmo _ -> 276 | quantity 277 | |> KilogramQuantity.create ​// to KilogramQuantity​ 278 | |> OrderQuantity.Kilogram ​// lift to OrderQuantity type​’ 279 | ``` 280 | 281 | - productCode가 Widget 형식 또는 Gizmo 형식에 따라 분기가 되어 있다. 리턴 타입을 맞추기 위해서 282 | OrderQuantity 타입으로 마지막에 맞춰줬다. 283 | 284 | - 다음은 toProductCode 헬퍼 함수다. 285 | 286 | ```f# 287 | let​ toProductCode (checkProductCodeExists:CheckProductCodeExists) productCode = 288 | productCode 289 | |> ProductCode.create 290 | |> checkProductCodeExists 291 | ​// returns a bool :(​ 292 | ``` 293 | 294 | - 이 함수는 ProductCode를 리턴해야하는데 checkProductCodeExists로 끝나기 때문에 bool 값을 리턴하는 295 | 문제가 있다. 296 | 297 | ### Creating Function Adapters 298 | 299 | - 앞에 toProductCode에서 bool을 리턴하는 문제를 checkProductCodeExists 스펙을 변경하지 않고 300 | adapter 함수를 만들어 해결할 수 있다. 301 | 302 | ```f# 303 | let​ convertToPassthru checkProductCodeExists productCode = 304 | if​ checkProductCodeExists productCode ​then​ 305 | productCode 306 | ​else​ 307 | failwith ​"Invalid Product Code"​ 308 | ``` 309 | 310 | - 이 함수는 더 일반화 할 수 있다. 311 | 312 | ```f# 313 | ​let​ predicateToPassthru errorMsg f x = 314 | ​if​ f x ​then​ 315 | x 316 | ​else​ 317 | failwith errorMsg 318 | 319 | val​ predicateToPassthru : errorMsg:​string​ -> f:(​'​a -> ​bool​) -> x:​'​a -> ​'​a 320 | ``` 321 | 322 | - 이 함수를 적용해 toProductCode 함수를 고쳐보자 323 | 324 | ```f# 325 | ​let​ toProductCode (checkProductCodeExists:CheckProductCodeExists) productCode = 326 | // create a local ProductCode -> ProductCode function​ 327 | // suitable for using in a pipeline​ 328 | ​let​ checkProduct productCode = 329 | ​let​ errorMsg = sprintf ​"Invalid: %A"​ productCode 330 | predicateToPassthru errorMsg checkProductCodeExists productCode 331 | ​  332 | ​// assemble the pipeline​ 333 | productCode 334 | |> ProductCode.create 335 | |> checkProduct 336 | ``` 337 | 338 | ## Implementing the Rest of the Steps 339 | 340 | - validateOrder는 위에서 해봤고 이제 priceOrder를 만들어보자. 역시 원래 버전에서 effect를 341 | 제거한 버전으로 타입을 바꾸자. 342 | 343 | ```f# 344 | type PriceOrder = 345 | GetPricingFunction // dependency 346 | -> ValidatedOrder // input 347 | -> Result // output 348 | ``` 349 | 350 | ```f# 351 | type GetProductPrice = ProductCode -> Price 352 | type PriceOrder = 353 | GetPricingFunction // dependency 354 | -> ValidatedOrder // input 355 | -> PricedOrder // output 356 | ``` 357 | 358 | - 구현은 아래와 같다. 359 | 360 | ```f# 361 | ​let​ priceOrder : PriceOrder = 362 | ​  ​fun​ getProductPrice validatedOrder -> 363 | ​  ​let​ lines = 364 | ​  validatedOrder.Lines 365 | ​  |> List.map (toPricedOrderLine getProductPrice) 366 | ​  ​let​ amountToBill = 367 | ​  lines 368 | ​  ​// get each line price​ 369 | ​  |> List.map (​fun​ line -> line.LinePrice) 370 | ​  ​// add them together as a BillingAmount​ 371 | ​  |> BillingAmount.sumPrices 372 | ​let​ pricedOrder : PricedOrder = { 373 | ​  OrderId = validatedOrder.OrderId 374 | ​  CustomerInfo = validatedOrder.CustomerInfo 375 | ​  ShippingAddress = validatedOrder.ShippingAddress 376 | ​  BillingAddress = validatedOrder.BillingAddress 377 | ​  Lines = lines 378 | ​  AmountToBill = amountToBill 379 | ​  } 380 | ​  pricedOrder 381 | ``` 382 | 383 | - `BillingAmount.sumPrices` 는 다음과 같다. 384 | 385 | ```f# 386 | /// Sum a list of prices to make a billing amount​ 387 | /// Raise exception if total is out of bounds​ 388 | ​let​ sumPrices prices = 389 | ​let​ total = prices |> List.map Price.value |> List.sum 390 | create total 391 | ``` 392 | 393 | - `toPricedOrderLine` 함수는 다음과 같다. 394 | 395 | ```f# 396 | /// Transform a ValidatedOrderLine to a PricedOrderLine​ 397 | let​ toPricedOrderLine getProductPrice (line:ValidatedOrderLine) : PricedOrderLine = 398 | let​ qty = line.Quantity |> OrderQuantity.value 399 | let​ price = line.ProductCode |> getProductPrice 400 | let​ linePrice = price |> Price.multiply qty 401 | { 402 | OrderLineId = line.OrderLineId 403 | ProductCode = line.ProductCode 404 | Quantity = line.Quantity 405 | LinePrice = linePrice 406 | } 407 | ``` 408 | 409 | - pricing 단계는 끝났고 다음은 acknowledgment 단계를 구현해보자. 410 | 411 | ### Implementing the Acknowledgment Step 412 | 413 | - 아래는 effect를 제거한 acknowledgment 단계다. 414 | 415 | ```f# 416 | type​ HtmlString = HtmlString ​of​ ​string​ 417 | type​ CreateOrderAcknowledgmentLetter = 418 | PricedOrder -> HtmlString 419 | 420 | type​ OrderAcknowledgment = { 421 | EmailAddress : EmailAddress 422 | Letter : HtmlString 423 | } 424 | type​ SendResult = Sent | NotSent 425 | type​ SendOrderAcknowledgment = 426 | OrderAcknowledgment -> SendResult 427 | 428 | ​type​ AcknowledgeOrder = 429 | CreateOrderAcknowledgmentLetter ​// dependency​ 430 | -> SendOrderAcknowledgment ​// dependency​ 431 | -> PricedOrder ​// input​ 432 | -> OrderAcknowledgmentSent option ​// output​ 433 | ``` 434 | 435 | - 아래는 구현 부분이다. 436 | 437 | ```f# 438 | let​ acknowledgeOrder : AcknowledgeOrder = 439 | ​fun​ createAcknowledgmentLetter sendAcknowledgment pricedOrder -> 440 | ​let​ letter = createAcknowledgmentLetter pricedOrder 441 | ​let​ acknowledgment = { 442 | EmailAddress = pricedOrder.CustomerInfo.EmailAddress 443 | Letter = letter 444 | } 445 | 446 | ​// if the acknowledgment was successfully sent,​ 447 | ​// return the corresponding event, else return None​ 448 | ​match​ sendAcknowledgment acknowledgment ​with​ 449 | | Sent -> 450 | ​let​ ​event​ = { 451 | OrderId = pricedOrder.OrderId 452 | EmailAddress = pricedOrder.CustomerInfo.EmailAddress 453 | } 454 | Some ​event​ 455 | | NotSent -> 456 | None 457 | ``` 458 | 459 | - 특별한 부분은 없다. 460 | 461 | ## Creating the Event 462 | 463 | ```f# 464 | /// Event to send to shipping context​ 465 | type​ OrderPlaced = PricedOrder 466 | 467 | ​/// Event to send to billing context​ 468 | /// Will only be created if the AmountToBill is not zero​ 469 | type​ BillableOrderPlaced = { 470 | OrderId : OrderId 471 | BillingAddress: Address 472 | AmountToBill : BillingAmount 473 | } 474 | ​ 475 | type​ PlaceOrderEvent = 476 | | OrderPlaced ​of​ OrderPlaced 477 | | BillableOrderPlaced ​of​ BillableOrderPlaced 478 | | AcknowledgmentSent ​of​ OrderAcknowledgmentSent 479 | 480 | ​type​ CreateEvents = 481 | PricedOrder ​// input​ 482 | -> OrderAcknowledgmentSent option ​// input (event from previous step)​ 483 | -> PlaceOrderEvent ​list​ ​// output​ 484 | ``` 485 | 486 | ```f# 487 | // PricedOrder -> BillableOrderPlaced option​ 488 | let​ createBillingEvent (placedOrder:PricedOrder) : BillableOrderPlaced option = 489 | ​let​ billingAmount = placedOrder.AmountToBill |> BillingAmount.value 490 | if​ billingAmount > 0M ​then​ 491 | ​let​ order = { 492 | OrderId = placedOrder.OrderId 493 | BillingAddress = placedOrder.BillingAddress 494 | AmountToBill = placedOrder.AmountToBill 495 | } 496 | Some order 497 | ​else​ 498 | None 499 | ``` 500 | 501 | ```f# 502 | ​let​ createEvents : CreateEvents = 503 | ​fun​ pricedOrder acknowledgmentEventOpt -> 504 | ​let​ event1 = 505 | pricedOrder 506 | ​// convert to common choice type​ 507 | |> PlaceOrderEvent.OrderPlaced 508 | ​let​ event2Opt = 509 | acknowledgmentEventOpt 510 | ​// convert to common choice type​ 511 | |> Option.map PlaceOrderEvent.AcknowledgmentSent 512 | ​let​ event3Opt = 513 | pricedOrder 514 | |> createBillingEvent 515 | ​// convert to common choice type​ 516 | |> Option.map PlaceOrderEvent.BillableOrderPlaced 517 | 518 | ​// return all the events how?​ 519 | ​  ... 520 | ``` 521 | 522 | - 일부 이벤트는 결과가 Optional 타입이기 때문아 아래 helper 함수로 리턴 타입을 맞춰준다. 523 | 524 | ```f# 525 | /// convert an Option into a List​ 526 | let​ listOfOption opt = 527 | match​ opt ​with​ 528 | | Some x -> [x] 529 | | None -> [] 530 | ``` 531 | 532 | ```f# 533 | let​ createEvents : CreateEvents = 534 | ​fun​ pricedOrder acknowledgmentEventOpt -> 535 | ​let​ events1 = 536 | pricedOrder 537 | ​// convert to common choice type​ 538 | |> PlaceOrderEvent.OrderPlaced 539 | ​// convert to list​ 540 | |> List.singleton 541 | ​let​ events2 = 542 | acknowledgmentEventOpt 543 | ​// convert to common choice type​ 544 | |> Option.map PlaceOrderEvent.AcknowledgmentSent 545 | ​// convert to list​ 546 | |> listOfOption 547 | ​let​ events3 = 548 | pricedOrder 549 | |> createBillingEvent 550 | ​// convert to common choice type​ 551 | |> Option.map PlaceOrderEvent.BillableOrderPlaced 552 | ​// convert to list​ 553 | |> listOfOption 554 | 555 | ​// return all the events​ 556 | [ 557 | ​yield​! events1 558 | ​yield​! events2 559 | ​yield​! events3 560 | ] 561 | ``` 562 | 563 | ## Composing the Pipeline Steps Together 564 | 565 | - 워크플로우는 아래 처럼 생겼는데 문제가 있다. 566 | 567 | ```f# 568 | let​ placeOrder : PlaceOrderWorkflow = 569 | ​fun​ unvalidatedOrder -> 570 | unvalidatedOrder 571 | |> validateOrder 572 | |> priceOrder 573 | |> acknowledgeOrder 574 | |> createEvents 575 | ``` 576 | 577 | - validateOrder 함수는 인자로 디팬던시 함수 CheckProductCodeExists, CheckAddressExists와 578 | 입력값 UnvalidatedOrder을 받아야하지만 이전 플로우에서는 unvalidatedOrder만 준다. 579 | - 마찬가지로 priceOrder 함수도 인자로 디팬던시 함수 getProductPrice와 입력값 validatedOrder가 580 | 필요하지만 이전 플로우인 validateOrder 함수에서는 validatedOrder만 리턴하기 때문에 입력값이 달라 581 | Composing할 수 없다. 582 | - 이 문제를 푸는 일반적인 방법은 모나드를 사용하는 것이지만 여기서는 partial로 간단하게 해결해보자. 583 | 584 | - 디팬던시를 이미 포함하고 있는 validateOrder 함수를 아래 처럼 만들어보자. 585 | 586 | ```f# 587 | let​ validateOrderWithDependenciesBakedIn = 588 | validateOrder checkProductCodeExists checkAddressExists 589 | 590 | // new function signature after partial application:​ 591 | // UnvalidatedOrder -> ValidatedOrder​ 592 | ``` 593 | 594 | - F#은 다행이 쉐도윙이라고 부르는 방법으로 `validateOrder` 함수이름을 그대로 쓸 수 있다. 595 | 596 | ```f# 597 | let​ validateOrder = 598 | validateOrder checkProductCodeExists checkAddressExists 599 | ``` 600 | 601 | - 다른 언어들은 이런 식으로 이름을 바꿔써도 좋다. 602 | 603 | ```f# 604 | ​let​ validateOrder' = 605 | validateOrder checkProductCodeExists checkAddressExists 606 | ``` 607 | 608 | - 이렇게 하면 앞에 두 디팬던시 파라미터가 부분적용되어 입력값이 UnvalidatedOrder 만 받을 수 있는 609 | 함수가 생겨 조합이 가능하다. 610 | - 그래서 디팬던시를 부분적용한 PlaceOrderWorkflow는 아래 처럼 만들 수 있다. 611 | 612 | ```f# 613 | ​let​ placeOrder : PlaceOrderWorkflow = 614 | ​// set up local versions of the pipeline stages​ 615 | ​// using partial application to bake in the dependencies​ 616 | let​ validateOrder = 617 | validateOrder checkProductCodeExists checkAddressExists 618 | ​let​ priceOrder = 619 | priceOrder getProductPrice 620 | ​let​ acknowledgeOrder = 621 | acknowledgeOrder createAcknowledgmentLetter sendAcknowledgment 622 | 623 | // return the workflow function​ 624 | ​fun​ unvalidatedOrder -> 625 | ​// compose the pipeline from the new one-parameter functions​ 626 | unvalidatedOrder 627 | |> validateOrder 628 | |> priceOrder 629 | |> acknowledgeOrder 630 | |> createEvents 631 | ``` 632 | 633 | - 이렇게해도 함수 조합이 힘든 경우가 있는데 acknowledgeOrder 함수의 결과가 그냥 이벤트이고 634 | pricedOrder가 아니기 때문에 createEvents에 전달 할 수 없다. 635 | - 이런경우에는 절차형 프로그래밍 처람 각 단계의 결과를 변수에 할하는 방법으로 해결 할 수 있다. 636 | 637 | ```f# 638 | let​ placeOrder : PlaceOrderWorkflow = 639 | ​// return the workflow function​ 640 | fun​ unvalidatedOrder -> 641 | ​let​ validatedOrder = 642 | unvalidatedOrder 643 | |> validateOrder checkProductCodeExists checkAddressExists 644 | ​let​ pricedOrder = 645 | validatedOrder 646 | |> priceOrder getProductPrice 647 | let​ acknowledgmentOption = 648 | pricedOrder 649 | |> acknowledgeOrder createAcknowledgmentLetter sendAcknowledgment 650 | let​ events = 651 | createEvents pricedOrder acknowledgmentOption 652 | events 653 | ``` 654 | 655 | - 다음은 디팬던시를 전역으로 선언하지 않고 inject 하는 방법을 살펴보자. 656 | 657 | ## Injecting Dependencies 658 | --------------------------------------------------------------------------------