GOF Design Patterns

Rayen Rejeb
21 min readFeb 1, 2023

--

Design Patterns — simplified — examples in JAVA

Design patterns are a fundamental aspect of software design and development. They are reusable solutions to common problems that arise in software design and provide a way for developers to write more maintainable, flexible, and scalable code.

The Gang of Four (GOF) is a group of four software engineers — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — who wrote the seminal book “Design Patterns: Elements of Reusable Object-Oriented Software” in 1994. This book introduced the concept of design patterns to the software development community and provided a comprehensive catalog of 23 design patterns that are commonly used in software development.

The GOF design patterns (23) are grouped into three categories: creational, structural, and behavioral. Each pattern provides a different solution to a common design problem and can be applied to various programming languages and domains. The patterns are widely used by software developers to write clean, maintainable, and scalable code.

Those design patterns have become an essential tool for software developers, providing a common vocabulary and understanding of common design problems and their solutions. They help developers write better code by providing a set of well-established solutions to common design problems, and they provide a foundation for further exploration and understanding of software design.

I. Creational patterns

1. Abstract Factory

A design pattern that provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes. In other words, it provides an interface for creating families of related or dependent objects without specifying their concrete classes.

interface Button {
void paint();
}

class WindowsButton implements Button {
@Override
public void paint() {
System.out.println("You have created WindowsButton.");
}
}

class MacOSButton implements Button {
@Override
public void paint() {
System.out.println("You have created MacOSButton.");
}
}

interface GUIFactory {
Button createButton();
}

class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
}

class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
}

class Application {
private Button button;

public Application(GUIFactory factory) {
button = factory.createButton();
}

public void paint() {
button.paint();
}
}

GUIFactory factory = new WindowsFactory();
Application app = new Application(factory);
app.paint();

This code creates a button using the WindowsFactory class and then paints the button. The output will be "You have created WindowsButton."

2. Builder

Allows building complex objects step by step using a builder object. It separates the construction of a complex object from its representation, so that the same construction process can create different representations.

class User {
private final String firstName;
private final String lastName;
private final int age;
private final String phone;
private final String address;

private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public int getAge() {
return age;
}

public String getPhone() {
return phone;
}

public String getAddress() {
return address;
}

public static class UserBuilder {
private final String firstName;
private final String lastName;
private int age;
private String phone;
private String address;

public UserBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public UserBuilder age(int age) {
this.age = age;
return this;
}

public UserBuilder phone(String phone) {
this.phone = phone;
return this;
}

public UserBuilder address(String address) {
this.address = address;
return this;
}

public User build() {
return new User(this);
}
}
}

User user = new User.UserBuilder("John", "Doe")
.age(30)
.phone("1234567890")
.address("Fake address 1234")
.build();

System.out.println(user.getFirstName());
System.out.println(user.getLastName());
System.out.println(user.getAge());
System.out.println(user.getPhone());
System.out.println(user.getAddress());

This code creates a User object using the UserBuilder class and then prints the attributes of the User object.

3. Factory Method

Provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. The Factory Method pattern is a way to create objects without specifying the exact class of object that will be created.

interface Animal {
void speak();
}

class Dog implements Animal {
@Override
public void speak() {
System.out.println("Bark");
}
}

class Cat implements Animal {
@Override
public void speak() {
System.out.println("Meow");
}
}

class AnimalFactory {
public Animal getAnimal(String animalType) {
if ("dog".equals(animalType)) {
return new Dog();
} else if ("cat".equals(animalType)) {
return new Cat();
}
return null;
}
}

AnimalFactory animalFactory = new AnimalFactory();
Animal animal = animalFactory.getAnimal("dog");
animal.speak();

This code creates an instance of the Animal class using the AnimalFactory class and then calls the speak method on the Animal object. The output will be "Bark".

4. Prototype

Allows creating an object by cloning a prototype object, instead of creating it from scratch. It involves implementing a clone method in the class that is to be cloned, and using this method to create a new instance of that class.

