Head First Design Patterns

We can think design patterns as a set of best practices in Object-Oriented Design (OOD). It gives us a shared vocabulary, and simplifies communications with others by thinking at the pattern level instead of the nitty-gritty object level.

This book contains most of the design patterns defined in the textbook “Design Patterns: Elements of Reusable Object-Oriented Software”. For each pattern, I will summarize its design principles, definition, relation graph among example classes, with some code snippet.

Strategy Pattern

Definition: strategy pattern defines a family of algorithms/classes, encapsulates each one, and makes them inter-changeable. Strategy lets algorithms vary independently from clients that use it.

Design Principle:

  1. Encapsulate what varies: identify the aspects of your application that vary and separate them from what stays the same.
  2. Program to an interface, not an implementation.
  3. Favor composition over inheritance.

Composition means a HAS-A relationship, whereas inheritance means IS-A. Composition gives many benefits such as loosely-coupled class structures, dynamically changing behaviors at runtime, etc.

We use an example to demonstrate the disadvantages of not using the pattern and how strategy pattern resolves them by applying these design principles.

Suppose we are designing a Duck class hierarchy, in which each type of duck can fly and quack but their flying/quacking behaviors are not the same. First, let’s see two designs without strategy pattern:

  1. Use inheritance: first define an abstract Duck class with two abstract methods (fly, quack). Then for each actual Duck subclass, we inherit from Duck and implement the two methods.
  2. Use interface: first define an abstract Duck class and two interfaces (Flyable, Quackable). Then for each actual Duck subclass, we inherit from Duck and implement the two interfaces when necessary (e.g., one Duck can only fly).
Before Strategy Pattern
Class relationships without strategy pattern. Left is using inheritance; right is using interface.

Both of the two designs have disadvantages, for example:

  1. Classes are tightly-coupled: we cannot separate Ducks from behaviors. If we want to change a behavior implementation, we have to change all the client code (Ducks) that use this behavior (implementations). If $n$ Duck classes use the behavior, we have to change it in all $n$ classes.
  2. Low-level code reusing: if we want to create a DuckCall class hierarchy, we cannot reuse the quack behaviors defined in Ducks.
  3. Behaviors are binded to a class implementation and cannot be changed at runtime: after initializing a Duck object, we cannot change its behaviors.

Let’s see how strategy pattern can mitigate these issues. We know that what changes are behaviors not ducks. So we first separate the two behaviors from ducks by defining two interfaces, FlyBehavior and QuackBehavior, each of which has a set of classes implementing the behavior (Principle 1). Then, we add two objects of the interfaces to the (abstract) Duck class that hold the specific behavior implementations (Principle 2 and 3). Finally, for different types of Ducks (subclasses), we just need to pass the expected behavior implementations (Principle 3).

After Strategy Pattern
Class relationships in strategy pattern. We can see that client code (Duck) is separated from algorithms (here, behaviors) and use them via composition.

Now, let’s discuss how the design with strategy pattern resolves the disadvantages mentioned:

  1. We have a loosely-coupled class structures where client code (Duck) is separated from algorithms (FlyBehavior and QuackBehavior). If we want to change a behavior implementation, we don’t need to change client code.
  2. Since we already encapsulate behavior implementaions into separate interface/class structures that are not binded to client code, we can use them in other client code (e.g., DuckCall) via composition.
  3. We no longer bind behaviors to client code implementations. Instead, we use composition that holds behavior objects, so we can change the objects at runtime.

The key here is that, a duck now delegates its flying and quacking behaviors to coresponding behavior objects, instead of using quacking/flying methods defined in the Duck class (or subclass).

Observer Pattern

Keep objects notified when something they care happens. Publishers/Subject + Subscribers/Observers = Observer Pattern

Definition: observer pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically.

Design Principles:

  1. Strive for loosely coupled desings between objects that interact.

Observer pattern can be used in scenarios where a set of objects want to be automatically notified by a subject whenever it has updates. For example, weather displays want to be notified/updated whenever the weather station has new data. Or in GUI, action listeners want to be called whenever the button they listen to is triggered. In general, observer pattern has two parts:

  1. A subject class: the subject can add/remove observers and send updates to them. It also has a list (set also works) to save the set of subscribed observers.
  2. A Observer interface: an observer will hold a subject so that it can (un)subscribe the subject. It also has a single update (or other name) method that will be called by the subject whenever it has update.
  3. A set of classes that implement the Observer interface.
class WeatherStation {
private:
  std::vector<Observer *> observers;
  // And more data, e.g., temperature.
public:
  void registerObserver(Observer *);
  void removeObserver(Observer *);
  void notifyObservers();

