I have a strong affinity for design patterns as they provide elegant solutions to common architectural design challenges.
The definitive resource for design patterns is the Gang of Four book, that builds their general classification around the conceptual motivations of the patterns taking into account the specific problems they tackle:
Behavioural patterns focus on managing variations in behaviour within a system. They provide solutions for scenarios where different objects or components need to exhibit different behaviours.
For example, suppose you have an application that involves different payment methods, each of which has its own specific behaviour, including authentication, transaction processing, and receipt generation. In this case, one of the behavioural patterns can be employed.
Structural patterns provide guidelines on how to construct larger structures by organising objects and classes in a manner that ensures flexibility and efficiency.
For example, you have a legacy system that uses a monolithic architecture, where different components are tightly coupled together. Now, you want to introduce a new feature without disrupting the existing system. In this case, you can apply the Adapter structural pattern, which allows you to adapt the new feature to the existing system without modifying its core components.
Creational (sometimes, I hear them referred to as Factory patterns or simply Factories) serve a key role in object creation, although their significance goes beyond that.
Factories are objects designed to produce other objects.
In OOP languages, when the code responsible for using objects is also burdened with their instantiation (which is frequently the case), complexity can arise. Such code becomes responsible for keeping track of multiple aspects, such as which objects to create, which construction parameters are necessary, how to utilise the object after construction, and sometimes even managing object pools. This lack of separation reduces cohesion, which is undesirable, and can lead developers to prematurely choose an instantiation scheme before fully understanding the objects that need to be instantiated.
Factory patterns address these issues by promoting object cohesion, decoupling, and testability. They contribute to maintaining a flexible design and facilitate problem decomposition into smaller, more manageable components.
When it comes to object creation and management, here is a good rule to follow: and object should either make and / or manage other objects or it should use other objects but it should never do the both.
This division of responsibilities simplifies the tasks for both groups. Moreover, by streamlining the creation and management of objects, it's possible to enhance their management without significantly increasing effort.
I would recommend following these steps:
Step 1 - Define the objects and their relations.
Step 2 - Implement factories for object instantiation and management: write factories that handle the creation of the appropriate objects based on specific situations, as well as managing existing objects if they are shared among different components.
As you can see, the code generated in Step 1 focuses solely on the behaviour and functionality of the objects without concerning itself with the instantiation details. Similarly, the code in Step 2 concentrates on the instantiation and management of objects, without being burdened by the complexities of their interactions.
Separating these concerns leads to increased cohesion in the code. If these aspects were not separated, you would end up with code that deals with both the functionality of the objects and the rules governing their creation and management in different circumstances. By isolating these concerns, each class can focus on either functionality or instantiation, resulting in more modular and cohesive code.
It's worth mentioning, that this approach provides us with enhanced encapsulation, a highly valued characteristic. It ensures complete separation between the implementing classes and the client code that utilises them. Consequently, I can introduce or remove implementations without requiring any modifications to the client code itself.
Moreover, this approach greatly facilitates the testing process. The behaviour of the client code remains consistent regardless of the specific set of implementations employed. It is sufficient to test each component individually, as the system will function in a consistent manner regardless of their combinations.
Other benefits include easier and more cost-effective maintenance in the future. They align with the open-closed principle, allowing for the addition of new code rather than modifying existing code when changes are necessary. This approach effectively reduces the overall maintenance costs.
Please consider this piece of code and think, what could be improved:
At first glance, the provided code appears fine, and I have encountered similar implementations in the past. However, there are a few key aspects that require attention:
1) OrderManager class creates instance of Order object (line #14) directly by itself. It would be okay, if only you won't need to do all other stuff:
- Generating the OrderId (line #15) and handling the associated logic internally (line #25)
- Setting the CustomerName (line #16)
- Setting the TotalAmount (line #17)
2) The OrderManager class also performs order management operations:
- Saving the order to the database (line #20) and keeping the logic within itself (line #31)
- Sending a confirmation email (line #21) and managing the process internally (line #36)
As you can observe, this class is responsible for multiple tasks, and it would greatly benefit from being divided into smaller, more understandable components. Since we are discussing creational design patterns, we can utilise one of them to eliminate lines #15-#17 and #25-#29. This modification would align the class more closely with the Single Responsibility Principle. I plan to cover this subject in our future articles.
One more thing, that you need to know is that there is a range of creational patterns exist:
- Factory Method
- Abstract Factory
- Singleton
- Prototype
- Builder
I will begin with the Factory Method, which is remarkably straightforward to comprehend and implement.
In conclusion, the use of creational patterns, provides significant benefits in software development. By separating the object creation process from object usage, factories enable more flexible, modular, and maintainable code. They promote encapsulation, allowing for easy addition or removal of implementations without impacting the client code. Additionally, creational patterns adhere to the open-closed principle, reducing maintenance costs by enabling the addition of new code rather than modifying existing code. Overall, incorporating factories and creational patterns in design and development leads to improved code organisation, testability, and future scalability.
Other examples of this patterns: DbProviderFactory Class
Top comments (0)