/learn-design-patterns

Helping me learn design patterns with real world scenarios.

MIT LicenseMIT

Learning Design Patterns

Helping me learn Object Orientated design patterns with real world scenarios.

See everything online.

Contents

Creational

Abstract Factory [B+]

Factory Method [B]

Pros:

  • hide implementation
  • Easily test application
  • Change design more easily (loose coupling)

Cons:

  • Abstraction
  • Tight coupling between layers
  • Violates interface Segregation sometimes

Factory Pattern UML

// Product Interface
interface IFactory {
  void drive(int miles);
}

// ConcreteProduct class
class Scooter : IFactory {
  void drive(int miles) { println("Driving " + miles + "km"); }
}
// ConcreteProduct class
class Scooter : IFactory {
  void drive(int miles) { println("Driving " + (miles * 2) + "km"); }
}

// Creator Abstract class
abstract class VehicleFactory { abstract IFactory getVehicle(string name); }

// ConcreteCreator class
class ConcreteVehicleFactory : VehicleFactory {
  override IFactory getVehicle(string name) {
    switch (name) {
    	case "Scooter": return new Scooter();
	case "Bike": return new Bike();
	default: throw new Exception("Vehicle cannot be created");
    }
  }
}
// Usage
void main() {
  VehicleFactory factory = new ConcreteVehicleFactory();
  IFactory vehicle = factory.getVehicle("Scooter");
  vehicle.drive(10); // Driving 20km
  vehicle = factory.getVehicle("Bike");
  vehicle.drive(10); // Driving 10km
}

Prototype [A]

Structural

Adapter [A]

Bridge [A]

Composite [A]

Decorator [A++]

A wrapper, to wrap a method, intervene the method by applying other functionality before or after it happens.

Decorator UML diagram

Allows you to easily chain lots of different classes together, with a history, so calling one method, calls the same method on all the other classes that are chained before it.

You offer a base service with additional items, each one of these items extends a base class via an interface. You can create a service then add on additional items to build up a list of multiple functions by chaining them together.

Is SOLID? Yes, supports Open for extension, closed for modification principle

Pros

  • At RUNTIME we can create different combinations of functionality.
  • So solves permutation issues, e.g. have to create N possible combinations of these classes

Cons

  • If you rely too much on lots of concrete decorators, results in lots of little classes with overuse being a problem

The interface:

public interface Coffee {

    int getCost();

    String getDescription();

}

The base class. This sets the base variables for each service.

public class BaseCoffee implements Coffee {

    String getDescription() { return "Base coffee"; }
    
    int getCost() {
      int base_cost = 10;
      return base_cost;
    }

}

This is like the base decorator:

public abstract class CoffeeDecorator implements Coffee {
	Coffee decoratedCoffee;
	CoffeeDecorator(Coffee c) { this.decoratedCoffee = c; }
	
    	String getDescription() { return decoratedCoffee.getDescription(); }
	int getCost() { return decoratedCoffee.getCost(); }
}

Another decorator:

public class MochaCoffee extends CoffeeDecorator {
   public Mocha(Coffee c) { super(c); }
   
   String getDescription() {
       return this.coffee.getDescription() + ", chocolate";
   }
   
   int getCost() {
       int chocolate_cost = 5;
       return this.coffee.getCost() + chocolate_cost;
   }
   
   int aNewMethod() {
   	// Additional functionality!
   }

}

// Add more decorators
public class XXXCoffee extends CoffeeDecorator {
   public XXXCoffee(Coffee c) { super(c); }
   
   // ... Same as above
   
}

Now lets make coffee with the decorators!

Coffee coffee = new BaseCoffee();
System.out.println(coffee.getCost()); // 10
System.out.println(coffee.getDescription()); // Base coffee

coffee = new MochaCoffee(coffee);
System.out.println(coffee.getCost()); // 15
System.out.println(coffee.getDescription()); // Base coffee, chocolate

coffee = new XXXCoffee(coffee);
System.out.println(coffee.getCost()); // 15 + ...
System.out.println(coffee.getDescription()); // Base coffee, chocolate + ...

Proxy [B]

Behavioral

Chain of Responsibility [B+]

Command [B+]

Use an object (a command) to encapsulate all information needed to perform an action at a later time.

Examples are event handlers, or add additional logging information.

Command pattern UML

Example: The Receiver is a Light, the ConcreteCommand are actions, e.g. TurnLightOn(), TurnLightOff(). The Invoker is a Remote control.

interface Command { void execute(); }

// Concrete Commands
class TurnLightOn : Command { 
  Light light;
  TurnLightOn(Light l) { this.light = l; }
  void execute() { l.on(); }
}

class TurnLightOff : Command {
  Light light;
  TurnLightOff(Light l) { this.light = l; }
  void execute() { l.off(); }
}

class OpenDoor : Command {
  Door door;
  OpenDoor(Door d) { this.door = d; }
  void execute() { d.open(); }
}
// Reciever
class Light {
  void on() { print("on"); }
  void on() { print("off"); }
}

// Another reciever
class Door {
  void open() { print("open"); }
  void close() { print("closed"); }
}
// Invoker
class RemoteControl {
    Command command;
    void set(Command c) { this.command = c; }
    void executeCommand() { command.execute(); }
}