  // And getters so that observers can pull data from subject.
}

class Observer {
private:
  WeatherStation *subject;
public:
  void update();  // Called by subject whenever it has update to send.
}
Observer pattern example
Class graph of the weather station example using observer pattern. The weather station is the subject and different displays are observers. Whenever the weather station has new data, it notify displays by calling their `update` method, which is the main component of `Observer` interface.

How Observer Pattern Achieves Loose Coupling:

  1. The only thing the subject knows about an observer is that it implements a certain interface (Observer interface).
  2. We can add or remove observers at any time.
  3. We don’t need to modify the subject to add new types of observers. We just create the new type which implements the Observer interface.
  4. We can reuse subjects and observers independently of each other.
  5. Changes to either the subject or an observer will not affect the other.

How to transfer the new data between subject and observers: subject can push data to observers via method arguments in Observer interface. Or observers can pull data from subject through its getter when there is update. Usually pull is more “correct” and extensible. If you use push, you may need to change the method API to include new arguments in the future.

Decorator Pattern

Definition: decorator pattern attaches additional responsibilities to an object dynamically and transparently. Decorators provide a flexible alternative to subclassing for extending functionality. Instead, decorators use subclassing only for type matching.

Design principles:

  1. Open-Closed: classes should be open for extension, but closed for modification.

Intuitively, we wrap the original object with one (or more) decorator(s), in a way such that the object can be used the same way (same APIs) as before, except that now it also have functionalities from these decorator(s). To make it work transparently:

  1. Decorators need to have the same APIs/interface as the object they decorate (inherit from the same interface or abstract class).
  2. Decorators need to HAS-A (wrap) an object, such that they can still access the object while adding new functionalities.
  3. We can implement decorator pattern by having a Decorator interface and a set of concrete Decorators classes that implement this interface and hold the decorating functionalities.

The below figure (from the book) shows the class structure of decorate pattern. Overall it consists of four types of elements:

  1. Component: the interface/abstract class that is implemented by both concrete components and decorators. Concrete components inherits it to implement expected behaviors, while decorators inherits it only for type matching.
  2. Concrete component: the set of objects/classes that can be decorated by decorators.
  3. Decorator: usually we also have an interface/abstract class so that we can have various concrete decorators that implement different decorations.
  4. Concrete decorators: various decorators that provide decoration implementations.
Decorator pattern class relationship
Classes and interfaces in decorator pattern.

Decorator pattern only works when you write code against the abstract component type, not the concrete component type. A concrete component will lose its own APIs (those not in the interface) after wrapped by a decorator. An example of decorator pattern is the Java.io package.

Let’s end up with a simple code snippet to demonstrate these elements:

// Base class. Every class/interface IS-A `Component`.
#include <string>

using std::string;

class Component {
public:
  virtual string description();
  virtual double price();
};

// Implement `Component` behaviors.
// Notice here, by using decorators, we will no longer have access to
// `ConcreteComponent`'s own behaviors, such as `discountPrice`.
class ConcreteComponent: public Component {
public:
  string description() override { return "coffee"; }
  double price() override { return 2.0; }
  double discountPrice(double percent) { return price() * percent; }
}

// Base decorator class. Decorator also IS-A Component, but only for type
// matching, not for extending behaviors. It HAS-A `ConcreteComponent` 
// (composition).
class Decorator: public Component {
public:
  Decorator(Component *comp) : component(comp) { }
protected:
  Component *component;
}

// Implement `Decorator`.
class ConcreteDecorator: public Decorator {
public:
  ConcreteDecorator(Component *comp): base(comp) { }
  string description() override { return "milk " + component->description(); }
  double price() override() { return 0.15 + component->price(); }
}

int main() {
  Component *coffee = new ConcreteComponent();
  Component *coffeeWithMilk = new ConcreteDecorator(coffee);
}

Factory Pattern

In Object-Oriented world, we usually have various classes that implement the same interface (e.g., different types of Pizza). This makes it difficult to creat object correctly and concisely, because we need to create a concrete object based on some inputs from user. And the same logic might be scattered across different locations in your codebase.

Imagine you need to “create” different Pizza objects based on a string input. And you need the Pizza creation logic both in createPizza and showPizzaMenu (or other) methods.

Factory pattern simplifies object creation by putting all object creation into a single place (either a factory method or a factory class). It has two main benefits:

  1. Decouple object creation from client code (code that uses the objects): client code no longer need to know how to create various objects. Whenever it needs an object (say, a Pizza), it can create the object by using the factory.
  2. Provide a consistent way of creating objects: only the factory knows and has the logic of object creation. Whenever we update the logic, we only need to change the factory.

