$30 off During Our Annual Pro Sale. View Details »

Expressing Business Logic with Types: Functiona...

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

May 31, 2024
Tweet

More Decks by YuitoSato

Other Decks in Technology

Transcript

  1. 1 ©2024 Loglass Inc. Expressing Business Logic with Types: Functional

    DDD for OOP Object-Oriented Conference 2024 Yuito Sato(@Yuiiitoto) Loglass Inc.
  2. 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. 4 ©2024 Loglass Inc. Key Message Incorporate functional programming essence

    to make DDD more Type-Safe and improve software quality.
  4. 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
  5. 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
  6. 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#."
  7. 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?
  8. 9 ©2024 Loglass Inc. Personal Interpretation of Functional DDD Adding

    functional essence to DDD to flexibly express business logic with types.
  9. 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(), )
  10. 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 }
  11. 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
  12. 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
  13. 14 ©2024 Loglass Inc. Three Steps to Detect Implementation Errors

    1. Compile 2. Automated Testing 3. Manual Testing
  14. 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
  15. 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
  16. 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.
  17. 18 ©2024 Loglass Inc. Summary So Far Enhancing DDD with

    type safety to catch implementation errors at the compile phase.
  18. 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
  19. 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.
  20. 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.
  21. 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
  22. 26 ©2024 Loglass Inc. DDD (Domain-Driven Design) is Development method

    that enhances software value through Modeling.
  23. 28 ©2024 Loglass Inc. Modeling of DDD Modeilng - Discuss

    with domain experts to create a domain model Domain Experts
  24. 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]』
  25. 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
  26. 32 ©2024 Loglass Inc. Today’s Subject - Order System Create

    an Order Confirm an Order Start Shipping Cancel an Order
  27. 33 ©2024 Loglass Inc. Domain Model Diagram of the Order

    System (≈ Abstracted Object Diagram)
  28. 38 ©2024 Loglass Inc. Wouldn't it be nice if these

    rule implementation errors could be prevented at compile time?
  29. 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
  30. 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
  31. 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
  32. 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, }
  33. 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
  34. 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
  35. 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
  36. 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.
  37. 47 ©2024 Loglass Inc. So, what should we do? -

    Separate the models for each state.
  38. 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, )
  39. 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(...), ... )
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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 {}
  46. 56 ©2024 Loglass Inc. Algebraic Data Types = Combination of

    Product Sets and Sum Sets Unconfirmed = (...) Confirmed = (...) Cancelled = (...) Order = Unconfirmed(...) ⊕ Confirmed(...) ⊕ Cancelled(...) ⊕ Shipping(...) Shipping = (...)
  47. 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
  48. 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.
  49. 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
  50. 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 }
  51. 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.
  52. 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
  53. 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
  54. 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
  55. 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
  56. 67 ©2024 Loglass Inc. By expressing model states with algebraic

    data types, we were able to prevent data inconsistencies!
  57. 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
  58. 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") } } }
  59. 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
  60. 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
  61. 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, ) } }
  62. 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 }
  63. 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") } } }
  64. 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.
  65. 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?
  66. 79 ©2024 Loglass Inc. It makes sense from the perspective

    that rules can be expressed with types.
  67. 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."
  68. 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
  69. 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 ~") }
  70. 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
  71. 85 ©2024 Loglass Inc. - Which is more important? Apply

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

    Applying rules is more important Apply Rules Handling Separating Rules from Handling
  73. 87 ©2024 Loglass Inc. - How to protect? Protected by

    Type as much as possible Protected by Test Apply Rules Handling Separating Rules from Handling
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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 }
  80. 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
  81. 96 ©2024 Loglass Inc. Returning to Order Cancellation Cancel Uncomfirmed

    Exception Comfirmed Cancelled Cancelled Exception Shipping Exception Order Order Input Output Output
  82. 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
  83. 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
  84. 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!
  85. 101 ©2024 Loglass Inc. Summary Incorporate functional programming essence to

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

    make DDD more Type-Safe and improve software quality.
  87. 106