Usage

Light light = new Light();
Command switchOn = new TurnLightOn(light);
Command switchOff = new TurnLightOff(light);

RemoteControl remote = new RemoteControl();
remote.set(switchOn);

println("I can now control the lights whenever I want.");
remote.execute(); // light on

println("I can also control other unrelated things, like my door");
Door door = new Door():
Command openDoor = new OpenDoor();
remote.set(openDoor);
remote.execute(); // opens the door

remote.set(switchOff);
remote.execute(); // light off

Observer [B]

Describes a one to many relationship between objects, so one state changes, the others get notified and update their state automatically.

void main() {
	JobSeeker harry = new JobSeeker("Harry"); // Observers
	JobPostings board = new JobPostings(); // Subject
	board.attach(harry);
	board.attach(new JobSeeker("Matt"));
	
	// This notifies harry and matt
	board.addJob(new JobPost("Software Engineer $$$"));
	
	board.detach(harry):

	// Only notifies matt
	board.addJob(new JobPost("SL engineer"))
}


class JobPost { Public String title; }
class JobSeeker implements Observer {
	String name;
	void onJobPosted(JobPost job) {
		System.out.println("Hello", name, "!", "New job posted", job.title);
	}
}

class JobPostings implements Observeral {
	protected List<Observer> observers = new ArrayList<>();
	void attach(JobSeeker seeker) { observers.add(seeker); }
	void detach(JobSeeker seeker) { observers.remove(seeker); }
	
	void addJob(JobPost job) {
		this.notify(job);
	}
	
	void notify(JobPost job) {
		for (Observer seekers : observers) {
			seekers.onJobPosted(job);
		}
	}
}

Strategy [A]

Allows easy switching of the algorithm or strategy.

interface SortStrategy {
	int[] sort(int[] array);
}
class BubbleSort implements SortStrategy { int[] sort(int[] array) { ... } };
class QuickSort  implements SortStrategy { int[] sort(int[] array) { ... } };

class Sorter {
	Sorter(SortStrategy algo) {
		this.sorter = algo;
	}
	int[] sort(int[] array) { return this.sorter.sort(array); }
}

// Usage:
Sorter bubble = new Sorter(new BubbleSort())
bubble.sort([3,1,2]);

Sorter quick = new Sorter(new QuickSort());
quick.sort([3,1,2]);

Pros:

  • Easy to test compared with if's and else's.
  • Does not violate the open/close principle
  • Removes condition based logic

Visitor [C]

Allows easy partitioning/sorting/manipulation of lists of different objects.

Fruit fruits[] = new Fruit[] {
	new Orange(), new Apple(), new Orange(),
	new Apple(), new Orange()
};

Have list of Fruit/objects we want to partition into 2 lists, 1 containing apples, the other oranges.

Without visitor pattern, we would need to store new lists when we partition/manipulate them. We dont get type safety and hard to catch runtime errors.

// BAD WAY
List<Orange> oranges = new ArrayList<Orange>(); // You need these lists here
List<Apple> apples = new ArrayList<Apple>();
for (Fruit fruit : fruits) {
	if(fruit.equals(Orange.class)) oranges.add((Orange)fruit); // No type safety
	if(fruit.equals(Apple.class)) apples.add((Apple)fruit); // Messy
}

With Visitor pattern:

// GOOD
FruitPartitioner p = new FruitPartitioner();
for (Fruit fruit : fruits) {
	fruit.accept(p);
}

The data is stored in the partitioner variable:

out.println("# Oranges " + p.oranges.size());
out.println("# Apples " + p.apples.size());

FruitPartitioner.java

// Actual visitor
class FruitPartitioner implements IFruitVisitor {
	List<Orange> oranges = new ArrayList<Orange>();
	List<Apple> apples = new ArrayList<Apple>();

	@Override
	public void visit(Orange f) {oranges.add(f); }

	@Override
	public void visit(Apple f) { apples.add(f); }
}

// Visitor interface
interface IFruitVisitor {
	public void visit(Orange f);
	public void visit(Apple f);
}

Fruit.java and Apple.java, Orange.java and more Fruit.

interface Fruit {
	void accept(IFruitVisitor visitor);
}

class Orange implements Fruit {
	@Override
	public void accept(IFruitVisitor visitor) {
		visitor.visit(this);
	}
}

class Apple implements Fruit {
	@Override
	public void accept(IFruitVisitor visitor) {
		visitor.visit(this);
	}
}

Architectural

Interpreter [B]

Specification [B]

Other

Object Pool [A]

Lazy Initialization [B+]

Null Object [A]

public interface Animal {
 	void makeSound();
}

public class Dog implements Animal {
	 public void makeSound() {
	 	 System.out.println("woof!");
	 }
}

public class NullAnimal implements Animal {
	 public void makeSound() { 
     // Silence
  }
}

To run

String animalType = ''; // or 'dog'
Animal animal;
switch (animalType) {
   case 'dog':
       animal = new Dog();
       break;
   default:
       animal = new NullAnimal();
       break;
}

animal.makeSound(); // ..the null animal makes no sound

RAII [A+]