class Prototype implements Cloneable {
private String name;

public Prototype(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public Prototype clone() {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

return null;
}
}

Prototype prototype = new Prototype("Original");
Prototype clone = prototype.clone();

System.out.println(prototype.getName());
System.out.println(clone.getName());

clone.setName("Clone");

System.out.println(prototype.getName());
System.out.println(clone.getName());

This code creates a Prototype object and then creates a clone of that object using the clone method. The names of the original object and the clone object are then printed, and the name of the clone object is changed to demonstrate that the two objects are separate instances.

5. Singleton

It restricts a class to have only one instance and provides a global point of access to that instance. The singleton class is responsible for creating its own instance, and making sure that only one instance is created.

class Singleton {
private static Singleton instance = null;

private Singleton() {}

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

return instance;
}
}

Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();

System.out.println(singleton1 == singleton2);

This code creates two instances of the Singleton class and then compares them to demonstrate that they are the same instance. The output will be true.

II. Structural patterns

1. Adapter

Allows objects with incompatible interfaces to work together. The Adapter pattern converts the interface of a class into another interface that a client is expecting. This allows classes that could not otherwise work together due to incompatible interfaces to work together.

interface Charger {
void charge();
}

class iPhoneCharger implements Charger {
@Override
public void charge() {
System.out.println("Charging iPhone");
}
}

class AndroidCharger {
void chargeAndroid() {
System.out.println("Charging Android");
}
}

class AndroidChargerAdapter implements Charger {
private AndroidCharger androidCharger;

public AndroidChargerAdapter(AndroidCharger androidCharger) {
this.androidCharger = androidCharger;
}

@Override
public void charge() {
androidCharger.chargeAndroid();
}
}

Charger iphoneCharger = new iPhoneCharger();
iphoneCharger.charge();

AndroidCharger androidCharger = new AndroidCharger();
Charger androidChargerAdapter = new AndroidChargerAdapter(androidCharger);
androidChargerAdapter.charge();

This code demonstrates how the AndroidCharger class can be adapted to work with the Charger interface by using the AndroidChargerAdapter class. The output will be "Charging iPhone" and "Charging Android".

2. Bridge

Decouples an abstraction from its implementation, allowing the two to vary independently. The Bridge pattern is used to separate the abstractions from their implementations so that they can evolve independently.

interface Color {
void applyColor();
}

class Red implements Color {
@Override
public void applyColor() {
System.out.println("Applying red color");
}
}

class Green implements Color {
@Override
public void applyColor() {
System.out.println("Applying green color");
}
}

abstract class Shape {
protected Color color;

public Shape(Color color) {
this.color = color;
}

abstract void applyColor();
}

class Triangle extends Shape {
public Triangle(Color color) {
super(color);
}

@Override
void applyColor() {
System.out.println("Triangle filled with color");
color.applyColor();
}
}

class Circle extends Shape {
public Circle(Color color) {
super(color);
}

@Override
void applyColor() {
System.out.println("Circle filled with color");
color.applyColor();
}
}

Color red = new Red();
Shape triangle = new Triangle(red);
triangle.applyColor();

Color green = new Green();
Shape circle = new Circle(green);
circle.applyColor();

This code demonstrates how the Shape class can be separated from its implementation, allowing the two to vary independently. The Triangle and Circle classes are implementations of the Shape class, and the Red and Green classes are implementations of the Color interface. The output will be "Triangle filled with color" and "Applying red color", followed by "Circle filled with color" and "Applying green color".

3. Composite

Allows you to treat individual objects and compositions of objects uniformly. The Composite pattern is used to represent part-whole hierarchies, where a composite object is an object that contains one or more other objects.

interface Component {
void display();
}

class Leaf implements Component {
private String name;

public Leaf(String name) {
this.name = name;
}

@Override
public void display() {
System.out.println(name);
}
}

class Composite implements Component {
private List<Component> components = new ArrayList<>();

@Override
public void display() {
for (Component component : components) {
component.display();
}
}

public void add(Component component) {
components.add(component);
}

public void remove(Component component) {
components.remove(component);
}
}

Component fileSystem = new Composite();

Component folder1 = new Composite();
folder1.add(new Leaf("File 1"));
folder1.add(new Leaf("File 2"));
fileSystem.add(folder1);

Component folder2 = new Composite();
folder2.add(new Leaf("File 3"));
folder2.add(new Leaf("File 4"));
fileSystem.add(folder2);

fileSystem.display();

This code creates a file system where a Composite object can contain other Component objects, including other Composite objects. The output will be:

File 1
File 2
File 3
File 4

4. Decorator

Allows you to add or override behavior to an individual object, dynamically, without affecting the behavior of other objects from the same class. The Decorator pattern is used to add additional responsibilities to an object dynamically.

