Download presentation
Presentation is loading. Please wait.
Published byMarian Montgomery Modified over 7 years ago
1
Design Patterns Technion – Institute of Technology 236700 Spring 2016
Author: Gal Lalouche Based on slides by Assaf Israel
2
Design patterns What is a design pattern?
‘Design Patterns, Elements of Reusable Object-Oriented Software’ by: Gemma, Helm, Johnson, Vlissides A.K.A. Gang of Four, or GoF Author: Gal Lalouche - Technion 2016 ©
3
Categories Behavioural
Describes a process/flow and classes can assign responsibilities between each other Examples taught: Strategy, Visitor, Template Creational Revolves around the creation and instantiation of objects Examples taught: Builder, Abstract Factory, Singleton Structural How classes and objects are composed to form new structures Examples taught: Proxy Author: Gal Lalouche - Technion 2016 © Factory and abstract Factory were taught in the DI tutorial Template will be taught in the lectures
4
Strategy Pattern – Motivation
The problem: We are developing a navigation application We are interested in displaying to the user the “best” way to get from point A to point B… …But what is the “best” way? It may be the “quickest”, i.e., takes the least amount of time What about “cheapest”? Taking toll booths and fuel costs into account Or just the shortest (in road traveled) route Author: Gal Lalouche - Technion 2016 ©
5
Strategy – Alternatives
Have an abstract base class DisplayRoute Create a class for each variation: DisplayQuickestRoute extends DisplayRoute DisplayCheapestRoute extends DisplayRoute DisplayShortestRoute extends DisplayRoute What’s the problem? We’ve hurt the cohesion of DisplayRoute It now does two things: displays the route, and calculates it If we want to change the policy, we have to create a new DisplayRoute object This could be expansive, as it may be tied to other resources Optimizing this solution could be hard What if we have other policies that can be changed? e.g., display driving friends, show advertisements Author: Gal Lalouche - Technion 2016 ©
6
Strategy Pattern Again, we will favor composition over inheritance
Separate the behaviour of an object from its representation so that the client is oblivious to the actual routing strategy used The solution: Client code “loads” (or is injected with) the appropriate strategy object The routing is calculated using the external object Author: Gal Lalouche - Technion 2016 ©
7
Strategy – Application
We will encapsulate the route calculation policy in its own object We will define a base class / interface RouteStrategy We will implement each routing strategy in its own class QuickestRoute implements RouteStrategy CheapestRoute implements RouteStrategy ShortestRoute implements RouteStrategy Note: in other languages (e.g., Ruby), we could have used mixins to achieve the same goal Author: Gal Lalouche - Technion 2016 ©
8
Strategy – Application (cont.)
RouteDisplayer RouteCalculator void display() Route calculate(map) QuickestRoute CheapesttRoute Route route = calculator .calculate(map); display(route); Route calculate(map) Route calculate(map) 8
9
Strategy – Implications
The algorithm within each family can vary independently of the client that uses it Increases the cohesion of composing class Can mix and match different strategies in different domains Can reuse strategies Works well with dependency injection and mocking A context object must be passed to strategy object This means we either increase the coupling between the client and the strategy (and perhaps break some of its encapsulation) Or we have to create a special context class Author: Gal Lalouche - Technion 2016 © In our example, the context object was the map
10
Creational We’ve already seen examples of creational DPs
Abstract Factory reminder: The goal was to decouple object configuration and instantiation But what happens when the instantiation process is complex and requires many steps? Author: Gal Lalouche - Technion 2016 ©
11
Builder – Motivation Problem:
A Graph can be represented in several different ways Adjacency list Adjacency matrix Sparse representations Many others… The topology of the graph is independent of its representation All graphs have vertices and edges between them Different algorithms may require different representations to achieve maximal efficiency We may want to enforce invariants, e.g., all graphs are simple We want to decouple the building of the graph from its implementation Author: Gal Lalouche - Technion 2016 ©
12
Builder – Alternatives
Create an Abstract Factory for each representation AdjacencyListFactory AdjacencyMatrixFactory SparseRepresentationsFactory Or just pass an instance created with new Use setters to change the topology Drawbacks? Graph has to be mutable Graph can exist in an illegal state before configuration is complete, e.g., non-connected, empty Author: Gal Lalouche - Technion 2016 ©
13
Builder Pattern Separate the construction of a complex object from its implementation so that the same construction process can create different implementations The solution: Client code “loads” the appropriate builder object The topology of the graph (i.e., its vertices and edges) is passed to the builder Finally, build() returns the built Graph Author: Gal Lalouche - Technion 2016 ©
14
Builder – Application We will create a satellite builder object to accompany each graph implementation We will define a base class/interface GraphBuilder We will implement each representation in its own class AdjencyListGraphBuilder implements GraphBuilder AdjencyMatrixGraphBuilder implements GraphBuilder etc. Author: Gal Lalouche - Technion 2016 ©
15
Builder – Application Director GraphBuilder MatrixBuilder
Graph buildGraph(GraphBuilder gb) void addVertex() void addEdge(int v1, int v2) Graph build() MatrixBuilder SparseBuilder gb.addVertex(); gb.addEdge(1,2); gb.addEdge(2,3); return gb.build(); void addVertex() void addEdge(int v1, int v2) MatrixGraph getResult() void addVertex() void addEdge(int v1, int v2) SparseGraph getResult() 15
16
Builder – Applicability
Use the Builder pattern when The process of creating an object should be independent of the parts that make up the object and how they’re assembled. The construction process must allow different representations for the constructed object. We can make the API a little more “friendly” by having the addX methods return a reference to the builder gb.addVertex().addVertex().addEdge(1,2).getResult() This is called a fluent API Author: Gal Lalouche - Technion 2016 © We did a similar trick for the builder object in the DI tutorial
17
Builder – Implications
We’ve decoupled the creation of the object from its implementation Can mix and match directors (clients) with different builders The built classes can be immutable Classes are always in a legal state Have to create a special builder class for each of our classes For the class to actually be immutable, we may have to work quite hard in the builder Author: Gal Lalouche - Technion 2016 © Hacky shortcut, expose an immutable interface of the mutable underlying class (this isn’t ideal, because the class isn’t really immutable)
18
Builder – Notes Much like the Abstract Factory pattern is a polymorphic new, a builder pattern is a polymorphic configuration of an object Do not confuse this with a builder object, e.g., StringBuilder A builder object is used to replace a complicated constructor, i.e., it always coupled with a concrete class Builder Pattern is used to decouple the implementation of an object from its configuration process A great number of online information confuses this! This arises from the fact the builder pattern often uses a builder object Author: Gal Lalouche - Technion 2016 ©
19
Proxy – Motivation Problem:
We have a remote server that we can send packets to When sending each packet, we first have to establish a connection, perform a “handshake” protocol, other boilerplate stuff… In order to minimize the overhead of packet transfer, we would like to buffer all packets, and send them 10 at a time… But putting all of that logic in the client would seriously hurt cohesion! interface Server { void sendPacket(Packet p); } // client code: server.sendPacket(new MessagePacket(…)); server.sendPacket(new BytePacket(…)); server.sendPacket(new NumberPacket(…)); //… Author: Gal Lalouche - Technion 2016 ©
20
Proxy – Alternatives Okay, let’s put the buffer logic in a buffer class, and invoke that! Problems? interface Buffer { void add(Packet p); int size(); void flush(); } // client code: buffer.add(new MessagePacket(…)); buffer.add(new BytePacket(…)); buffer.add(new NumberPacket(…)); if (buffer.size() >= 10) buffer.flush(); Author: Gal Lalouche - Technion 2016 ©
21
Proxy – Alternatives (cont.)
We just moved the logic to another class, but the client still has to account for it What if we want to replace the buffer logic with a retry logic? What if we want to compose logics? I know, this is a Strategy pattern! Not exactly… If we use a strategy pattern, the client still has to be aware that some strategy is used Instead, we would like the client to be oblivious to the fact that any strategy is taking place Strategies can’t be composed Author: Gal Lalouche - Technion 2016 ©
22
Proxy – Solution Solution: wrap the server with a buffer, but expose the same interface The buffer acts as a proxy to the original server Client is oblivious to the change Can compose proxies together class ServerBuffer implements Server { public ServerBuffer(Server s) { this.s = s; } private Buffer b = new BufferImpl(); void sendPacket(Packet p) { b.add(p); if (buffer.size() >= 10) b.flush; } Author: Gal Lalouche - Technion 2016 ©
23
Proxy – Solution (cont.)
Client Server void sendPacket(Packet p) BufferServer OriginalServer server.sendPacket(p); void sendPacket(Packet p) void sendPacket(Packet p) 23
24
Proxy – Applicability Use the Proxy pattern when
You want to wrap an existing implementation You want to hide the refinement from the client You want to enable linear composition of behaviors Other types of proxies include: RetryProxy CacheProxy RemoteProxy ProtectiveProxy Author: Gal Lalouche - Technion 2016 ©
25
Proxy – Implications Client is oblivious to the change
Can switch proxies and original implementation at ease Can compose proxies The extra layer of abstraction may surprise the client For example, the client may be surprised when the messages are buffered rather than sent immediately (Integration) tests may fail Author: Gal Lalouche - Technion 2016 ©
26
Visitor – Motivation Problem: Account management application
We have several types of accounts Guest account Basic account Gold/VIP Account Developer Account Etc. Each type of account has different properties associated with it: CSS/Theme Access to special areas in the application Display of ads Author: Gal Lalouche - Technion 2016 ©
27
Visitor – Alternatives I
Add the methods, e.g., adsDisplayed(), getTheme() to Account, and implement in the sons Pros: Basic use of polymorphism Client code is very simple Cons: Account is less cohesive Should an account really know the theme associated with it, or what it can access? Can get ridiculously large as new features are added Adds unnecessary edges to the Web of Objects i.e., unnecessary dependencies to Account Not always possible (e.g., can’t modify Account) Author: Gal Lalouche - Technion 2016 ©
28
If We had Multiple Dispatch…
Visitor – If We had Multiple Dispatch… interface AccountHandler { public void handle(Account m); } class ThemeChanger implements AccountHandler { public void handle(BasicAccount a) { … } public void handle(GoldAccount a) { … } public void handle(DevAccount a) { … } } Author: Gal Lalouche - Technion 2016 © class AdDisplayer implements AccountHandler { public void handle(BasicAccount a) { … } public void handle(GoldAccount a) { … } public void handle(DevAccount a) { … } } Multiple dispatch is when the method invoked is bounded at runtime based on the arguments (as opposed to just dynamic binding, where it depends on the message receiver)
29
Visitor – Alternatives II
What about faking it? Adding new handlers is hard Forgetting an account type fails at runtime (or worse, doesn’t fail at all!) We can’t check at compile time that we’ve covered all the cases Adding new types of accounts would break all existing code with no warnings Lots of code duplication on the client side We could move this logic to the AdDisplayer, but that only hides the problem AdDisplayer displayer = new AdDisplayer(); if (a instanceof BasicAccount) displayer((BasicAccount)a); else if (a instanceof GoldAccount) displayer((GoldAccount)a); else if (a instanceof DevAccount) displayer((DevAccount)a); else throw new RuntimeException("no match"); Author: Gal Lalouche - Technion 2016 © In Scala you can check using sealed classes and pattern matchings
30
Visitor Pattern Double dispatch
When a method of object A is called with parameter B, it calls a method on B with itself as the parameter A way to implement Multiple Dispatch in Java, C++, etc. Author: Gal Lalouche - Technion 2016 ©
31
void accept(Visitor v)
Visitor – Solution Visitor Account void accept(Visitor v) void visit(BasicAccount a) void visit(GoldAccount a) void visit(DevAccount a) BasicAccount GoldAccount ThemeChanger AdDisplayer void accept(Visitor v) { v.visit(this); } void accept(Visitor v) { v.visit(this); } void visit(…) void visit(…) Author: Gal Lalouche - Technion 2016 ©
32
Visitor – Account Code Account implementations:
(Why can’t we implement the accept method in the parent interface?) interface Account { void accept(AccountVisitor v); … } class BasicAccount { void accept(AccountVisitor v) { v.visit(this); } … Author: Gal Lalouche - Technion 2016 © class GoldAccount { void accept(AccountVisitor v) { v.visit(this); } … The tiny bit of code duplication is sadly unavoidable
33
Visitor – Visitor Code Visitor implementations:
The client’s code doesn’t care about the dynamic type: interface AccountVisitor { void visit(BasicAccount a); void visit(GoldAccount a); void visit(DevAccount a); } class ThemeChhanger implements Visitor{ void visit(BasicAccount a) { /* ThemeChanger specific code */ } void visit(GoldAccount a) { /* ThemeChanger specific code */ } void visit(DevAccount a) { /* ThemeChanger specific code */ } } Author: Gal Lalouche - Technion 2016 © class AdDisplayer implements Visitor{ void visit(BasicAccount a) { /* AdDisplayer specific code */ } void visit(GoldAccount a) { /* AdDisplayer specific code */ } void visit(DevAccount a) { /* AdDisplayer specific code */ } } AccountVisitor visitor = new AdDisplayer(); account.accept(visitor);
34
Visitor – Return Values
What if we want to add functionality that returns a value? This is easily achieved: We can return “nothing” by using Void Only null can be returned if the return type is Void interface AccountVisitor<T> { T visit(BasicAccount a); T visit(GoldAccount a); } interface Account { <T> T accept(AccountVisitor<? extends T> v); } Author: Gal Lalouche - Technion 2016 © class BasicAccount { <T> T accept(AccountVisitor<? extends T> v) { return v.visit(this); } Notice the capital V! Void, not void class AdDisplayer implements AccountVisitor<Void> { Void visit(BasicAccount pm); Void visit(GoldAccount fr); }
35
Visitor – Applicability
Use the Visitor pattern when: You want to add new virtual methods to a class without actually modifying it directly We want the classes to remain cohesive while robust and extendible (open-close principle) You don’t want the client to check (or even be aware of) the dynamic type The visitor pattern is one of the most powerful design patterns… …but if we replaced every virtual method with a class, it would be impossible to manage Author: Gal Lalouche - Technion 2016 ©
36
Visitor – Implications
Adding behaviours (visitors) to existing classes is easy Elements stays “untouched” There is quite a bit of boilerplate involved But not for the client Can be auto-generated by the IDE Adding a new account type is tedious Need to modify all existing Visitors But it will be checked in compile time There is some code duplication in the Account classes But this code will never change Beware of programming against concrete classes instead of abstract (i.e., no default methods) interfaces! Subtyping and overloading can have surprising results For example, forgetting to override accept / calling the wrong visit method Author: Gal Lalouche - Technion 2016 ©
37
Singleton – The Anti-Pattern
Problem: Sometimes it make sense to have only one instance of a class We might want to have global functionality available throughout our program without resorting to passing objects around Logs Guice Modules FileSystem UtilityFunctions Author: Gal Lalouche - Technion 2016 ©
38
Singleton – Static Methods?
OK, let’s use Static Methods (The class object is a singleton) Problems: Can’t be used as an object (i.e. passed as a parameter) Can’t use inheritance Can’t program against an interface (i.e., can’t implement) All dependencies needs to be known at compile-time Testing is a pain! You should avoid public static methods except for object creation! // There can be Only One! class Highlander { public static void chopHead() {…} } Author: Gal Lalouche - Technion 2016 © It is impossible to mock a static method use Even using it for object creation can be problematic if you want to mock it! (replace with factories).
39
Singleton – Take I (classic)
Ensures only one instance of the class. Can use a static init(…) method to pass parameters What happens in a multithreading environment? class Highlander { private static Highlander instance; private Highlander (){…} public static Highlander getInstance() { if (null == instance) instance = new Highlander(); return instance; } public void chopHead() {…} Author: Gal Lalouche - Technion 2016 © You should only pass parameters in the main method or its equivalent, otherwise your code is untestable again In a multithreaded environment, there can be two instances created!
40
Singleton – Take II Use lazy synchronization (also called double checking) class Highlander { private static Highlander instance; private Highlander(){…} public static Highlander getInstance() { if (null == instance) // synchronize optimization synchronized(Highlander.class) { if (null == instance) instance = new Highlander(); } return instance; Author: Gal Lalouche - Technion 2016 © This is called the double-check-lock pattern, it saves the overhead of synchronization during runtime
41
Singleton – Take III Use the static constructor:
The instance is initialized during class loading in the JVM Object can be loaded even if it is not used Can’t pass parameters class Highlander { public final static Highlander instance = new Highlander (); private Highlander(){…} public void chopHead() {…} } Author: Gal Lalouche - Technion 2016 ©
42
Singleton – enum alternative
Since Java 1.5 there’s a simpler alternative using enums Pros: Simpler code Serialization is free! Can be used as an object (i.e. passed as a parameter) Can program against an interface Cons: Can’t use inheritance (but we do get default methods!) Can’t pass parameters See “Effective Java” by Joshua Bloch, for more details public enum Highlander { INSTANCE; public void chopHead() {…} } Author: Gal Lalouche - Technion 2016 ©
43
Singleton – OOP Loophole
Singleton looks and feels very “object-oriented” We’re using constructors, static fields, visibility modifier and even enums to ensure correct behaviour However, we have an object that is accessible from anywhere in the code, and thus is highly unpredictable Essentially, a Global Variable! Doesn’t get less object-oriented than that… Everything that is bad about global variables also applies to (mutable) singletons Author: Gal Lalouche - Technion 2016 ©
44
Singleton – How deep does the rabbit hole goes?
The Singleton pattern has many pitfalls; to name a few: Lifecycle is Global – It’s not (necessarily) lazy, and it’s never collected by the Garbage Collector It opposes the principle that entities should exist in the smallest possible context Testing nightmare – tests are no longer independent Most implementations are impossible to sub-class (and rightfully so) Classes using Singletons basically hide their dependencies Singletons actually make other classes harder to test! Can be a pain to replace with mocks What happens if after a while you find out you actually do need 2 instances of the Singleton? Author: Gal Lalouche - Technion 2016 ©
45
Singleton – If you insist…
There are few justifiable cases for Singletons: Immutable Singletons – There is no state, everything is constant Data flows into the Singleton and not out – This means the Singleton does not affect the program behavior (e.g. Logs) Both of the above need to hold Strategies are actually great singletons! You only need one QuickSort Generally speaking, if your class has no constructor and is immutable, it’s probably a good singleton Recipe for a well-designed Singleton Define an interface Implement a concrete class Pass it around as a parameter Sounds like a job for Dependency Injection… Avoid mutable singletons like the plague “Singleton I love you, but you’re bringing me down” - Author: Gal Lalouche - Technion 2016 ©
46
Design Patterns and Programming Languages
Design patterns are a function of the language Usually, it is a feature missing in the language That means some patterns don’t make sense in some languages Haskell and Scala don’t need Visitor since they have (much more powerful) typeclasses Kotlin and Scala: flexible, easy to use Singletons called objects Kotlin and Groovy: built-in builders C♯: built in Observers (seen in ) called events Suggested Readings: Design Patterns by GoF (the book that started it all) Wikipedia actually has a lot of information, including java implementations Author: Gal Lalouche - Technion 2016 ©
Similar presentations
© 2025 SlidePlayer.com Inc.
All rights reserved.