Design Patterns

Software Design Patterns

 

  • Helps developers reuse successful designs by basing new designs on prior experience.

  • Improves understandability for developers familiar with the pattern.

  • Identifies participating classes and instances, their roles and collaborators, and the distribution of responsibilities. This helps ensure adherence to Design Principles.

A design pattern is a general reusable solution to a commonly occurring problem within software design. It is a description or template for how to solve a problem that can be used in many different situations.

Software Design ANTI-Patterns

Common processes or solutions that, although they might seem effective, have been found to be ineffective. 

  • Lasagna code: consisting of too many layers 
  • God object: granting an object too much control
  • Sequential coupling: requiring methods to be called in order
  • Gold plating: working on a project past the point of adding value

Software Design Patterns

Creational

  • Factory
  • Builder
  • Singleton

Structural

  • Adapter
  • Facade

Behavioral

  • Iterator
  • Command
  • Observer
  • Strategy

Creation of objects for you rather than instantiating objects directly

Composition of classes and objects to obtain new functionality

Communication and interaction between objects

Factory Pattern

Encapsulates object creation logic by delegating to implementations of a common factory interface without specifying the exact class of object to create.

Use when: 

  • creation of a concrete object is complex
  • deciding which object to create depends on state that is not the responsibility of the created class or the creating class
  • the created object is not available at compile time
  • framework classes are expected to be overridden
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
  • There are many of of different types of loggers and which one to use is decided at runtime.
  • So how does the application determine which logger to use? The separate logger classes shouldn't contain this logic nor should the classes using the logger.
  • Here, the factory maintains a separation of concerns by isolating the loggers and classes using the loggers from application state. 
CreditCardAccountFactory accountFactory = new CreditCardAccountFactory();

foreach (Transaction transaction : customer.getTransactionHistory())
{
    AccountType accountType = transaction.getAccountType();
    CreditCardAccount account = accountFactory.AccountFactory(accountType);
    totalRewards += account.calculateRewards();
}
  • The types of card do not need to be known at compile time, they can be loaded in via plugins.
  • Only accounts that are needed are created and can be cached or leverage object pools.

 

Factories are useful for creating "curtains" where systems need to be plug and play or details may not be known at compile time.

The JDBC allows you to interact with various RDBMS without knowing about the specifics of each connector.

 

At runtime the GetConnection method iterates though all of the drivers loaded into the JVM (information not know at compile time) and returns the driver that is able to create a connection using the provided URL.

Connection conn = DriverManager.getConnection(URL, USER, PASS);

Builder Pattern

Solves the "telescoping constructor" problem by encapsulating construction steps so that they can be invoked individually.

Use when:

  • class has too many constructors or constructors are too complex (many parameters, optional parameters)
  • creation logic is highly conditional
  • constructors are doing real work
  • the algorithm for creating a complex object should be independent of the parts of object and how they're assembled
  • the object needs to be immutable after creation

sourcemaking.com

Sandwich blt = new Sandwich("BLT", Bread.WHEAT, 3);
blt.addCondiment(Condiment.MAYO);
blt.addMeat(Meat.BACON);
blt.addVegetable(Vegetable.Lettuce);
blt.addVegetable(Vegetable.Tomato);

. . .

public class Sandwich {

    // private instance variables

    public Sandwich() { }

    public Sandwich(String name) {
        this();
        this.name = name;
    }
    
    // telescoping constructors
    public Sandwich(String name, Bread bread) {
        this(name, bread, bread.minNumSlices);
    }
    
    public Sandwich(String name, Bread bread, int numSlices) {
        this(name);
        this.bread = bread;
        this.numBreadSlices = numSlices;
    }

    public void addCondiment(Condiment meat) { ... }
    public void addMeat(Meat meat) { ... }
    public void addVegetable(Vegetable veggie) { ... }
}
Sandwich blt = new Sandwich.Builder()
.name("BLT")
.withBread(Bread.WHEAT, 3)
.withCondiment(Condiment.MAYO)
.withMeat(Meat.BACON)
.withVegetable(Vegetable.Lettuce)
.withVegetable(Vegetable.Tomato)
.build();

. . .

public class Sandwich {

    // private instance variables

    public Sandwich() { }

    public void addCondiment(Condiment meat) { ... }
    public void addMeat(Meat meat) { ... }
    public void addVegetable(Vegetable veggie) { ... }

    public class Builder {

        //private instance variables

        public Sandwich build() {
            Sandwich s = new Sandwich();
            s.setName(this.name);
            ...
            return s;
        }

        public SandwichBuilder withBread(Bread bread, int numSlices) {
            this.bread = bread;
            this.numBreadSlices = numSlices;
        }

    }

}

Singleton Pattern

Restricts the number of instantiations of a class to one single instance.

Use when: 

  • almost never!
  • to store immutable read-only state or there is no state

 

Don't use when:

  • you want to be able to unit test your code
  • you can use dependency injection or a factory instead 

 

What's wrong with Singletons?

Singletons make your API a liar. You should be able to understand an API without memorizing all of the source code. 

testDebitAccount() 
{
    Account c =  new Account("1234123412341234", "360", 5000);
    c.debit(200);
    assert(c.availableBalance == 4800);
}

In the background there is some "stuff" going on:

  • there is an AccountTransactionProcessor
  • here is an AccountTransactionQueue

However, we don't know this is the case just by looking at the API. The API clearly states that you can create an account and then debit it.

testDebitAccount() 
{
    AccountTransactionQueue.getInstance();
    AccountTransactionProcessor.getInstance();    

    Account c =  new Account("1234123412341234", "360", 5000);
    c.debit(200);
    assert(c.availableBalance == 4800);
}