interface Shape {
String info();
}

class Circle implements Shape {
private float radius;

public Circle() {
}

public Circle(float radius) {
this.radius = radius;
}

void resize(float factor) {
radius *= factor;
}

@Override
public String info() {
return "A circle of radius " + radius;
}
}

class ShapeDecorator implements Shape {
protected Shape decoratedShape;

public ShapeDecorator(Shape decoratedShape) {
this.decoratedShape = decoratedShape;
}

@Override
public String info() {
return decoratedShape.info();
}
}

class ColoredCircle extends ShapeDecorator {
private String color;

public ColoredCircle(Shape decoratedShape, String color) {
super(decoratedShape);
this.color = color;
}

@Override
public String info() {
return decoratedShape.info() + " has the color " + color;
}
}

class TransparentCircle extends ShapeDecorator {
private int transparency;

public TransparentCircle(Shape decoratedShape, int transparency) {
super(decoratedShape);
this.transparency = transparency;
}

@Override
public String info() {
return decoratedShape.info() + " has " + transparency + "% transparency";
}
}

Shape circle = new Circle(10);
Shape coloredCircle = new ColoredCircle(circle, "red");
Shape transparentCircle = new TransparentCircle(coloredCircle, 50);

System.out.println(circle.info());
System.out.println(coloredCircle.info());
System.out.println(transparentCircle.info());

This code demonstrates how the behavior of a Circle object can be changed dynamically through the use of ShapeDecorator and its subclasses. The output will be:

A circle of radius 10.0
A circle of radius 10.0 has the color red
A circle of radius 10.0 has the color red has 50% transparency

5. Facade

Provides a simplified interface to a complex system of objects, hiding its internal complexity. The Facade pattern is used to provide a unified and high-level interface to a subsystem, making it easier to use.

class CPU {
public void freeze() {
System.out.println("CPU: freeze");
}

public void jump(long position) {
System.out.println("CPU: jump to " + position);
}

public void execute() {
System.out.println("CPU: execute");
}
}

class Memory {
public void load(long position, byte[] data) {
System.out.println("Memory: load to " + position);
}
}

class HardDrive {
public byte[] read(long lba, int size) {
System.out.println("HardDrive: read from " + lba);
return new byte[0];
}
}

class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;

public ComputerFacade() {
cpu = new CPU();
memory = new Memory();
hardDrive = new HardDrive();
}

public void start() {
cpu.freeze();
memory.load(0, hardDrive.read(0, 0));
cpu.jump(0);
cpu.execute();
}
}

ComputerFacade computer = new ComputerFacade();
computer.start();

This code demonstrates how a ComputerFacade can simplify the process of starting a computer by hiding the internal complexity of its components, CPU, Memory, and HardDrive. The output will be:

CPU: freeze
Memory: load to 0
HardDrive: read from 0
CPU: jump to 0
CPU: execute

6. Flyweight

Enables an application to maintain a large number of objects efficiently. The idea behind Flyweight is to share objects that are similar in terms of their state and behavior, and only keep unique data in memory.

interface Shape {
void draw();
}

class Circle implements Shape {
private int x;
private int y;
private int radius;
private String color;

public Circle(String color) {
this.color = color;
}

public void setX(int x) {
this.x = x;
}

public void setY(int y) {
this.y = y;
}

public void setRadius(int radius) {
this.radius = radius;
}

public void draw() {
System.out.println("Circle: Draw() [Color : " + color + ", x : " + x + ", y :" + y + ", radius :" + radius);
}
}

class ShapeFactory {
private static final Map<String, Shape> circleMap = new HashMap<>();

public static Shape getCircle(String color) {
Circle circle = (Circle)circleMap.get(color);

if(circle == null) {
circle = new Circle(color);
circleMap.put(color, circle);
System.out.println("Creating circle of color : " + color);
}
return circle;
}
}

public class FlyweightPatternDemo {
private static final String colors[] = { "Red", "Green", "Blue", "White", "Black" };
public static void main(String[] args) {
for(int i=0; i < 20; ++i) {
Circle circle = (Circle)ShapeFactory.getCircle(getRandomColor());
circle.setX(getRandomX());
circle.setY(getRandomY());
circle.setRadius(100);
circle.draw();
}
}
private static String getRandomColor() {
return colors[(int)(Math.random()*colors.length)];
}
private static int getRandomX() {
return (int)(Math.random()*100 );
}
private static int getRandomY() {
return (int)(Math.random()*100);
}
}

