Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 56

Choosing Design Patterns Wisely

Recommended resources

Hello! Welcome to the final lesson in our module on Foundational Design Patterns.

In our previous lessons, we've implemented several key Creational, Structural, and Behavioral patterns, culminating with the Command pattern. You've built a solid toolbox of solutions to common software design problems. Now, we'll shift our focus from implementing individual patterns to the crucial skill of selecting and justifying the right one for a given scenario. This is a cornerstone of the "architectural mindset" you're aiming to cultivate—it's about making deliberate, defensible design decisions.

Our goal today is to select and justify an appropriate design pattern for a given design problem. We will develop a systematic approach for analyzing a problem, identifying candidate patterns, and articulating why one choice is more suitable than another.

1. A Framework for Choosing Patterns

With over 23 patterns in the original "Gang of Four" book, how do you even begin to choose? The first step is to identify the fundamental nature of the design problem you're trying to solve. Does your problem concern object creation, the composition of objects, or the interaction between them?

This leads to the three main categories we've discussed:

  • Creational Patterns: Deal with object instantiation mechanisms, trying to create objects in a manner suitable to the situation.
  • Structural Patterns: Deal with object composition, forming larger structures from individual parts.
  • Behavioral Patterns: Deal with communication between objects, their responsibilities, and the algorithms they use.
GoF Design Patterns Classification by Type and Scope
This chart organizes the Gang of Four (GoF) design patterns by their purpose (Creational, Structural, Behavioral) and scope (Class vs. Object). It's a useful map of the pattern landscape.

To go from this high-level map to a concrete choice, we need a more detailed guide.

Why You Should Master Design Patterns

The following resource provides an excellent quick-reference guide to all 23 GoF patterns and a checklist for making a decision. This will form the basis of our selection framework.

Please read 'Section 4: How to Choose the Right Design Pattern'. Pay close attention to two parts: Tabular Comparison: Scan the table to refamiliarize yourself with the purpose and use case of the various patterns. You don't need to memorize it, but get a feel for the variety. Decision-Making Checklist: This is the key takeaway. Internalize these five steps as a structured thought process.

The checklist you just read provides a great strategic overview. To summarize, your process should be:

  1. Identify the Problem Type: Is it creational, structural, or behavioral?
  2. Assess Scalability & Complexity: Will the pattern support future growth without being overly complex for the current problem?
  3. Consider Team Expertise: Is this a pattern your team can easily understand and maintain?
  4. Prototype and Test: Does the pattern actually solve the problem elegantly in practice?

2. A Heuristic Approach: Asking the Right Questions

The framework above is a great start. Now, let's get more tactical. How do you map a problem description to a specific pattern? Often, it comes down to asking the right questions about what needs to be flexible or what needs to be encapsulated.

The following resource offers a brilliant, question-based guide to finding patterns.

Design Patterns: From Analysis to Implementation

This 'Design Pattern Matrix' from Net Objectives provides a set of diagnostic questions to help you pinpoint the right pattern.

Please review the section 'Guide to finding patterns in the problem domain' on page 11. Then, quickly scan the 'Indicators in analysis' and 'Indicators in design' for each pattern category on the preceding pages to see how these questions connect to specific patterns.

Let's distill those heuristics into a mental model:

  • Are you trying to manage how objects are created?
    • Do you need to create families of related objects? → Abstract Factory
    • Do you need to construct a complex object step-by-step? → Builder
    • Do you want subclasses to decide which class to instantiate? → Factory Method
    • Do you need to ensure only one instance of an object exists? → Singleton
  • Are you trying to manage the structure of your classes and objects?
    • Do you need to make an existing class's interface match what a client expects? → Adapter
    • Do you want to provide a single, simplified interface to a complex subsystem? → Facade
    • Do you need to add new responsibilities to an object dynamically? → Decorator
    • Do you want to treat a group of objects the same way as a single object? → Composite
  • Are you trying to manage the behavior and communication between objects?
    • Do you need a list of objects to be notified when a state changes? → Observer
    • Do you need to select an algorithm at runtime? → Strategy
    • Do you need to encapsulate a request as an object (e.g., for queues or undo)? → Command
    • Do you want to reduce coupling between many objects by having them communicate through a central point? → Mediator

This question-driven approach helps you quickly narrow down the possibilities from 23 to a handful of likely candidates.


3. Justifying Your Choice by Comparing Alternatives

Selecting a pattern is half the battle. Justifying it is what demonstrates deep understanding. This often involves explaining why you chose pattern A instead of pattern B, especially when they seem similar.

For example, Adapter, Facade, Proxy, and Decorator are all "wrapper" patterns. How do you choose? It comes down to intent.

Let's return to the "Design Pattern Matrix" to see how to differentiate them.

Design Patterns: From Analysis to Implementation

This section of the document provides a direct comparison of commonly confused patterns, which is essential for justifying your design choices.

Please read the section 'Comparing patterns' on page 12. Focus on the two tables: 'Comparing Adapter, Façade, Proxy and Decorator' and 'Comparing Strategy and Bridge'.

Here's a summary of the key distinctions based on their intent:

Pattern Intent Analogy
Adapter To make an incompatible interface work with a client that expects another. A travel plug adapter for different outlets.
Facade To provide a simplified interface to a complex subsystem. A single "Start Car" button for a complex engine.
Decorator To add responsibilities to an object dynamically and transparently. Adding toppings to a pizza.
Proxy To control access to an object (e.g., for lazy loading, security). A security badge to access a building.

Being able to articulate these differences is a hallmark of a good software architect.


4. Case Study: Designing a Notification System

Let's apply this framework to a practical problem.

Scenario: You are designing a notification system for an e-commerce platform.

  • The system must send a notification when an order is shipped.
  • Initially, it only needs to send emails.
  • In the future, it will need to support SMS and in-app push notifications.
  • A user should be able to select their preferred notification method. Eventually, they might want to receive notifications through multiple channels (e.g., email and SMS).
  • The core OrderService should not have to be modified every time a new notification channel is added.

Let's walk through the decision process:

  1. Identify the Problem Type: The core of the problem is about handling different ways of doing something (sending a notification) and swapping them based on user preference. This is a classic Behavioral problem. We also need to create the right notification sender, which hints at a Creational pattern.

  2. Ask the Right Questions:

    • "Is there variation of a business rule or an implementation?" Yes, the notification logic varies. The heuristic guide points to the Strategy pattern. We can define an INotifier interface and have EmailNotifier, SmsNotifier, etc., as concrete strategies.
    • "Do we need to add some function?" Yes, users might want to combine notification channels. The guide suggests Decorator to add multiple functions. We could have a MultiChannelNotifier that wraps several INotifier instances.
    • "Are we trying to make things?" Yes, we need to create the correct notifier object based on user settings. This suggests a Factory pattern (either Factory Method or Abstract Factory) to encapsulate the creation logic.
  3. Synthesize and Justify:
    A combination of patterns seems ideal here.

    • Strategy is the core. It allows the OrderService to depend on the INotifier interface, upholding the Open/Closed Principle. The service can execute a notification strategy without knowing its concrete type.
    • Factory is needed to instantiate the correct strategy. The OrderService shouldn't contain a switch statement to decide which notifier to create. Instead, it can ask a NotifierFactory to provide the correct object based on the user's preferences.
    • Decorator could be used to handle the requirement of multiple channels. A SmsAndEmailNotifier could be a decorator that holds both an SmsNotifier and an EmailNotifier.

This combination of Strategy and Factory is extremely common and powerful. Let's see it in action.

Strategy and Factory Pattern, The Best Software Design Pattern Combo

This video walks through the exact problem we just analyzed. It's a perfect demonstration of how to combine the Strategy and Factory patterns to build a flexible and extensible system.

Please watch the entire video (it's short!). Pay attention to how: Strategy decouples the OrderService from the notification implementations. Factory is introduced to create the correct strategy at runtime. Decorator is proposed as a solution for handling multiple channels. The video concludes by discussing the trade-offs of abstraction (complexity vs. flexibility).

Test your understanding!

You are building a data export module for a web application. The module should allow users to export data as either a CSV file or a PDF document. The core logic for fetching the data is the same in both cases, but the formatting and file generation steps are completely different. The process is as follows:

  1. Fetch data from the database.
  2. Format the data.
  3. Generate the file.
  4. Return the file to the user.

You want to structure this so that adding a new format (e.g., XML) in the future requires minimal changes to the existing structure. Which primary design pattern would you use to structure the overall algorithm, and why?

Show answer

The most appropriate pattern here is the Template Method pattern.

Justification:
The problem describes a fixed algorithm (fetch, format, generate, return) where some steps are invariant (fetch data) while others vary (format, generate file).

The Template Method pattern is designed for this exact scenario. You would create an abstract base class, DataExporter, with a export() method that defines the skeleton of the algorithm.

abstract class DataExporter {
    // The template method
    public final File export() {
        Data data = fetchData();
        FormattedData formatted = formatData(data);
        File file = generateFile(formatted);
        return file;
    }

    // Common step implemented in the base class
    private Data fetchData() {
        // ... logic to fetch data from DB
    }

    // Steps to be implemented by subclasses
    protected abstract FormattedData formatData(Data data);
    protected abstract File generateFile(FormattedData formatted);
}

Concrete subclasses like CsvExporter and PdfExporter would then implement the formatData and generateFile methods. This keeps the overall process consistent while allowing for different implementations of specific steps, fulfilling the requirement for easy extension.


Conclusion

You've now reached a significant milestone in your journey toward an architectural mindset. Choosing the right design pattern is not about memorization; it's a process of systematic analysis, questioning, and justification.

Key Takeaways:

  • Analyze First: Start by identifying the core intent of your design problem: Is it about creation, structure, or behavior?
  • Use Heuristics: Use question-based guides and pattern catalogs to narrow down your options to a few likely candidates.
  • Compare and Justify: For similar patterns, focus on their specific intent to justify your choice. Articulating the "why" is as important as the "what."
  • Combine Patterns: Real-world problems often require combining multiple patterns (like Strategy and Factory) to create a robust and flexible solution.

Preview of the Next Lesson:
This lesson concludes our module on code-level design patterns. We will now move up a level of abstraction to system-level architecture. In the next module, "Database Architecture & Design," our first lesson will be on the CAP Theorem. This fundamental principle governs the trade-offs in distributed database systems and will be your first step into designing for large-scale, resilient data storage.