Learning Design Patterns
Helping me learn Object Orientated design patterns with real world scenarios.
See everything online.
Contents
- Creational
- Abstract Factory B+ (todo)
- Factory Method B
- Prototype A (todo)
- Structural
- Adapter A (todo)
- Bridge A (todo)
- Composite A (todo)
- Decorator A++
- Proxy B (todo)
- Behavioral
- Architectural
- Interpreter B (todo)
- Specification B (todo)
- Other
- Object Pool A (todo)
- Lazy Initialization B+ (todo)
- Null Object A
- RAII A+ (todo)
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
// 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.
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.
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