The 3 Types of Design Patterns All Developers Should Know (with Code Examples)

Introduction

Design patterns are a crucial tool in every developer‘s toolbox. At their core, design patterns are proven solutions to common problems in software design. Rather than having to solve every design problem from scratch, developers can leverage the collective wisdom and experience of the software engineering community by learning and applying design patterns.

The concept of design patterns originated in the field of architecture with the work of Christopher Alexander in the 1970s. In his book "A Pattern Language," Alexander described common problems in architectural design and their corresponding solutions. This idea was later adapted to software engineering by the so-called "Gang of Four" (GoF) in their seminal 1994 book, "Design Patterns: Elements of Reusable Object-Oriented Software." 1

Since then, design patterns have become a staple of software engineering education and practice. In a 2013 survey of over 2,000 developers by the software consulting firm Agylisys, 67% of respondents reported using design patterns in their work. 2 And in the popular coding challenge website HackerRank‘s 2023 Developer Skills Report, proficiency in software design patterns was ranked as the 3rd most important skill for developers, just behind problem-solving skills and language proficiency. 3

The GoF identified 23 classic software design patterns and categorized them into three types: creational, structural, and behavioral. As a professional developer, having a solid grasp of at least the most commonly used patterns from each category is essential. Proper application of design patterns can make your code more modular, reusable, and maintainable. It provides a common language to communicate design ideas with your team. And it demonstrates your knowledge of time-tested, expert-level solutions.

In the rest of this article, we‘ll take a deep dive into one key pattern from each category, complete with detailed explanations, real-world examples, and code samples in Java. Even if Java isn‘t your primary language, the concepts are applicable to any object-oriented programming language. Let‘s get started!

Creational Patterns: The Factory Method

The first pattern we‘ll explore is the factory method, one of the most widely used creational design patterns. The intent of the factory method is to define an interface for creating an object, but let subclasses decide which class to instantiate. 1 In other words, it defers the instantiation logic to subclasses.

The factory method is particularly useful when you have a superclass with multiple potential subclasses and you want to let the subclass decide which object to create. It encapsulates the object creation logic, providing a cleaner and more maintainable code structure.

Here‘s a UML diagram illustrating the structure of the factory method pattern:

Factory Method UML Diagram

Let‘s look at a concrete example. Suppose you‘re building a logistics management application that needs to handle different types of deliveries – by truck, train, or ship. Each type of delivery follows a similar high-level process, but the specifics of how the delivery is carried out will differ.

We can model this scenario using the factory method pattern in Java:

public abstract class Logistics {
   public void planDelivery() {
      Transport transport = createTransport();
      transport.deliver();
   }

   protected abstract Transport createTransport();
}

public class RoadLogistics extends Logistics {
   @Override
   protected Transport createTransport() {
      return new Truck();
   }
}

public class SeaLogistics extends Logistics {
   @Override
   protected Transport createTransport() {
      return new Ship();
   }
}

public interface Transport {
   void deliver();
}

public class Truck implements Transport {
   @Override
   public void deliver() {
      System.out.println("Delivering by land in a truck");
   }
}

public class Ship implements Transport {
   @Override
   public void deliver() {
      System.out.println("Delivering by sea in a ship");
   }
}

In this example, the Logistics abstract class represents our factory. It declares the factory method createTransport(), which returns a Transport object. Each concrete logistics subclass (RoadLogistics and SeaLogistics) provides its own implementation of this factory method to instantiate the appropriate Transport object (Truck or Ship).

This pattern is used extensively in many popular Java frameworks and libraries. For instance, the Java Collection Framework uses the factory method pattern for the polymorphic construction of collections via the Collection.iterator() method. 4 Other creational patterns like the abstract factory and builder patterns are variations on the same theme of encapsulating object creation logic, but they each have their specific use cases and implementations.

Structural Patterns: The Adapter

Moving on to structural patterns, the adapter pattern is a key one to have in your toolkit. An adapter allows objects with incompatible interfaces to collaborate. 1 It acts as a bridge between two incompatible interfaces, converting the interface of one class into another interface that clients expect.

The adapter pattern is useful in situations where you want to use an existing class, but its interface doesn‘t match the one you need. It‘s also handy when you want to create a reusable class that cooperates with unrelated or unforeseen classes.

Here‘s a UML diagram of the adapter pattern:

Adapter Pattern UML Diagram

Let‘s consider a real-world example. Imagine you‘re building an e-commerce application that needs to process payments from multiple payment gateways. Each gateway has its own unique API. You want to create a generic PaymentProcessor interface that can handle payments from any of these gateways, without the client code needing to know the specifics of each gateway.

We can solve this using the adapter pattern:

public interface PaymentProcessor {
   void processPayment(double amount);
}

public class StripeAdapter implements PaymentProcessor {
   private StripeAPI stripeAPI;

   public StripeAdapter(StripeAPI stripeAPI) {
      this.stripeAPI = stripeAPI;
   }

   @Override
   public void processPayment(double amount) {
      stripeAPI.makePayment(amount);
   }
}

public class StripeAPI {
   public void makePayment(double amount) {
      System.out.println("Processing $" + amount + " payment using Stripe");
   }  
}

public class PayPalAdapter implements PaymentProcessor {
   private PayPalAPI payPalAPI;

   public PayPalAdapter(PayPalAPI payPalAPI) {
      this.payPalAPI = payPalAPI;
   }

