Interestingly, the idea of patterns originates from architect Christopher Alexander, whose book “The Timeless Way of Building” was published in the late 1970s. In this book, he attempted to describe professional techniques that could help even beginner architects design good buildings quickly. Of course, this is not the only connection between architecture and programming — just consider that in IT, the person who designs the foundational structure of software is called an “architect,” even though the word originally meant “building engineer.”
Until the end of the 20th century, many viewed programming as the IT equivalent of constructing a house. A finished product was expected to operate for years without major renovation. Designers focused on planning, foundations, and — as much as possible — “cementing” code lines together. The architectural metaphor worked well. However, at the beginning of the 2000s, the agile movement changed this perspective. It became clear that software changes far more often than buildings. It is unrealistic for a tenant to walk out their door one morning and realize the staircase was repainted overnight, bars were installed on the windows, and the elevator was moved five meters to the side. In software development, this kind of change happens constantly. Clearly, we cannot fully copy the production methods of architecture.
Since the publication of the foundational book by the “Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) in 1994, design patterns have received much criticism — but just as much praise. Programming has its own “craft techniques” that modern developers must know if they want to succeed. Patterns are among these techniques. Each one offers a generalized yet optimized solution to a frequently occurring problem. Using them makes software more resistant to change. They shorten the learning curve, help with planning, provide developers with a shared vocabulary, and enable a higher level of programming. It was clear to us at ponte.hu that we eventually had to deal with this topic.
We started with a brief overview. Design patterns are traditionally divided into three categories. Creational patterns provide guidance on how to create new class instances. In Java, this essentially revolves around where — and where not — to place the new keyword. Structural patterns focus on relationships between objects, aiming to create structures that are long-lasting yet easy to extend. Finally, behavioral patterns address how objects collaborate. Better algorithms yield better outcomes.
The first pattern we explored was the Abstract Factory. It is so fundamental that most developers have likely heard of it. It belongs to the family of creational patterns and helps create groups of objects without tying business logic to specific implementations.
Before diving deeper into the abstract factory, it’s worth considering why it is not ideal to fill business logic with new statements. In Java, objects cannot be created from an abstract class, so the new keyword must always be followed by a concrete implementation. This couples the caller to that specific implementation. If it changes, the caller may also need modifications. Since the SOLID principles, we know that having changes trigger more changes is undesirable because it increases the chance of errors. If we expect a class to change frequently or do not yet know which concrete implementation we will want, it is better to outsource object creation into a separate module. The Abstract Factory pattern provides this opportunity.
The core idea is to create frequently changing or not-yet-finalized object types (let’s call them Products) inside a dedicated class — the Factory. The Factory has a method that returns a new Product. The internal logic of how the Product is created — what resources or helper classes are used — is the Factory’s responsibility. The client simply receives a Factory and requests a Product from it.
In its original form, the pattern contains two abstractions. (Abstraction means separating essential characteristics from non-essential ones.) We reference the Product through an abstract interface containing only the methods that the client actually uses — usually very few. This prevents the client from depending on the Product’s concrete implementation, making future replacements easy. Similarly, the Factory itself is also accessed through an interface, ensuring the client is not tied to a specific Factory implementation either. The Factory interface’s most important (and often only) method is the one responsible for creating a Product — nothing else needs to be known. If we can inject a Factory into the client (e.g., as a parameter), we solve object creation with double abstraction — slightly complex, but very elegant.
We can understand all this better with a simple example. Let the Product be a button in a user interface! The Product interface could look like this:
interface Button {
void paint();
}
The concrete implementations may depend on many factors, the most significant being the operating system. Therefore, we may have several kinds of buttons:
class WinButton implements Button {
@Override
void paint() { … }
}
class OSXButton implements Button {
@Override
void paint() { … }
}
The Factory interface that creates buttons is also very simple:
interface GUIFactory {
Button createButton();
}
Its implementation decides which concrete button the client receives:
class GUIFactoryImpl implements GUIFactory {
private final String appearance;
GUIFactoryImpl(String appearance) {
this.appearance = appearance;
}
Button createButton() {
switch (this.appearance) {
case "osx": return new OSXButton();
case "win": return new WinButton();
}
throw new IllegalArgumentException("unknown " + appearance);
}
static GUIFactory factory(String appearance) {
return new GUIFactory(appearance);
}
}
To use all this, we first create a GUIFactory somewhere, pass it to the client, and it can call createButton. If done correctly, the client knows only the GUIFactory and Button interfaces — not their implementations.
GUIFactory factory = GUIFactoryImpl.factory("win");
…
Button button = factory.createButton();
button.paint();
You cannot learn programming without actually writing code. That’s why we not only reviewed an example — we also practiced through a hands-on exercise. Different colleagues implemented different parts while the others gave guidance.
The task was to create a message-sending class that sends a message from a sender to a recipient with a subject and body. However, we had two environments requiring different behaviors. In the production environment, a real email had to be sent; in the test environment, it was sufficient to write the message details into a log file. The MessageSender interface already existed with a single sendMessage method, along with two implementations matching the two environments. The task was to implement the Abstract Factory (MessageSenderFactory) and its two concrete implementations. One produced email-sending objects; the other produced logging objects. This wasn’t difficult to implement — especially with prewritten unit tests helping with verification.