Design patterns are reusable solutions to common software design problems. They provide a way to describe and document software architectures, as well as a common vocabulary for developers to communicate about software design.
There are several types of design patterns, including creational, structural, and behavioral patterns.
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
Structural patterns deal with object composition, creating relationships between objects to form larger structures.
Behavioral patterns focus on communication between objects, what goes on between objects and how they operate together.
2. Creational patterns
The Singleton design pattern is used to ensure that a class has only one instance, and to provide a global access point to that instance.
- One advantage of using the Singleton design pattern is that it ensures that there is only one instance of a class, which can be useful for classes that manage resources such as database connections or network sockets.
- It also provides a global access point to the instance, which can make it easier to use the instance in different parts of the code.
Provides a way to create objects without specifying the exact class of object that will be created. Has a method that creates objects of a specific type. The method takes the type of object to be created as an argument and returns a new object of that type.
- One advantage of using the Factory design pattern is that it allows the creation of objects to be centralized in a single location, which can make the code more modular and easier to maintain.
- It also allows the implementation of object creation to be changed easily, which can make the design more flexible and extensible.
- Allows objects to be created without specifying their exact class, which can make the code more generic and reusable.
2.3 Abstract Factory
Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
- A system should be independent of how its products are created, composed, and represented.
- A system should be configured with one of the multiple families of products.
- A family of related product objects is designed to be used together, and you need to enforce this constraint.
- Useful when you want to create objects that are compatible with a certain application or framework, but you don’t want to specify the concrete classes of the objects until runtime.
Allows for the creation of complex objects in a step-by-step manner. It separates the construction of an object from its representation, allowing for different representations to be created.
- Object creation algorithms should be decoupled from the system.
- Multiple representations of creation algorithms are required.
- The addition of new creation functionality without changing the core code is necessary.
- Runtime control over the creation process is required.
Allows for the creation of new objects by copying existing objects, rather than creating new objects from scratch.
- Useful when creating complex objects or when the cost of creating a new object is high.
- A class will not know what classes it will be required to create.
- Subclasses may specify what objects should be created.
- Parent classes wish to defer creation to their subclasses.
3. Structural patterns
Allows two incompatible interfaces to work together by wrapping an adapter class around one of the interfaces. This adapter class converts the interface of the adapted class into the interface that the client is expecting.
- Adapters can not only convert data into various formats but can also help objects with different interfaces collaborate.
- Possible to create a two-way adapter that can convert the calls in both directions.
Allows for the separation of abstraction and implementation, so that the two can vary independently.
- Abstractions and implementations should not be bound at compile time.
- Abstractions and implementations should be independently extensible.
- Changes in the implementation of an abstraction should have no impact on clients.
- Implementation details should be hidden from the client.
Allows objects to be treated as a single unit. It is used to compose objects into tree structures, and to create complex objects from simpler ones.
- Hierarchical representations of objects are needed.
- Objects and compositions of objects should be treated uniformly.
Allows for the dynamic addition of new behavior to an existing object without changing its structure.
- It can be used to add new functionality to a class or to wrap an existing class with additional functionality.
Provides a simplified interface to a complex system.
- Useful when a system has a large number of interconnected classes or when a client only needs to access a limited number of the system’s capabilities.
- It decouples the client from the complex subsystems and allows for easier maintenance and modification of the system.
Aims to minimize the use of memory by sharing common data among objects. This is done by creating a shared object that can be used by multiple objects, rather than each object having its own separate instance of the data.
- We can reduce the memory footprint of our application and improve its performance.
- Carefully consider the trade-off between the benefits of memory savings and the added complexity of implementing the pattern.
Provides an intermediary object between a client and a real subject.
The proxy pattern can be used to:
- Provide a placeholder for a potentially expensive or resource-intensive object. The proxy can be used to create the real object only when it is needed, rather than creating it upfront.
- Control access to the real subject. The proxy can be used to enforce access restrictions or implement authentication and authorization checks.
- Add additional functionality to the real subject. The proxy can be used to intercept requests to the real subject and perform additional tasks before or after forwarding the request.
4. Behavioral patterns
4.1 Chain of Responsibility
Allows an object to send a request to a chain of objects in order to handle the request.
- Useful for situations where multiple objects may be able to handle a request, and the specific object that should handle the request is not known in advance.
- Allows for the easy addition or removal of objects from the chain without disrupting the overall functionality.
Allows for the encapsulation of a request as an object, which can then be passed to a receiver to be executed.
- Allows for the separation of the sender and receiver of a request.
- Ability to queue or log requests, and to support undo/redo functionality.
Allows clients to access elements of an aggregate object sequentially without exposing the object’s underlying representation.
- Allows clients to traverse a collection of objects in a consistent, uniform manner, regardless of the specific implementation of the collection.
Allows multiple objects to communicate with each other without knowing the details of their implementation.
- It provides a central point of communication, known as the mediator, which acts as an intermediary between the objects.
- Useful in cases where there are a large number of objects that need to communicate with each other, as it reduces the complexity of the system by separating the communication logic from the objects themselves.
Allows an object (the subject) to notify a set of objects (the observers) when its state changes. The observer pattern is also known as the publish-subscribe pattern.
- Useful when you want to ensure that various objects are kept in sync with each other, or when you want to be able to reuse subjects and observers independently of each other.
Allows an object to change its behavior or strategy at runtime by switching to a different strategy object.
- The only difference between many related classes is their behavior.
- Multiple versions or variations of an algorithm are required.
- Algorithms access or utilize data that calling code shouldn’t be exposed to.
- The behavior of a class should be defined at runtime.
- Conditional statements are complex and hard to maintain.
4.7 Template Method
Defines the steps of an algorithm and allows subclasses to override certain steps, while still preserving the overall structure of the algorithm.
- A single abstract implementation of an algorithm is needed.
- Common behavior among subclasses should be localized to a common class.
- Parent classes should be able to uniformly invoke behavior in their subclasses.
- Most, or all subclasses need to implement the behavior.