Slide 1

Slide 1 text

1 ©2024 Loglass Inc. Expressing Business Logic with Types: Functional DDD for OOP Object-Oriented Conference 2024 Yuito Sato(@Yuiiitoto) Loglass Inc.

Slide 2

Slide 2 text

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.

Slide 3

Slide 3 text

3 ©2024 Loglass Inc. Today's Theme Expressing Business Logic with Types: Functional DDD for OOP

Slide 4

Slide 4 text

4 ©2024 Loglass Inc. Key Message Incorporate functional programming essence to make DDD more Type-Safe and improve software quality.

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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#."

Slide 8

Slide 8 text

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?

Slide 9

Slide 9 text

9 ©2024 Loglass Inc. Personal Interpretation of Functional DDD Adding functional essence to DDD to flexibly express business logic with types.

Slide 10

Slide 10 text

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(), )

Slide 11

Slide 11 text

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 }

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

14 ©2024 Loglass Inc. Three Steps to Detect Implementation Errors 1. Compile 2. Automated Testing 3. Manual Testing

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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.

Slide 18

Slide 18 text

18 ©2024 Loglass Inc. Summary So Far Enhancing DDD with type safety to catch implementation errors at the compile phase.

Slide 19

Slide 19 text

19 ©2024 Loglass Inc. Functional...? We use Object Oriented Programming…🤔

Slide 20

Slide 20 text

20 ©2024 Loglass Inc. Functional programming essence can be added to existing OOP × DDD projects.

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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.

Slide 23

Slide 23 text

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.

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

25 ©2024 Loglass Inc. 2. What is DDD?

Slide 26

Slide 26 text

26 ©2024 Loglass Inc. DDD (Domain-Driven Design) is Development method that enhances software value through Modeling.

Slide 27

Slide 27 text

27 ©2024 Loglass Inc. DDD (Domain-Driven Design) is Modeling Implementation Pattern

Slide 28

Slide 28 text

28 ©2024 Loglass Inc. Modeling of DDD Modeilng - Discuss with domain experts to create a domain model Domain Experts

Slide 29

Slide 29 text

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]』

Slide 30

Slide 30 text

30 ©2024 Loglass Inc. 3. Practice and Techniques

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

32 ©2024 Loglass Inc. Today’s Subject - Order System Create an Order Confirm an Order Start Shipping Cancel an Order

Slide 33

Slide 33 text

33 ©2024 Loglass Inc. Domain Model Diagram of the Order System (≈ Abstracted Object Diagram)

Slide 34

Slide 34 text

34 ©2024 Loglass Inc. Adding Rules to the Domain Model Diagram

Slide 35

Slide 35 text

35 ©2024 Loglass Inc. Too many nullables make it complicated

Slide 36

Slide 36 text

36 ©2024 Loglass Inc. Organize Items and Transitions for Each State

Slide 37

Slide 37 text

37 ©2024 Loglass Inc. Organize Items and Transitions for Each State

Slide 38

Slide 38 text

38 ©2024 Loglass Inc. Wouldn't it be nice if these rule implementation errors could be prevented at compile time?

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

42 ©2024 Loglass Inc. Implementing Order States with Enum class Order { val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: List, 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, }

Slide 43

Slide 43 text

43 ©2024 Loglass Inc. class Order { val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: List, 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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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.

Slide 47

Slide 47 text

47 ©2024 Loglass Inc. So, what should we do? - Separate the models for each state.

Slide 48

Slide 48 text

48 ©2024 Loglass Inc. Define as Completely Separate Classes class UnconfirmedOrder( val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: NonEmptyList, ) 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, )

Slide 49

Slide 49 text

49 ©2024 Loglass Inc. Issue: Lack of Polymorphism Makes It Difficult to Use val order: Any = orderRepository.findBy(orderId) val orders: List = listOf( UnconfirmedOrder(...), ConfirmedOrder(...), CancelledOrder(...), ... )

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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 {}

Slide 56

Slide 56 text

56 ©2024 Loglass Inc. Algebraic Data Types = Combination of Product Sets and Sum Sets Unconfirmed = (...) Confirmed = (...) Cancelled = (...) Order = Unconfirmed(...) ⊕ Confirmed(...) ⊕ Cancelled(...) ⊕ Shipping(...) Shipping = (...)

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

58 ©2024 Loglass Inc. Algebraic Data Types = Combination of Product Sets and Sum Sets

Slide 59

Slide 59 text

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.

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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 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 }

Slide 62

Slide 62 text

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 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.

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

67 ©2024 Loglass Inc. By expressing model states with algebraic data types, we were able to prevent data inconsistencies!

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

69 ©2024 Loglass Inc. Express state transitions with types

Slide 70

Slide 70 text

70 ©2024 Loglass Inc. Express state transitions with types

Slide 71

Slide 71 text

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") } } }

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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, ) } }

Slide 75

Slide 75 text

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 }

Slide 76

Slide 76 text

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") } } }

Slide 77

Slide 77 text

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.

Slide 78

Slide 78 text

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?

Slide 79

Slide 79 text

79 ©2024 Loglass Inc. It makes sense from the perspective that rules can be expressed with types.

Slide 80

Slide 80 text

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."

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

82 ©2024 Loglass Inc. Handling Separating Rules from Handling Apply Rules

Slide 83

Slide 83 text

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 ~") }

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

85 ©2024 Loglass Inc. - Which is more important? Apply Rules Handling Separating Rules from Handling

Slide 86

Slide 86 text

86 ©2024 Loglass Inc. - Which is more important? → Applying rules is more important Apply Rules Handling Separating Rules from Handling

Slide 87

Slide 87 text

87 ©2024 Loglass Inc. - How to protect? Protected by Type as much as possible Protected by Test Apply Rules Handling Separating Rules from Handling

Slide 88

Slide 88 text

88 ©2024 Loglass Inc. Considering the Totality of Functions

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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 }

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

96 ©2024 Loglass Inc. Returning to Order Cancellation Cancel Uncomfirmed Exception Comfirmed Cancelled Cancelled Exception Shipping Exception Order Order Input Output Output

Slide 97

Slide 97 text

97 ©2024 Loglass Inc. Cancel Uncomfirmed Comfirmed Cancelled Cncelled Shiping Input Output Returning to Order Cancellation

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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!

Slide 101

Slide 101 text

101 ©2024 Loglass Inc. Summary Incorporate functional programming essence to make DDD more Type-Safe and improve software quality.

Slide 102

Slide 102 text

102 ©2024 Loglass Inc. Summary Too many nullable fieleds

Slide 103

Slide 103 text

103 ©2024 Loglass Inc. Expressing Model States with Types Using Algebraic Data Types Summary

Slide 104

Slide 104 text

104 ©2024 Loglass Inc. Expressing State Transitions with Types Using Total Functions Summary

Slide 105

Slide 105 text

105 ©2024 Loglass Inc. Summary Incorporate functional programming essence to make DDD more Type-Safe and improve software quality.

Slide 106

Slide 106 text

106