let placeOrder : PlaceOrderWorkflow =
fun unvalidatedOrder ->
unvalidatedOrder
|> validateOrder
|> priceOrder
|> acknowledgeOrder
|> createEvents
let validateOrderWithDependenciesBakedIn =
validateOrder checkProductCodeExists checkAddressExists
// new function signature after partial application:
// UnvalidatedOrder -> ValidatedOrder
let validateOrder =
validateOrder checkProductCodeExists checkAddressExists
let placeOrder : PlaceOrderWorkflow =
// set up local versions of the pipeline stages
// using partial application to bake in the dependencies
let validateOrder =
validateOrder checkProductCodeExists checkAddressExists
let priceOrder =
priceOrder getProductPrice
let acknowledgeOrder =
acknowledgeOrder createAcknowledgmentLetter sendAcknowledgment
// return the workflow function
fun unvalidatedOrder ->
// compose the pipeline from the new one-parameter functions
unvalidatedOrder
|> validateOrder
|> priceOrder
|> acknowledgeOrder
|> createEvents
let placeOrder : PlaceOrderWorkflow =
// return the workflow function
fun unvalidatedOrder ->
let validatedOrder =
unvalidatedOrder
|> validateOrder checkProductCodeExists checkAddressExists
let pricedOrder =
validatedOrder
|> priceOrder getProductPrice
let acknowledgmentOption =
pricedOrder
|> acknowledgeOrder createAcknowledgmentLetter sendAcknowledgment
let events =
createEvents pricedOrder acknowledgmentOption
events
파이프라인만큼 우아하지는 않지만 여전히 이해하고 유지 관리하기 쉽습니다.의존성 주입
서비스를 의미하는, 함수를 파라미터로 받는 toValidProductCode와 같은 여러 하위 수준 도우미 함수가 있습니다.
이것들은 디자인 면에서 매우 저수준에 존재합니다.
따라서 최상위 수준에서 이를 필요로 하는 함수까지 종속성을 어떻게 전달할 수 있을까요?
객체 지향 프로그래밍을 하고 있다면 의존성 주입과 IoC 컨테이너를 사용할 것입니다.
하지만 우리는 종속성을 암시적으로 전달하고 싶지 않습니다.
우리는 항상 종속성을 명시적 매개변수로 전달하기를 원하므로 종속성이 분명합니다.
"Reader Monad" 및 "Free Monad"와 같이 함수형 프로그래밍에서 이러한 종류의 작업을 수행하는 기술이 많이 있지만
이 책은 입문서이므로 가장 간단한 접근 방식을 고수합니다.
모든 종속성을 최상위 함수로 전달하면 내부 함수로 전달되고, 다시 내부 함수로 전달되는 식입니다.
이전에 정의한 대로 도우미 함수를 구현했다고 가정해 보겠습니다.
// low-level helper functions
let toAddress checkAddressExists unvalidatedAddress =
...
let toProductCode checkProductCodeExists productCode =
...
// helper function
let toValidatedOrderLine checkProductExists unvalidatedOrderLine =
// ^ needed for toProductCode, below
// create the components of the line
let orderLineId = ...
let productCode =
unvalidatedOrderLine.ProductCode
|> toProductCode checkProductExists //use service
...
한 수준 위로 이동하면 validateOrder 함수는 toAddress와 toValidatedOrderLine을 모두 사용해야 하므로
두 서비스를 추가 매개변수로 전달해야 합니다.
let validateOrder : ValidateOrder =
fun checkProductExists // dependency for toValidatedOrderLine
checkAddressExists // dependency for toAddress
unvalidatedOrder ->
// build the validated address using the dependency
let shippingAddress =
unvalidatedOrder.ShippingAddress
|> toAddress checkAddressExists
...
// build the validated order lines using the dependency
let lines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkProductExists)
...
let placeOrder
checkProductExists // dependency
checkAddressExists // dependency
getProductPrice // dependency
createOrderAcknowledgmentLetter // dependency
sendOrderAcknowledgment // dependency
: PlaceOrderWorkflow = // function definition
fun unvalidatedOrder ->
...
let app : WebPart =
// set up the services used by the workflow
let checkProductExists = ...
let checkAddressExists = ...
let getProductPrice = ...
let createOrderAcknowledgmentLetter = ...
let sendOrderAcknowledgment = ...
let toHttpResponse = ...
// set up the "placeOrder" workflow
// by partially applying the services to it
let placeOrder =
placeOrder
checkProductExists
checkAddressExists
getProductPrice
createOrderAcknowledgmentLetter
sendOrderAcknowledgment
// set up the other workflows
let changeOrder = ...
let cancelOrder = ...
// set up the routing
choose
[ POST >=> choose
[ path "/placeOrder"
>=> deserializeOrder // convert JSON to UnvalidatedOrder
>=> placeOrder // do the workflow
>=> postEvents // post the events onto queues
>=> toHttpResponse // return 200/400/etc based on the output
path "/changeOrder"
>=> ...
path "/cancelOrder"
>=> ...
]
]
val ( >=> ):
x: ('a -> 'b) ->
y: ('b -> 'c)
-> 'a -> 'c
종속성이 너무 많은가요?
let checkAddressExists endPoint credentials =
...
let toAddress checkAddressExists endPoint credentials unvalidatedAddress =
// only ^ needed ^ for checkAddressExists
// call the remote service
let checkedAddress = checkAddressExists endPoint credentials unvalidatedAddress
// 2 extra parameters ^ passed in ^
...
let validateOrder
checkProductExists
checkAddressExists
endPoint // only needed for checkAddressExists
credentials // only needed for checkAddressExists
unvalidatedOrder =
...
let placeOrder : PlaceOrderWorkflow =
// initialize information (e.g from configuration)
let endPoint = ...
let credentials = ...
// make a new version of checkAddressExists
// with the credentials baked in
let checkAddressExists = checkAddressExists endPoint credentials
// etc
// set up the steps in the workflow
let validateOrder =
validateOrder checkProductCodeExists checkAddressExists
// the new checkAddressExists ^
// is a one parameter function
// etc
// return the workflow function
fun unvalidatedOrder ->
// compose the pipeline from the steps
...
의존성 테스트하기
종속성을 전달의 한 가지 장점은 특별한 모킹 라이브러리 없이도 가짜 종속성을 제공하기 쉽기 때문에 핵심 기능을 테스트하기가 매우 쉽다는 것입니다.
open NUnit.Framework
[<Test>]
let ``If product exists, validation succeeds``() =
// arrange: set up stub versions of service dependencies
let checkAddressExists address =
CheckedAddress address // succeed
let checkProductCodeExists productCode =
true // succeed
// arrange: set up input
let unvalidatedOrder = ...
// act: call validateOrder
let result = validateOrder checkProductCodeExists checkAddressExists ...
// assert: check that result is a ValidatedOrder, not an error
...
checkAddressExists 및 checkProductCodeExists 함수(서비스를 나타냄)의 스텁 버전이 작성하기 쉽고
테스트에서 바로 정의할 수 있음을 알 수 있습니다.
NUnit 프레임워크를 사용하고 있지만 FsUnit, Unquote, Expecto 또는 FsCheck와 같은 F# 친화적인 라이브러리 또는 더 나은 .NET 테스트 프레임워크 중 하나를 사용할 수 있습니다.
let checkProductCodeExists productCode =
false // fail
[<Test>]
let ``If product doesn't exist, validation fails``() =
// arrange: set up stub versions of service dependencies
let checkAddressExists address = ...
let checkProductCodeExists productCode =
false // fail
// arrange: set up input
let unvalidatedOrder = ...
// act: call validateOrder
let result = validateOrder checkProductCodeExists checkAddressExists ...
// assert: check that result is a failure
...
- validateOrder 함수는 stateless합니다. 아무 것도 변경하지 않으며 동일한 입력으로 호출하면 동일한 출력을 얻습니다. 이것은 함수를 테스트하기 쉽게 만듭니다.
- 모든 종속성은 명시적으로 전달되므로 작동 방식을 쉽게 이해할 수 있습니다.
- 모든 사이드 이펙트는 함수 자체가 아니라 매개변수에 캡슐화됩니다. 함수를 테스트하기 쉽게 하고, 부작용이 무엇인지 제어하기 쉽게 만듭니다.
테스트는 큰 주제입니다. 다음은 조사할 가치가 있는 몇 가지 인기 있는 F# 친화적 테스트 도구입니다.
- FsUnit은 NUnit 및 XUnit과 같은 표준 테스트 프레임워크를 F# 친화적인 구문으로 래핑합니다.
- Unquote는 테스트 실패(말하자면 "스택 풀기")로 이어지는 모든 값들을 보여줍니다.
- NUnit과 같은 "example-based" 테스트 접근 방식에만 익숙하다면 테스트에 대한 "property-based" 접근 방식을 반드시 살펴봐야 합니다. FsCheck는 F#의 기본 속성 기반 테스트 라이브러리입니다.
- Expecto는 [<Test>]와 같은 특수 attribute를 요구하는 대신 표준 함수를 테스트 픽스처로 사용하는 경량 F# 테스트 프레임워크입니다.
조립된 파이프라인
우리는 이 장 전체에 걸쳐 흩어져 있는 코드를 보았습니다.
모든 것을 함께 모아서 전체 파이프라인이 어떻게 조립되는지 봅시다.
- 특정 워크플로를 구현하는 모든 코드를 워크플로의 이름을 따서 명명된 동일한 모듈에 넣습니다 (예: PlaceOrderWorkflow.fs).
- 파일 맨 위에 타입 정의를 넣습니다.
- 그 후, 우리는 각 단계에 대한 구현을 넣습니다.
- 맨 아래에서 주요 워크플로 함수로 단계를 조합합니다.
종속성은 컴포지션 루트 함수에서 전달될 것입니다!
module CompletePipeline =
open SimpleTypes
open CommonTypes
module API = ()
//>CompletePipeline1
module PlaceOrderWorkflow =
// make the shared simple types (such as
// String50 and ProductCode) available.
open SimpleTypes
// make the public types exposed to the
// callers available
open API
// ==============================
// Part 1: Design
// ==============================
// NOTE: the public parts of the workflow -- the API --
// such as the `PlaceOrderWorkflow` function and its
// input `UnvalidatedOrder`, are defined elsewhere.
// The types below are private to the workflow implementation.
// ----- Validate Order -----
type CheckProductCodeExists =
ProductCode -> bool
type CheckedAddress =
CheckedAddress of UnvalidatedAddress
type CheckAddressExists =
UnvalidatedAddress -> CheckedAddress
type ValidateOrder =
CheckProductCodeExists // dependency
-> CheckAddressExists // dependency
-> UnvalidatedOrder // input
-> ValidatedOrder // output
// ----- Price order -----
type GetProductPrice = DotDotDot
type PriceOrder = DotDotDot
// etc
//<
//>CompletePipeline2
// ==============================
// Part 2: Implementation
// ==============================
// ------------------------------
// ValidateOrder implementation
// ------------------------------
let toCustomerInfo (unvalidatedCustomerInfo: UnvalidatedCustomerInfo) =
dotDotDot()
let toAddress (checkAddressExists:CheckAddressExists) unvalidatedAddress =
dotDotDot()
let predicateToPassthru _ = dotDotDot()
let toProductCode (checkProductCodeExists:CheckProductCodeExists) productCode =
dotDotDot()
let toOrderQuantity productCode quantity =
dotDotDot()
let toValidatedOrderLine checkProductExists (unvalidatedOrderLine:UnvalidatedOrderLine) =
dotDotDot()
/// Implementation of ValidateOrder step
let validateOrder : ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
let orderId =
unvalidatedOrder.OrderId
|> OrderId.create
let customerInfo = dotDotDot()
let shippingAddress = dotDotDot()
let billingAddress = dotDotDot()
let lines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkProductCodeExists)
let validatedOrder : ValidatedOrder = {
OrderId = orderId
CustomerInfo = customerInfo
ShippingAddress = shippingAddress
BillingAddress = billingAddress
Lines = lines
}
validatedOrder
//<
// mask previous implementation
let validateOrder _ = dotDotDot()
let priceOrder _ = dotDotDot()
let acknowledgeOrder _ = dotDotDot()
let createEvents _ = dotDotDot()
type PlaceOrderWorkflow = DotDotDot -> unit
//>CompletePipeline3
// ------------------------------
// The complete workflow
// ------------------------------
let placeOrder
checkProductExists // dependency
checkAddressExists // dependency
getProductPrice // dependency
createOrderAcknowledgmentLetter // dependency
sendOrderAcknowledgment // dependency
: PlaceOrderWorkflow = // definition of function
fun unvalidatedOrder ->
let validatedOrder =
unvalidatedOrder
|> validateOrder checkProductExists checkAddressExists
let pricedOrder =
validatedOrder
|> priceOrder getProductPrice
let acknowledgmentOption =
pricedOrder
|> acknowledgeOrder createOrderAcknowledgmentLetter sendOrderAcknowledgment
let events =
createEvents pricedOrder acknowledgmentOption
events
//<
'BackEnd' 카테고리의 다른 글
DDD 구현 : 모나드로 Result 생성 함수 연결 (0) | 2022.03.20 |
---|---|
DDD 구현 : 오류 모델링 (0) | 2022.03.20 |
함수 파이프라인 합성하기 with F# 1. 검증 로직 구현하기 (0) | 2022.03.19 |
DDD 기능 구현을 위한 함수 이해하기 with F# (0) | 2022.03.19 |
파이프라인으로 워크플로 모델링하기 - 워크플로 합성 및 나머지 (0) | 2022.03.19 |