Design Patterns
When we learn programming and consistently expand our knowledge in this area, it is certain that we will encounter design patterns. What are they? What are they for? And after mastering them, are they used in every case?
Let’s start with the fact that every program is created for a purpose – it has a task to perform. While writing it, the programmer will encounter some problems (major or minor) that he must solve. He may not realize that other programmers have run into the same problems writing completely different programs. If the solutions he uses will also work for other programmers, we will call these solutions Design Patterns. In other words – it is a catalog of best practices with ready-made solutions for frequently occurring design problems.
It is important to remember that design patterns are not laws or rigid rules – they are recommendations. It is the programmer’s task to adapt them to the needs of a specific application.
There are many proven patterns and new ones are still being created. In this article, I will discuss only 3 of them in more detail.
1) Factory method
The Factory method allows you to create different objects in an orderly manner without revealing the logic of their creation, which implement the same interface (it doesn’t have to be an interface, it can also be an abstract class). We do not create the required objects directly, but “report” to a special class (often called Factory), which will create them for us. How the factory constructs them remains invisible to us. This pattern provides one of the best ways to create an object.
Consider the following example:
We create a winery and order three types of wine (red, white and rosé) from it. For simplicity, we’ll assume there’s only one type of each:
– Red Rosso Italiano 2020, semi-dry for 35 PLN.
– White Androsela 2019, semi-sweet for 39 PLN.
– Pink Espanillo Rosado 2019, dry for 33 PLN.
We have three fields: name, flavor and price.
An example implementation might look like this:
Step 1 – Create the Wine class.
public class Wine {
private String name;
private String flavor;
private double price;
public Wine(String name, String flavor, double price) {
this.name = name;
this.flavor = flavor;
this.price = price;
}
public String getName() {
return name;
}
public String getFlavor() {
return flavor;
}
public double getPrice() {
return price;
}
}
Step 2 – We create the Winery class, where we buy wines.
public class Winery {
public static void main(String[] args) {
Wine redWine = new Wine("Rosso Italiano 2020", "semidry", 35.0);
System.out.println("You bought: ");
System.out.print("Red ");
System.out.println(redWine.getName() + ", " + redWine.getFlavor() + ", for " + redWine.getPrice() +" PLN");
Wine whiteWine = new Wine("Androsela 2019", "semisweet", 39.0);
System.out.print("White ");
System.out.println(whiteWine.getName() + ", " + whiteWine.getFlavor() + ", for " + whiteWine.getPrice() +" PLN");
Wine pinkWine = new Wine("Espanillo Rosado 2019", "dry", 33.0);
System.out.print("Pink ");
System.out.println(pinkWine.getName() + ", " + pinkWine.getFlavor() + ", for " + pinkWine.getPrice() +" PLN");
}
}
Result verification:
You bought:
Red Rosso Italiano 2020, semidry, for 35.0 PLN
White Androsela 2019, semisweet, for 39.0 PLN
Pink Espanillo Rosado 2019, dry, for 33.0 PLN
By simply using the new operator, we create the required objects for each type of wine. And in our example, everything is fine, but when we have a more complicated situation (or application), then creating new objects in this way should not always be publicly available, moreover, it can lead to problems with object bindings.
Now let’s use the Factory method to improve our program.
Step 1 – We create the Wine interface.
public interface Wine {
String getName();
String getFlavor();
double getPrice();
}
Step 2 – We create actual classes implementing the same interface.
public class RedWine implements Wine {
@Override
public String getName() {
return "Rosso Italiano 2020";
}
@Override
public String getFlavor() {
return "semidry";
}
@Override
public double getPrice() {
return 35.0;
}
}
public class WhiteWine implements Wine {
@Override
public String getName() {
return "Androsela 2019";
}
@Override
public String getFlavor() {
return "semisweet";
}
@Override
public double getPrice() {
return 39.0;
}
}
public class PinkWine implements Wine {
@Override
public String getName() {
return "Espanillo Rosado 2019";
}
@Override
public String getFlavor() {
return "dry";
}
@Override
public double getPrice() {
return 33.0;
}
}
Step 3 – We create a Winery (Factory) to produce an object of a specific class (wine).
public class WineFactory {
public Wine buyWine(String type) {
if (type.equals("Red")) {
return new RedWine();
} else if (type.equals("White")) {
return new WhiteWine();
} else if (type.equals("Pink")) {
return new PinkWine();
} else {
return null;
}
}
}
Step 4 – We use the Factory to create a wine (specific object) based on its color.
public class FactoryPatternDemo {
public static void main(String[] args) {
WineFactory wine = new WineFactory();
Wine red = wine.buyWine("Red");
System.out.println("You bought red wine:");
System.out.println("Name: " + red.getName());
System.out.println("Flavor: " + red.getFlavor());
System.out.println("Price: " + red.getPrice() + " PLN");
System.out.println();
Wine white = wine.buyWine("White");
System.out.println("You have chosen white wine:");
System.out.println(white.getName() + ", " + white.getFlavor() + ", " + white.getPrice() + " PLN");
System.out.println();
Wine pink = wine.buyWine("Pink");
System.out.println("You bought pink " + pink.getFlavor() + " wine " + pink.getName() + " for " + pink.getPrice()
+ " PLN");
}
}
Result verification:
You bought red wine:
Name: Rosso Italiano 2020
Flavor: semidry
Price: 35.0 PLN
You have chosen white wine:
Androsela 2019, semisweet, 39.0 PLN
You bought pink dry wine Espanillo Rosado 2019 for 33.0 PLN
As you can see, the Factory method in a simple and effective way separates the client from the implementation of real classes.
2) The Observer pattern
We use the Observer pattern when we have a one-to-many relationship between objects in a set of objects. When the selected object (called the Subject or Observer) changes its state, all objects that depend on it (the Observers) are notified and updated.
Consider a simple example of converting numbers to another number system.
We create a number systems class and perform conversions in it using separate functions, where we pass a number to them as an argument.
An example implementation might look like this:
public class NumberSystemsDemo {
public static void main(String[] args) {
System.out.println("First state change: 15");
System.out.print("Binary String: ");
changeIntToBinary(15);
System.out.print("Octal String: ");
changeIntToOctal(15);
System.out.print("Hexadecimal String: ");
changeIntToHexadecimal(15);
System.out.println();
System.out.println("Second state change: 10");
System.out.print("Binary String: ");
changeIntToBinary(10);
System.out.print("Octal String: ");
changeIntToOctal(10);
System.out.print("Hexadecimal String: ");
changeIntToHexadecimal(10);
}
public static void changeIntToBinary(int number) {
System.out.println(Integer.toBinaryString(number));
}
public static void changeIntToOctal(int number) {
System.out.println(Integer.toOctalString(number));
}
public static void changeIntToHexadecimal(int number) {
System.out.println(Integer.toHexString(number).toUpperCase());
}
}
Result verification:
First state change: 15
Binary String: 1111
Octal String: 17
Hexadecimal String: F
Second state change: 10
Binary String: 1010
Octal String: 12
Hexadecimal String: A
The program works fine, but each state change (different number) requires us to run the appropriate functions manually. This is not an efficient solution.
We will now try to use the Observer Pattern for our program.
Step 1 – We create the Subject and Observer interfaces.
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyAllObservers();
}
public interface Observer {
void update();
}
Step 2 – We create specific observers.
public class BinaryObserver implements Observer {
SomeSubject subject;
public BinaryObserver(SomeSubject subject) {
this.subject = subject;
this.subject.registerObserver(this);
}
@Override
public void update() {
System.out.println("Binary String: " + Integer.toBinaryString(subject.getState()));
}
}
public class OctalObserver implements Observer {
SomeSubject subject;
public OctalObserver(SomeSubject subject) {
this.subject = subject;
this.subject.registerObserver(this);
}
@Override
public void update() {
System.out.println("Octal String: " + Integer.toOctalString(subject.getState()));
}
}
public class HexadecimalObserver implements Observer {
SomeSubject subject;
public HexadecimalObserver(SomeSubject subject) {
this.subject = subject;
this.subject.registerObserver(this);
}
@Override
public void update() {
System.out.println("Hexadecimal String: " + Integer.toHexString(subject.getState()).toUpperCase());
}
}
Step 3 – Create the Observed Object class.
import java.util.ArrayList;
public class SomeSubject implements Subject {
private ArrayList observers;
private int state;
public SomeSubject() {
observers = new ArrayList();
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyAllObservers() {
for (int i = 0; i < observers.size(); i++) {
Observer Obs = (Observer) observers.get(i);
Obs.update();
}
}
}
Step 4 – We use the Observer Object class to create, delete and notify observers when the state (number) changes.
public class ObserverPatternDemo {
public static void main(String[] args) {
SomeSubject subject = new SomeSubject();
Observer obs1 = new BinaryObserver(subject);
Observer obs2 = new OctalObserver(subject);
Observer obs3 = new HexadecimalObserver(subject);
System.out.println("First state change: 15");
subject.setState(15);
System.out.println();
System.out.println("Second state change: 10");
subject.setState(10);
System.out.println();
subject.removeObserver(obs2);
System.out.println("Third state change: 59");
subject.setState(59);
}
}
Result verification:
First state change: 15
Binary String: 1111
Octal String: 17
Hexadecimal String: F
Second state change: 10
Binary String: 1010
Octal String: 12
Hexadecimal String: A
Third state change: 59
Binary String: 111011
Hexadecimal String: 3B
Notice that on the third state transition (number 59) we are missing the octal conversion. This is because we previously removed its observer (obs2) from the group of objects following our subject (the number to be converted). However, we can see that with each state (number) change, we automatically notify all observers about it, which triggers the appropriate conversion. Using the Observer Pattern in our example is much more efficient.
3) Flyweight pattern
The Flyweight pattern is used wherever it is required to create many objects of a given class, and all of them can be controlled in the same way. One instance of such a class can later be used to create many “virtual instances”. In this way, we save application memory, because we reduce the number of created objects.
Consider the following example.
Let’s “draw” 10 circles in different places (we’ll use random x and y coordinates and a radius). We will also set the 5 available colors for these wheels.
An example implementation might look like this:
Step 1 – We create the Circle class.
public class Circle {
private String color;
private int x;
private int y;
private int r;
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 r) {
this.r = r;
}
public void draw() {
System.out.println("Drawing circle - Color: " + color + ", x: " + x + ", y: " + y + ", r: " + r);
}
}
Step 2 – We create a main class in which we “draw” circles.
public class DrawingCirclesDemo {
private static final String colors[] = {"Red", "Blue", "Yellow", "White", "Black"};
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
String color = getRandomColor();
Circle circle = new Circle(color);
System.out.println("Creating circle of color: " + color);
circle.setX(getRandomX());
circle.setY(getRandomY());
circle.setRadius(getRandomRadius());
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() * 10);
}
private static int getRandomRadius() {
return (int)(Math.random() * 100);
}
}
Result verification:
Creating circle of color: White
Drawing circle - Color: White, x: 23, y: 5, r: 11
Creating circle of color: White
Drawing circle - Color: White, x: 51, y: 2, r: 48
Creating circle of color: White
Drawing circle - Color: White, x: 59, y: 1, r: 97
Creating circle of color: White
Drawing circle - Color: White, x: 1, y: 0, r: 22
Creating circle of color: White
Drawing circle - Color: White, x: 18, y: 6, r: 16
Creating circle of color: Blue
Drawing circle - Color: Blue, x: 30, y: 6, r: 9
Creating circle of color: Black
Drawing circle - Color: Black, x: 49, y: 7, r: 8
Creating circle of color: Black
Drawing circle - Color: Black, x: 21, y: 9, r: 90
Creating circle of color: Yellow
Drawing circle - Color: Yellow, x: 16, y: 5, r: 30
Creating circle of color: Blue
Drawing circle - Color: Blue, x: 44, y: 9, r: 24
As you can see, each time we create a new object of the same type (color), which is not a good solution.
Now let’s use the Flyweight Pattern for our example.
Step 1 – We create the Shape interface.
public interface Shape {
void draw();
}
Step 2 – We create a concrete Circle class that implements the created interface.
public class Circle implements Shape {
private String color;
private int x;
private int y;
private int r;
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 r) {
this.r = r;
}
@Override
public void draw() {
System.out.println("Drawing circle - Color: " + color + ", x: " + x + ", y: " + y + ", r: " + r);
}
}
Step 3 – Create a Shape Factory class to create a Circle object.
import java.util.HashMap;
public class ShapeFactory {
private static final HashMap 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;
}
}
Step 4 – We use the factory to get an object of a given class by passing a specific argument (in our example it will be a color).
public class FlyweightPatternDemo {
private static final String colors[] = {"Red", "Blue", "Yellow", "White", "Black"};
public static void main(String[] args) {
for (int i = 0; i < 10; ++i) {
Circle circle = (Circle) ShapeFactory.getCircle(getRandomColor());
circle.setX(getRandomX());
circle.setY(getRandomY());
circle.setRadius(getRandomRadius());
circle.draw();
System.out.println();
}
}
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() * 10);
}
private static int getRandomRadius() {
return (int)(Math.random() * 100);
}
}
Result verification:
Creating circle of color: White
Drawing circle - Color: White, x: 72, y: 5, r: 5
Creating circle of color: Black
Drawing circle - Color: Black, x: 25, y: 3, r: 68
Drawing circle - Color: White, x: 80, y: 2, r: 1
Drawing circle - Color: White, x: 31, y: 8, r: 86
Creating circle of color: Blue
Drawing circle - Color: Blue, x: 16, y: 9, r: 31
Drawing circle - Color: Blue, x: 78, y: 9, r: 24
Drawing circle - Color: Blue, x: 68, y: 8, r: 34
Drawing circle - Color: Blue, x: 72, y: 0, r: 46
Drawing circle - Color: White, x: 24, y: 4, r: 38
Creating circle of color: Yellow
Drawing circle - Color: Yellow, x: 10, y: 0, r: 60
Let’s see that we first created a new object (the white circle). Then we drew them. Same with black color. When we then drew the white color again, we don’t create a new white circle object, we use the existing object (but with different values of coordinates and radius). Then we create a new blue circle object, since we didn’t have a blue circle before. And just like with the white circle, the next blue ones are drawn using the already existing blue circle object. In other words, we drew 10 circles, creating only 4 objects.