Simple Factory

Simple factory is not a real design pattern, but it’s a widely-used type of factory. We simply put the object creation logic to a new Factory class, and whenever client code needs objects, it can use the Factory.

You can either create a factory object and pass it to client code, or write the object creation as a static method in the Factory and uses the static method directly in client code.

// Factory class. We only use the static method to create `Pizza`, so put 
// constructor in private.
class PizzaFactory {
public:
  static Pizza *createPizza(string pizzaType) {
    if (pizzaType == "cheese")
      return new CheesePizza();
    else if (pizzaType == "pepperoni")
      return new PepperoniPizza();
    else
      return UnknowPizza();
  }
private:
  PizzaFactory() = default;
}

// Client class. It uses factory to create pizza.
class PizzaStore {
public:
  Pizza *orderPizza(string pizzaType) {
    // Deligate pizza creation to the factory. 
    Pizza *pizza = PizzaFactory.createPizza(pizzaType);

    pizza->prepare();
    pizza->bake();
    pizza->cut();
    pizza->box();
    return pizza;
  }
}

However, simple factory does have some disadvantages, such as:

  1. Too flexible to be used correctly: in the above example, PizzaFactory and PizzaStore are two separate classes. After creating a Pizza, PizzaFactory no longer has control over the objects it creates. In fact, any code can use the factory to create pizza.
  2. Not extensible: if we want to have two factories that create NY-style pizzas and Chicago-style pizzas respectively, we need to have two separate Factorys.

Factory Method Pattern

Definition: factory method pattern defines an interface/method (a.k.a factory method) for creating objects, but lets subclasses decide which class to instantiate. Factory method lets a class defer instantiation to subclasses.

Design principle:

  1. Dependency inversion principle: depend upon abstractions. Do not depend upon concrete classes.

Now let’s see how factory method pattern resolve disadvantages in simple factory. First, it combines the two classes and moves the createPizza method to PizzaStore. Second, it defers object creation/instantiation to subclasses by declaring the createPizza as an abstract method. Then whenever there is a new style of pizza, we just need to inheriate the base PizzaStore and implement the createPizza method.

createPizza is the so-called factory method. A factory method handles object creation and encapsulates it in a subclass. This decouples the client code in the superclass from the object creation code in the subclass.

Factory Method Pattern
Factory Method Pattern
class PizzaStore {
public:
  Pizza *orderPizza(string pizzaType) {
    // Subclasses will handle object creation.
    Pizza *pizza = createPizza(pizzaType);

    pizza->prepare();
    pizza->bake();
    pizza->cut();
    pizza->box();
    return pizza;
  }
protected:
  abstract Pizza *createPizza(string pizzaType);
}

class NYStylePizzaStore: public PizzaStore {
protected:
  Pizza *createPizza(string pizzaType) override {
    if (pizzaType == "cheese")
      return new NYCheesePizza();
    else if (pizzaType == "pepperoni")
      return new NYPepperoniPizza();
    else
      return UnknowPizza();
  }
}

class ChicagoStylePizzaStore: public PizzaStore {
protected:
  Pizza *createPizza(string pizzaType) override {
    if (pizzaType == "cheese")
      return new ChicagoCheesePizza();
    else if (pizzaType == "pepperoni")
      return new ChicagoPepperoniPizza();
    else
      return UnknowPizza();
  }
}

As we can see, with factory method pattern, we can extend our PizzaStore to include different styles of pizza. Also only PizzaStore can create pizzas using the factory method (and the orderPizza method is also fixed, no other changes possible).

Abstract Factory Pattern

Definition: abstract factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Compared to factory method pattern which decouples object creation from client code by inheritance, abstract factory method uses composition to create different factories that follow the same Factory interface. Then with the same client code, we can create different types of objects by passing different factories.

Let’s assume that the key difference between different pizza styles is ingredients used. Then we can create an PizzaIngredientFactory interface, based on which we create two classes that implement the interface and provide ingredients for a specific pizza style. Every concrete Pizza class will hold a PizzaIngredientFactory reference and use it to produce different styles of pizza. When we implement concrete PizzaStores, we just need to pass the corresponding PizzaIngredientFactory (e.g., NYPizzaIngredientFactory) to the createPizza method.

By using abstract factory pattern, we also reduce the number of concrete Pizza classes needed. For example, we no longer need two cheese pizza classes (NYCheesePizza, ChicagoPepperoniPizza). Instead, we only need one CheesePizza and let the PizzaIngredientFactory passed to it decide the specific style.

Abstract Factory Pattern
Abstract Factory Pattern
updated_at 23-09-2021