https://pragprog.com/titles/swdddf/domain-modeling-made-functional/
도메인 타입(simple type, compound type)과 워크플로(함수)를 모델링 하였으므로,
데이터 타입의 persistent ID 여부로 VO와 Entity를 분류, F# 문법으로 구현하는 법을 알아봅니다.
Value Object
F#에서는 이렇게 사용합니다. (simple type - wrapper type)
let widgetCode1 = WidgetCode "W1234"
let widgetCode2 = WidgetCode "W1234"
printfn "%b" (widgetCode1 = widgetCode2) // prints
let name1 = {FirstName="Alex"; LastName="Adams"}
let name2 = {FirstName="Alex"; LastName="Adams"}
printfn "%b" (name1 = name2) // prints "true"
"address" 유형도 값 개체입니다. 두 값의 거리 주소, 도시 및 우편 번호가 같으면 동일한 주소입니다.
let address1 = {StreetAddress="123 Main St"; City="New York"; Zip="90001"}
let address2 = {StreetAddress="123 Main St"; City="New York"; Zip="90001"}
printfn "%b" (address1 = address2) // prints "true"
Value Object의 동등 비교
F# 대수 타입 시스템을 사용하여 도메인을 모델링할 때 생성하는 타입은 기본적으로 이러한 종류의 필드 기반 동등성 테스트를 구현합니다. 우리는 특별한 동등 비교 코드를 직접 작성할 필요가 없습니다.
정확히 말하면 두 개의 레코드 타입 값(동일한 형식)은 모든 필드가 같으면 F#에서 같고
초이스 타입은 동일한 케이스가 있고 해당 케이스와 연결된 데이터(wrapping 하는 데이터)도 같으면 동일합니다.
이것을 structural equality라고 합니다.
Entity
우리는 종종 실제 세계에서 컴포넌트가 변경되더라도 고유한 ID를 갖는 것을 모델링합니다.
예를 들어, 이름이나 주소를 변경해도 나는 여전히 나입니다.
"VO"와 "Entitiy"의 구분은 컨텍스트에 따라 다릅니다.
예를 들어 휴대폰의 수명 주기를 생각해 봅시다.
제조 과정에서 각 전화기에는 고유한 일련 번호(고유 ID)가 부여되므로 해당 컨텍스트에서 전화기는 엔터티로 모델링됩니다.
그러나 판매 시 일련 번호는 사양과 관련이 없기에, 동일한 사양의 모든 전화기는 상호 교환 가능하며 VO로 모델링할 수 있습니다.
특정 전화가 특정 고객에게 판매되면 ID가 다시 해당 단말과 관계가 있으며, Entitiy로 모델링되어야 합니다. (ex - AS를 위해)
고객은 화면이나 배터리를 교체한 후에도 동일한 전화로 생각합니다.
엔터티 식별자
type ContactId = ContactId of int
type Contact = {
ContactId : ContactId
PhoneNumber : ...
EmailAddress: ...
}
Adding Identifiers to Data Definitions
도메인 객체를 엔터티로 식별한 경우 해당 정의에 식별자를 어떻게 추가합니까? // Info for the unpaid case (without id)
type UnpaidInvoiceInfo = ...
// Info for the paid case (without id)
type PaidInvoiceInfo = ...
// Combined information (without id)
type InvoiceInfo =
| Unpaid of UnpaidInvoiceInfo
| Paid of PaidInvoiceInfo
// Id for invoice
type InvoiceId = ...
// Top level invoice type
type Invoice = {
InvoiceId : InvoiceId // "outside" the two child cases
InvoiceInfo : InvoiceInfo
}
실제로는 "내부" 접근 방식을 사용하여 ID를 저장하는 것이 더 일반적입니다.
여기서 각 케이스에는 식별자 사본이 있습니다.
이 예에 적용하면 각각의 경우에 대해 하나씩(UnpaidInvoice 및 PaidInvoice) 두 가지 개별 타입을 생성합니다.
type UnpaidInvoice = {
InvoiceId : InvoiceId // id stored "inside"
// and other info for the unpaid case
}
type PaidInvoice = {
InvoiceId : InvoiceId // id stored "inside"
// and other info for the paid case
}
// top level invoice type
type Invoice =
| Unpaid of UnpaidInvoice
| Paid of PaidInvoice
let invoice = Paid {InvoiceId = ...}
match invoice with
| Unpaid unpaidInvoice ->
printfn "The unpaid invoiceId is %A" unpaidInvoice.InvoiceId
| Paid paidInvoice ->
printfn "The paid invoiceId is %A" paidInvoice.InvoiceId
Implementing Equality for Entities
- Equals 메서드 재정의
- GetHashCode 메서드 재정의
- CustomEquality 및 NoComparison 속성을 유형에 추가하여 컴파일러에 기본 동작을 변경하고 싶다고 알립니다.
[<CustomEquality; NoComparison>]
type Contact = {
ContactId : ContactId
PhoneNumber : PhoneNumber
EmailAddress: EmailAddress
}
with
override this.Equals(obj) =
match obj with
| :? Contact as c -> this.ContactId = c.ContactId
| _ -> false
override this.GetHashCode() =
hash this.ContactId
이것은F#의 객체 지향 구문입니다.
여기서는 동등성 테스트 재정의를 보여주기 위해서만 사용하고 있지만
객체 지향 F#은 앞으로 사용하지 않습니다.
let contactId = ContactId 1
let contact1 = {
ContactId = contactId
PhoneNumber = PhoneNumber "123-456-7890"
EmailAddress = EmailAddress "bob@example.com"
}
// same contact, different email address
let contact2 = {
ContactId = contactId
PhoneNumber = PhoneNumber "123-456-7890"
EmailAddress = EmailAddress "robert@example.com"
}
// true even though the email addresses are different
printfn "%b" (contact1 = contact2)
[<NoEquality; NoComparison>]
type Contact = {
ContactId : ContactId
PhoneNumber : PhoneNumber
EmailAddress: EmailAddress
}
// compiler error!
printfn "%b" (contact1 = contact2)
// ^ the Contact type does not
// support equality
printfn "%b" (contact1.ContactId = contact2.ContactId) // true
[<NoEquality;NoComparison>]
type OrderLine = {
OrderId : OrderId
ProductId : ProductId
Qty : int
}
with
member this.Key =
(this.OrderId,this.ProductId)
printfn "%b" (line1.Key = line2.Key)
요약 :Entity의 경우 Equal 오버라이딩 하지 말고, 동등 비교를 키 비교를 통해 명시하자.
Immutability and Identity
F#과 같은 함수형 프로그래밍 언어의 값은 기본적으로 불변입니다.
이것이 우리 디자인에 어떤 영향을 줍니까?
다음은 F#에서 엔터티를 업데이트하는 방법의 예입니다. 먼저 초기 값으로 시작합니다.
let initialPerson = {PersonId=PersonId 42; Name="Joseph"}
일부 필드만 변경하면서 레코드의 복사본을 만들기 위해 F#은 다음과 같이 with 키워드를 사용합니다.
let updatedPerson = {initialPerson with Name="Joe"}
updatedPerson는 initialPerson과 Name은 다르지만 PersonId는 동일합니다.
type UpdateName = Person -> Name -> unit
이 함수에는 출력이 없습니다. 이는 아무 것도 변경되지 않았음을 의미합니다 (또는 Person가 side-effect에 의해 mutated 되었음을 의미합니다).
대신, 우리 함수는 다음과 같이 출력으로 Person 타입이 있는 서명을 가져야 합니다.
type UpdateName = Person -> Name -> Person
이것은 Person과 Name이 주어지면 원래 Person의 변형이 반환된다는 것을 분명히 나타냅니다.
요약 : vo와 entitiy 전부 불변입니다.
'BackEnd' 카테고리의 다른 글
도메인의 무결성(Integrity)과 일관성(Consistency) 관리하기 - Make illegal states unrepresentable (0) | 2022.03.19 |
---|---|
타입으로 도메인 모델링하기 with F# - Aggregate (집합체) (0) | 2022.03.18 |
타입으로 도메인 모델링하기 with F# - 단순, 복합 타입과 함수 (0) | 2022.03.18 |
타입으로 코드 문서화하기 With F# - 타입을 조합하여 도메인 모델링 (0) | 2022.03.18 |
타입으로 코드 문서화하기 With F# - 타입 기초 (0) | 2022.03.18 |