(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.
3. Functional programming:
what is it good for?
• Mathematical things only
• Interactive & collaborative domain modeling
• Representing a domain model accurately
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?
16. 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
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
* means a pair. Choose one from each type
list type is built in
20. 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
21. 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
22. 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?
31. 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
32. 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
33. In the real world
Suit
Rank
Card
Hand
Deck
Player
Deal
In the code
Suit
Rank
Card
Hand
Deck
Player
Deal
34. 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
35. In the real world
Suit
Rank
Card
Hand
Deck
Player
Deal
In the code
Suit
Rank
Card
Hand
Deck
Player
Deal
PlayerController
DeckBase
AbstractCardProxyFactoryBean
36. 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
37. 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
38. Again: The process of building the
shared mental model is critical!
Collaboration!
52. New types are built from smaller types by:
Composing with “AND”
Composing with “OR”
53. 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
}
54. Snack = or or
Compose with “OR”
type Snack =
| Apple of AppleVariety
| Banana of BananaVariety
| Cherry of CherryVariety
56. 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
57. 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:
58. type EmailAddress = string
type CardNumber = string
In FP you would probably implement by
composing types, like this:
59. type EmailAddress = ...
type CardNumber = …
type CardType = Visa | Mastercard
type CreditCardInfo = {
CardType : CardType
CardNumber : CardNumber
}
60. type EmailAddress = ...
type CardNumber = ...
type CardType = ...
type CreditCardInfo = ...
type PaymentMethod =
| Cash
| PayPal of EmailAddress
| Card of CreditCardInfo
61. type EmailAddress = ...
type CardNumber = ...
type CardType = ...
type CreditCardInfo = ...
type PaymentMethod =
| Cash
| PayPal of EmailAddress
| Card of CreditCardInfo
62. type EmailAddress = ...
type CardNumber = ...
type CardType = ...
type CreditCardInfo = ...
type PaymentMethod =
| Cash
| PayPal of EmailAddress
| Card of CreditCardInfo
type PaymentAmount = decimal
type Currency = EUR | USD
63. 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 }
64. 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 }
72. Null is not the same as “optional”
Length
string –› int
“a”
“b”
“c”
1
2
3
“a”
“b”
“c”
null
73. Null is not the same as “optional”
Length
string –› int
“a”
“b”
“c”
null
1
2
3
74. Null is not allowed
Length
string –› int
“a”
“b”
“c”
null
1
2
3
X
75. A better way for optional values
+
=
“a”
“b”
“c”
“a”
“b”
“c”
missing
or
Tag with “Nothing”
type OptionalString =
| SomeString of string
| Nothing
76. type OptionalInt =
| SomeInt of int
| Nothing
type OptionalString =
| SomeString of string
| Nothing
type OptionalBool =
| SomeBool of bool
| Nothing
Defining optional types
82. 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?"
83. 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
85. 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
86. 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
89. let createEmailAddress (s:string) =
if s.Contains("@")
then Some (EmailAddress s)
else None
createEmailAddress:
string –› EmailAddress option
90. type String50 = String50 of string
let createString50 (s:string) =
if s.Length <= 50
then Some (String50 s)
else None
createString50 :
string –› String50 option
92. type OrderLineQty = OrderLineQty of int
let createOrderLineQty qty =
if qty > 0 && qty <= 99
then Some (OrderLineQty qty)
else None
createOrderLineQty:
int –› OrderLineQty option
99. What about this?
type EmailContactInfo = {
EmailAddress : EmailAddress
IsEmailVerified : bool }
100. • 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 }
101. 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"
102. "Email contact info is either Verified OR Unverified"
type EmailContactInfo =
| Unverified of EmailAddress
| Verified of ???
103. "Email contact info is either Verified OR Unverified"
type EmailContactInfo =
| Unverified of EmailAddress
| Verified of ???
104. 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.
105. "Email contact info is either Verified OR Unverified"
type EmailContactInfo =
| Unverified of EmailAddress
| Verified of VerifiedEmail
106. 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
107. 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?
108. 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?
109. 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?
110. 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!
120. 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"
121. type VerifiedEmail = ...
type SendPasswordReset =
VerifiedEmail -> ...
Business rule: Only send password resets to verified emails
123. type Contact = {
Name: Name
Email: EmailContactInfo
Address: PostalContactInfo
}
124. type Contact = {
Name: Name
Email: EmailContactInfo
Address: PostalContactInfo
}
New rule:
“A contact must have an email or a postal address”
125. type Contact = {
Name: Name
Email: EmailContactInfo option
Address: PostalContactInfo option
}
New rule:
“A contact must have an email or a postal address”
127. “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
128. 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”
131. “A contact must have an email or a postal address”
“A contact must have at least one way of being contacted”
132. “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
133. type ContactInfo =
| Email of EmailContactInfo
| Addr of PostalContactInfo
| Facebook of FacebookInfo
| SMS of PhoneNumber
|Twitter of TwitterId
| Skype of SkypeId
134. 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
135. 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