In this example, we have a Shape interface and a Circle implementation of the Shape interface. The ShapeFactory maintains a pool of Circle objects and reuses them whenever a Circle object of a certain color is requested. The main method creates 20 Circle objects, each with random color, position, and radius, and reuses Circle objects from the pool whenever possible, reducing memory usage and improving performance.

7. Proxy

Provides an object that acts as an intermediary for accessing another object. A proxy is used to control access to an underlying object, by providing a surrogate or placeholder for the real object, for example, to add security or caching.

interface Image {
void display();
}

class RealImage implements Image {
private String fileName;

public RealImage(String fileName){
this.fileName = fileName;
loadFromDisk(fileName);
}

@Override
public void display() {
System.out.println("Displaying " + fileName);
}

private void loadFromDisk(String fileName){
System.out.println("Loading " + fileName);
}
}

class ImageProxy implements Image {
private RealImage realImage;
private String fileName;

public ImageProxy(String fileName){
this.fileName = fileName;
}

@Override
public void display() {
if(realImage == null){
realImage = new RealImage(fileName);
}
realImage.display();
}
}

public class ProxyPatternDemo {
public static void main(String[] args) {
Image image = new ImageProxy("test_10mb.jpg");
image.display();
System.out.println("");
image.display();
}
}

In this example, we have an Image interface, a RealImage implementation of the Image interface, and an ImageProxy implementation of the Image interface. The RealImage class represents the real object, and the ImageProxy class acts as an intermediary for accessing the RealImage object. The ImageProxy object loads the real image only when it's first requested, and caches the image in memory for subsequent requests, reducing the overhead of loading the image from disk each time.

III. Behavioral patterns

1. Chain of Responsibility

Provides a way to pass requests along a dynamic chain of handlers. Each handler has the opportunity to handle the request or pass it to the next handler in the chain. This helps to decouple the sender of a request from its receiver, and gives multiple objects a chance to handle the request.

abstract class AbstractLogger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;

protected int level;
protected AbstractLogger nextLogger;

public void setNextLogger(AbstractLogger nextLogger){
this.nextLogger = nextLogger;
}

public void logMessage(int level, String message){
if(this.level <= level){
write(message);
}
if(nextLogger !=null){
nextLogger.logMessage(level, message);
}
}

abstract protected void write(String message);
}

class ConsoleLogger extends AbstractLogger {
public ConsoleLogger(int level){
this.level = level;
}

@Override
protected void write(String message) {
System.out.println("Standard Console::Logger: " + message);
}
}

class ErrorLogger extends AbstractLogger {
public ErrorLogger(int level){
this.level = level;
}

@Override
protected void write(String message) {
System.out.println("Error Console::Logger: " + message);
}
}

class FileLogger extends AbstractLogger {
public FileLogger(int level){
this.level = level;
}

@Override
protected void write(String message) {
System.out.println("File::Logger: " + message);
}
}

public class ChainOfResponsibilityDemo {
private static AbstractLogger getChainOfLoggers(){
AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);
AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);
AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);

errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);

return errorLogger;
}

public static void main(String[] args) {
AbstractLogger loggerChain = getChainOfLoggers();

loggerChain.logMessage(AbstractLogger.INFO, "This is an information.");
loggerChain.logMessage(AbstractLogger.DEBUG, "This is an debug level information.");
loggerChain.logMessage(AbstractLogger.ERROR, "This is an error information.");
}
}

In this example, we have an abstract AbstractLogger class, and three concrete implementations: ConsoleLogger, ErrorLogger, and FileLogger. The AbstractLogger class provides the mechanism for building the chain of loggers, and the concrete loggers are responsible for logging messages of different levels (info, debug, and error). When a message is logged, the loggers in the chain are checked in order, and the first logger that is capable of handling the message logs it. If the current logger can’t handle the message, it passes the request to the next logger in the chain. This way, multiple loggers have the opportunity to handle the request, and the sender of the request does not need to know which logger will handle it.

2. Command

Turns a request into a stand-alone object that contains all information about the request. This allows for deferred or scheduled execution of operations, queuing of operations, and undo/redo.

interface Command {
void execute();
}