   @Override
   public void processPayment(double amount) {
      payPalAPI.processPayment(amount);
   }
}

public class PayPalAPI {
   public void processPayment(double amount) {
      System.out.println("Processing $" + amount + " payment using PayPal");  
   }
}

In this example, the PaymentProcessor interface defines the contract for processing a payment. We create two adapter classes, StripeAdapter and PayPalAdapter, which adapt the StripeAPI and PayPalAPI respectively to the PaymentProcessor interface.

The adapter pattern aligns with several of the SOLID design principles. It supports the Single Responsibility Principle by separating the interface or data conversion code from the primary business logic of the classes. It also adheres to the Open-Closed Principle, as you can introduce new adapters without changing the existing client code. 5

Many popular Java libraries use the adapter pattern. For instance, the Java I/O libraries use various adapters (InputStreamReader, OutputStreamWriter, etc.) to convert byte streams to character streams and vice versa. 6

Behavioral Patterns: The Observer

Finally, let‘s dive into a behavioral pattern that‘s crucial for event-driven systems and user interfaces: the observer pattern. The observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. 1

The observer pattern is applicable whenever a subject has to be observed by one or more observers. The subject maintains a list of observers and notifies them automatically of any state changes. This promotes loose coupling between the subject and its observers.

Here‘s a UML sequence diagram illustrating the interactions in the observer pattern:

Observer Pattern Sequence Diagram

A practical example where the observer pattern shines is in a news feed application. The news feed (the subject) can have many users (observers) subscribed to it. Whenever a new post is added to the feed, all subscribed users should be notified.

Here‘s how we might implement this in Java using the observer pattern:

public interface Observer {
   void update(String post);
}

public interface Subject {
   void registerObserver(Observer o);
   void removeObserver(Observer o);
   void notifyObservers(String post);
}

public class NewsFeed implements Subject {
   private List<Observer> observers = new ArrayList<>();
   private String latestPost;

   @Override
   public void registerObserver(Observer o) {
      observers.add(o);
   }

   @Override
   public void removeObserver(Observer o) {
      observers.remove(o);
   }

   @Override
   public void notifyObservers(String post) {
      this.latestPost = post;
      for (Observer o : observers) {
         o.update(post);
      }
   }

   public void addPost(String post) {
      notifyObservers(post);
   }
}

public class User implements Observer {
   private String name;

   public User(String name) {
      this.name = name;
   }

   @Override
   public void update(String post) {
      System.out.println("Hey " + name + ", there‘s a new post: " + post);
   }
}

The Subject interface declares the methods for managing observers and notifying them of state changes. The NewsFeed class, our concrete subject, maintains a list of Observers and notifies them whenever a new post is added.

The Observer interface declares the update() method that gets called by the subject when its state changes. The User class, our concrete observer, implements this interface to define what happens when it‘s notified of a new post.

The observer pattern is closely related to the publish-subscribe pattern, which is commonly used in message-oriented middleware systems. The main difference is that in pub-sub, publishers and subscribers don‘t need to know about each other, and they communicate via message queues or brokers. 7

Many GUI toolkits and event-driven systems use the observer pattern or a variation of it. For example, Java‘s Swing GUI framework uses the Action Listener interface to allow multiple GUI components to react to the same event. 8

The Bigger Picture

Design patterns are a fundamental aspect of software architecture and design. They provide a common vocabulary for developers to discuss and document the structure and interaction of software systems. When used appropriately, they can significantly improve code readability, modularity, and maintainability.

However, it‘s important to remember that design patterns are not a silver bullet. Overuse or misuse of patterns can lead to over-engineering, added complexity, and reduced performance. A famous quote by the GoF authors puts it well: "Design patterns should not be applied indiscriminately. Often they achieve flexibility and variability by introducing additional levels of indirection, and that can complicate a design and/or cost you some performance. A design pattern should only be applied when the flexibility it affords is actually needed." 1

In the context of agile software development, the role of design patterns is nuanced. Agile methodologies prioritize working software over comprehensive documentation, and they welcome changing requirements. This might seem at odds with the upfront design work that patterns require. However, many agile thought leaders argue that patterns, when used judiciously, can enable agility by promoting clean, adaptable code. 9

As a professional developer, your goal should be to understand design patterns well enough to know when they‘re applicable and beneficial, and when they‘re overkill. This comes with experience and continuous learning.

Conclusion

In this deep dive, we‘ve explored one key design pattern from each of the creational, structural, and behavioral categories. We‘ve seen how the factory method encapsulates object creation logic, how the adapter allows incompatible interfaces to work together, and how the observer enables objects to notify each other of state changes.

But remember, this is just a small sample of the rich world of design patterns. There are many more patterns out there, each solving a specific design problem. As you grow in your development career, make it a point to continually learn about and practice implementing these patterns.

At the same time, always consider the context and requirements of your specific project. Don‘t use patterns just for the sake of using them. Apply them when they truly make your code more flexible, more maintainable, and easier to understand.

If you‘re interested in learning more, I highly recommend reading the original "Design Patterns" book by the Gang of Four. For a more modern and practical perspective, "Head First Design Patterns" by Eric Freeman and Elisabeth Robson is a great resource. And for an in-depth look at patterns in the context of Java, check out "Effective Java" by Joshua Bloch.

Happy coding, and may your designs be forever flexible and maintainable!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *