GOF Design Patterns
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.