3 people like it.
Like the snippet!
F# Strategy Pattern
Playing with the OO Strategy Pattern and the F# type system
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
|
(* Having witnessed a painful attempt at the Strategy design pattern I was inspired
to share some of the little knowledge I have.
If you are unsure whether you got the pattern right you should always ask yourself
the question: What problem is it trying to solve?
If you are not solving the following problem, then it isn't called the Strategy pattern:
How can I modify the behavior of a class (even at run-time)
without having to change its implementation.
The general idea is to parameterize the class with the implementation of another interface
passed in via the constructor.
Our class then delegates the behavior to the implementation it's being passed: *)
type IMakeNoise =
abstract Talk: Animal -> unit
and Animal(name: string, makeNoise: IMakeNoise) =
member x.Name = name
member this.Talk() = makeNoise.Talk this
type FrogNoise() =
interface IMakeNoise with
member x.Talk frog = printfn "%s croaks" frog.Name
type MouseNoise() =
interface IMakeNoise with
member x.Talk _ = printfn "A mouse squeals"
let frog = Animal("Billy", FrogNoise())
let mouse = Animal("", MouseNoise())
frog.Talk()
mouse.Talk()
(* Notice a few things:
- We have a single implementation of animals and adapting the behavior of the animal
talking will not require a change to its implementation.
- We have the choice of defining the interface IMakeNoise with or without accepting
the Animal instance it will act for:
- Passing the instance to the talk function makes it more flexible,
notice how I couldn't have implemented the "nameless" mouse squeal
without changing our Animal implementation otherwise.
- On the other hand, this requires a cycle, for which I invite you
to read Scott Wlaschin's excellent serie on the subject:
http://fsharpforfunandprofit.com/series/dependency-cycles.html
Making the interface generic breaks the cycle, is more flexible and preserves type safety: *)
type IMakeNoise<'T> =
abstract Talk: 'T -> unit
type Creature(name: string, makeNoise: IMakeNoise<Creature>) =
member __.Name = name
member this.Talk() = makeNoise.Talk this
(* A great feature of the F# type system is Object Expression which allow you to instantiate
an object/interface without the need for an explicit class definition: *)
let horse =
let makeNoise =
{
new IMakeNoise<Creature> with
member __.Talk horse = printfn "%s snorts" horse.Name
}
Creature("Fred", makeNoise)
horse.Talk()
(* We said that our class *delegates* the responsibility to an implementation of IMakeNoise.
This is where the difference between the Delegation and Strategy pattern lies,
wherea the Delegation pattern has our class instantiate the class to which it will delegate
the work, the Strategy pattern has the implementation passed in at construction.
Being more flexible it can also be done at run-time making our code
less vulnerable to evolving requirements.
Another concept we know from C# called a delegate which is a poor man's function type. Our
IMakeNoise interface defines a single method with a fixed signature and can thus be expressed
without having to rely on the interface at all: Creature -> Unit *)
type Monster(name: string, makeNoise: Monster -> unit) =
member __.Name = name
member x.Talk() = makeNoise x
let troll = Monster("Grendel", fun troll -> printfn "%s bellows" troll.Name)
troll.Talk()
(* Object Expression can push the limit further switching the class to an interface.
We can also untangle the constructor from the type and still avoid any code duplication.
Notice how you could squeeze a Factory pattern for free right there. *)
type IMonster =
abstract Name: string
abstract Talk: unit -> unit
let monster name makeNoise =
{
new IMonster with
member __.Name = name
member x.Talk() = makeNoise x
}
let dragon = monster "Onyxia" (fun d -> printfn "%s roars" d.Name)
let whelp = monster "" (fun _ -> printfn "The whelp screeches")
dragon.Talk()
whelp.Talk()
(* You can see how this is at least as flexible and we have maintained type safety.
Records also allows us to untangle the type and a fixed class *)
type Beast =
{
Name: string
Talk: unit -> unit
}
let beast name makeNoise =
{
Name = name
Talk = fun () -> makeNoise name
}
let thunders = printfn "%s thunders"
let hulk = beast "The Hulk" thunders
hulk.Talk()
(* At this point you really have to squint to see an OO design pattern as this is much
closer to functional programming.
In fact functional programmers tend not to label this at all since programming with
higher order function is simply called "Programming".
Some might argue this is still object oriented as an F# Record is implemented as a
class under the cover. There is also the fact that our beast instance closes over our
thunders function.
One should notice that inheritance isn't possible when using the Record, on the other
hand many good OO programmers would argue that inheritance is misused more often than
not. And being a type as opposed to a class, the record allows for multiple implementations.
Mutation whose bad usage tend to outnumber the good ones 10 to 1 isn't OO per-se but
is frequently seen as OO's natural companion. Without making it impossible, Records
thwart you from using mutation which will generally lead you to better designs.
A commonly seen case for mutation gone awry that also shows poor understanding of Design
patterns is when you see a factory that generates instances of a class who's been endowed
with a parameterless constructor and uses mutable properties to set the instance's state.
This is an obvious misuse of the pattern as one of the roles of the constructor is to prevent
the creation of instances in an invalid state. *)
|
abstract member IMakeNoise.Talk : Animal -> unit
Full name: Script.IMakeNoise.Talk
Multiple items
type Animal =
new : name:string * makeNoise:IMakeNoise -> Animal
member Talk : unit -> unit
member Name : string
Full name: Script.Animal
--------------------
new : name:string * makeNoise:IMakeNoise -> Animal
type unit = Unit
Full name: Microsoft.FSharp.Core.unit
val name : string
Multiple items
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
val makeNoise : IMakeNoise
type IMakeNoise =
interface
abstract member Talk : Animal -> unit
end
Full name: Script.IMakeNoise
val x : Animal
member Animal.Name : string
Full name: Script.Animal.Name
val this : Animal
member Animal.Talk : unit -> unit
Full name: Script.Animal.Talk
abstract member IMakeNoise.Talk : Animal -> unit
Multiple items
type FrogNoise =
interface IMakeNoise
new : unit -> FrogNoise
Full name: Script.FrogNoise
--------------------
new : unit -> FrogNoise
val x : FrogNoise
override FrogNoise.Talk : frog:Animal -> unit
Full name: Script.FrogNoise.Talk
val frog : Animal
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
property Animal.Name: string
Multiple items
type MouseNoise =
interface IMakeNoise
new : unit -> MouseNoise
Full name: Script.MouseNoise
--------------------
new : unit -> MouseNoise
val x : MouseNoise
override MouseNoise.Talk : Animal -> unit
Full name: Script.MouseNoise.Talk
val frog : Animal
Full name: Script.frog
val mouse : Animal
Full name: Script.mouse
member Animal.Talk : unit -> unit
Multiple items
type IMakeNoise =
interface
abstract member Talk : Animal -> unit
end
Full name: Script.IMakeNoise
--------------------
type IMakeNoise<'T> =
interface
abstract member Talk : 'T -> unit
end
Full name: Script.IMakeNoise<_>
abstract member IMakeNoise.Talk : 'T -> unit
Full name: Script.IMakeNoise`1.Talk
Multiple items
type Creature =
new : name:string * makeNoise:IMakeNoise<Creature> -> Creature
member Talk : unit -> unit
member Name : string
Full name: Script.Creature
--------------------
new : name:string * makeNoise:IMakeNoise<Creature> -> Creature
val makeNoise : IMakeNoise<Creature>
member Creature.Name : string
Full name: Script.Creature.Name
val this : Creature
member Creature.Talk : unit -> unit
Full name: Script.Creature.Talk
abstract member IMakeNoise.Talk : 'T -> unit
val horse : Creature
Full name: Script.horse
val horse : Creature
property Creature.Name: string
member Creature.Talk : unit -> unit
Multiple items
type Monster =
new : name:string * makeNoise:(Monster -> unit) -> Monster
member Talk : unit -> unit
member Name : string
Full name: Script.Monster
--------------------
new : name:string * makeNoise:(Monster -> unit) -> Monster
val makeNoise : (Monster -> unit)
member Monster.Name : string
Full name: Script.Monster.Name
val x : Monster
member Monster.Talk : unit -> unit
Full name: Script.Monster.Talk
val troll : Monster
Full name: Script.troll
val troll : Monster
property Monster.Name: string
member Monster.Talk : unit -> unit
type IMonster =
interface
abstract member Talk : unit -> unit
abstract member Name : string
end
Full name: Script.IMonster
abstract member IMonster.Name : string
Full name: Script.IMonster.Name
abstract member IMonster.Talk : unit -> unit
Full name: Script.IMonster.Talk
val monster : name:string -> makeNoise:(IMonster -> unit) -> IMonster
Full name: Script.monster
val makeNoise : (IMonster -> unit)
val x : IMonster
abstract member IMonster.Talk : unit -> unit
val dragon : IMonster
Full name: Script.dragon
val d : IMonster
property IMonster.Name: string
val whelp : IMonster
Full name: Script.whelp
type Beast =
{Name: string;
Talk: unit -> unit;}
Full name: Script.Beast
Beast.Name: string
Beast.Talk: unit -> unit
val beast : name:string -> makeNoise:(string -> unit) -> Beast
Full name: Script.beast
val makeNoise : (string -> unit)
val thunders : (string -> unit)
Full name: Script.thunders
val hulk : Beast
Full name: Script.hulk
More information