Far Beyond ‘New’: Exploring Creational Patterns and How They Can Revolutionize Your Code
In building solid and flexible software, few aspects are as essential as the way objects are created and organized. Although many developers are accustomed to instantiating objects directly using the new operator, this practice often leads to rigid code that is hard to modify and even more complex to maintain. This is where creational design patterns come in— a set of solutions that offer elegant and structured ways to handle object creation, promoting more adaptable and reusable code..
Creational patterns help solve common object creation problems, allowing you to abstract and simplify the process. Instead of worrying about the specific details of instantiation every time a new need arises, you can rely on tested frameworks to handle various creation scenarios. From the classic Singleton, which ensures a single instance, each pattern offers a unique approach to object creation, adapting to different development contexts.
In this article, we’ll explore how these patterns—Factory Method, Abstract Factory, Builder, Prototype, and Singleton—can transform the way you structure your code, providing more freedom to evolve your application over time.
Design Patterns:
Factory Method
The Factory Method is like a "custom factory". Imagine you want to buy coffee at a café. There are different types of coffee: espresso, cappuccino, and so on. Each is made differently, but you don’t need to understand how each type of coffee is prepared. You just say which one you want, and the café takes care of the whole process to deliver what you ordered.
In programming, the Factory Method works the same way. It defines a "factory" that knows which object to create based on your needs. If you want a PDF document, it creates a PDF; if you want a text document, it creates a DOC. You don’t need to know how each document is made—the Factory Method handles that for you.
To illustrate this in a more practical way, imagine a company generating reports for various departments, such as finance, sales, and HR. Each department needs reports in different formats. Instead of having a single process that manually handles each format, the company decides to automate this task using the Factory Method and RabbitMQ..
How this process works
Report Request: When someone at the company requests a report, the system identifies the desired format—e.g., PDF for a financial report, Excel for sales data, and HTML for a visual presentation of statistics.
The Report Factory (Factory Method): Here comes the Factory Method. Instead of having a complex, rigid system that needs to "decide" how to create each type of report, the company uses a "report factory" that automatically selects and generates the correct report based on the requested type. This factory abstracts all the creation logic, so no one needs to understand or know how each report type is generated.
Sending to RabbitMQ: After being created by the Factory Method, the report is automatically sent to a specific queue in RabbitMQ. For example:
- PDF reports go to the "pdf.report.queue".
- Excel reports go to the "excel.report.queue".
- HTML reports go to the "html.report.queue".
Each of these queues functions like a production line for that specific type of report.
Independent Processing: On the other side, each RabbitMQ queue is monitored by specialized consumers—programs or services that know exactly how to handle each format. The PDF queue consumer, for example, is optimized to process PDF reports, the Excel queue consumer handles spreadsheets, and so on. Thus, each report is processed and distributed independently without interfering with the other formats.
So, the Factory Method is an elegant solution for situations where different types of objects need to be created flexibly and independently. By separating object creation from its use, this pattern keeps the code cleaner and easier to maintain, reducing the number of conditions and "workarounds" that often arise when dealing with various formats or specific processes.
Abstract Factory
The Abstract Factory can be compared to a "factory of factories". Imagine you go to a clothing store, and inside the store, there are different sections selling clothes for various occasions: casual wear, formal wear, winter clothes, and so on. Instead of selecting each piece of clothing individually, you simply say what type of clothes you need—"I need winter clothes," for example—and the entire store adjusts to provide you with the right pieces.
In programming, the Abstract Factory works similarly. It doesn’t just create one object; it creates an entire "factory" that can produce a family of related objects. If you need a graphical interface, for instance, it can generate both buttons, menu bars, and text fields, all compatible with the desired style (Windows, Mac, or Linux), without you needing to worry about how each piece is made.
Practical Example
Let’s imagine a company that develops custom software for different platforms (Windows, macOS, and Linux). Each platform has its own set of graphical elements and behaviors—buttons, text boxes, scroll bars, etc. Instead of manually creating each of these components for every platform, the company decides to use the Abstract Factory.
How this process works
- The Windows Factory generates buttons and scroll bars in the Windows style.
- The macOS Factory generates buttons and scroll bars in the typical macOS style.
- The Linux Factory generates components compatible with the Linux interface.
The Abstract Factory is a more powerful pattern than the Factory Method because it allows you to create families of related objects without worrying about the detailed creation of each one. This makes the code more modular, scalable, and easier to maintain, especially when the system needs to be adapted to different environments or platforms. It further abstracts complexity and provides an organized way to work with interdependent objects. In the example of software for different platforms, the Abstract Factory makes it possible to create consistent and adaptable applications cleanly and efficiently.
Builder
The Builder pattern is used when we want to build a complex object step by step. Instead of creating a constructor with multiple parameters, which can be difficult to read and maintain, the Builder allows you to create objects in a more controlled and flexible manner. It’s ideal when the object needs to be built in multiple stages or has optional configurations.
Imagine a construction company offering various models of custom homes with different options, such as the number of rooms, types of flooring, and so on. Instead of creating a new class every time an order is placed, they can use a Builder to build the house according to the customer's preferences. Another example might be a travel company offering vacation packages, as explained next:
How this process works
Package Builder (Builder): Instead of building a complex travel package all at once, the company uses a Builder to build the package step by step. Based on the customer’s preferences, the Builder assembles each part of the package: the type of transport, hotel choice, and everything else needed for their trip.
Some advantages of this pattern are flexibility in defining only necessary attributes and immutability, as the final object can be immutable. Disadvantages include the fact that for simpler objects, this pattern can be overkill, creating more code than necessary.
Currently, the Builder can be used as a Lombok annotation (@Builder).
Prototype
The Prototype is useful when we want to create new instances of an object from an existing model or prototype. Instead of building the object from scratch, it allows us to create a copy of an already existing object, being efficient in terms of performance for certain scenarios. In Java, it’s used with the Cloneable interface.
For example, consider mass document issuance. Think of a company that issues contracts for multiple clients; the general structure of the contract is the same, only a few details change. Instead of creating a contract from scratch every time, we can use the Prototype to make copies from a model.
How this process works
Model Creation: The company defines a standard contract model with clauses containing common information for all clients.
Cloning and Customization: A new contract is generated by cloning the model. For each client, specific customizations are made to suit their needs.
Some advantages of this pattern include performance improvements by avoiding the creation of objects from scratch, and reduced complexity, as it allows for the creation of multiple objects with the same initial configurations. However, if a class has many associated objects, this copying process can become complex and error-prone if not managed properly.
Singleton
The Singleton pattern can be likened to the CEO of a company. Imagine you're in a large corporation with a single CEO responsible for making major decisions and leading the company. Regardless of how many departments or branches there are, there will always be one CEO who maintains central control. If everyone tried to create a new CEO, the company would become disorganized and contradictory.
In programming, the Singleton pattern follows this same principle. It ensures that a class has only a single instance, accessible globally, without creating new instances during program execution. This pattern is useful when we need to guarantee that only one instance of a class is managing certain resources, like a database connection or a message queue.
How this process works
Single Instance: The company decides that the logging system needs only one instance to function correctly. This ensures that all logs are recorded in a centralized way and that data isn’t mixed or overwritten. Once the logging system is started, it creates a single instance of this class, and whenever something needs to be logged, it accesses that same instance.
Accessing the Log: Whenever a developer or process wants to log information, they simply call a method of the single instance. This instance is ensured to be unique, so regardless of how many processes try to access the log at the same time, they’re all using the same instance. This prevents conflicts and duplication issues.
Ensuring Uniqueness: If, by chance, another process attempts to create a new instance of the logging class, the Singleton pattern prevents this, ensuring only one instance is maintained. This can be achieved through mechanisms such as a lock or a static variable that controls the single instance.
Example: Imagine that the company has various departments like finance, sales, and HR, all of which need to log their actions to a centralized logging system. Instead of each department creating its own logging system, the Singleton pattern ensures they all use the same instance of the logging system, avoiding redundancy and overload issues.
The Singleton pattern is very useful when you need to ensure a class has only one instance throughout the program’s lifecycle. It’s ideal for scenarios where data consistency or resource limitation is important, such as logging systems, database connection management, or even managing global configurations. By preventing the creation of multiple instances, the Singleton pattern ensures that all system components access the same data or resources, maintaining integrity and performance in an efficient manner.
Back-end Developer Java | Spring Boot | Quarkus | Microservices | SQL & NOSQL | SOLID | Design Patterns
Comments
Post a Comment