Software Craftsmanship

Design Patterns

October 25, 2016

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

  • Singleton
  • Factory
  • Builder

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

https://sourcemaking.com/design_patterns

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);
}

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
  • there 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 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 application's launch. 
        

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

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 known 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);

Another exercise!

https://github.kdc.capitalone.com/kat269/FactoryExercise

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;
        }

    }

}

The card must have the following. 

  • Credit Limit
  • Interest Rate
  • Grace Period

The card may have an arbitrary number of incentives/rewards.

  • % Cash Back on Purchase
  • % Cash Back on Payment
  • Rental Car Insurance
  • % Cash Back on Amazon Purchases 

The card may have an arbitrary number of fees associated with it.

  • Annual Fee
  • Late Fee
  • Over-the-limit fee

Hint: Create an incentives and fees base class. 

Individually customizable credit card exercise.

Structural Patterns

Adapter Pattern

Creates a "bridge" between two incompatible interfaces.

Use when:

  • existing components don't quite work within the 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 LegacySquarePeg(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 double getRadius() { return radius; }
    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);
        }
    }
}

The Evils of Duplication

DRY - Don't Repeat Yourself

  • Requirements, understanding, and environments change. Your code will need to change also.

  • It's not a matter of if you'll remember, but when you'll forget.

  • Imposed duplication
  • Inadvertent duplication
  • Impatient duplication
  • Inter-developer duplication

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 convient, 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.
    
    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;
    }
}

Adapter vs. Facade

  • Facade defines a new interface, adapter uses old interface
  • Adapter - makes two old interfaces (client and target) work together as opposed to defining a new interface
  • Both wrappers - but different kinds of wrappers. Difference is in intent, not how many classes they wrap
  • Facade is to produce a simpler interface, while Adapter is to design to an existing interface
  • Adapter might wrap many classes to provide a nice interface the client is expecting, and facade might just create a simplified interface to a single class with a very complex interface

Behavioral Patterns

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. 

Example: Before Iterator

class SomeClassWithData
{
    private TreeSet < Integer > m_data = new TreeSet < Integer > ();

    public void add(int in)
    {
        m_data.add(in);
    }
    public Collection get_data()
    {
        return m_data;
    }
}

class IteratorDemo
{
    public static void main(String[] args)
    {
        SomeClassWithData some_object = new SomeClassWithData();
        for (int i = 9; i > 0; --i)
          some_object.add(i);
        Collection data = some_object.get_data();
        for (java.util.Iterator it = data.iterator(); it.hasNext();)
          System.out.print(it.next() + "  ");
        System.out.println();

        // Do we really want a client to be able to
        // trash encapsulated state?
        data.clear();
        data = some_object.get_data();
        System.out.println("size of data is " + data.size());
    }
}


// Output

1  2  3  4  5  6  7  8  9
size of data is 0

Example: After Iterator

class SomeClassWithData
{
    private TreeSet < Integer > m_data = new TreeSet < Integer > ();

    public class Iterator
    {
        private SomeClassWithData m_collection;
        private java.util.Iterator m_it;
        private int m_current;
        public Iterator(SomeClassWithData in)
        {
            m_collection = in;
        }
        public void first()
        {
            m_it = m_collection.m_data.iterator();
            next();
        }
        public void next()
        {
            try
            {
                m_current = m_it.next();
            }
            catch (NoSuchElementException ex)
            {
                m_current =  - 999;
            }
        }
        public boolean is_done()
        {
            return m_current ==  - 999;
        }
        public int current_item()
        {
            return m_current;
        }
    }

    public void add(int in)
    {
        m_data.add(in);
    }
    public Iterator create_iterator()
    {
        return new Iterator(this);
    }
}

class IteratorDemo
{
    public static void main(String[] args)
    {
        SomeClassWithData some_object = new SomeClassWithData();
        for (int i = 9; i > 0; --i)
          some_object.add(i);

        // get_data() has been removed.
        // Client has to use Iterator.
        SomeClassWithData.Iterator it1 = some_object.create_iterator();
        SomeClassWithData.Iterator it2 = some_object.create_iterator();

        for (it1.first(); !it1.is_done(); it1.next())
          System.out.print(it1.current_item() + "  ");
        System.out.println();

        // Two simultaneous iterations
        for (it1.first(), it2.first(); !it1.is_done(); it1.next(), it2.next())
          System.out.print(it1.current_item() + " " + it2.current_item() + "  ")
            ;
        System.out.println();
    }
}


// Output

1  2  3  4  5  6  7  8  9
1 1  2 2  3 3  4 4  5 5  6 6  7 7  8 8  9 9

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 (e.g. timers).
  • 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 require 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

Checklist

  1. Identify a common behavior/action
  2. Design an abstract interface
  3. Bury the alternative implementation details in derived classes.
  4. Clients of the algorithm couple themselves to the interface.

 

//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;

Example

// 1. Define the interface of the algorithm
interface Strategy { public void solve(); }          

// 2. Bury implementation
abstract class TemplateMethod1 implements Strategy { // 3. Template Method 
   public void solve() {
      start();
      while (nextTry() && ! isSolution())
         ;
      stop();
   }
   protected abstract void    start();
   protected abstract boolean nextTry();
   protected abstract boolean isSolution();
   protected abstract void    stop();
}

class Impl1 extends TemplateMethod1 {
   private int state = 1;
   protected void start() {
     System.out.print( "start  " );
   }
   protected void stop() {
     System.out.println( "stop" );
   }
   protected boolean nextTry() {
      System.out.print( "nextTry-" + state++ + "  " );
      return true;
   }
   protected boolean isSolution() {
      System.out.print( "isSolution-" + (state == 3) + "  " );
      return (state == 3);
   }
}

// 2. Bury implementation
abstract class TemplateMethod2 implements Strategy { // 3. Template Method
   public void solve() {                             
      while (true) {
         preProcess();
         if (search()) break;
         postProcess();
      }
   }
   protected abstract void preProcess();
   protected abstract boolean search();
   protected abstract void postProcess();
}

class Impl2 extends TemplateMethod2 {
   private int state = 1;
   protected void    preProcess()  { System.out.print( "preProcess  " ); }
   protected void    postProcess() { System.out.print( "postProcess  " ); }
   protected boolean search() {
      System.out.print( "search-" + state++ + "  " );
      return state == 3 ? true : false;
   }
}

// 4. Clients couple strictly to the interface
public class StrategyDemo {
   public static void clientCode( Strategy strat ) {
     strat.solve();
   }
   public static void main( String[] args ) {
      Strategy[] algorithms = { new Impl1(), new Impl2() };
      for (int i=0; i < algorithms.length; i++) {
         clientCode( algorithms[i] );
      }
   }
}

Another One

Design Pattern Group Activity

Software Craftsmanship - Design Patterns (Alyssa)

By cof_softwarecraftsmanship

Software Craftsmanship - Design Patterns (Alyssa)

2016 - Day 2.1

  • 589