Domain Modeling Made Functional
Use domain-driven design to effectively model your business domain, and implement that model with F#.
pragprog.com
type CheckNumber = CheckNumber of int
type CardNumber = CardNumber of string
type CardType =
Visa | Mastercard // 'OR' type
type CreditCardInfo = { // 'AND' type (record)
CardType : CardType
CardNumber : CardNumber
}
type PaymentMethod =
| Cash
| Check of CheckNumber
| Card of CreditCardInfo
type PaymentAmount = PaymentAmount of decimal
type Currency = EUR | USD
type Payment = {
Amount : PaymentAmount
Currency: Currency
Method: PaymentMethod
}
type PayInvoice =
UnpaidInvoice -> Payment -> PaidInvoice
이는 다음을 의미합니다. UnpaidInvoice와 Payment가 차례로 주어지면 PaidInvoice를 생성할 수 있습니다.
type ConvertPaymentCurrency =
Payment -> Currency -> Payment
여기서 첫 번째 지불은 입력이고 두 번째 매개변수(통화)는 변환할 통화이며 두 번째 지불(출력)은 변환 후의 결과입니다.
Modeling Optional Values, Errors, and Collections
- Optional or missing values - 널러블
- Errors
- Functions that return no value - void
- Collections - 컬렉션 (리스트)
주 : Wrapper타입은 제네릭 타입과 모나드와 잘 어울립니다!
선택적 값 모델링 - 널러블
지금까지 사용한 타입(레코드 및 초이스 타입)은 F#에서 null이 허용되지 않습니다. 즉, 도메인 모델에서 타입을 참조할 때마다 값이 반드시 있어야 합니다.
그렇다면 누락된 데이터나 선택적 데이터를 어떻게 모델링할 수 있습니까?
답은 누락된 데이터가 무엇을 의미하는지 생각하는 것입니다.
존재하거나 부재합니다. 거기에 무언가가 있거나 거기에 아무것도 없습니다.
다음과 같이 정의된 Option이라는 초이스 타입으로 이를 모델링할 수 있습니다.
'a는 타입 파라미터, 제네릭 타입
즉, Option 유형을 사용하여 다른 유형을 래핑할 수 있습니다. C# 또는 Java는 Option<T> 입니다.
type Option<'a> =
| Some of 'a
| None
예를 들어 PersonalName 유형이 있고 이름과 성은 필수지만 중간 이니셜은 선택 사항인 경우 다음과 같이 모델링할 수 있습니다.
type PersonalName = {
FirstName : string
MiddleInitial: Option<string> // optional
LastName : string
}
type PersonalName = {
FirstName : string
MiddleInitial: string option
LastName : string
}
에러 모델링 - 실패 가능성의 모델링
type Result<'Success,'Failure> =
| Ok of 'Success
| Error of 'Failure
F# 4.1 이상(또는 Visual Studio 2017)을 사용하는 경우 표준 F# 라이브러리의 일부이기 때문에 결과 유형을 직접 정의할 필요가 없습니다.
type PayInvoice =
UnpaidInvoice -> Payment -> Result<PaidInvoice,PaymentError>
type PaymentError =
| CardTypeNotRecognized
| PaymentRejected
| PaymentProviderOffline
값이 전혀 없음을 모델링하기 - void
type SaveCustomer = Customer -> unit
주 : 뮤테이션은 결과값을 리턴하지 않는게 CQRS의 개념입니다.
또는 난수를 생성하는 함수와 같이 입력이 없는 함수가 유용한 것을 반환한다고 가정해 보겠습니다.
F#에서는 다음과 같이 단위와 함께 "입력 없음"도 표시합니다.
type NextRandom = unit -> int
서명에서 Unit type(void)를 보면 사이드 이펙트가 있다는 강력한 표시입니다.
어딘가에서 상태가 바뀌고 있지만 그것은 당신에게 숨겨져 있습니다.
일반적으로 함수형 프로그래머는 사이드 이펙트를 피하거나 최소한의 코드 영역으로 범위를 제한하려 합니다.
리스트와 컬렉션 모델링
F#은 표준 라이브러리에서 다양한 컬렉션 형식을 지원합니다.
- list는 고정 크기의 변경할 수 없는 컬렉션입니다(링크드 리스트로 구현됨).
- array는 개별 요소를 인덱스로 가져와 할당할 수 있는 고정 크기의 변경 가능한 컬렉션입니다.
- ResizeArray는 가변 크기 배열입니다. 즉, 배열에서 항목을 추가하거나 제거할 수 있습니다. C# List<T> 형식에 대한 F# 별칭입니다. seq는 요청 시 각 요소가 반환되는 지연 컬렉션입니다. C# IEnumerable<T> 형식에 대한 F# 별칭입니다.
- Map(Dictionary와 유사) 및 Set에 대한 내장 유형도 있지만 도메인 모델에서 직접 사용되는 경우는 거의 없습니다.
type Order = {
OrderId : OrderId
Lines : OrderLine list // a collection . List<OrderLine>
}
let aList = [1; 2; 3]
let aNewList = 0 :: aList // new list is [0;1;2;3]
let printList1 aList =
// matching against list literals
match aList with
| [] ->
printfn "list is empty"
| [x] ->
printfn "list has one element: %A" x
| [x;y] -> // match using list literal
printfn "list has two elements: %A and %A" x y
| longerList -> // match anything else
printfn "list has more than two elements"
(주 : 패턴 매치는 데이터의 생김새로 if, switch문을 대체하며, 데이터가 해당 모양일 경우 값을 끌어쓸 수 있습니다.)
cons operator를 이용한 매칭도 가능합니다.
let printList2 aList =
// matching against "cons"
match aList with
| [] ->
printfn "list is empty"
| first::rest ->
printfn "list is non-empty with the first element being: %A" first
마무리
선택사항 : F#의 타입과 파일, 프로젝트 구조화
Common.Types.fs
Common.Functions.fs
OrderTaking.Types.fs
OrderTaking.Functions.fs
Shipping.Types.fs
Shipping.Functions.fs
module Payments =
// simple types at the top of the file
type CheckNumber = CheckNumber of int
// domain types in the middle of the file
type PaymentMethod =
| Cash
| Check of CheckNumber // defined above
| Card of ...
// top-level types at the bottom of the file
type Payment = {
Amount: ...
Currency: ...
Method: PaymentMethod // defined above
}
module rec Payments =
type Payment = {
Amount: ...
Currency: ...
Method: PaymentMethod // defined BELOW
}
type PaymentMethod =
| Cash
| Check of CheckNumber // defined BELOW
| Card of ...
type CheckNumber = CheckNumber of int
type Payment = {
Amount: ...
Currency: ...
Method: PaymentMethod // defined BELOW
}
and PaymentMethod =
| Cash
| Check of CheckNumber // defined BELOW
| Card of ...
and CheckNumber = CheckNumber of int
'BackEnd' 카테고리의 다른 글
타입으로 도메인 모델링하기 with F# - Value Object, Entitiy (0) | 2022.03.18 |
---|---|
타입으로 도메인 모델링하기 with F# - 단순, 복합 타입과 함수 (0) | 2022.03.18 |
타입으로 코드 문서화하기 With F# - 타입 기초 (0) | 2022.03.18 |
도메인 모델을 함수형 아키텍처로 - 컨텍스트 간 통신 2 (0) | 2022.03.17 |
도메인 모델을 함수형 아키텍처로 - 컨텍스트 간 통신 1 (0) | 2022.03.17 |