Successfully reported this slideshow.
Your SlideShare is downloading. ×

Domain Modeling Made Functional (DevTernity 2022)

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 136 Ad

Domain Modeling Made Functional (DevTernity 2022)

Download to read offline

(video at https://fsharpforfunandprofit.com/ddd/)

Statically typed functional programming languages encourage a very different way of thinking about types. The type system is your friend, not an annoyance, and can be used in many ways that might not be familiar to OO programmers. Types can be used to represent the domain in a fine-grained, self documenting way. And in many cases, types can even be used to encode business rules so that you literally cannot create incorrect code. You can then use the static type checking almost as an instant unit test — making sure that your code is correct at compile time. In this talk, we'll look at some of the ways you can use types as part of a domain driven design process, with some simple real world examples in F#. No jargon, no maths, and no prior F# experience necessary.

(video at https://fsharpforfunandprofit.com/ddd/)

Statically typed functional programming languages encourage a very different way of thinking about types. The type system is your friend, not an annoyance, and can be used in many ways that might not be familiar to OO programmers. Types can be used to represent the domain in a fine-grained, self documenting way. And in many cases, types can even be used to encode business rules so that you literally cannot create incorrect code. You can then use the static type checking almost as an instant unit test — making sure that your code is correct at compile time. In this talk, we'll look at some of the ways you can use types as part of a domain driven design process, with some simple real world examples in F#. No jargon, no maths, and no prior F# experience necessary.

Advertisement
Advertisement

More Related Content

More from Scott Wlaschin (20)

Recently uploaded (20)

Advertisement

Domain Modeling Made Functional (DevTernity 2022)

  1. 1. Domain Modeling Made Functional (DevTernity 2022) @ScottWlaschin fsharpforfunandprofit.com
  2. 2. Functional programming: what is it good for? • Mathematical things only
  3. 3. Functional programming: what is it good for? • Mathematical things only • Interactive & collaborative domain modeling • Representing a domain model accurately
  4. 4. Functional Programming Domain Driven Design
  5. 5. Part I The importance of design
  6. 6. Input Output Process The software development process
  7. 7. Input Output Process The software development process
  8. 8. Input Output Process Garbage in Garbage out The software development process
  9. 9. Input Output Process (reduce) Garbage in (reduced) Garbage out The software development process
  10. 10. • Borrow from Agile and DDD • Agile contribution: – Rapid feedback during design • DDD contribution: – Stakeholders have a shared mental model – …which is also represented in the code How can we do design right?
  11. 11. Can you really make code represent the domain?
  12. 12. What non-developers think source code looks like and some C developers!
  13. 13. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank =Two |Three | Four | Five | Six | Seven | Eight | Nine |Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand Shared language What DDD source code should look like
  14. 14. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank =Two |Three | Four | Five | Six | Seven | Eight | Nine |Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand * means a pair. Choose one from each type list type is built in
  15. 15. Deal (original) Deck (remaining) Deck (on table) Card Modeling an action with a function type Deal = Deck -> (Deck * Card) Input Output
  16. 16. Pickup Card (updated) Hand (original) Hand (on table) Card Modeling an action with a function type PickupCard = (Hand * Card) –> Hand Input Output
  17. 17. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank =Two |Three | Four | Five | Six | Seven | Eight | Nine |Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand
  18. 18. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank =Two |Three | Four | Five | Six | Seven | Eight | Nine |Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand
  19. 19. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank =Two |Three | Four | Five | Six | Seven | Eight | Nine |Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand Can non-programmers provide useful feedback?
  20. 20. Rapid feedback during the design stage
  21. 21. Building a shared mental model is an interactive process
  22. 22. ... type Deck = Card list type Deal = Deck –› (Deck * Card)
  23. 23. ... type Deck = Card list type Deal = ShuffledDeck –› (ShuffledDeck * Card)
  24. 24. ... type Deck = Card list type Deal = ShuffledDeck –› (ShuffledDeck * Card) type ShuffledDeck = Card list
  25. 25. ... type Deck = Card list type Deal = ShuffledDeck –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck
  26. 26. ... type Deck = Card list type Deal = ShuffledDeck –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck
  27. 27. Final version of the domain
  28. 28. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank = Two |Three | Four | Five | Six | Seven | Eight | ... type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = ShuffledDeck –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck type PickupCard = (Hand * Card) –› Hand
  29. 29. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank = Two |Three | Four | Five | Six | Seven | Eight | ... type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = ShuffledDeck –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck type PickupCard = (Hand * Card) –› Hand
  30. 30. In the real world Suit Rank Card Hand Deck Player Deal In the code Suit Rank Card Hand Deck Player Deal
  31. 31. In the real world Suit Rank Card Hand Deck Player Deal In the code Suit Rank Card Hand Deck Player Deal ShuffledDeck Shuffle ShuffledDeck Shuffle 
  32. 32. In the real world Suit Rank Card Hand Deck Player Deal In the code Suit Rank Card Hand Deck Player Deal PlayerController DeckBase AbstractCardProxyFactoryBean 
  33. 33. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank = Two |Three | Four | Five | Six | Seven | Eight | ... type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = ShuffledDeck –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck type PickupCard = (Hand * Card) –› Hand
  34. 34. module CardGame = type Suit = Club | Diamond | Spade | Heart type Rank = Two |Three | Four | Five | Six | Seven | Eight | ... type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = ShuffledDeck –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck type PickupCard = (Hand * Card) –› Hand
  35. 35. Again: The process of building the shared mental model is critical! Collaboration!
  36. 36. Key DDD principle: Communicate the design in the code
  37. 37. A domain modeling challenge!
  38. 38. type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool } // true if ownership of // email address is confirmed
  39. 39. type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool } Prologue: which values are optional?
  40. 40. type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool } Prologue: what are the constraints?
  41. 41. type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool } Prologue: domain logic?
  42. 42. Prologue: F# can help type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool }
  43. 43. Prologue: F# can help type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool }
  44. 44. Part II Understanding FP type systems
  45. 45. FP principle: Composition everywhere
  46. 46. Composition = Make big things from small things
  47. 47. Algebraic type system
  48. 48. New types are built from smaller types by: Composing with “AND” Composing with “OR”
  49. 49. All languages have this. Example: pairs, tuples, records FruitSalad = One each of and and Compose with “AND” type FruitSalad = { Apple: AppleVariety Banana: BananaVariety Cherry: CherryVariety }
  50. 50. Snack = or or Compose with “OR” type Snack = | Apple of AppleVariety | Banana of BananaVariety | Cherry of CherryVariety
  51. 51. A real world example of composing types
  52. 52. Some requirements: We accept three forms of payment: Cash, PayPal, or Card. For Cash we don't need any extra information For PayPal we need a email address For Cards we need a card type and card number
  53. 53. interface IPaymentMethod {..} class Cash() : IPaymentMethod {..} class PayPal(string emailAddress): IPaymentMethod {..} class Card(string cardType, string cardNo) : IPaymentMethod {..} In OO design you would probably implement it as an interface and a set of subclasses, like this:
  54. 54. type EmailAddress = string type CardNumber = string In FP you would probably implement by composing types, like this:
  55. 55. type EmailAddress = ... type CardNumber = … type CardType = Visa | Mastercard type CreditCardInfo = { CardType : CardType CardNumber : CardNumber }
  56. 56. type EmailAddress = ... type CardNumber = ... type CardType = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo
  57. 57. type EmailAddress = ... type CardNumber = ... type CardType = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo
  58. 58. type EmailAddress = ... type CardNumber = ... type CardType = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo type PaymentAmount = decimal type Currency = EUR | USD
  59. 59. type EmailAddress = ... type CardNumber = ... type CardType = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo type PaymentAmount = decimal type Currency = EUR | USD type Payment = { Amount : PaymentAmount Currency : Currency Method : PaymentMethod }
  60. 60. type EmailAddress = ... type CardNumber = ... type CardType = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo type PaymentAmount = decimal type Currency = EUR | USD type Payment = { Amount : PaymentAmount Currency : Currency Method : PaymentMethod }
  61. 61. Part III Domain modeling with composable types
  62. 62. type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool } // true if ownership of // email address is confirmed This looks suspiciously like database-driven design...
  63. 63. type Contact = { Name: PersonalName Email: EmailContactInfo } "A contact has a name AND email info" "Like this?..."
  64. 64. type Contact = { Name: PersonalName Email: EmailContactInfo } "A contact has a name AND email info" We have two new concepts already!
  65. 65. type PersonalName = { FirstName: string MiddleInitial: string LastName: string } "What's a personal name?"
  66. 66. type PersonalName = { FirstName: string MiddleInitial: string LastName: string } required required optional "What's required or optional?"
  67. 67. Modeling optional values
  68. 68. Null is not the same as “optional” Length string –› int “a” “b” “c” 1 2 3 “a” “b” “c” null
  69. 69. Null is not the same as “optional” Length string –› int “a” “b” “c” null 1 2 3
  70. 70. Null is not allowed Length string –› int “a” “b” “c” null 1 2 3 X
  71. 71. A better way for optional values + = “a” “b” “c” “a” “b” “c” missing or Tag with “Nothing” type OptionalString = | SomeString of string | Nothing
  72. 72. type OptionalInt = | SomeInt of int | Nothing type OptionalString = | SomeString of string | Nothing type OptionalBool = | SomeBool of bool | Nothing Defining optional types
  73. 73. type Option<'T> = | Some of 'T | None
  74. 74. type Option<'T> = | Some of 'T | None
  75. 75. type PersonalName = { FirstName: string MiddleInitial: Option<string> LastName: string }
  76. 76. type PersonalName = { FirstName: string MiddleInitial: string option LastName: string }
  77. 77. Modeling simple values and constrained values
  78. 78. Modeling simple values • Avoid "Primitive Obsession" • Simple values should not be modelled with primitive types like "int" or "string" or "float" "Does 'float' have something to do with water?"
  79. 79. Modeling constrained values • It's rare to have an unconstrained int or string: – An EmailAddress must not be empty, it must match a pattern – A PhoneNumber must not be empty, it must match a pattern – A CustomerId must be a positive integer
  80. 80. Is an EmailAddress just a string? No! Is a CustomerId just a int? No!
  81. 81. type EmailAddress = EmailAddress of string Use wrapper types to keep domain concepts distinct from their representation type CustomerId = CustomerId of int type String50 = String50 of string
  82. 82. type EmailAddress = EmailAddress of string type PhoneNumber = PhoneNumber of string type CustomerId = CustomerId of int type OrderId = OrderId of int Two benefits: - Clearer domain modelling - Can't mix them up accidentally
  83. 83. Implementing constructors for constrained types
  84. 84. let createEmailAddress (s:string) = if s.Contains("@") then (EmailAddress s) else ? createEmailAddress: string –› EmailAddress
  85. 85. let createEmailAddress (s:string) = if s.Contains("@") then Some (EmailAddress s) else None createEmailAddress: string –› EmailAddress option
  86. 86. type String50 = String50 of string let createString50 (s:string) = if s.Length <= 50 then Some (String50 s) else None createString50 : string –› String50 option
  87. 87. + – 999999 Qty: Add To Cart
  88. 88. type OrderLineQty = OrderLineQty of int let createOrderLineQty qty = if qty > 0 && qty <= 99 then Some (OrderLineQty qty) else None createOrderLineQty: int –› OrderLineQty option
  89. 89. The "Contact" challenge, after first refactor
  90. 90. type Contact = { FirstName: string MiddleInitial: string LastName: string EmailAddress: string IsEmailVerified: bool }
  91. 91. type Contact = { FirstName: string MiddleInitial: string option LastName: string EmailAddress: string IsEmailVerified: bool } Use option type for potentially missing values
  92. 92. type Contact = { FirstName: String50 MiddleInitial: String1 option LastName: String50 EmailAddress: EmailAddress IsEmailVerified: bool } Use wrapper types instead of primitives
  93. 93. type PersonalName = { FirstName : String50 MiddleInitial : String1 option LastName : String50 } type EmailContactInfo = { EmailAddress : EmailAddress IsEmailVerified : bool } Also acts as "consistency boundaries" 2 different domain concepts
  94. 94. Replacing flags with choices
  95. 95. What about this? type EmailContactInfo = { EmailAddress : EmailAddress IsEmailVerified : bool }
  96. 96. • Rule 1: If the email is changed, the verified flag must be reset to false. • Rule 2: The verified flag can only be set by a special verification service type EmailContactInfo = { EmailAddress : EmailAddress IsEmailVerified : bool }
  97. 97. Listen closely to what the domain expert says... type EmailContactInfo = | Unverified of EmailAddress | Verified of ??? So model it as a choice "An email address is either Verified OR Unverified"
  98. 98. "Email contact info is either Verified OR Unverified" type EmailContactInfo = | Unverified of EmailAddress | Verified of ???
  99. 99. "Email contact info is either Verified OR Unverified" type EmailContactInfo = | Unverified of EmailAddress | Verified of ???
  100. 100. type VerifiedEmail = VerifiedEmail of EmailAddress "there is no problem that can’t be solved by wrapping it in another type" Q: Is a verified email different from an unverified email? Are there different business rules? A: Yes, it must not be mixed up with unverified.
  101. 101. "Email contact info is either Verified OR Unverified" type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  102. 102. type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option So model it as a function Q: Where do we get Verified emails from? A: A special verification process
  103. 103. type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option Q: Are the business rules clear now?
  104. 104. type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option Q: Are the business rules clear now?
  105. 105. type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option Q: Are the business rules clear now?
  106. 106. type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option Those business rules are automatically enforced by the design!
  107. 107. The "Contact" challenge, completed
  108. 108. type EmailAddress = ... type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail type PersonalName = { FirstName: String50 MiddleInitial: String1 opt LastName: String50 } type Contact = { Name: PersonalName Email: EmailContactInfo }
  109. 109. type EmailAddress = ... type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail type PersonalName = { FirstName: String50 MiddleInitial: String1 opt LastName: String50 } type Contact = { Name: PersonalName Email: EmailContactInfo }
  110. 110. type PersonalName = { FirstName: String50 MiddleInitial: String1 opt LastName: String50 } type Contact = { Name: PersonalName Email: EmailContactInfo } type EmailAddress = ... type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  111. 111. type PersonalName = { FirstName: String50 MiddleInitial: String1 opt LastName: String50 } type Contact = { Name: PersonalName Email: EmailContactInfo } type EmailAddress = ... type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  112. 112. type PersonalName = { FirstName: String50 MiddleInitial: String1 opt LastName: String50 } type Contact = { Name: PersonalName Email: EmailContactInfo } type EmailAddress = ... type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  113. 113. type PersonalName = { FirstName: String50 MiddleInitial: String1 opt LastName: String50 } type Contact = { Name: PersonalName Email: EmailContactInfo } type EmailAddress = ... type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  114. 114. type PersonalName = { FirstName: String50 MiddleInitial: String1 opt LastName: String50 } type Contact = { Name: PersonalName Email: EmailContactInfo } type EmailAddress = ... type VerifiedEmail = VerifiedEmail of EmailAddress type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  115. 115. Refactoring towards deeper insight
  116. 116. type SendPasswordReset = EmailAddress -> ... If emailAddress.IsVerified then send password else error "this should never happen" Business rule: Only send password resets to verified emails "this should never happen" 
  117. 117. type VerifiedEmail = ... type SendPasswordReset = VerifiedEmail -> ... Business rule: Only send password resets to verified emails
  118. 118. Part IV Encoding business rules with types "Make illegal states unrepresentable!" –Yaron Minsky
  119. 119. type Contact = { Name: Name Email: EmailContactInfo Address: PostalContactInfo }
  120. 120. type Contact = { Name: Name Email: EmailContactInfo Address: PostalContactInfo } New rule: “A contact must have an email or a postal address”
  121. 121. type Contact = { Name: Name Email: EmailContactInfo option Address: PostalContactInfo option } New rule: “A contact must have an email or a postal address”
  122. 122. Let’s make illegal states unrepresentable!
  123. 123. “A contact must have an email or a postal address” implies: • email address only, or • postal address only, or • both email address and postal address
  124. 124. type ContactInfo = | EmailOnly of EmailContactInfo | AddrOnly of PostalContactInfo | EmailAndAddr of EmailContactInfo * PostalContactInfo type Contact = { Name: Name ContactInfo : ContactInfo } “A contact must have an email or a postal address”
  125. 125. Composable types are almost as awesome as this
  126. 126. Collaboration is two-way. It's OK to push back
  127. 127. “A contact must have an email or a postal address” “A contact must have at least one way of being contacted”
  128. 128. “A contact must have at least one way of being contacted” type Contact = { Name: Name PrimaryContactInfo: ContactInfo SecondaryContactInfo: ContactInfo option } type ContactInfo = | Email of EmailContactInfo | Addr of PostalContactInfo
  129. 129. type ContactInfo = | Email of EmailContactInfo | Addr of PostalContactInfo | Facebook of FacebookInfo | SMS of PhoneNumber |Twitter of TwitterId | Skype of SkypeId
  130. 130. Summary • Represent the shared mental model in code – The developers should become domain experts too – Design collaboratively to build the shared mental model • Designs will evolve – Embrace change. This is not Big Design Up Front – Refactor towards deeper insight – Static types give you confidence to make changes
  131. 131. Summary • Use the power of a composable type system – Choices rather than inheritance – Options instead of null – Avoid primitive types – Model constraints with new types – Make illegal states unrepresentable
  132. 132. Slides and video at fsharpforfunandprofit.com/ddd Thanks!

×