class Light {
public void turnOn() {
System.out.println("The light is on");
}

public void turnOff() {
System.out.println("The light is off");
}
}

class TurnOnLightCommand implements Command {
private Light light;

public TurnOnLightCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.turnOn();
}
}

class TurnOffLightCommand implements Command {
private Light light;

public TurnOffLightCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.turnOff();
}
}

class RemoteControl {
private Command command;

public void setCommand(Command command) {
this.command = command;
}

public void pressButton() {
command.execute();
}
}

public class CommandDemo {
public static void main(String[] args) {
Light light = new Light();
Command turnOnCommand = new TurnOnLightCommand(light);
Command turnOffCommand = new TurnOffLightCommand(light);

RemoteControl remoteControl = new RemoteControl();
remoteControl.setCommand(turnOnCommand);
remoteControl.pressButton();

remoteControl.setCommand(turnOffCommand);
remoteControl.pressButton();
}
}

In this example, we have a Light class and two Command implementations: TurnOnLightCommand and TurnOffLightCommand. The RemoteControl class holds a reference to a Command object, and when its pressButton() method is called, it delegates the execution to the Command object. The client code can use the RemoteControl class to turn the light on or off by setting different Command objects and pressing the button. This allows the client code to decouple from the implementation of the light and from the implementation of the commands, making it possible to change either one without affecting the other.

3. Interpreter

Lets you represent grammar rules using objects. The pattern defines the grammar and implements it using a set of interpreter classes, so that client code can evaluate expressions in the defined grammar.

interface Expression {
int interpret();
}

class NumberExpression implements Expression {
private int number;

public NumberExpression(int number) {
this.number = number;
}

@Override
public int interpret() {
return number;
}
}

class AdditionExpression implements Expression {
private Expression leftExpression;
private Expression rightExpression;

public AdditionExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}

@Override
public int interpret() {
return leftExpression.interpret() + rightExpression.interpret();
}
}

public class InterpreterDemo {
public static void main(String[] args) {
Expression expression = new AdditionExpression(
new NumberExpression(1),
new NumberExpression(2)
);

System.out.println(expression.interpret());
}
}

In this example, we have an Expression interface and two implementations: NumberExpression and AdditionExpression. The NumberExpression represents a number in the grammar, and its interpret() method returns the number. The AdditionExpression represents an addition operation in the grammar, and its interpret() method returns the sum of the results of the interpret() methods of its left and right expressions. The client code creates an AdditionExpression object with two NumberExpression objects, and evaluates the expression by calling the interpret() method. This allows the client code to parse expressions in the defined grammar and evaluate them, without having to implement a parser.

4. Iterator

Lets you traverse elements of a collection without exposing the underlying representation of the collection. The pattern defines an iterator interface that includes methods for accessing and manipulating elements of the collection.

interface Iterator {
boolean hasNext();
Object next();
}

interface Collection {
Iterator createIterator();
}

class ConcreteIterator implements Iterator {
private ConcreteAggregate aggregate;
private int current = 0;

public ConcreteIterator(ConcreteAggregate aggregate) {
this.aggregate = aggregate;
}

@Override
public boolean hasNext() {
return current < aggregate.count();
}

@Override
public Object next() {
return aggregate.get(current++);
}
}

class ConcreteAggregate implements Collection {
private Object[] items = new Object[10];
private int count = 0;

public ConcreteAggregate() {
for (int i = 0; i < 10; i++) {
items[i] = i;
}
}

public Object get(int index) {
return items[index];
}

public int count() {
return count;
}

@Override
public Iterator createIterator() {
return new ConcreteIterator(this);
}
}

