이전 두 장에서 타입을 사용하여 일반적인 방식으로 도메인 모델링을 수행하는 방법을 보았습니다.
이 장에서는 거기서 배운 내용을 주문 처리(order-placing ) 워크플로에 적용할 것입니다.
그 과정에서 모든 워크플로를 모델링하는 데 유용한 여러 기술을 살펴보겠습니다.
목표는 항상 그렇듯이 도메인 전문가가 읽을 수 있는 것을 만드는 것입니다.
workflow "Place Order" =
input: UnvalidatedOrder
output (on success):
OrderAcknowledgmentSent
AND OrderPlaced (to send to shipping)
AND BillableOrderPlaced (to send to billing)
output (on error):
ValidationError
// step 1
do ValidateOrder
If order is invalid then:
return with ValidationError
// step 2
do PriceOrder
// step 3
do AcknowledgeOrder
// step 4
create and return the events
https://itchallenger.tistory.com/411?category=1086398
워크플로 입력
type UnvalidatedOrder = {
OrderId : string
CustomerInfo : UnvalidatedCustomerInfo
ShippingAddress : UnvalidatedAddress
...
}
입력으로서의 Command
이 책의 시작 부분에서 워크플로가 이를 시작하는 커맨드와 연관이 있음을 보았습니다.
어떤 의미에서 워크플로에 대한 실제 입력은 실제로는 주문 양식(form)이 아니라 커맨드입니다.
주문 배치 워크플로의 경우 이 명령을 PlaceOrder라고 부르겠습니다.
커맨드에는 워크플로가 요청을 처리하는 데 필요한 모든 것이 포함되어야 합니다.
이 경우 위의 UnvalidatedOrder입니다.
또한 로깅 및 감사(logging and auditing)를 위해 커맨드, 타임스탬프 및 기타 메타데이터를 만든 사람을 추적하고 싶을 수도 있으므로 커맨드 유형은 다음과 같이 될 수 있습니다.
type PlaceOrder = {
OrderForm : UnvalidatedOrder
Timestamp: DateTime
UserId: string
// etc
}
제네릭을 사용하여 공통 구조 공유
type Command<'data> = {
Data : 'data
Timestamp: DateTime
UserId: string
// etc
}
그런 다음 데이터 슬롯에 들어갈 타입을 지정하여 워크플로별 커맨드을 만들 수 있습니다.type PlaceOrder = Command<UnvalidatedOrder>
여러 커맨드를 하나의 타입으로 결합
(하나의 메세지 큐에서 여러 케이스의 커맨드가 들어옴)
type OrderTakingCommand =
| Place of PlaceOrder
| Change of ChangeOrder
| Cancel of CancelOrder
Order Domain을 상태 집합으로 모델링
이러한 상태를 어떻게 모델링해야 합니까? 나이브한 접근 방식은 다음과 같이 플래그를 통해 모든 종류의 상태를 캡처하는 단일 레코드 유형을 만드는 것입니다.
type Order = {
OrderId : OrderId
...
IsValidated : bool // set when validated
IsPriced : bool // set when priced
AmountToBill : decimal option // also set when priced
}
- (암시적 스테이트) 다양한 플래그를 통해 상태를 표현하지만, 상태는 암시적이며 처리하려면 많은 조건부 코드가 필요합니다.
- (옵셔널 문제) 일부 상태에는 다른 상태에 필요하지 않은 데이터가 있으며, 모두 하나의 레코드에 넣으면 설계가 복잡해집니다. 예를 들어 AmountToBill은 "가격이 책정된" 상태에서만 필요하지만 다른 상태에는 필요 없기 때문에 필드를 선택 사항으로 만들어야 합니다.
- (필드와 플래그의 상관관계) 어떤 필드가 어떤 플래그와 함께 사용되는지 명확하지 않습니다. AmountToBill은 IsPriced가 설정되어 있을 때 값을 설정해야 하지만 디자인은 이를 강제하지 않으며 데이터 일관성을 유지하기 위해 주석에 의존해야 합니다.
주석에 의존하는 설계가 좋은 설계일까요?
data ValidatedOrder =
ValidatedCustomerInfo
AND ValidatedShippingAddress
AND ValidatedBillingAddress
AND list of ValidatedOrderLine
type ValidatedOrder = {
OrderId : OrderId
CustomerInfo : CustomerInfo
ShippingAddress : Address
BillingAddress : Address
OrderLines : ValidatedOrderLine list
}
type PricedOrder = {
OrderId : ...
CustomerInfo : CustomerInfo
ShippingAddress : Address
BillingAddress : Address
// different from ValidatedOrder
OrderLines : PricedOrderLine list
AmountToBill : BillingAmount
}
type Order =
| Unvalidated of UnvalidatedOrder
| Validated of ValidatedOrder
| Priced of PricedOrder
// etc
요구 사항 변경에 따라 새 상태 타입 추가
State Machine
몇 가지 예:
장바구니에는 "비어 있음", "활성(상품 있음)" 및 "지불됨(추가 지불 및 상품 추가 불가능)" 상태가 있을 수 있습니다.
패키지 배송에는 "배송되지 않음", "배송 예정" 및 "배송됨"의 세 가지 상태가 있을 수 있습니다.
여기서 패키지를 배송 트럭에 올려서 "배송되지 않음" 상태에서 "배송 준비 중" 상태로 전환할 수 있습니다.
상태 머신을 사용하는 이유
다음과 같은 경우 상태 머신을 사용하면 여러 가지 이점이 있습니다.
- 각 상태는 다른 허용되는 동작을 가질 수 있습니다. 예를 들어 장바구니 예에서는 상품이 있는 장바구니만 지불할 수 있으며 이미 지불된 장바구니에는 상품을 추가할 수 없습니다. 이전 장에서 확인되지 않은/확인된 이메일 디자인에 대해 논의할 때 확인된 이메일 주소로만 비밀번호 재설정을 보낼 수 있다는 비즈니스 규칙을 보았습니다. 각 상태에 대해 고유한 타입을 사용하여 비즈니스 규칙이 준수되었는지 확인하기 위해 컴파일러를 사용하여 해당 요구 사항을 함수 시그니처에서 직접 인코딩할 수 있습니다.
- 모든 상태는 명시적으로 문서화되어 있습니다. 암묵적이지만 문서화되지 않은 중요한 상태를 갖는 것은 쉽습니다. 장바구니 예에서 "빈 장바구니"는 "활성(상품이 있는) 장바구니"와 동작이 다르지만 코드에서 명시적으로 문서화되는 경우는 드뭅니다.
- 발생할 수 있는 모든 가능성에 대해 생각하게 하는 디자인 도구입니다. 설계 오류의 일반적인 원인은 특정 엣지 케이스가 처리되지 않는다는 것입니다. 상태 머신은 모든 경우를 고려하도록 합니다. 예를 들어:
- 이미 확인된 이메일을 확인하려고 하면 어떻게 됩니까?
- 빈 장바구니에서 항목을 제거하려고 하면 어떻게 됩니까?
- 이미 "Delivered" 상태에 있는 패키지를 배송하려고 하면 어떻게 됩니까? 등등.
상태 측면에서 디자인에 대해 생각하면 이러한 질문이 표면화되고 도메인 논리가 명확해질 수 있습니다.
F#에서 간단한 상태 머신을 구현하는 방법
언어 파서 등에 사용되는 복잡한 상태 기계는 규칙 집합이나 문법에서 생성되며 구현하기가 상당히 복잡합니다.
그러나 위에서 설명한 간단한 비즈니스 지향 상태 머신은 특별한 도구나 라이브러리 없이 수동으로 코딩할 수 있습니다.
그렇다면 이러한 간단한 상태 머신을 어떻게 구현해야 할까요?
우리가 원하지 않는 한 가지는 플래그, 열거형 또는 다른 종류의 조건부 논리를 사용하여 모든 상태를 공통 레코드로 결합하여 구별하는 것입니다.
훨씬 더 나은 접근 방식은 각 상태에 해당 상태(있는 경우)와 관련된 데이터를 저장하는 고유한 타입을 만드는 것입니다.
그런 다음 각 상태에 대한 케이스가 있는 초이스 타입으로 전체 상태 세트를 나타낼 수 있습니다.
다음은 장바구니 상태 머신을 사용하는 예입니다.
type Item = ...
type ActiveCartData = { UnpaidItems: Item list }
type PaidCartData = { PaidItems: Item list; Payment: float }
type ShoppingCart =
| EmptyCart // no data
| ActiveCart of ActiveCartData
| PaidCart of PaidCartData
// 파라미터 : 쇼핑카트 추가할_아이템
let addItem cart item =
match cart with
| EmptyCart ->
// create a new active cart with one item
ActiveCart {UnpaidItems=[item]}
| ActiveCart {UnpaidItems=existingItems} ->
// create a new ActiveCart with the item added
ActiveCart {UnpaidItems = item :: existingItems}
| PaidCart _ ->
// ignore
cart
let makePayment cart payment =
match cart with
| EmptyCart ->
// ignore
cart
| ActiveCart {UnpaidItems=existingItems} ->
// create a new PaidCart with the payment
PaidCart {PaidItems = existingItems; Payment=payment}
| PaidCart _ ->
// ignore
cart
'BackEnd' 카테고리의 다른 글
파이프라인으로 워크플로 모델링하기 - 워크플로 합성 및 나머지 (0) | 2022.03.19 |
---|---|
파이프라인으로 워크플로 모델링하기 - 함수 타입으로 워크플로 모델링, 이펙트 모델링 (0) | 2022.03.19 |
도메인의 무결성(Integrity)과 일관성(Consistency) 관리하기 - Make illegal states unrepresentable (0) | 2022.03.19 |
타입으로 도메인 모델링하기 with F# - Aggregate (집합체) (0) | 2022.03.18 |
타입으로 도메인 모델링하기 with F# - Value Object, Entitiy (0) | 2022.03.18 |