In this part of the material about patterns, we will deal with what creational design patterns are, what tasks they solve, and in addition, we will also analyze the three most commonly used ones.
Other articles of this series: «What are design patterns», «Structural patterns», «Behavioral patterns».
ONE MORE TIME ABOUT PATTERNS
Design patterns are solutions to common application development problems. They are also known as object-oriented programming patterns. Unlike ready-made functions or libraries, a pattern is not a specific code, but a general concept for solving a problem which still needs to be adjusted to the tasks.
In total, there are 23 classic patterns that were described in the book by the “Gang of Four”. Depending on what tasks they solve, they are divided into creational, structural and behavioral.
CREATIONAL PATTERNS
According to Wikipedia, creational patterns are design patterns that make a system independent of how objects are created, composed, and represented.
Simply put, creational patterns are designed to create an instance of an object or a group of related objects. These include:
- Singleton
- Builder
- Factory Method
- Prototype
- Abstract Factory
3 MOST POPULAR CREATIONAL PATTERNS
According to many developers, Singleton, Builder and Factory Method are the most used generative patterns in development. Let’s see what tasks they help to cope with, and look at examples of their implementation.
Singleton
According to Wikipedia, Singleton is a creational design pattern that guarantees there will be a single instance of some class in a single-threaded application, so it provides a global access point to this instance.
Simply put, Singleton guarantees that the created object will be the only one in the application and allows you to access it from any part of the application.
Even simpler: a good real-life example of this pattern is a class-book at school. Each class has only one, and if the teacher asks for it, he/she will always receive the same copy.
When needed: Useful if the application has a management object that stores the entire application context. For example, in data storage services.
How to create:
- Create a class where we put logic.
- Create a static field and initialize it.
- Next, we create a private initializer. This ensures that no other client or class can instantiate that class.
- Now, in order to use the methods of our class, we access it through a static field.
Example of implementation:
Java
public class Singleton { private static Singleton instance; private Singleton () {}; public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
JavaScript
// create a Singleton class that checks if it has the instance property // if not, then a new instance is created, but if the property does exist // then we return the previously created instance of the class class Singleton { constructor(data) { if (Singleton.instance) { return Singleton.instance; } Singleton.instance = this; this.data = data; } consoleData() { console.log(this.data); } }; // we can make sure that Singleton works by creating 2 instances of the class const firstSingleton = new Singleton('firstSingleton'); const secondSingleton = new Singleton('secondSingleton'); // in both cases, the console will display the same message firstSingleton.consoleData(); //firstSingleton secondSingleton.consoleData(); //firstSingleton
Commentary: There is an opinion that the Singleton is an anti-pattern – its antagonists argue that the Singleton brings global state to the application and is difficult to test. Although there is some truth in this, in reality this pattern does not cause problems if you remember the following: the essence of a singleton is to ensure that at a certain point in the execution of the program there is one and only one instance of a certain class. Creating a singleton for “convenience” is a sure sign of a bad code.
Builder
According to Wikipedia, Builder is a creational design pattern that provides a way to create a composite object. It separates the construction of a complex object from its representation so that the same construction process can result in different representations.
Simply put, Builder allows you to create different objects with a given state using the same code.
Even simpler: an example of this pattern in real life is buying a computer in a store. When choosing, we indicate what characteristics the equipment should have (for example, 16 GB memory, Intel core i7 processor, and so on). Thus, we can create different representations of the same object.
When needed: Useful when compiling an SQL query, as well as in unit tests.
How to create:
- Create an abstract class Builder where we declare methods for initializing product parameters.
- Create classes that inherit the Builder and override the product parameters initialization methods.
- Create a manager class that performs coordinated actions to create a Product using the Builder
Example of implementation:
Java
class Pizza { private String dough = ""; private String sauce = ""; public void setDough(String dough) { this.dough = dough; } public void setSauce(String sauce) { this.sauce = sauce; } } abstract class PizzaBuilder { // Abstract Builder protected Pizza pizza; public void createNewPizzaProduct() { pizza = new Pizza(); } public abstract void buildDough(); public abstract void buildSauce(); } class SpicyPizzaBuilder extends PizzaBuilder { // Concrete Builder public void buildDough() { pizza.setDough("pan baked"); } public void buildSauce() { pizza.setSauce("hot"); } } class Waiter { // Director private final PizzaBuilder pizzaBuilder; public Waiter(PizzaBuilder pb) { pizzaBuilder = pb; } public void constructPizza() { pizzaBuilder.createNewPizzaProduct(); pizzaBuilder.buildDough(); pizzaBuilder.buildSauce(); } } public class BuilderExample { // A customer ordering a pizza public static void main(String[] args) { Waiter waiter = new Waiter(new SpicyPizzaBuilder()); waiter.constructPizza(); } }
JavaScript
class Apartment { constructor(options) { for (const option in options) { this[option] = options[option]; } } getOptions() { return Number of rooms: ${this.roomsNumber}, square: ${this.square}, floor: ${this.floor}; } } class ApartmentBuilder { setRoomsNumber(roomsNumber) { this.roomsNumber = roomsNumber; return this; } setFloor(floor) { this.floor = floor; return this; } setSquare(square) { this.square = square; return this; } build() { return new Apartment(this); } } const bigApartment = new ApartmentBuilder() .setFloor(10) .setRoomsNumber(5) .setSquare(120) .build(); console.log(bigApartment.getOptions());
Commentary: The key idea of the pattern is to move the complex logic of creating an object into a separate Builder class. This class allows you to create complex objects in stages, at the output getting a ready-made object with the characteristics we need and providing a simplified interface for the class to work with.
Although convenient to use, the builder class itself in some cases will be heavyweight, complicating the program code and, under certain circumstances, will be redundant. In such cases, it makes sense to use a separate constructor/initializer.
Factory Method
According to Wikipedia, a factory method is a creational design pattern that provides an interface for subclasses to create instances of a class. At creation time, descendants can determine which class to create.
Simply put, the pattern allows you to use one class to create objects of different interface implementations and delegate logic to child classes.
Even simpler: when ordering a ticket, we indicate only information from the passport, flight number and seats. Such data as terminal number, departure time, aircraft model, etc. are initialized without our involvement. This saves passengers time and reduces amount of errors.
When needed: the pattern is suitable for situations where we need to perform an action (for example, send a request), but all the data is not known in advance, since it depends on the input parameters (for example, on the data transfer protocol – rest, soap or socket).
How to create:
- Create the TicketFactory class.
- Add methods that take the required parameters and create entities that are initialized with the passed parameters.
- Inside these methods, constructors and setters are called to initialize parameters.
- At the output, an initialized entity is returned to us.
Example of implementation:
Java
interface Product { } class ConcreteProductA implements Product { } class ConcreteProductB implements Product { } abstract class Creator { public abstract Product factoryMethod(); } class ConcreteCreatorA extends Creator { public Product factoryMethod() { return new ConcreteProductA(); } } class ConcreteCreatorB extends Creator { public Product factoryMethod() { return new ConcreteProductB(); } } public class FactoryMethodExample { public static void main(String[] args) { List<Creator> creators = List.of(new ConcreteCreatorA(), new ConcreteCreatorB()); creators.stream().map(Creator::factoryMethod).map(Object::getClass).forEach(System.out::println); } }
JavaScript
class Apartment { constructor(roomsNumber, square, floor) { this.roomsNumber = roomsNumber; this.floor = floor; this.square = square; } getOptions() { return Number of rooms: ${this.roomsNumber}, square: ${this.square}, floor: ${this.floor}; } }; class ApartmentFactory { createApartament(type, floor) { switch(type) { case('oneRoom'): return new Apartment(1, 35, floor); case('twoRooms'): return new Apartment(2, 55, floor); case('threeRooms'): return new Apartment(3, 75, floor); default: throw new Error('Such room is not found'); } } } const oneRoomAparnament = new ApartmentFactory().createApartament('oneRoom', 10); console.log(oneRoomAparnament.getOptions());
CONCLUSION
Design patterns are solutions to common problems in code development. Knowing and using them allows you to save time by using ready-made solutions, standardize code and increase your common vocabulary.
Depending on what tasks design patterns solve, they are divided into three types: creational, structural and behavioral.
Creational patterns are designed to create an instance of an object or a group of related objects. The three most popular ones are Singleton, Builder, and Factory Method.
Singleton guarantees that the created object will be the only one in the application and allows you to access it from any part of the application. Often used in an application where there is a management object that holds the entire application context.
Builder allows you to create different objects with a given state using the same code. It will be useful when compiling a SQL query or in unit tests.
Factory method allows you to use one class to create objects of different interface implementations and delegate logic to child classes. Suitable for situations where we need to perform an action but the method of execution is unknown in advance.
In the following articles, we will talk in more detail about structural and behavioral patterns and analyze the most popular of them.
MATERIALS FOR ADDITIONAL STUDY
“Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, with a foreword by Grady Booch. — that very book by the “Gang of Four”.
Refactoring.guru — Design Patterns and Principles eBook