Share
Expressing Business Logic with Types: Functional DDD for OOP
This slide deck was presented at the Object-Oriented Conference 2024 held in Japan.
The original content is in Japanese, and it has been translated into English.
https://speakerdeck.com/yuitosato/functional-and-type-safe-ddd-for-oop
This presentation introduces a more type-safe functional DDD by incorporating functional paradigms, such as algebraic data types, into domain-driven design, which centers on business expertise (domain).
In this session, we will add a more type-focused design when reflecting the models and business logic discovered through domain modeling into the software. By expanding the range expressed by types, we can represent business logic more clearly in the code. Furthermore, since it is expressed by types, more mistakes can be caught at the compile phase, leading to improved software quality.
Incorporating functional ideas does not simply mean using functional languages like Haskell to perform domain-driven design. Type-safe concepts and techniques that apply functional knowledge can be introduced on a small scale. This presentation introduces methods to improve production code starting tomorrow while retaining the advantages of existing object-oriented languages like Java.
YuitoSato
Transcript
106 slides
-
1 ©2024 Loglass Inc. Expressing Business Logic with Types: Functional
DDD for OOP Object-Oriented Conference 2024 Yuito Sato(@Yuiiitoto) Loglass Inc. -
2 2 ©2024 Loglass Inc. Self-Introduction Developer at Loglass Inc.
Yuito Sato(X: @Yuiiitoto) Joined Loglass as a software engineer in December 2020. Developing Loglass, a management cloud service, using Kotlin and TypeScript. Occasionally develop and maintain Kotlin OSS. -
3 ©2024 Loglass Inc. Today's Theme Expressing Business Logic with
Types: Functional DDD for OOP -
4 ©2024 Loglass Inc. Key Message Incorporate functional programming essence
to make DDD more Type-Safe and improve software quality. -
5 ©2024 Loglass Inc. Agenda 1. Introduction a. What is
Functional DDD? b. Benefits of expressing business logic with types. c. Object-oriented and functional programming. 2. What is DDD? a. DDD modeling. b. DDD implementation patterns. 3. Practical techniques a. Expressing model state with algebraic data types. b. Expressing model state transitions with types and total functions. 4. Summary -
6 ©2024 Loglass Inc. Agenda 1. Introduction a. What is
Functional DDD? b. Benefits of expressing business logic with types. c. Object-oriented and functional programming. 2. What is DDD? a. DDD modeling. b. DDD implementation patterns. 3. Practical techniques a. Expressing model state with algebraic data types. b. Expressing model state transitions with types and total functions. 4. Summary -
7 ©2024 Loglass Inc. What is Functional DDD? - Originally
described by Scott Wlaschin in "Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#." -
8 ©2024 Loglass Inc. Domain-driven design (DDD) combined with functional
programming is the innovative combo that will get you there. In this pragmatic, down-to-earth guide, you'll see how applying the core principles of functional programming can result in software designs that model real-world requirements both elegantly and concisely - often more so than an object-oriented approach. —『Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#』by Scott Wlaschin What is Functional DDD? -
9 ©2024 Loglass Inc. Personal Interpretation of Functional DDD Adding
functional essence to DDD to flexibly express business logic with types. -
10 ©2024 Loglass Inc. Benefits of Expressing Business Logic with
Types - Implementation errors can be detected at the compile phase. - Example: Only tasks with a "Completed" status have a completion date. Common way (in Kotlin) class Task { val title: String val status: TaskStatus val completedAt: LocalDateTime? // … } enum class TaskStatus { InProgress, Completed } val task = Task("Shopping", TaskStatus.InProgress, null) val task = Task( "Shopping", TaskStatus.Completed, LocalDateTime.now(), ) -
11 ©2024 Loglass Inc. val task = Task("Shopping", TaskStatus.InProgress, null)
val task = Task( "Shopping", TaskStatus.Completed, LocalDateTime.now(), ) // NG: In Progress but with a completedAt value val task = Task( "買い物", TaskStatus.InProgress, LocalDateTime.now() ); Common way (in Kotlin) - Implementation errors can be detected at the compile phase. - Example: Only tasks with a "Completed" status have a completion date. Benefits of Expressing Business Logic with Types class Task { val title: String val status: TaskStatus val completedAt: LocalDateTime? // … } enum class TaskStatus { InProgress, Completed } -
12 ©2024 Loglass Inc. constructor(title: String, status: TaskStatus, completedAt: LocalDateTime?)
{ if (status == TaskStatus.InProgress && completedAt != null) { throw IllegalArgumentException("Only tasks with a "Completed" status have a completion date.") } this.title = title this.status = status this.completedAt = completedAt } - Implementation errors can be detected at the compile phase. - Example: Only tasks with a "Completed" status have a completion date. Benefits of Expressing Business Logic with Types -
13 ©2024 Loglass Inc. class CompletedTask { val title: String
val completedAt: LocalDateTime // … } class InProgressTask { val title: String // … } Create a class for completed tasks. val task = InProgressTask("Shopping"); val task = CompletedTask( "Shopping", LocalDateTime.now() ); // Compilation Error val task = InProgressTask( "Shopping", LocalDateTime.now() ); - Implementation errors can be detected at the compile phase. - Example: Only tasks with a "Completed" status have a completion date. Benefits of Expressing Business Logic with Types -
14 ©2024 Loglass Inc. Three Steps to Detect Implementation Errors
1. Compile 2. Automated Testing 3. Manual Testing -
15 ©2024 Loglass Inc. 型で表現されている 範囲に限り 品質を担保 Protected by Test
Codes Protected By Type 1. Compile 2. Automated Testing 3. Manual Testing Three Steps to Detect Implementation Errors -
16 ©2024 Loglass Inc. 型で表現されている 範囲に限り 品質を担保 Test code can’t
cover all cases. 1. Compile 2. Automated Testing 3. Manual Testing Three Steps to Detect Implementation Errors Protected By Type Protected by Test Codes -
17 ©2024 Loglass Inc. Detecting implementation errors here is preferred
1. Compile 2. Automated Testing 3. Manual Testing Three Steps to Detect Implementation Errors Protected By Type Protected by Test Codes Test code can’t cover all cases. -
18 ©2024 Loglass Inc. Summary So Far Enhancing DDD with
type safety to catch implementation errors at the compile phase. -
20 ©2024 Loglass Inc. Functional programming essence can be added
to existing OOP × DDD projects. -
21 ©2024 Loglass Inc. Programming Languages Adopting Functional Programming -
Java 21 finally supports algebraic data type. https://openjdk.org/jeps/440 https://openjdk.org/jeps/441 -
22 ©2024 Loglass Inc. The distinction between Object-Oriented and Functional
is becoming blurred in recent years. Many languages can now effectively handle functional techniques. -
23 ©2024 Loglass Inc. We will focus on small yet
powerful techniques that can be introduced into OOP × DDD projects. We will not discuss changes at the architectural level. -
24 ©2024 Loglass Inc. Agenda 1. Introduction a. What is
Functional DDD? b. Benefits of expressing business logic with types. c. Object-oriented and functional programming. 2. What is DDD? a. DDD modeling. b. DDD implementation patterns. 3. Practical techniques a. Expressing model state with algebraic data types. b. Expressing model state transitions with types and total functions. 4. Summary -
26 ©2024 Loglass Inc. DDD (Domain-Driven Design) is Development method
that enhances software value through Modeling. -
28 ©2024 Loglass Inc. Modeling of DDD Modeilng - Discuss
with domain experts to create a domain model Domain Experts -
29 ©2024 Loglass Inc. DDD Implementation Patterns Implementation Patterns -
Create an independent layer (domain layer) to represent the model without depending on DB or external services 『新卒にも伝わるドメイン駆動設計のアーキテクチャ説明 (オニオンアーキテクチャ )[DDD]』 -
31 ©2024 Loglass Inc. Agenda 1. Introduction a. What is
Functional DDD? b. Benefits of expressing business logic with types. c. Object-oriented and functional programming. 2. What is DDD? a. DDD modeling. b. DDD implementation patterns. 3. Practical techniques a. Expressing model state with algebraic data types. b. Expressing model state transitions with types and total functions. 4. Summary -
32 ©2024 Loglass Inc. Today’s Subject - Order System Create
an Order Confirm an Order Start Shipping Cancel an Order -
33 ©2024 Loglass Inc. Domain Model Diagram of the Order
System (≈ Abstracted Object Diagram) -
38 ©2024 Loglass Inc. Wouldn't it be nice if these
rule implementation errors could be prevented at compile time? -
39 ©2024 Loglass Inc. Agenda 1. Introduction a. What is
Functional DDD? b. Benefits of expressing business logic with types. c. Object-oriented and functional programming. 2. What is DDD? a. DDD modeling. b. DDD implementation patterns. 3. Practical techniques a. Expressing model state with algebraic data types. b. Expressingmodel state transitions with types and total functions. 4. Summary -
40 ©2024 Loglass Inc. Practice and Three Techniques - Three
Techniques Using Functional Knowledge - 1. Expressing Model States with Algebraic Data Types - 2. Expressing Model State Transitions with Types and Total Functions -
41 ©2024 Loglass Inc. Practice and Three Techniques - Three
Techniques Using Functional Knowledge - 1. Expressing Model States with Algebraic Data Types - 2. Expressing Model State Transitions with Types and Total Functions -
42 ©2024 Loglass Inc. Implementing Order States with Enum class
Order { val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: List<OrderLine>, val status: OrderStatus, val confirmedAt: LocalDateTime?, val cancelledAt: LocalDateTime?, val cancelReason: String?, val shippingStartedAt: LocalDateTime?, val shippedBy: ShipperId?, val scheduledArrivalDate: LocalDate?, } enum class OrderStatus { UNCONFIRMED, CONFIRMED, CANCELLED, SHIPPING, } -
43 ©2024 Loglass Inc. class Order { val orderId: OrderId,
val customerId: CustomerId, val shippingAddress: Address, val lines: List<OrderLine>, val status: OrderStatus, val confirmedAt: LocalDateTime?, val cancelledAt: LocalDateTime?, val cancelReason: String?, val shippingStartedAt: LocalDateTime?, val shippedBy: ShipperId?, val scheduledArrivalDate: LocalDate?, } enum class OrderStatus { UNCONFIRMED, CONFIRMED, CANCELLED, SHIPPING, } Too many nullable fields. Some fields are mandatory depending on the state. There is a risk of data inconsistency. Implementing Order States with Enum -
44 ©2024 Loglass Inc. val order = Order( ..., status
= OrderStatus.SHIPPING, cancelReason = "Broken", ) The order can have a cancellation reason even though it is in the shipping state. Implementing Order States with Enum -
45 ©2024 Loglass Inc. constructor( orderId: OrderId, …, status: OrderStatus,
cancelledAt: LocalDateTime?, cancelReason: String?, ... ) { if (status != OrderStatus.CANCELLED) { if (cancelledAt != null || cancelReason != null) { throw IllegalArgumentException("Cancel reason and time can only be set for cancelled orders") } } this.orderId = orderId … } You need the guard codes for data consistency Implementing Order States with Enum -
46 ©2024 Loglass Inc. Errors can only be detected at
runtime. There is a risk of serious data inconsistencies. For example, a shipped product could be canceled, leading to a completed refund process while the product is still delivered. -
47 ©2024 Loglass Inc. So, what should we do? -
Separate the models for each state. -
48 ©2024 Loglass Inc. Define as Completely Separate Classes class
UnconfirmedOrder( val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: NonEmptyList<OrderLine>, ) class ConfirmedOrder( val orderId: OrderId, // … val confirmedAt: LocalDateTime, ) class CancelledOrder( val orderId: OrderId, // … val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) class ShippingOrder( val orderId: OrderId, // … val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, ) -
49 ©2024 Loglass Inc. Issue: Lack of Polymorphism Makes It
Difficult to Use val order: Any = orderRepository.findBy(orderId) val orders: List<Any> = listOf( UnconfirmedOrder(...), ConfirmedOrder(...), CancelledOrder(...), ... ) -
50 ©2024 Loglass Inc. Summarize so far - Different mandatory
fields for each order state - Want to collectively refer to unconfirmed, confirmed, cancelled, and shipping as orders -
51 ©2024 Loglass Inc. Summarize so far - Different mandatory
fields for each order state - Want to collectively refer to unconfirmed, confirmed, cancelled, and shipping as orders →Use Algebraic Data Types -
52 ©2024 Loglass Inc. What is an Algebraic Data Type
(ADT)? - A data structure derived from algebra - A data structure represented by Product Sets and Sum Sets -
53 ©2024 Loglass Inc. Product Sets - A × B
= { (a,b) ∣ a ∈ A, b ∈ B } (=A and B) (1, “a”) (1, “b”) (1, “c”) (2, “a”) (2, “b”) (2, “c”) (3, “a”) (3, “b”) (3, “c”) A: 1, 2, 3, B: “a”, “b”, “c” A ✖ B class CancelledOrder( val orderId: OrderId, // … val cancelledAt: LocalDateTime, val cancelReason: String?, ) CancelledOrder = OrderId × LocalDateTime × String -
54 ©2024 Loglass Inc. Sum Sets - A ⊕ B=
A ∪ B ただし A ∩ B = {} (=A or B but A but not both) - Achieved in some object-oriented languages with sealed classes (restricting inheritance) A B A ⊕ B sealed interface Order { class UnconfirmedOrder : Order class ConfirmedOrder : Order class CancelledOrder : Order class ShippingOrder : Order } Order = Unconfirmed ⊕ Confirmed ⊕ Cancelled ⊕ Shipping -
55 ©2024 Loglass Inc. Sum Sets - A ⊕ B=
A ∪ B ただし A ∩ B = {} (=A or B but A but not both) - TypeScript might be more intuitive type Order = UnconfirmedOrder | ConfirmedOrder | CancelledOrder | ShippingOrder; class UnconfirmedOrder {} class ConfirmedOrder {} class CancelledOrder {} class ShippingOrder {} -
56 ©2024 Loglass Inc. Algebraic Data Types = Combination of
Product Sets and Sum Sets Unconfirmed = (...) Confirmed = (...) Cancelled = (...) Order = Unconfirmed(...) ⊕ Confirmed(...) ⊕ Cancelled(...) ⊕ Shipping(...) Shipping = (...) -
57 ©2024 Loglass Inc. Unconfirmed = (...) Confirmed = (...)
Cancelled = ( OrderId × … × LocalDateTime × String ) Order = Unconfirmed(...) ⊕ Confirmed(...) ⊕ Cancelled(...) ⊕ Shipping(...) Shipping = (...) Algebraic Data Types = Combination of Product Sets and Sum Sets -
58 ©2024 Loglass Inc. Algebraic Data Types = Combination of
Product Sets and Sum Sets -
59 ©2024 Loglass Inc. How is it Different from Inheritance?
A B C = int ✖ String - Inheritance cannot close the range (parent class does not know child classes) - Inheritance does not have the properties of sum sets. D = boolean ✖ boolean Z Inheritance from the module you don’t know. -
60 ©2024 Loglass Inc. How is it Different from Enum?
A B C = int ✖ String - Enums cannot have individual structures - Enums does not have the properties of product sets Z -
61 ©2024 Loglass Inc. Reimplementing Order with Algebraic Data Types
sealed interface Order { val orderId: OrderId val customerId: CustomerId val shippingAddress: Address val lines: List<OrderLine> class UnconfirmedOrder( override val …, ) : Order class ConfirmedOrder( …, val confirmedAt: LocalDateTime, ) : Order class CancelledOrder( …, val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) : Order class ShippingOrder( …, val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, ) : Order } -
62 ©2024 Loglass Inc. Reimplementing Order with Algebraic Data Types
sealed interface Order { val orderId: OrderId val customerId: CustomerId val shippingAddress: Address val lines: List<OrderLine> class UnconfirmedOrder( override val …, ) : Order class ConfirmedOrder( …, val confirmedAt: LocalDateTime, ) : Order class CancelledOrder( …, val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) : Order class ShippingOrder( …, val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, ) : Order } Eliminated data inconsistencies. -
63 ©2024 Loglass Inc. val order = ShippingOrder( ..., cancelReason
= "It was broken", ) => Compilation Error class CancelledOrder( …, val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) : Order class ShippingOrder( …, val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, ) : Order } ShippingOrder does not have cancelReason, so it results in a compile error. Reimplementing Order with Algebraic Data Types -
64 ©2024 Loglass Inc. - Exhaustiveness is also ensured fun
cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order") is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } Reimplementing Order with Algebraic Data Types -
65 ©2024 Loglass Inc. - Exhaustiveness is also ensured fun
cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order") is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } Reimplementing Order with Algebraic Data Types no else branch -
66 ©2024 Loglass Inc. fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder
{ return when (this) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order"") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order") // is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } A compile error occurs if a branch is missing. Reimplementing Order with Algebraic Data Types - Exhaustiveness is also ensured -
67 ©2024 Loglass Inc. By expressing model states with algebraic
data types, we were able to prevent data inconsistencies! -
68 ©2024 Loglass Inc. Practice and Three Techniques - Three
Techniques Using Functional Knowledge - 1. Expressing Model States with Algebraic Data Types - 2. Expressing Model State Transitions with Types and Total Functions -
71 ©2024 Loglass Inc. Written Commonly sealed interface Order {
fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order) is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order") is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } } -
72 ©2024 Loglass Inc. sealed interface Order { fun cancel(cancelReason:
String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order”) is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order") is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } } 4 test cases are necessary Common Way -
73 ©2024 Loglass Inc. Common Way sealed interface Order {
fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order") is ConfirmedOrder -> CancelledOrder(...) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order") is ShippingOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } } } Implementation Mistakes -
74 ©2024 Loglass Inc. What should we do? - Simply
define it as a method of ConfirmedOrder class ConfirmedOrder(...) : Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } } -
75 ©2024 Loglass Inc. Order Class sealed interface Order {
... class UnconfirmedOrder(...) : Order { fun confirm(now: LocalDateTime): ConfirmedOrder } class ConfirmedOrder(...) : Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder {...} fun startShipping(shipperId: ShipperId, now: LocalDateTime): ShippingOrder {...} } class CancelledOrder(...) : Order class ShippingOrder(...) : Order } -
76 ©2024 Loglass Inc. Caller class CancelOrderUseCase( private val orderRepository:
OrderRepository, ) { fun execute(orderId: OrderId, cancelReason: String?) { val order = orderRepository.findById(orderId) ?: throw Exception("Order not found. ID: ${orderId.value}") val now = LocalDateTime.now() when (order) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order"") is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } } -
77 ©2024 Loglass Inc. Caller class CancelOrderUseCase( private val orderRepository:
OrderRepository, ) { fun execute(orderId: OrderId, cancelReason: String?) { val order = orderRepository.findById(orderId) ?: throw Exception("Order not found. ID: ${orderId.value}") val now = LocalDateTime.now() when (order) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order"") is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } } A compile error occurs because ShippingOrder does not have a cancel method. -
78 ©2024 Loglass Inc. Caller class CancelOrderUseCase( private val orderRepository:
OrderRepository, ) { fun execute(orderId: OrderId, cancelReason: String?) { val order = orderRepository.findById(orderId) ?: throw Exception("Order not found. ID: ${orderId.value}") val now = LocalDateTime.now() when (order) { is UnconfirmedOrder -> throw Exception("Cannot cancel an unconfirmed order") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("Cannot cancel an already cancelled order"") is ShippingOrder -> throw Exception("Cannot cancel a shipped order") } } } Aren't we just pushing this problem to the caller? 🤔 Does this make sense? -
79 ©2024 Loglass Inc. It makes sense from the perspective
that rules can be expressed with types. -
80 ©2024 Loglass Inc. Separating Rules from Handling - If
unconfirmed, return "Cannot cancel an unconfirmed order." - Only cancel the order if it is confirmed. - If already cancelled, return "The order is already cancelled." - If shipped, return "Cannot cancel a shipped order." -
81 ©2024 Loglass Inc. Separating Rules from Handling Only cancel
the order if it is confirmed - If unconfirmed, return "Cannot cancel an unconfirmed order." - Only cancel the order if it is confirmed. - If already cancelled, return "The order is already cancelled." - If shipped, return "Cannot cancel a shipped order." Rules Handling class ConfirmedOrder(...) : Order { fun cancel( cancelReason: String?, now: LocalDateTime ): CancelledOrder {...} } when (order) { is UnconfirmedOrder -> throw Exception("If unconfirme~,) is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("Only cancel ther order if~") is ShippingOrder -> throw Exception("If shipped ~") } Extract only the rules -
83 ©2024 Loglass Inc. Apply Rules Handling class ConfirmedOrder(...) :
Order { fun cancel( cancelReason: String?, now: LocalDateTime ): CancelledOrder {...} } Separating Rules from Handling when (order) { is UnconfirmedOrder -> throw Exception("If unconfirme~,) is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("Only cancel ther order if~") is ShippingOrder -> throw Exception("If shipped ~") } -
84 ©2024 Loglass Inc. class ConfirmedOrder(...) : Order { fun
cancel( cancelReason: String?, now: LocalDateTime ): CancelledOrder {...} } How to communicate rule violations? when (order) { is UnconfirmedOrder -> throw Exception("If unconfirme~,) is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("Only cancel ther order if~") is ShippingOrder -> throw Exception("If shipped ~") } Apply Rules Handling Separating Rules from Handling -
85 ©2024 Loglass Inc. - Which is more important? Apply
Rules Handling Separating Rules from Handling -
86 ©2024 Loglass Inc. - Which is more important? →
Applying rules is more important Apply Rules Handling Separating Rules from Handling -
87 ©2024 Loglass Inc. - How to protect? Protected by
Type as much as possible Protected by Test Apply Rules Handling Separating Rules from Handling -
89 ©2024 Loglass Inc. What is Totality / Total Function?
A mathematical function links each possible input to an output. In functional programming we try to design our functions the same way, so that every input has a corresponding output. These kinds of functions are called total functions. —『Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#』by Scott Wlaschin -
90 ©2024 Loglass Inc. X × 2 Example of a
Total Function: Multiplication - A function that takes an integer and doubles it → Total function 1 2 3 6 3 6 12 Input Output 0 0 Integers Intergers -
91 ©2024 Loglass Inc. Example of a Non-Total Function: Division
- A function that takes an integer and divides 12 by it → Not a total function 12 / X 1 12 3 4 3 6 2 0 ArithmeticException Input Output Integers Integers -
92 ©2024 Loglass Inc. - A function that takes a
non-zero integer and divides 12 by it → Total function 12 / X 1 12 3 4 3 6 2 Non-zero Integers 0 Input Output Integers Example of a Non-Total Function: Division -
93 ©2024 Loglass Inc. - A function that takes a
non-zero integer and divides 12 by it → Total function - Implement NonZero Int type class NonZeroInt(val value: Int) { init { if (value == 0) { throw Exception("NonZeroInt cannot be 0") } } } fun divide12By(denominator: NonZeroInt): Int { return 12 / denominator.value } val result = divide12By(0) => Compilation Error val result = divide12By(NonZeroInt(3)) => 4 Example of a Non-Total Function: Division -
94 ©2024 Loglass Inc. Applying the Concept of Rules and
Handling Apply Rules Handling Enforcing the rule of not dividing by zero with types fun divide12By(denominator: NonZeroInt): Int { return 12 / denominator.value } -
95 ©2024 Loglass Inc. What does this do after all?
- Narrowing input values with types to only those that return valid results - Expressing the rule with types and enforcing it with types 12 / X 1 12 3 4 3 6 2 Non-zero Integers Integers 0 Input Output -
96 ©2024 Loglass Inc. Returning to Order Cancellation Cancel Uncomfirmed
Exception Comfirmed Cancelled Cancelled Exception Shipping Exception Order Order Input Output Output -
97 ©2024 Loglass Inc. Cancel Uncomfirmed Comfirmed Cancelled Cncelled Shiping
Input Output Returning to Order Cancellation -
98 ©2024 Loglass Inc. - Make it a total function
sealed interface Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("Unconfirmed order cannot be cancelled") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("Cancelled order cannot be cancelled") is ShippingOrder -> throw Exception("Shipping order cannot be cancelled") } } } Returning to Order Cancellation -
99 ©2024 Loglass Inc. class ConfirmedOrder(...) : Order { fun
cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } } Returning to Order Cancellation - Make it a total function -
100 ©2024 Loglass Inc. By narrowing the input range to
return valid values and making it a global function, we were able to enforce rules using types! -
101 ©2024 Loglass Inc. Summary Incorporate functional programming essence to
make DDD more Type-Safe and improve software quality. -
103 ©2024 Loglass Inc. Expressing Model States with Types Using
Algebraic Data Types Summary -
104 ©2024 Loglass Inc. Expressing State Transitions with Types Using
Total Functions Summary -
105 ©2024 Loglass Inc. Summary Incorporate functional programming essence to
make DDD more Type-Safe and improve software quality.