Modelleren en Systeemontwikkeling

advertisement
Design Pattern 1
[Eric Freeman & Elisabeth Freeman 1 – 5 ]
Design of “simDuck” , 1st attempt
fly()
Client
<<use>>
WildDuck
display()
Duck
display()
quack()
RedHeadDuck
display()
Inheritance allows you to reuse code, but also
“forces” attributes and behavior to the subclasses.
RubberDuck
display()
fly()
DecoyDuck
display()
quack()
fly()
rubber doesn’t fly; decoy doesn’t quack nor fly.
• Maintenance is expensive!
• Software evolves; is your design flexible enough??
2
2nd attempt: how about factoring out to
multiple interfaces?
Duck
display()
WildDuck
display()
quack()
fly()
•
•
<<interface>>
QuackBehavior
quack()
RedHeadDuck
display()
quack()
fly()
RubberDuck
display()
quack()
<<interface>>
FlyBehavior
fly()
DecoyDuck
display()
Unfortunately, now you lose code reuse; bad for
maintenance ...
Use multiple inheritance instead? Well, not supported in
all OO languages.
3
FF proposes a number of “design principles”
• Separate and encapsulate varying aspects from constant
ones
– “varying” across instances
• Program against “interface” rather than implementation
– relying on the signature of a superclass allow you the flexibility
to replace its instances with those of subclasses
– for extensibility
• Consider using “composition” over inheritance as an
option
4
Separating and encapsulating quack and fly
has
Duck
display()
quack()
fly()
WildDuck
display()
RedHeadDuck
display()
<<interface>>
QuackBehavior
quack()
has
RubberDuck
display()
...
<<interface>>
FlyBehavior
fly()
StandardQuack
quack()
StadardFly
fly()
Squeak
quack()
noFly
fly()
...
quack() { quackbehavior.quack() }
Programming against “interface”; Duck does not care which
actual quack-behavior you supply; any subclass is just as
good.
5
Composition instead of inheritence
flybehavior
has
Duck
display()
quack()
fly()
WildDuck
display()
RedHeadDuck
display()
<<interface>>
QuackBehavior
quack()
has
RubberDuck
display()
...
<<interface>>
FlyBehavior
fly()
StandardQuack
quack()
StadardFly
fly()
Squeak
quack()
NoFly
fly()
...
Duck now gets some behavior from
“composition” rather than inheritance.
Composition also has the advantage of
“can be changed dynamically” (but on
the other hand you lose some static
checking).
changeFly(f : FlyBehavior) {
flybehavior = f
}
6
Design pattern
Strategy-1
algorithm(...)
“Strategy Pattern”
SomeClass
some_method(...)
<<interface>>
Strategy
algorithm(...)
Strategy-2
algorithm(...)
...
• Is a solution “pattern” for a certain problem; in OO typically
problems around the “flexibility” of your design.
• Unfortunately often cannot be implemented as library, nor do
we have a satisfactory formalization of them.
• Also (very) useful as common vocab for engineers to
communicate solutions.
7
Books
1995, “Gang of 4”
Catalog of 23 patterns
Classic book in SE, sold over 0.5 M copies.
2004
Eric Freeman & Elisabeth Freeman
Much better explanation, good reviews!
(Some disagreement)
Do read it with “open mind”
8
A bit unortodox...
9
Weather “plugins”
d:
WeatherDisplay
get data
s : Weather
Station
w:
WeatherDataPro
vider
provides live data on temp,
humidity, pressure.
Three kinds of displays:
• current weather
• weather statistics
• prediction
Have to be regularly updated.
10
Design
CurrentWeatherDisplay
update(t,h,p)
WeatherDataProvider
- temp
- humidity
- pressure
notify()
setMeasurement(t,h,p)
temp = t
humidity = h
pressure = p
notify()
WeatherStatDisplay
update(t,h,p)
ForecastDisplay
update(t,h,p)
currentWeather.update(temp,humidity,pressure)
weatherStat.update(temp,humidity,pressure)
forecast.update(temp,humidity,pressure)
11
Observer pattern
Subject
addObserver (o)
removeObserver(o)
notifyObservers()
setState(...)
subject
observers
Observer
* update(data)
0..1
obs1
subject
client
setState(..)
obs2
notifyObservers(..)
update(data)
update(data)
12
Should we push data, or let observers pull
them?
Subject
addObserver (o)
removeObserver(o)
notifyObservers()
setState(...)
getState() : ...
client
subject
s : Subject
setState(..)
Observer
update(data)
* update(subject)
observers
obs
notifyObservers(..)
update(s)
s.getState ()
data
do something with data...
13
Abstract or interface?
Subject
addObserver (o)
removeObserver(o)
notifyObservers()
setState(...)
getState()
<<interface>>
Observer
update(data)
* update(subject)
ObserverImpl 1
SubjectImpl 1
ObserverImpl 2
SubjectImpl 2
Nice, so you can instantiate this pattern by subclassing it!
But ….
Doesn’t work if you don’t have multiple inheritance 
Making Subject an <<interface>> does not solve the problem (now we can’t inherit!).
Puzzle for you: propose a solution for this.
14
Java provides the pattern as “classes” !
Observable
addObserver (obsvr)
removeObserver(obsvr)
notifyObservers()
notifyObservers(data)
setChanged()
* <<interface>>
Observer
update(o : Observable, data: Object)
WeatherDisplay
WeatherData
Provider
• Good thing about this is that you can use the
pattern through subclassing/impl.
• Does have its limitation...
CurrentWeatherDisp
WeatherStatDisp
ForecastDisp
15
Starbuzz Coffee, initial design
But what if the business expands
and wishes to offer more choices;
e.g. with condiments
steamed milk, soy , mocha,
whipped cream ?
Beverage
desc
cost()
Espresso
cost()
Darkroast
cost()
Each implement its own cost calculation
Decaf
cost()
Houseblend
cost()
Pure inheritance-based approach:
SoyDarkroastWithWhip
cost()
16
Maintenance??
17
Ok, so that not good … ; how about this:
Beverage
desc
milk
cocoa
soy
whip
cost()
Espresso
cost()
Darkroast
cost()
Decaf
cost()
Houseblend
cost()
Name change factors that may impact this design:
• Change in condiment prices
• You want to add new condiments
• New kind of beverage (e.g. tea)  some condiments
are inappropriate
• How about double mocha?
Open-closed principle: classes should ideally open for extension
but closed for modification.
18
Ideas
• Factor out varying factors; exploit compositions:
Beverage
Milk
Soy
Cocoa
Whip
• Make condiment a feature you can wrap around a beverage:
Whip
d : Darkroast
• Beter yet, make them stackable:
d : Darkroast
19
Decorator pattern
Beverage
Client
<<use>>
Component
operation(…)
Condiment
Concrete Component
operation(…)
Esperesso, Darkroast,
Decaf
Decorator
operation(…)
ConcreteDecorator A
operation(…)
ConcreteDecorator B
operation(…)
Milk, Cocoa, Whip
20
Distributing the behavior
Beverage
cost()
Espresso
cost()
component
class Espresso extends Beverage {
cost() { return 1.50 euro }
}
Condiment
Cocoa
cost()
Whip
cost()
class Cocoa extends Condiment {
component : Beverage
Cocoa (b:Beverage) { component = b }
cost() { return b.cost() + 50 cent }
}
How to make double mocha espresso :
cost
new Cocoa( new Cocoa (new Espresso())
cost
e : esperesso
Notice how the “cost” functionality propagates over your decorators-stack.
How about adding “Tea” and “rum” ? Can we keep the Open-Closed principle?
21
Real world decorators: Java I/O
<<abstract>>
InputStream
FileInputStream
ByteArrayInputStream
<<abstract>>
FilterInputStream
BufferedInputStream
DataInputStream
LineNumberInputStream (Depracated!)
So, you can stack your InputStream decorators.
And even make your own decorators (by subclassing FilterInputStream)
22
Your question
Beverage
cost()
Beverage
cost()
Espresso
cost()
Condiment
VS
Espresso
cost()
0..1
0..1
Condiment
Cocoa
cost()
Cocoa
cost()
Whip
cost()
Whip
cost()
In the Right solution, cost() in Beverage has to anticipate the cost-logic of condiments. This
might do:
cost() { return 1.50 + condiment.cost() }
but this already presumes that cost should be just additive.
Whereas in the Decorator-solution, each condiment can use and override the cost-logic of its
beverage-base.
23
Late/dynamic binding
• Many OO languages allows the type of the object created to
be decided “late” (at the run-time)
cookPizza(availableBudget) {
Pizza p
if availableBudget < 10 euro then p = new MargheritaPizza()
else p = new PeperoniPizza()
p.cook()
return p
}
• Implies that behavior can also be bound dynamically
• Pro: gives you a lot of flexibility
• Cons: you lose some static checking
24
Pizza store
PizzaStore
view()
order()
view(type)
order(type)
Pizza
view()
prepare()
bake()
box()
A well-proven “order” algorithm:
order() {
p = new Pizza()
p.prepare()
p.bake()
p.box()
return p
}
Margherita
Peperoni
Now we want to add more types of pizzas,
and be able to order different types of
pizzas.
Veggie
order(type) {
if type=“Margherita” p = new Margherita()
else if type=“Peperoni” p = new Peperoni()
else p = new Veggie()
p.prepare()
p.bake()
p.box()
return p
}
• Similar creation routine for view(type)
•As you can see, object creation can involve a more complex logic.
• The above works… but notice that you program against
implementation; disfavoring flexibility…
25
Separating and encapsulating the “sub-type dependency”part
Margherita
PizzaStore
view(type)
order(type)
PizzaFactory
createPizza(type)
Pizza
view()
prepare()
bake()
box()
order(type) {
p = factory.createPizza(type)
p.prepare()
p.bake()
p.box()
return p
}
Peperoni
Veggie
createPizza(type) {
if type=“Margherita” p = new Margherita()
else if type=“Peperoni” p = new Peperoni()
else p = new Veggie()
return p
}
Aren’t we just moving piece of code around?? Yes, but note that there were multiple places
(order & view) that need “createPizza” ; now we have put the behavior in a single place.
26
“Factory”
PizzaFactory
createPizza(type)
• A “factory” encapsulates a subtype-dependent object
creation in one place.
• “PizzaFactory” is a factory class
• But actually, it is the operation “createPizza” that does the
work  factory method
• We can also put this method elsewhere, e.g. we could have
put “createPizza” in the pizza class.
• We’ll see an example of a “factory method solution” next
27
Let’s now franchise the pizza store…
• NY and Chicago want to franchise  ok, we just create instances of PizzaStore
for them.
• After sometime, the branches want to introduce their own “local” variants :
• NY pizzas: thin crust, tasty sauce, light cheese
• Chicago pizzas: thick crust, rich sauce, much cheese
PizzaStore
view(type)
order(type)
NYPizzaStore
view(type)
order(type)
ChicPizzaStore
view(type)
order(type)
PizzaFactory
NYPizzaFactory
ChicPizzaFactory
override order to select the
right pizza factory
order(type){ factory = new NYPizzaFactory() ; super.order(type) }
But now it may be tempting for the branches to override the standard “order
algorithm”, e.g. to use local boxes to pack the pizzas. What if we want to impose
more control on this?
28
Fixing the “order”, and putting a place holder for the
variating part..
<<abstract>>
PizzaStore
order(type) // final ?
createPizza(type) : Pizza // abs
NYPizzaStore
createPizza(type)
ChicPizzaStore
createPizza(type)
We’ll fix the logic of “order”:
order(type) {
p = createPizza(type)
p.prepare() ; p.bake()
p.box()
return p
}
Pizza
prepare()
bake()
box()
NYMargherita
NYPeperoni
NYVeggie
ChicMargherita
ChicPeperoni
ChicVeggie
We’ll move the fm createPizza to PizzaStore, and
leave it for the branches to implement:
createPizza(type) {
if type=“Margherita” p = new NYMargherita()
else if type=“Peperoni” p = new NYPeperoni()
else p = new NYVeggie()
return p
}
29
Factory Method pattern
Factory method pattern is used to encapsulate the subtypedependent creation-part of an operation. (different formulation than FF)
PizzaStore
<<abstract>>
Creator
operation(type) //
createProduct(type) : Product // FM, abs
Concrete Creator A
createProduct(type)
Concrete Creator A
createProduct(type)
NYPizzaStore
ChicPizzaStore
Pizza
Product
operations
Product 1A
Product 2A
Product 3A
NYMargherita,
NYPeperoni,
…
Product 1B
Product 2B
Product 3B
ChichMargherita,
ChicPeperoni,
…
• Creator’s operation depends on subclass, but does not (want to) know apriori which
subclass is used.
• Provide better encapsulation than the 1st approach using a simple factory class
30
What a hassle! why don’t we just do it this way??
PizzaStore
view(branch,type)
order(branch,type)
if branch=NY then
if type = margherita then p = new NYMargherita()
else if type = paperoni …
…
else if branch = Chicago then …
• Code duplication  maintenance
• Breaking open-closed principle  adding branches force you
to change the code. (New solution still does that, but to a
lesser degree; ‘break point’ at the factory-methods)
31
Dependency Inversion
• Mentioned in FF.
• It is natural that a class depends on its parts. However this
also limits how you can combine/use this class in various
settings.
• Inversion: try to decouple this dependency, hence making the
class more composable.
32
On with the pizzas..
Pizza
prepare()
NYMargeritha
NYPeperoni
NYVeggie
ChicMargeritha
ChicPeperoni
ChicVeggie
• As it is now, each subclass has full control on how to implement its
own “prepare”  too much freedom at the subclasses?
• Now suppose we want to put more “organization” into this:
– Margherita should be prepared differently than Peperoni, but the
preparation should largely the same accross branches (NY,Chic, etc)
– Branches only differ in e.g. ingredients used, each uses e.g. locally popular
substitutes for cheese, sauce, etc.
• We’ll transform to a different design to facilitate this…
33
Let’s take a closer look…
NY Margherita
Dough, mozzarella, plum tomato sauce,
no-topping , no-meat
Chicago Margharita
Dough, reggiano cheese, marinara
sauce, no-topping , no-meat
NY Veggie
Dough, mozzarella, plum tomato sauce,
spinach, black-olive , no-meat
Chicago Veggie
Dough, reggiano, marinara sauce,
onion, red-peper, no-meat
NY Peperoni
Dough, mozzarella, plum tomato sauce,
spinach, black-olive , peperoni
Chicago Peperoni
Dough, reggiano, marinara sauce,
onion, red-peper, peperoni
34
Separate and encapsulate…
<<abstract>>
Pizza
prepare() // abs
Margherita
prepare()
Peperoni
prepare()
IngredientFactory
createDough() : Dough
createSauce() : Sauce
createCheese() : Cheese
createToppings() : Topping[]
createMeat() : Meat
NYIngredientFactory
ChicIngredientFactory
prepare() {
sauce = ingredientFactory.createSauce()
cheese = ingredientFactory.createCheese()
meat = null
…
}
createSauce() {
return new PlumTomatoSauce()
}
createCeese() {
return new Mozzarella()
}
createSauce() {
return new MarinaraSauce()
}
createCeese() {
return new Regiano()
}
35
Abstract Factory Pattern
Providing an abstract interface for creating a family of products.
(abstract  the interface does not expose coupling to concrete classes)
ingredientFacrory
<<interface>>
AbstractFactory
createA() : Abstract Ingredient A
createB() : Abstract Ingredient B
...
ConcreteFactory
NY
<<interface>
AbstractIngredient B
<<interface>
AbstractIngredient A
NY-concrete B
Chic-concrete B
NY-concrete A
Chic-concrete A
ConcreteFactory
Chic
36
The store has to be a bit different as well..
<<abstract>>
PizzaStore
order(type)
createPizza(type) : Pizza // abs
createIngrFactory()
NYPizzaStore
createPizza(type)
createIngrFactory()
ChicPizzaStore
createPizza(type)
createIngrFactory()
createIngrFactory() {
return NYIngredientFactory() ;
}
Pizza
NYMargherita
--NYPeperoni
--NYVeggie
---
ChicMargherita
ChicPeperoni
ChicVeggie
order(type) {
ifc = createIngrFactory()
p = createPizza(type)
p.prepare() ; p.bake()
p.box()
return p
}
createPizza(type) {
if type == “margherita” then p = new Margherita(ifc)
else if type == “Peperoni” then p = new Peperoni(ifc)
…
37
Overview of the workflow
:PizzaCoHQ
create
:NYPizzaStore
: Customer
order(margherita)
createPizza(margherita)
create
create
prepare
:NYIngredientFactory
:Margherita
createDough()
createSauce()
etc..
bake
box
deliver(pizza)
pizza
38
“factory” with boiler
• A factory has 1x physical boiler that it needs to control from
software.
Boiler
- empty = true
- boiled = false
+ Boiler() // constructor
+ fill()
+ boil()
+ drain()
only fill when it is not empty:
fill() { if empty, empty = false }
Similarly:
boil() { if not empty, boil = false }
• So, what may happen if there are two instances of the same
physical Boiler (which you only have 1) in your app?
39
Singleton pattern
Boiler
...
-Boiler(...)
- uniqueInstance : Boiler
+ getInstance() : Boiler
private Boiler() { empty=true; boiled=false ; }
private static Boiler uniqueInstance
public static Boiler getInstance {
if uniqueInstance == null then uniqueInstance = new Boiler()
return uniqueInstance
}
Notice the lazy instantiation here...
40
Your app is multi-threaded
Boiler
- empty = true
- boiled = false
+ fill()
+ boil()
+ drain()
fill() { if (not empty) empty = false }
thread-1 doing fill()
(Java) we need to “synchronize”
the operations.
How about the “getInstance”
method; do we sync that too?
not empty
filling;
setting empty to false
thread-2 doing fill()
not empty
filling;
setting empty to false
The boiler is filled 2x !!
41
Another solution
Boiler
...
-Boiler(...)
-uniqueInstance : Boiler
+ getInstance() : Boiler
empty = true
boiled = false
...
private Boiler() { }
private static Boiler uniqueInstance = new Boiler()
// thread-safe by JVM
public static Boiler getInstance { return uniqueInstance }
// no need to sync this
But you lose lazy instantiation. Else use “double-checked locking”  see FF.
42
Download