public class IteratorDemo {
public static void main(String[] args) {
ConcreteAggregate aggregate = new ConcreteAggregate();
Iterator iterator = aggregate.createIterator();

while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}

In this example, we have an Iterator interface, a Collection interface, and two implementations: ConcreteIterator and ConcreteAggregate. The ConcreteAggregate implements the Collection interface and represents a concrete collection of elements. The ConcreteIterator implements the Iterator interface and provides an iterator for the ConcreteAggregate. The client code creates a ConcreteAggregate object and gets its iterator, then iterates over the elements of the collection using the next() method and checking the hasNext() method. This way, the client code can traverse the elements of the collection without having to know anything about its underlying representation.

5. Mediator

Allows objects to communicate with each other indirectly, through a mediator object. The mediator acts as a middleman and decouples the communication between objects, so that objects don’t have to communicate directly with each other.

interface Mediator {
void send(String message, Colleague colleague);
}

abstract class Colleague {
private Mediator mediator;

public Colleague(Mediator mediator) {
this.mediator = mediator;
}

public void send(String message) {
mediator.send(message, this);
}

public abstract void receive(String message);
}

class ConcreteColleague1 extends Colleague {
public ConcreteColleague1(Mediator mediator) {
super(mediator);
}

@Override
public void receive(String message) {
System.out.println("Colleague 1 received: " + message);
}
}

class ConcreteColleague2 extends Colleague {
public ConcreteColleague2(Mediator mediator) {
super(mediator);
}

@Override
public void receive(String message) {
System.out.println("Colleague 2 received: " + message);
}
}

class ConcreteMediator implements Mediator {
private Colleague colleague1;
private Colleague colleague2;

public void setColleague1(Colleague colleague) {
this.colleague1 = colleague;
}

public void setColleague2(Colleague colleague) {
this.colleague2 = colleague;
}

@Override
public void send(String message, Colleague colleague) {
if (colleague == colleague1) {
colleague2.receive(message);
} else {
colleague1.receive(message);
}
}
}

public class MediatorDemo {
public static void main(String[] args) {
ConcreteMediator mediator = new ConcreteMediator();

ConcreteColleague1 colleague1 = new ConcreteColleague1(mediator);
ConcreteColleague2 colleague2 = new ConcreteColleague2(mediator);

mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);

colleague1.send("How are you?");
colleague2.send("Fine, thanks");
}
}

In this example, we have a Mediator interface, an abstract Colleague class, and two concrete colleague classes ConcreteColleague1 and ConcreteColleague2. The ConcreteMediator implements the Mediator interface and acts as a mediator between the two concrete colleagues. The ConcreteColleague1 and ConcreteColleague2 classes represent concrete objects that need to communicate with each other, but don't have direct references to each other. Instead, they have a reference to the ConcreteMediator object and send messages to each other through the mediator. In this example, the client code creates ConcreteColleague1 and ConcreteColleague2 objects, sets them in the ConcreteMediator object, and then sends messages between them through the mediator. The output of this program would be:

Colleague 1 received: Fine, thanks
Colleague 2 received: How are you?

This example shows how the Mediator design pattern can help to decouple communication between objects, making the communication more flexible and scalable. By using a mediator, you can add or remove objects from the communication without affecting the other objects, and you can also change the way that objects communicate without affecting the objects themselves. The Mediator design pattern is often used in GUI programming, where multiple components need to communicate with each other in a complex and dynamic way.

6. Mediator

Used to capture and restore the state of an object. The basic idea is to allow an object to save its state without exposing the details of its implementation.

class Originator {
private String state;

public void setState(String state) {
this.state = state;
}

public String getState() {
return state;
}

public Memento saveStateToMemento() {
return new Memento(state);
}

public void getStateFromMemento(Memento memento) {
state = memento.getState();
}
}

class Memento {
private String state;

public Memento(String state) {
this.state = state;
}

public String getState() {
return state;
}
}

class CareTaker {
private List<Memento> mementoList = new ArrayList<Memento>();

public void add(Memento state) {
mementoList.add(state);
}

public Memento get(int index) {
return mementoList.get(index);
}
}

In this example, the Originator class contains the state that needs to be saved, and the Memento class provides a way to save the state of the Originator. The CareTaker class is used to store and retrieve Memento objects.

To use this pattern, you would create an instance of the Originator class, set its state, and then create a Memento from the Originator using the saveStateToMemento method. You can then use the CareTaker to store the Memento, and later retrieve it using the get method. To restore the state of the Originator, you would pass the Memento to the getStateFromMemento method.

7. Observer

Used to implement a one-to-many dependency between objects. The basic idea is to allow multiple objects to be notified when the state of a single object changes. This can be useful in a variety of situations, such as in GUI programming, where multiple components need to be updated when the state of a single component changes.

interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}

interface Observer {
void update(float temperature, float humidity, float pressure);
}

class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;

public WeatherData() {
observers = new ArrayList<Observer>();
}

public void registerObserver(Observer o) {
observers.add(o);
}

public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}

public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}

public void measurementsChanged() {
notifyObservers();
}

