420 likes | 633 Views
Design Pattern 1 [Eric Freeman & Elisabeth Freeman 1 – 5 ]. Design of “ simDuck ” , 1 st attempt. fly(). Inheritance allows you to reuse code, but also “forces” attributes and behavior to the subclasses. Client. Duck display() quack(). <<use>>. WildDuck display(). RedHeadDuck
E N D
Design of “simDuck” , 1st attempt fly() Inheritance allows you to reuse code, but also “forces” attributes and behavior to the subclasses. Client Duck display() quack() <<use>> WildDuck display() RedHeadDuck display() DecoyDuck display() quack() fly() RubberDuck display() fly() rubber doesn’t fly; decoy doesn’t quack nor fly. • Maintenance is expensive! • Software evolves; is your design flexible enough??
2nd attempt: how about factoring out to multiple interfaces? <<interface>> QuackBehavior quack() <<interface>> FlyBehavior fly() Duck display() WildDuck display() quack() fly() RedHeadDuck display() quack() fly() DecoyDuck display() RubberDuck display() quack() • Unfortunately, now you lose code reuse; bad for maintenance ... • Use multiple inheritance instead? Well, not supported in all OO languages.
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
Separating and encapsulating quack and fly has <<interface>> FlyBehavior fly() <<interface>> QuackBehavior quack() Duck display() quack() fly() has quackbehavior StandardQuack quack() StadardFly fly() RedHeadDuck display() RubberDuck display() WildDuck display() ... 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.
Composition instead of inheritence flybehavior has Duck display() quack() fly() <<interface>> FlyBehavior fly() <<interface>> QuackBehavior quack() has StandardQuack quack() StadardFly fly() RedHeadDuck display() RubberDuck display() WildDuck display() ... 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 }
Design pattern Strategy-1 algorithm(...) “Strategy Pattern” Strategy-2 algorithm(...) <<interface>> Strategy algorithm(...) SomeClass some_method(...) ... • 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.
Books 2004 Eric Freeman & Elisabeth Freeman Much better explanation, good reviews! (Some disagreement) Do read it with “open mind” 1995, “Gang of 4” Catalog of 23 patterns Classic book in SE, sold over 0.5 M copies.
Weather “plugins” d : WeatherDisplay get data s : Weather Station w : WeatherDataProvider provides live data on temp, humidity, pressure. • Three kinds of displays: • current weather • weather statistics • prediction • Have to be regularly updated.
Design CurrentWeatherDisplay update(t,h,p) WeatherDataProvider - temp - humidity - pressure notify() setMeasurement(t,h,p) WeatherStatDisplay update(t,h,p) ForecastDisplay update(t,h,p) temp = t humidity = h pressure = p notify() currentWeather.update(temp,humidity,pressure) weatherStat.update(temp,humidity,pressure) forecast.update(temp,humidity,pressure)
Observer pattern Subject addObserver (o) removeObserver(o) notifyObservers() setState(...) observers subject Observer update(data) 0..1 * obs1 subject obs2 client setState(..) notifyObservers(..) update(data) update(data)
Should we push data, or let observers pull them? Subject addObserver (o) removeObserver(o) notifyObservers() setState(...) getState() : ... Observer update(data) update(subject) observers subject * s : Subject obs client setState(..) notifyObservers(..) update(s) s.getState () data do something with data...
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.
Java provides the pattern as “classes” ! Observable addObserver (obsvr) removeObserver(obsvr) notifyObservers() notifyObservers(data) setChanged() <<interface>> Observer update(o : Observable, data: Object) * WeatherDisplay WeatherDataProvider CurrentWeatherDisp WeatherStatDisp • Good thing about this is that you can use the pattern through subclassing/impl. • Does have its limitation... ForecastDisp
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 ? Pure inheritance-based approach: Beverage desc cost() Espresso cost() Decaf cost() Houseblend cost() Darkroast cost() SoyDarkroastWithWhip cost() Each implement its own cost calculation
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.
Ideas • Factor out varying factors; exploit compositions: • Make condiment a feature you can wrap around a beverage: • Beter yet, make them stackable: Milk Soy Beverage Cocoa Whip Whip d : Darkroast d : Darkroast
Decorator pattern Beverage Component operation(…) <<use>> Client Condiment Concrete Component operation(…) Decorator operation(…) Esperesso, Darkroast, Decaf ConcreteDecorator A operation(…) ConcreteDecorator B operation(…) Milk, Cocoa, Whip
Distributing the behavior class Espresso extends Beverage { cost() { return 1.50 euro } } component Beverage cost() Espresso cost() Condiment class Cocoa extends Condiment { component : Beverage Cocoa (b:Beverage) { component = b } cost() { return b.cost() + 50 cent } } Cocoa cost() Whip cost() How to make double mocha espresso : new Cocoa( new Cocoa (new Espresso()) Notice how the “cost” functionality propagates over your decorators-stack. How about adding “Tea” and “rum” ? Can we keep the Open-Closed principle? cost cost e : esperesso
Real world decorators: Java I/O <<abstract>> InputStream <<abstract>> FilterInputStream FileInputStream ByteArrayInputStream BufferedInputStream DataInputStream LineNumberInputStream (Depracated!) So, you can stack your InputStream decorators. And even make your own decorators (by subclassingFilterInputStream)
Your question Beverage cost() Beverage cost() 0..1 Espresso cost() Espresso cost() Condiment VS Condiment 0..1 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.
Late/dynamic binding • Many OO languages allows the type of the object created to be decided “late” (at the run-time) • Implies that behavior can also be bound dynamically • Pro: gives you a lot of flexibility • Cons: you lose some static checking cookPizza(availableBudget) { Pizza p if availableBudget < 10 euro then p = new MargheritaPizza() else p = new PeperoniPizza() p.cook() return p }
Pizza store Margherita Now we want to add more types of pizzas, and be able to order different types of pizzas. Pizza view() prepare() bake() box() PizzaStore view() order() Peperoni Veggie view(type) order(type) A well-proven “order” algorithm: order() { p = new Pizza() p.prepare() p.bake() p.box() return p } 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…
Separating and encapsulating the “sub-type dependency”-part Margherita Pizza view() prepare() bake() box() PizzaStore view(type) order(type) Peperoni Veggie PizzaFactory createPizza(type) order(type) { p = factory.createPizza(type) p.prepare() p.bake() p.box() return p } 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.
“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
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) NYPizzaFactory PizzaFactory ChicPizzaFactory ChicPizzaStore view(type) order(type) NYPizzaStore view(type) order(type) 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?
Fixing the “order”, and putting a place holder for the variating part.. Pizza prepare() bake() box() <<abstract>> PizzaStore order(type) // final ? createPizza(type) : Pizza // abs ChicMargherita NYMargherita NYPizzaStore createPizza(type) ChicPizzaStore createPizza(type) ChicPeperoni NYPeperoni ChicVeggie NYVeggie We’ll fix the logic of “order”: order(type) { p = createPizza(type) p.prepare() ; p.bake() p.box() return p } We’ll move the fmcreatePizza 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 }
Factory Method pattern Factory method pattern is used to encapsulate the subtype-dependent creation-part of an operation. (different formulation than FF) PizzaStore Pizza <<abstract>> Creator operation(type) // createProduct(type) : Product // FM, abs Product operations Product 1B Product 1A Concrete Creator A createProduct(type) Concrete Creator A createProduct(type) Product 2B Product 2A Product 3B Product 3A ChichMargherita, ChicPeperoni, … NYMargherita, NYPeperoni, … NYPizzaStore ChicPizzaStore • 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
What a hassle! why don’t we just do it this way?? if branch=NY then if type = margherita then p = new NYMargherita() else if type = paperoni … … else if branch = Chicago then … PizzaStore view(branch,type) order(branch,type) • 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)
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.
On with the pizzas.. Pizza prepare() • 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… ChicMargeritha NYMargeritha ChicPeperoni NYPeperoni ChicVeggie NYVeggie
Let’stake a closer look… NY Margherita Dough, mozzarella, plum tomato sauce, no-topping , no-meat NY Veggie Dough, mozzarella, plum tomato sauce, spinach, black-olive , no-meat NY Peperoni Dough, mozzarella, plum tomato sauce, spinach, black-olive , peperoni Chicago Margharita Dough, reggianocheese, marinara sauce, no-topping , no-meat Chicago Veggie Dough, reggiano, marinara sauce, onion, red-peper, no-meat Chicago Peperoni Dough, reggiano, marinara sauce, onion, red-peper, peperoni
Separate and encapsulate… createSauce() { return newPlumTomatoSauce() } createCeese() { return new Mozzarella() } <<abstract>> Pizza prepare() // abs IngredientFactory createDough() : Dough createSauce() : Sauce createCheese() : Cheese createToppings() : Topping[] createMeat() : Meat Margherita prepare() NYIngredientFactory Peperoni prepare() createSauce() { return newMarinaraSauce() } createCeese() { return newRegiano() } ChicIngredientFactory prepare() { sauce = ingredientFactory.createSauce() cheese = ingredientFactory.createCheese() meat = null … }
Abstract Factory Pattern Providing an abstract interface for creating a family of products. (abstract the interface does not expose coupling to concrete classes) <<interface> AbstractIngredient B ingredientFacrory <<interface>> AbstractFactory createA() : Abstract Ingredient A createB() : Abstract Ingredient B ... <<interface> AbstractIngredient A NY-concrete B Chic-concrete B NY-concrete A Chic-concrete A ConcreteFactory NY ConcreteFactory Chic
The store has to be a bit different as well.. Pizza <<abstract>> PizzaStore order(type) createPizza(type) : Pizza // abs --- ChicMargherita NYMargherita --- ChicPeperoni NYPeperoni createIngrFactory() --- ChicVeggie NYVeggie order(type) { ifc = createIngrFactory() p = createPizza(type) p.prepare() ; p.bake() p.box() return p } NYPizzaStore createPizza(type) ChicPizzaStore createPizza(type) createIngrFactory() createIngrFactory() createIngrFactory() { return NYIngredientFactory() ; } createPizza(type) { iftype == “margherita” then p = newMargherita(ifc) elseif type == “Peperoni” then p = newPeperoni(ifc) …
Overview of the workflow :PizzaCoHQ create :NYPizzaStore : Customer order(margherita) createPizza(margherita) create :NYIngredientFactory create :Margherita prepare createDough() createSauce() etc.. bake box pizza deliver(pizza)
“factory” with boiler • A factory has 1x physical boiler that it needs to control from software. • So, what may happen if there are two instances of the same physical Boiler (which you only have 1) in your app? only fill when it is not empty: fill() { if empty, empty = false } Similarly: boil() { if not empty, boil = false } • Boiler • empty = true • boiled = false • + Boiler() // constructor • + fill()+ boil() • + drain()
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...
Your app is multi-threaded • Boiler • empty = true • boiled = false • + fill()+ boil() • + drain() fill() { if (not empty) empty = false } thread-1 doing fill() thread-2 doing fill() (Java) we need to “synchronize” the operations. How about the “getInstance” method; do we sync that too? not empty not empty filling; setting empty to false filling; setting empty to false The boiler is filled 2x !!
Another solution • Boiler • ... • Boiler(...) • uniqueInstance : Boiler • + getInstance() : Boiler empty = true boiled = false ... private Boiler() { } private static Boiler uniqueInstance = new Boiler() public static Boiler getInstance { return uniqueInstance } // thread-safe by JVM // no need to sync this But you lose lazy instantiation. Else use “double-checked locking” see FF.