OO Design Patterns
An Introduction to Design Patterns
By now you should understand the importance of a good OO design! Good OO designs should be reusable, extensible and maintainable. In fact, useful software constantly changes to add new features, extend existing features, fix bugs, improve performance etc. Software systems with a poor OO design are very hard to maintain and even performing a simple change might take a lot of time and effort. Do you know that software developers spend more time reading the code than actually writing it?
Do you think that crafting good OO designs is difficult?… don’t worry Design Patterns are here to help you!
Software design patterns describe common (and very successful) ways of building software. More specifically, a software design pattern is a general, reusable solution (template) to a commonly occurring problem in software design.
Let’s see a real world example: The “door” pattern
Problem:
- We want a portal between two spaces
- Must be able to open and close
Solution: Build a door!
Doors have multiple components: lock, handle, frame etc…
The design pattern “door” specifies how these components interact to solve our problem. The “door” design is reusable, in fact the implementation can be very different!
In this course we are interested in object-oriented design patterns, which show relationships and interactions between classes or instances.
There are 23 main OO Design patterns, which can be classified into three main categories:
-
Creational Patterns (5) provide ways to create instances while hiding the creation logic, instead of instantiating objects directly using the
new
operator. This gives the program more flexibility in deciding which instances need to be created. -
Structural Patterns (7) deal with class and object composition. The concept of inheritance is used to compose interfaces and define ways to compose objects to obtain new functionality.
-
Behavioural Patterns (11) deal with communication between objects.
The chart below shows all of the 23 patterns.
We will cover only few of the most used and important. You are of course encouraged to study more about them in the future. Indeed, design patterns are widely used, and often software developer job interviews have always a couple of questions about them.
There are key advantages of using OO design patterns.
- They will make your life easier by not reinventing the wheel, design patterns are solution to common OO problems.
- They will improve your OO skills.
- OO design patterns are widely used, you will be able to recognizes them if you read code.
- It is easier to communicate software design ideas if developers have a shared vocabulary. Design patterns will help with that.
- OO design patterns are not specific to a certain programming language, once you learn them you can use them with any OO programming language (e.g., C#, Ruby, Python, TypeScript)
Some History
In 1977, Design Patterns originated as an architectural concept in the book “A Pattern Language” by Christopher Alexander. He defined patterns as “successful solutions to problems” in the context of building.
In 1987, Ward Cunningham and Kent Beck leverage to idea and applied it to OO programs.
In 1994, the book “Design Patterns: Elements of reusable object-oriented software”, written by four authors (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) also called the “Gang of Four”, proposed 23 (classic) design patterns for OO programs.
Caution!
Do not abuse design patterns! Design patterns do not magically improve the design and quality of your code, they might even make it worst if used when are not needed.
It is important to identify the scenarios and problems which the patterns are meant to address. Design Patterns are solutions for common problems, if your program does not suffer from the problem that a design pattern is meant to address do not use that design pattern!
Factory (Creational)
The Factory Design Pattern is one of the most used design patterns in Java. It is very useful when you need to provide high flexibility for your code.
Problem:
In OOP we often have to create different instances o similar classes. In such cases, we want to have highly maintainable code so that when we want to add a new class type, we just want to change the code in one place.
For example, let’s assume that you have opened a Coffee Shop and you only make espressos. You implemented an automated system to make coffee. The code looks like this:
public class Main {
public static void main(String[] args) {
EspressoCoffee espressoCoffee = new EspressoCoffee();
espressoCoffee.grindCoffeeBeans();
espressoCoffee.make();
espressoCoffee.serve();
}
}
This is the EspressoCoffee
class:
public class EspressoCoffee {
public void grindCoffeeBeans() {
System.out.println("Use espresso roast coffee, about 9 grams for a single espresso shot");
}
public void make() {
System.out.println("Place the portafilter in the espresso machine and press the button to pull the shot.");
}
public void serve() {
System.out.println("Espresso can only be served in small ceramic cups");
}
}
As your coffee shop gets popular, you want to add more types of coffee: Cappuccino and Americano. However, adding a new coffee to the program is not that simple if the rest of the code is already coupled to the EspressoCoffee
type. Adding new coffee types into the program would require making changes to the entire codebase.
For example, something like this:
switch (coffeeType) {
case "E":
EspressoCoffee espresso = new EspressoCoffee();
espresso.grindCoffeeBeans();
espresso.make();
espresso.serve();
break;
case "A":
AmericanoCoffee americano = new AmericanoCoffee();
americano.grindCoffeeBeans();
americano.make();
americano.serve();
break;
case "C":
CappuccinoCoffee cappuccino = new CappuccinoCoffee();
cappuccino.grindCoffeeBeans();
cappuccino.make();
cappuccino.serve();
break;
}
Moreover, if later you decide to add another type of coffee, you will probably need to make all of these changes again.
As a result, you will end up with bad code, riddled with conditionals that switch the program behavior depending on the type of coffee.
Solution:
The Factory pattern can come to the rescue!
This pattern delegates the responsibility of initializing a class to a particular factory class by creating a type of virtual constructor.
First, let’s create a common interface for all types of Coffee. Note that you could also create an abstract class.
public interface Coffee {
public void grindCoffeeBeans();
public void make();
public void serve();
}
Then, let’s create the coffee classes that implement this interface.
public class EspressoCoffee implements Coffee {
@Override
public void grindCoffeeBeans() {
System.out.println("Use espresso roast coffee, about 9 grams for a single espresso shot");
}
@Override
public void make() {
System.out.println("Place the portafilter in the espresso machine and press the button to pull the shot.");
}
@Override
public void serve() {
System.out.println("Espresso can only be served in small ceramic cups");
}
}
public class AmericanoCoffee implements Coffee {
@Override
public void grindCoffeeBeans() {
System.out.println("Use espresso roast coffee, about 6 grams for an americano");
}
@Override
public void make() {
System.out.println("add 100ml of hot water");
}
@Override
public void serve() {
System.out.println("Americano can only be served in big glasses");
}
}
public class CappuccinoCoffee implements Coffee {
@Override
public void grindCoffeeBeans() {
System.out.println("Use espresso roast coffee, about 3 grams for a cappuccino");
}
@Override
public void make() {
System.out.println("add 100ml of milk, add the foam");
}
@Override
public void serve() {
System.out.println("Cappuccino can only be served in big ceramic mugs");
}
}
Then we create a Factory class and method to create the coffee instance based on the type (“A”, “C”, or “E”). The method creataCoffee
returns a Coffee
object with an implementation that depends on the coffee type given as a parameter.
public class CoffeeFactory {
public static Coffee createCoffee(String type) {
switch (type) {
case "E":
return new EspressoCoffee();
case "A":
return new AmericanoCoffee();
case "C":
return new CappuccinoCoffee();
default:
System.err.println("wrong coffee type");
System.exit(0);
}
return null;
}
}
Then, every time we want to create a coffee instance, we will use the Factory method like this:
Coffee coffee = CoffeeFactory.createCoffee("A"); // this will create an Americano
coffee.grindCoffeeBeans();
coffee.make();
coffee.serve();
In the meantime, this is the final result of what we developed above:
Get your hands dirty!
A bank offers credit cards to its customers. They offer three types of credit cards: Silver, Gold, and Platinum cards. Each card type has a different credit limit: Silver NZD 1,000, Gold NZD 2,000, and Platinum NZD 30,000. The type of issued card depends on the yearly income of the customers. Platinum for an annual salary greater than NZD 200,000. Gold for an annual salary from NZD 100,000 to NZD 199,999. For annual salaries of 99,999 or less, the Bank issues a Silver card. The Bank is planning to add more types of cards in the future. Help the Bank to make their software easier to maintain by applying the Factory design pattern.
- Create an interface
CreditCard
. The classesPlatinumCard
,GoldCard
, andSilverCard
should implement it. - Create a
CreditCartFactory
class, with a static methodcreateCard(int)
following the Factory design pattern.
BONUS Change the interfaceCreditCard
to an abstract class. Is it better ifCreditCard
is an interface or an abstract class? Why?
Builder (Creational)
Problem: In OOP we often have classes holding some data that we are setting and later accessing. If we have many instance fields, things can get a bit tedious!
For example, let’s consider the following Pizza
class:
public class Pizza{
private int size; // mandatory
private boolean onion; // optional
private boolean cheese; // optional
private boolean olives; // optional
private boolean tomato; // optional
private boolean mushroom; // optional
private boolean ham; // optional
private int sausageCount; // optional
We have one mandatory attribute (size
), and seven optional attributes. The constructor would be:
public Pizza(int size, boolean onion, boolean cheese,
boolean olives, boolean tomato,
boolean mushroom, boolean ham, int sausageCount){
this.size = size;
this.onion = onion;
this.cheese = cheese;
this.olives = olives;
this.tomato = tomato;
this.mushroom = mushroom;
this.ham = ham;
this.sausageCount = sausageCount;
}
This is very error prone: the parameters might easily be mixed up by the developers as many are of the same type! Seven parameters are not even mandatory. But what if we have new ingredients in the future? Should this constructor be extended with even more parameters to cater for more ingredients? If we do so, we had to change all the previously-written code that used the (old) constructor to add the extra parameter(s)!
Sure, one possible solution is to use a default constructor and then use setter methods to set the optional parameters. For example, something like this:
Pizza pizza = new Pizza();
pizza.setSize(10);
pizza.setTomato(true);
pizza.setSausageCount(2);
pizza.setHam(true);
But we still might not want this… what if a developer forgets to set the mandatory field? What if we want that the Pizza is an immutable instance, meaning that once it is created it should not be able to change its state?
Solution: Use the Builder design pattern: the main idea is to separate mandatory from optional parameters, and to move the construction logic out of the object’s class into a separate static
inner class referred to as a “builder” class:
- The
Builder
class has a constructor only for mandatory parameters, and setter methods for all the optional parameters. - In addition, there is a
build()
method that glues everything together and returns a complete instance.
Consider this:
// Pizza class:
public class Pizza {
...
// Pizza's inner static Builder class:
public static class Builder {
private int size; // mandatory
private boolean onion = false; // optional with default value
private boolean cheese = false; // optional with default value
private boolean olives = false; // optional with default value
private boolean tomato = false; // optional with default value
private boolean mushroom = false; // optional with default value
private boolean ham = false; // optional with default value
private int sausageCount = 0; // optional with default value
// We set size in the constructor, enforce the client sets it (as it's a mandatory field)
public Builder(int size) {
this.size = size;
}
public Builder addOnions() {
this.onion = true;
return this;
}
public Builder addCheese() {
this.cheese = true;
return this;
}
public Builder addOlives() {
this.olives = true;
return this;
}
public Builder addTomato() {
this.tomato = true;
return this;
}
public Builder addMushrooms() {
this.mushroom = true;
return this;
}
public Builder addHam() {
this.ham = true;
return this;
}
public Builder sausageCount(int sausageCount) {
this.sausageCount = sausageCount;
return this;
}
public Pizza build() {
return new Pizza(this);
}
}
}
All the builder setter methods return the builder itself (notice how they all return this
). This allows the setter invocations can be “chained”.
Coming back to the construction of the Pizza
, its constructor needs to be private
so that it cannot be instantiated directly by “outsider” classes. However, it will of course be instantiated through the inner class Builder
’s build()
method!
public class Pizza {
private int size; // mandatory
private boolean onion; // optional
private boolean cheese; // optional
private boolean olives; // optional
private boolean tomato; // optional
private boolean mushroom; // optional
private boolean ham; // optional
private int sausageCount; // optional
private Pizza(Builder builder) {
this.size = builder.size;
this.onion = builder.onion;
this.cheese = builder.cheese;
this.olives = builder.olives;
this.tomato = builder.tomato;
this.mushroom = builder.mushroom;
this.ham = builder.ham;
this.sausageCount = builder.sausageCount;
}
So, creating an instance of the Pizza
using the builder pattern will look like this:
Pizza pizza = new Pizza.Builder(10) // note that this "new" creates a "Pizza.Builder" instance, not a "Pizza"!
.addCheese() // add cheese to the builder instance, returns the builder instance,
.addOlives() // add olives to the builder instance, returns the builder instance,
.sausageCount(2) // add 2 sausages to the builder instance, returns the builder instance,
.addTomato() // add tomato to the builder instance, returns the builder instance,
.build(); // now we create the "Pizza" instance!
Note that, in our case the pizza
instance is immutable. If we want to be able change its state after creation, we would have to add public setter methods to the Pizza
class.
In the meantime, this is the final result of what we developed above:
Get your hands dirty!
Apply the Builder design pattern to this SoupNoodle
class:
The fields noodleType
and size
are mandatory, while the fields extraNoodle
, extraVeggies
, bambooShoots
, egg
are optional (default values are false).
Prototype (Creational)
Problem: In OOP, we often need to create copies of objects. For example, when implementing a game with an object representing an avatar, we might find it convenient to use this object as a prototype to create thousands of copies and then make minor modifications, such as slightly changing the size. For instance, we may want to create a crowd of people walking on the street. How can this be achieved? How do we create an exact copy of an object? By now, it should be clear that an object is characterized by its field values. So, to make a copy of an object, we can create an object of the same type and set the same field values. This might sound easy but can be impossible in some cases.
For example, let’s consider the following Avatar
class:
public class Avatar {
private int size;
private int walkingSpeed;
private int walkingDirection;
public Avatar(int size, int walkingSpeed, int walkingDirection) {
this.size = size;
this.walkingSpeed = walkingSpeed;
this.walkingDirection = walkingDirection;
}
public int getSize() {
return size;
}
@Override
public String toString() {
return "Avatar [size=" + size + ", walkingSpeed=" + walkingSpeed + ", walkingDirection=" + walkingDirection
+ "]";
}
}
Now, let’s say we have an Avatar object with size 5, walkingSpeed 4, and walkingDirection 5. This object is created at runtime we don’t know these values when writing the code. We could do something like this:
Avatar avatar = new Avatar(getSize(),
…. WAIT! walkingSpeed
and walkingDirection
are private but do not have getters. This is quite common, as not every private field necessarily needs a getter. Some fields are only internal and are not meant to be visible from outside of the object itself. Can we add the getters and do? Avatar avatar = new Avatar(getSize(), getWalingSpeed(), getWalkingDirection());
but what if we want to clone an object implemented not by us but imported from a third-party library? We cannot change their code! What if we don’t know the type of the object, but only the one of its superclass?
Solution: Use the Prototype design pattern, which involves delegating the cloning procedure to the objects being replicated. This pattern establishes a shared interface for all objects capable of cloning, allowing you to duplicate an object without tying our code to the object’s specific class. Typically, this interface includes only one method for cloning.
public interface Prototype {
Prototype copy();
}
Now, we just have to implement this interface and write the implementation of the method copy()
. This time it’s easy because we are inside the object and have access to all fields.
public class Avatar implements Prototype{
private int size;
private int walkingSpeed;
private int walkingDirection;
public Avatar(int size, int walkingSpeed, int walkingDirection) {
this.size = size;
this.walkingSpeed = walkingSpeed;
this.walkingDirection = walkingDirection;
}
public int getSize() {
return size;
}
@Override
public String toString() {
return "Avatar [size=" + size + ", walkingSpeed=" + walkingSpeed + ", walkingDirection=" + walkingDirection
+ "]";
}
@Override
public Prototype copy() {
return new Avatar(size, walkingSpeed, walkingDirection);
}
}
The following code
Avatar avatar = new Avatar(50, 30, 6);
System.out.println(avatar);
Avatar clone = (Avatar) avatar.copy();
System.out.println(clone);
prints:
Avatar [size=50, walkingSpeed=30, walkingDirection=6]
Avatar [size=50, walkingSpeed=30, walkingDirection=6]
Actually, Java implements this design pattern, the interface Cloneable
tells the JDK that is safe to use the clone
method of java.lang.Object
to clone an object. clone
is anlougus to copy
above. Let’s mofify the Avatar
class to use the Cloneable
interfaces instead of our Prototype
interface.
public class Avatar implements Cloneable {
private int size;
private int walkingSpeed;
private int walkingDirection;
public Avatar(int size, int walkingSpeed, int walkingDirection) {
this.size = size;
this.walkingSpeed = walkingSpeed;
this.walkingDirection = walkingDirection;
}
public int getSize() {
return size;
}
@Override
public String toString() {
return "Avatar [size="
+ size
+ ", walkingSpeed="
+ walkingSpeed
+ ", walkingDirection="
+ walkingDirection
+ "]";
}
public static void main(String[] args) throws CloneNotSupportedException {
Avatar avatar = new Avatar(50, 30, 6);
System.out.println(avatar);
Avatar clone = (Avatar) avatar.clone();
System.out.println(clone);
Object a
}
}
We even no need to implement the clone
method, as in this case we can use the standard of Java that copies each of the fields. If we want to use a custom type of clone that does not copy all fields, we can override the clone
method and give our implementation.
Get your hands dirty!
Apply the Prototype design pattern to these classes:
public class Classroom {
private String id;
private List<Student> students;
public Classroom(String id) {
this.id = id;
this.students = new ArrayList<>();
}
public void addStudent(Student student) {
this.students.add(student);
}
@Override
public String toString() {
return "Classroom{id='" + id + "', students=" + students.toString() + "}";
}
}
public class Student {
private String name;
private String upi;
public Student(String name, String upi) {
this.name = name;
this.upi = upi;
}
@Override
public String toString() {
return "Student{name='" + name + "' upi='" + upi + "'}";
}
}
then implement the Main
class:
public class Main{
public static void main(String[] args) {
// create a classroom
// add some students
// clone the classroom
}
}
Does both Classroom
and Student
have to implement Prototype
interface and implement the copy
method?
BONUS do the same exercise but use the JDK Cloneable
interface. Do you need to implement the copy
method(s)?
Adapter (Structural)
Let’s assume that you’re visiting Europe. Your laptop expects a NZ power supply (type I). To get your laptop plugged in, you need to get a power adapter that accepts your NZ plug, and allows it to plug in to the European power outlet. The AC adapter knows how to deal with both sides, acting as a middleperson - this is the adapter pattern.
Problem: We have classes with similar behaviour that have incompatible interfaces.
Solution: Create a class that translates the request from one class to another (Adapter design pattern). In a nutshell, Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.
Let’s see an example.
Assume that we have an interface with a sort()
method that sorts an array of integers (in reverse order).
public interface Sorter {
int[] sort(int[] numbers);
}
Assume that we want to use an existing library that provides a sorting functionality with reverse order through its ListSorter
class.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// library class that we cannot change
public class ListSorter {
public List<Integer> sortList(List<Integer> numbers) {
//copy the list number into another list
List<Integer> copyList = new ArrayList<>(numbers);
Collections.sort(copyList, Collections.reverseOrder());
return copyList; // return the sorted list
}
}
There is a problem! The ListSorter
class sorts lists, not arrays! What can we do?
We cannot change the ListSorter
class to take input arrays. This is because we are not the developers of the library. If we change the method sortList
, we might introduce bugs and also we need to change it every time a new version of the library will be released. Moreover, we might not have access to the library source code, making it impossible to change the method.
The Adapter design pattern tells us to create a class SortListAdapter
that implements the interface Sorter
. Its sort()
method converts int[]
to List<Integer>
, calls the sortList()
method of the class ListSorter
, and then converts the sorted list back to an array.
public class SortListAdapter implements Sorter {
@Override
public int[] sort(int[] numbers) {
List<Integer> numberList = convertArray2List(numbers);
ListSorter sorter = new ListSorter();
List<Integer> listSorted = sorter.sortList(numberList);
int[] arraySorted = convertList2Array(listSorted);
return arraySorted;
}
}
The conversion is done with two helper methods:
private List<Integer> convertArray2List(int[] numbers) {
List<Integer> list = new ArrayList<>();
for (int number : numbers) {
list.add(number);
}
return list;
}
private int[] convertList2Array(List<Integer> numbers) {
int[] array = new int[numbers.size()];
for (int i = 0; i < numbers.size(); i++) {
array[i] = numbers.get(i);
}
return array;
}
Now, if we want to sort an array of integers we can do so like this:
public static void main(String[] args) {
int[] numbers = new int[] { 5, 34, 2, -1, 3, 444, 89 };
System.out.println("array in input: " + Arrays.toString(numbers));
Sorter sorter = new SortListAdapter();
int[] sortedNumbers = sorter.sort(numbers);
System.out.println("ordered array (reverse order): " + Arrays.toString(sortedNumbers));
}
This is the final result:
Get your hands dirty!
Apply the Adapter design pattern to this program.
Similarly to the sort example, we have an interface Max
:
public interface Max{
int getMax(int[] numbers);
}
that returns the biggest elements of an array.
We have the class ListMax
public class ListMax {
public int getMaxFromList(List<Integer> numbers) {
//TODO implement by yourself
return 0; //fix me
}
}
You need to complete the classes ListMax
and Main
using the Adapter design pattern. In particular, you need to also create a MaxListAdapter
class to adapt the two interfaces.
Composite (Structural)
Problem:
Sometimes you have to work on a program that deals with entities that can be organized into a tree-like structure.
For instance, let’s say you’re building a File System. You’ll have two classes: File
and Folder
. A Folder
can contain other folders or files. This is a classic example of a tree-like structure, with files as the leaves and folders as the nodes.
Both folders and files might also share similar properties, like having a name and size.
Check out this example implementation:
class File {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public void display() {
System.out.println("File -> " + name);
}
public int getSize() {
return size;
}
}
A Folder
has a name and a list of files and folders.
class Folder {
private String name;
private List<File> files = new ArrayList<>();
private List<Folder> folders = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addFile(File file) {
files.add(file);
}
public void removeFile(File file) {
files.remove(file);
}
public void addFolder(Folder folder) {
folders.add(folder);
}
public void removeFolder(Folder folder) {
folders.remove(folder);
}
public void display() {
System.out.println("Folder -> " + name);
for (File file : files) {
file.display();
}
for (Folder folder : folders) {
folder.display();
}
}
}
But there’s a problem with this design. What if we want to compute the size of a folder? We’d have to do something like this:
public int totalSize() {
int size = 0;
for (File file : files) {
size = size + file.getSize();
}
for (Folder folder : folders) {
size = size + folder.totalSize();
}
return size;
}
This isn’t great, because we end up with two similar for loops that do the same thing. The design leads to lots of duplicated code and makes the program hard to maintain and extend. If we want to add new functionality or extend the current functionality, we’ll have to make changes in both File
and Folder
classes, which is time-consuming and error-prone.
In cases like this, we want to treat objects in the structure the same way.
Solution
The Composite design pattern can help! It lets you work with tree-like structures by treating individual objects and groups of objects (called composites) uniformly (i.e, in the same way).
To implement the Composite pattern, you’ll need to create three things:
Element – a base interface or abstract class with common methods for managing child composites. It should be either an interface or an abstract class.
Leaf – implements the default behavior of the base component.
Composite – implements the base component methods.
Here’s how to implement the Composite pattern in our example:
Element Create a common interface (or abstract class) to treat files and folders uniformly.
interface FileSystemElement {
void display();
int getSize();
}
Leaf The leaf File
will be:
class File implements FileSystemElement {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public void display() {
System.out.println("File -> " + name);
}
@Override
public int getSize() {
return size;
}
}
Next, create the Composite class.
class Folder implements FileSystemElement {
private String name;
private List<FileSystemElement> components = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addComponent(FileSystemElement component) {
components.add(component);
}
public void removeComponent(FileSystemElement component) {
components.remove(component);
}
@Override
public void display() {
System.out.println("Folder -> " + name);
for (FileSystemElement component : components) {
component.display();
}
}
@Override
public int getSize() {
int size = 0;
for (FileSystemElement component : components) {
size = size + component.getSize();
}
return size;
}
}
By applying the Composite
desing pattern we will have a less redundant, more maintainable, and flexible code. By having a common interface, we can easily add new functionality or extend existing functionality without having to duplicate code or modify multiple classes.
Get your hands dirty!
Apply the Composite design pattern to this program.
import java.util.ArrayList;
import java.util.List;
class Developer {
private String name;
public Developer(String name) {
this.name = name;
}
public void displayEmployeeDetails() {
System.out.println("Developer: " + name);
}
}
class Manager {
private String name;
public Manager(String name) {
this.name = name;
}
public void displayEmployeeDetails() {
System.out.println("Manager: " + name);
}
}
class Department {
private String name;
private List<Developer> developers = new ArrayList<>();
private List<Manager> managers = new ArrayList<>();
public Department(String name) {
this.name = name;
}
public void addDeveloper(Developer developer) {
developers.add(developer);
}
public void removeDeveloper(Developer developer) {
developers.remove(developer);
}
public void addManager(Manager manager) {
managers.add(manager);
}
public void removeManager(Manager manager) {
managers.remove(manager);
}
public void displayEmployeeDetails() {
System.out.println("Department: " + name);
for (Developer developer : developers) {
developer.displayEmployeeDetails();
}
for (Manager manager : managers) {
manager.displayEmployeeDetails();
}
}
}
Command (Behavioural)
Problem
Consider the processing of ordering at a restaurant. We have three entities:
Customer
, who makes an order.Waiter
, who communicates the order to the chefs.Chefs
, who cook the meal.
A similar situation is common in OO programs where we have Invoker objects that invoke operations on different Receiver objects. Following the restaurant analogy, the Invoker
is the Waiter, and the Receivers are the Chefs
. If the Invoker and Receiver interact with each other directly, the Invoker will have a series of if statements to decide which receiver to invoke based on a given command (a kitchen might have multiple chefs that cook different meals).
if (command == A){
// invoke receiver 1
}else if(command == B){
// invoke receiver 2
} else if(command == C){
....
This situation results in a big headache when we need to add new receivers (chefs) and/or command types (meals). You will need to add more if-elses. The complexity and maintainability of the code will increase in case there are lots of receivers and/or command types.
Solution:
The Command Design Pattern decouples an object making a request (Invoker
), from the one that knows how to perform it (Receiver
).
It separates the object that invokes the operation from the object that actually performs the operation. It makes it easy to add new commands because existing classes remain unchanged.
Following the analogy of the restaurant, we can apply the Command Design Pattern as follows:
- The
Customer
makes an order. - The
Waiter
writes the order on a piece of paper and places it on the Kitchen table and rings a bell. - The right
Chef
will take the order from the table and cook the meal.
All the information on what to cook is in the order itself (the piece of paper), the Chef
does not communicate directly with neither the Waiter
nor the Costumer
. The Waiter
might not know how to cook the meal or which Chef
is going to cook the meal, the Waiter
just delivers the order.
This is the Command Design Pattern!
The components of this design pattern are:
-
Command
declares an interface for abstract commands (e.g.,execute()
). -
ConcreteCommand
are classes that implement theCommand
interface. Internally, they have the reference of who is the receiver of the implemented command. -
Receiver
knows how to execute a particular command. -
Invoker
holds theConcreteCommand
that has to be executed. -
Client
creates aConcreteCommand
and give it to theInvoker
.
In this way, the Command Design Pattern decouples the Invoker
from Receiver
. The Invoker
has complete knowledge of which ConcreteCommand
to be executed and the ConcreteCommand
knows which Receiver
to be invoked to execute a particular operation.
Let’s see a concrete example. We want to implement a remote controller for a home automation system that controls different electrical units of a home. For simplicity, let’s assume we have two electrical units: a Light
and a GarageDoor
. Both are the Receivers
using the terminology of this design pattern.
// Receiver
public class Light {
private String location;
public Light(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " light is on");
}
public void off() {
System.out.println(location + " light is off");
}
}
// Receiver
public class GarageDoor {
public void up() {
System.out.println("Garage Door is Up");
}
public void down() {
System.out.println("Garage Door is Down");
}
public void lightOn() {
System.out.println("Garage light is on");
}
public void lightOff() {
System.out.println("Garage light is off");
}
}
Now let’s create the Command interface, which is very simple, it just declares the method execute
.
// Command
public interface Command {
void execute();
}
Now let’s create 4 concrete commands implementations:
//Concrete Command
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
//Concrete Command
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
//Concrete Command
public class GarageDoorUpCommand implements Command {
private GarageDoor door;
public GarageDoorUpCommand(GarageDoor door) {
this.door = door;
}
@Override
public void execute() {
door.up();
door.lightOn();
}
}
//Concrete Command
public class GarageDoorDownCommand implements Command {
private GarageDoor door;
public GarageDoorDownCommand(GarageDoor door) {
this.door = door;
}
@Override
public void execute() {
door.down();
door.lightOff();
}
}
Now we can implement the RemoteController
that is our Invoker
.
//Invoker
public class RemoteController{
private Command command;
public void setCommand(Command command){
this.command = command;
}
public void pressButton(){
command.execute();
}
}
This is an example of Client that use the classes that we have just created.
//Client
public class Main {
public static void main(String[] args) {
RemoteController invoker = new RemoteController();
Light lightBedroom = new Light("Bedroom");
Command lightsOn = new LightOnCommand(lightBedroom);
Command lightsOff = new LightOffCommand(lightBedroom);
Command garageUp = new GarageDoorUpCommand(new GarageDoor());
Command garageDown = new GarageDoorDownCommand(new GarageDoor());
// light switch on
invoker.setCommand(lightsOn);
invoker.pressButton();
// light switch off
invoker.setCommand(lightsOff);
invoker.pressButton();
// garage up
invoker.setCommand(garageUp);
invoker.pressButton();
// garage down
invoker.setCommand(garageDown);
invoker.pressButton();
}
}
Invoker with history
Now with the Command Design Pattern implemented we can do cool things like adding a queue and a history of executed commands. We just need to change the implementation of the Invoker
.
import java.util.ArrayList;
import java.util.List;
//Invoker
public class RemoteController {
private List<Command> historyCommands;
private List<Command> queueCommands;
public RemoteController() {
historyCommands = new ArrayList<>();
queueCommands = new ArrayList<>();
}
public List<Command> getHistoryCommands() {
return historyCommands;
}
public List<Command> getQueueCommands() {
return queueCommands;
}
public void addCommand(Command command) {
queueCommands.add(command);
}
public void executeAll() {
for (Command command : queueCommands) {
command.execute();
historyCommands.add(command);
}
queueCommands.clear();
}
}
Now the Invoker
stores all executed commands and queue the ones to be executed.
Command design pattern with undo
This design pattern is also useful when we want to implement an “undo” of a command. In such a case, we need to add the unexecute
operation in the Command
interface and implement it in all of the ConcreteCommands
. Intuitively, the “unexecute” operation has to mirror the execute
operation. For example,
public interface Command {
public abstract void execute();
public abstract void unexecute();
}
public class GarageDoorUpCommand implements Command {
private GarageDoor door;
public GarageDoorUpCommand(GarageDoor door) {
this.door = door;
}
@Override
public void execute() {
door.up();
door.lightOn();
}
@Override
public void unexecute() {
door.down();
door.lightOff();
}
}
Get your hands dirty!
The REPL below has an additional Receiver
, the following Fan
class:
public class Fan {
static final int HIGH = 3;
static final int MEDIUM = 2;
static final int LOW = 1;
static final int OFF = 0;
private int speed;
public Fan() {
speed = OFF;
}
public void high() {
speed = HIGH;
System.out.println("Fan is on high");
}
public void medium() {
speed = MEDIUM;
System.out.println("Fan is on medium");
}
public void low() {
speed = LOW;
System.out.println("Fan is on low");
}
public void off() {
speed = OFF;
System.out.println("Fan is off");
}
public int getSpeed() {
return speed;
}
}
This Receiver
represents a Fan with three speeds, high, medium, and low. Implement three concrete commands: FanHighCommand
, FanMediumCommand
, and FanLowCommand
that set the speed high, medium, and low, respectively. You need to also implement the unexecute
operations. This means that you need to save the state of the current speed to be restored in case the client executes unexecute
.
</div>
Strategy (Behavioural)
Let’s assume that we need to implement the payment system of a shop. Currently, the shop handles three different types of payment: by cash, by Visa card, or by MasterCard. If cards are used, a fee is to be applied.
The most obvious (but not very good) implementation could be something like this:
public class Order {
private final String payment;
private final int amount;
private double feeMastercard = 0.0015;
private double feeVisaCard = 0.001;
public Order(int amount, String payment) {
this.amount = amount;
this.payment = payment;
}
public void payCash(int amount) {
System.out.println("Executing cash payment: Charging $" + amount);
}
public void payMastercard(int amount) {
System.out.println("Executing Mastercard payment: Charging $" + amount);
System.out.println("fees " + feeMastercard * amount);
}
public void payVisaCard(int amount) {
System.out.println("Executing Visacard payment: Charging $" + amount);
System.out.println("fees " + feeVisaCard*amount);
}
public void process(){
switch (payment){
case "visa":
payVisaCard(amount);
break;
case "master":
payMastercard(amount);
break;
case "cash":
payCash(amount);
break;
default:
System.out.println("unknonw payment");
}
}
public static void main(String[] args) {
Order order1 = new Order(15, "visa");
order1.process();
Order order2 = new Order(100, "master");
order2.process();
Order order3 = new Order(100, "cash");
order3.process();
}
}
There are several “problems” with this solution:
- We need to update the
process()
method every time we add or delete a payment type. - We can easily pass a wrong payment String (for example, “visacard” instead of “visa”)
This scenario is the perfect case to apply the Strategy Pattern.
We can isolate each of the payment in different classes, called strategies, that define an interface common to all supported payments. Then the Order
class uses this interface to execute the algorithm implemented by a Concrete Strategy. In this way, ordering a payment is independent from the strategy. We can add a new algorithm or update the existing one without changing the Order
class. With this approach, our payment program becomes much more flexible.
Problem: We have a class that switches between algorithms to accomplish the same task. This means that we would like the option to change the algorithms at runtime.
Solution: The Strategy pattern takes a class that does something specific in a lot of different ways and extract all of these algorithms into separate classes called strategies. The Strategy pattern helps to define a family of algorithms, to encapsulate each one of them and make them interchangeable and independent from the classes that use them. Most importantly, it allows changing the strategy at runtime.
Let’s see how to apply this design pattern in our example.
We need an interface for our strategy (the payment):
public interface Payment {
void pay(int amount);
}
This interface has a single method pay()
.
Let’s create our “family” of the three payment algorithms:
public class CashPayment implements Payment {
@Override
public void pay(int amount) {
System.out.println("Executing cash payment: Charging $" + amount);
}
}
public class MastercardPayment implements Payment {
double fee = 0.0015;
@Override
public void pay(int amount) {
System.out.println("Executing Mastercard payment: Charging $" + amount);
System.out.println("fees " + fee * amount);
}
}
public class VisacardPayment implements Payment {
double fee = 0.001;
@Override
public void pay(int amount) {
System.out.println("Executing Visacard payment: Charging $" + amount);
System.out.println("fees " + fee * amount);
}
}
Note that the three algorithms can implement very different behaviours. The important thing is, that they must all implement the same Payment
interface.
Now we need to modify the Order
class that makes use of the strategies:
public class Order {
private final Payment payment;
private final int amount;
public Order(int amount, Payment payment) {
this.amount = amount;
this.payment = payment;
}
public void process() {
payment.pay(amount);
}
}
Note that now the process()
method is no longer responsible for selecting an appropriate payment. Instead, we pass the desired strategy as an object (Payment payment
).
The class Order
will make the payment through the interface Payment
. The Order
class does not need to know the concrete payment processing. That means that in the future, we can add more strategies without changing the Order
class at all!
Now we can use the Order
class as follows:
Order order1 = new Order(15, new VisacardPayment());
order1.process();
Order order2 = new Order(100, new MastercardPayment());
order2.process();
Order order3 = new Order(100, new CashPayment());
order3.process();
One of the greatest advantage of this design pattern is that it allows to easily change algorithms (strategies) at runtime. We just need to add the setter method of the strategy instance:
public class Order {
private Payment payment;
private final int amount;
public Order(int amount, Payment payment) {
this.amount = amount;
this.payment = payment;
}
public void setPayment(Payment payment){
this.payment = payment;
}
public void process() {
payment.pay(amount);
}
}
Now at runtime we can change the payment strategy! How cool is that!?
Putting everything together:
Get your hands dirty!
Consider the example above. Add the feature that credit card payments can be declined, if a card payment is declined the payment strategy should be switch to cash payment.
REWIND!
Download the ACP exercise 281_Rewind_Rock_Paper_Scissor
Let’s implement some Artificial Intelligence (AI) to play rock-paper-scissors against a human player!
- Run the
Main
class and get familiar with the game. Currently theCPU
andHumans
always answer the same thing:
public class CPU {
public Action play() {
return Action.ROCK;
}
}
- implement the logic for the Human (human will interact with the console)
Let’s use the Strategy design pattern and create three strategies for the AI, and then we will change them at runtime.
- Random The first strategy is random, it randomly choose an Action (Rock, Paper, or Scissors).
- Cycle The second strategy cycles between Rock, Paper, and Scissors. First game Rock, second game Paper, third game Scissors, fourth game Rock, fifth game Paper and so on.
- Last The third strategies plays the hand that the opponent (Human player) just played. At the first round it plays PAPER.
Now at runtime we want to change these strategies. We start with the Cycle strategy, when the CPU losses 3 times in a row the game changes the strategy to Last, because the player guessed the CPU strategy!. Then, if the CPU losses other 3 times in a row, it likely means that the Human player guessed also this strategy and the game changes it to Random. The game keeps the Random strategy until the player decides to stop the game.
BONUS When the user ends the game print the game statistics (number of games won by the human player and the CPU )
SUPER BONUS When the user ends the game print also which strategy performed the best (in percentage of round played)