What's wrong with Singletons?

testDebitAccount() 
{
    AccountTransactionQueue atQueue = 
        new AccountTransactionQueue();
    AccountTransactionProcessor atProcessor = 
        new AccountTransactionProcessor(atQueue);    

    Account c =  new Account("1234123412341234", "360", 5000);
    atProcessor.debit(c, 200);
    assert(c.availableBalance == 4800);
}

What's wrong with Singletons?

When are Singletons ok?

  • they don't affect the execution of your code
    • e.g. loggers have global state but they don't affect the execution of the application

 

  • the singleton is immutable and all objects returned by the singleton are immutable
    • e.g. environment variables pulled at the applications launch. 
        

        public class Singleton {
        
            private static Singleton instance = null;
        
            private Singleton() { }
         
            public static Singleton getInstance() {
                if (instance == null) {
                    instance = new Singleton();
                }
         
                return instance;
            }
        
        }

Adapter Pattern

Creates a "bridge" between two incompatible interfaces.

Use when:

  • existing components don't quite work with architecture of the new system you're developing
  • you don't want to modify the code of existing components
  • you don't want to re-create existing functionality

Cables are a real-life example of an adapter. They allow us to connect two previously incompatible devices. e.g. connect your laptop to your monitor, speakers to your smartphone, etc.  

class LegacySquarePeg {
    private double width;
    public SquarePeg(double w) { width = w; }
    public double getWidth() { return width; }
    public void setWidth(double w) { width = w; }
}

class RoundHole {
    private int radius;
    public RoundHole( int r ) { radius = r; }
    public int getRadius() { return radius; }
}

class SquarePegAdapter {

    private LegacySquarePeg sp;

    public SquarePegAdapter(double w) { 
        sp = new LegacySquarePeg(w); 
    }

    public void makeFit(RoundHole rh) {
        double amount = sp.getWidth() - rh.getRadius() * Math.sqrt(2);
        if (amount > 0) {
            sp.setWidth(sp.getWidth() - amount);
        }
    }
}

Facade Pattern

A facade object provides a simplified interface to a larger body of code to make a subsystem easier to use.

Use when:

  • a simpler, easy to use interface is desired
  • a software library needs to be more accessible
  • using a poorly designed API or legacy code that is difficult to understand

sourcemaking.com

    
    public boolean createNewFile() throws IOException {
      SecurityManager security = System.getSecurityManager();
      if (security != null) security.checkWrite(path);
      return fs.createFileExclusively(path);
    }
    
    public boolean delete() {
      SecurityManager security = System.getSecurityManager();
      if (security != null) {
        security.checkDelete(path);
      }
      return fs.delete(this);
    }
    
    public String getName() {
      int index = path.lastIndexOf(separatorChar);
      if (index < prefixLength) return path.substring(prefixLength);
        return path.substring(index + 1);
      }
    }
// a simple facade that masks the various browser-specific methods
function addEvent( element, event, callback ) {
  
    if( window.addEventListener ) {
        element.addEventListener( event, callback, false );
    } 
    else if( document.attachEvent ) {
        element.attachEvent( 'on' + event, callback );
    }
    else {
        element[ 'on' + event ] = callback;
    }
}

Iterator Pattern

Encapsulates and decouples the algorithm for traversing a data structure, providing a way to access the elements sequentially without exposing the structure's underlying implementation.

Use when:

  • a uniform traversal interface is required
  • a complex data structure is traversed in multiple places

The "seek" button on a radio is a real-life example of the iterator pattern. Pressing the seek button is similar to using a ".next()" method that returns the next available radio station. 

Command Pattern

Uses a command object to represent the information needed to call a method at a later time. Command information includes the method's name, object that owns the method, and values for the method's parameters.

Use when:

  • a sequence of various method calls needs to occur at a later time
  • a collection of different method calls needs to be handled by a shared invoker
  • a history of actions is needed

sourcemaking.com

interface TransactionExecutor {
    void addTransactionCommand(TransactionCommand transaction);
}

interface TransactionCommand {
    void execute();
}



class DepositCommand implements TransactionCommand {
    Account account;
    double amount;

    void execute() {
        account.addFunds(amount);
    }
}

class WithrawalCommand implements TransactionCommand {
    Account account;
    double amount;

    void execute() {
        account.removeFunds(amount);
    }
}

Observer Pattern

An object (the subject) maintains a list of its dependents and notifies them of any state changes.

Use when

  • changes to one object requires notifying numerous dependent objects
  • an object should be able to notify other objects without making assumptions about who those objects are
  • process is event-driven

sourcemaking.com

A common example of the observer pattern is the Model-View-Controller (MVC) architecture that is used extensively in web applications. 

Strategy (Policy) Pattern

There are many situations when cases only differ by their behavior. It is a good idea to isolate the algorithms into different classes to be able to be chosen at runtime. 

Use when:

  • A family or group of algorithms are necessary to execute.
  • Decision on which algorithm to use is dependent on the situation.

Credit

Debit

//Strategy pattern
DamageResistedStrategy dmgStrat = null;

switch (damage.getType()) {
    case MAGIC:
        dmgStrat = new MagicDamageResistedStrategy(damageAmount, target);
    case PHYSICAL:
        dmgStrat = new PhysicalDamageResistedStrategy(damageAmount, target);
    case PURE:
        dmgStrat = new PureDamageResistedStrategy(damageAmount, target);
}

resistedDamageAmount = dmgStrat.calculateResistance();
damageAmount -= resistedDamageAmount;

Design Pattern Group Activity

Design Patterns

By dyanos91

Design Patterns

Software Craftsmanship 2016

  • 346