public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}

class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
private Subject weatherData;

public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}

public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}

In this example, the Subject interface provides methods for registering and removing observers, and for notifying observers when the state of the Subject changes. The Observer interface defines a single method, update, that is called when the state of the Subject changes.

The WeatherData class implements the Subject interface and maintains a list of registered observers. When the state of the WeatherData changes, the notifyObservers method is called to notify all registered observers.

The CurrentConditionsDisplay class implements the Observer interface and is used to display the current conditions. When the state of the WeatherData changes, the update method is called, which updates the display.

To use this pattern, you would create an instance of the WeatherData class and one or more instances of the CurrentConditionsDisplay class, and register each CurrentConditionsDisplay instance as an observer of the WeatherData instance. When the state of the WeatherData instance changes, the `observers` will be notified update methods will be called to reflect the changes.

8. State

Allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

interface State {
void handle();
}

class ConcreteStateA implements State {
@Override
public void handle() {
System.out.println("State A handled");
}
}

class ConcreteStateB implements State {
@Override
public void handle() {
System.out.println("State B handled");
}
}

class Context {
private State state;

public Context(State state) {
this.state = state;
}

public void setState(State state) {
this.state = state;
}

public void request() {
state.handle();
}
}

public class Main {
public static void main(String[] args) {
Context context = new Context(new ConcreteStateA());
context.request();
context.setState(new ConcreteStateB());
context.request();
}
}

In this example, the Context class holds the current state and can change its behavior by changing the state. The ConcreteStateA and ConcreteStateB classes are two concrete states that handle requests differently. When a request is made, the current state handles it.

9. Strategy

Enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.

interface Strategy {
int doOperation(int num1, int num2);
}

class AdditionStrategy implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}

class SubtractionStrategy implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}

class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}

public class Main {
public static void main(String[] args) {
Context context = new Context(new AdditionStrategy());
System.out.println("Result: " + context.executeStrategy(3, 4));

context = new Context(new SubtractionStrategy());
System.out.println("Result: " + context.executeStrategy(3, 4));
}
}

In this example, the Context class holds the strategy to use and can change the strategy at runtime. The AdditionStrategy and SubtractionStrategy classes are concrete strategies that implement different algorithms. The executeStrategy method uses the strategy to calculate the result.

10. Template Method

Defines the skeleton of an algorithm in a method, deferring some steps to subclasses.

abstract class Game {
protected int playersCount;

abstract void initializeGame();

abstract void makePlay(int player);

abstract boolean endOfGame();

abstract void printWinner();

public final void playOneGame(int playersCount) {
this.playersCount = playersCount;
initializeGame();
int j = 0;
while (!endOfGame()) {
makePlay(j);
j = (j + 1) % playersCount;
}
printWinner();
}
}

class Chess extends Game {
@Override
void initializeGame() {
// Chess-specific initialization actions
}

@Override
void makePlay(int player) {
// Chess-specific play actions
}

@Override
boolean endOfGame() {
// Chess-specific end of game check
return false;
}

@Override
void printWinner() {
// Chess-specific winner printing actions
}
}

In this example, the abstract Game class defines the template method playOneGame that implements the common steps of the game playing process. The Chess class is a concrete implementation that provides specific implementation for the abstract methods defined in the Game class.

11. Visitor

Lets you separate algorithms from the objects on which they operate.

interface Visitor {
void visit(ConcreteElementA elementA);
void visit(ConcreteElementB elementB);
}

interface Element {
void accept(Visitor visitor);
}

class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

class ConcreteVisitor1 implements Visitor {
@Override
public void visit(ConcreteElementA elementA) {
// ConcreteVisitor1-specific operations on ConcreteElementA
}

@Override
public void visit(ConcreteElementB elementB) {
// ConcreteVisitor1-specific operations on ConcreteElementB
}
}

In this example, the Visitor interface declares a visit operation for each concrete element. The Element interface defines the accept method, which takes a visitor as an argument. The ConcreteElementA and ConcreteElementB classes implement the Element interface and define the accept method, which calls the visit method of the visitor and passes itself as an argument. The ConcreteVisitor1 class implements the Visitor interface and provides the implementation for the visit methods, which perform operations on elements.

aaand that’s it ! These patterns provide a common vocabulary and understanding of common design problems and their solutions, which can help improve the overall design of software systems.

--

--