Skip to content

A Journey Toward Simplicity: How Should We Approach Design Patterns?

I'm not sure myself

Makonea·Apr 21, 2026·35 min

A whole academic field has grown up around the idea of “design methods”—and I have been hailed as one of the leading exponents of these so-called design methods. I am very sorry that this has happened, and want to state, publicly, that I reject the whole idea of design methods as a subject of study, since I think it is absurd to separate the study of designing from the practice of design. In fact, people who study design methods without also practicing design are almost always frustrated designers who have no sap in them, who have lost, or never had, the urge to shape things. (Alexander 1971)

Before We Begin

People often call design patterns a silver bullet. OOP was born, and the Gang of Four's Design Patterns arrived as a kind of gospel,

and here we are today.

Many developers open the pattern book, look only at the structural diagrams, and write code accordingly, which tends to produce rigid implementations with little flexibility.

When learning design methodologies and patterns, we always find ourselves torn between "schematic understanding" and "practical experience."

As Alexander said in the passage above, separating design method from the substance of design ultimately leaves us talking about nothing more than a hollow abstraction.

There is actually a lot to say before talking about patterns, including idioms and other topics, but those discussions can wait for another time;

in this post, let's focus on patterns themselves.

(On Alan Kay's 2003 email)

The background behind the patterns we talk about today?

The background behind the patterns we talk about today?

The origins of what we commonly call Object-Oriented Programming (OOP) trace back to Simula, but the person generally credited as its first proponent is Alan Kay, who used the term as early as 1967.

However, in a 2003 email, Alan Kay clarified that what he had in mind when he coined object-oriented programming was messaging, encapsulation, and dynamic binding.

Simply put, the kind of object Kay envisioned was something like a tiny computer. Each object exists independently, cooperates by exchanging messages with others, and the core concern is not designing class hierarchies but rather "what messages pass between objects."

Yet when framed that way, it is not quite the OOP concept most of us have in mind.

So where did the concept we actually use come from?

(The OOP concept as we actually understand it)

The OOP we use today is a set of ideas built on top of the concept championed by Grady Booch.

1. OOP (object-oriented programming) uses objects, not algorithms, as the fundamental logical building blocks.

2. Each object is an instance of some class.

3. Classes are related to one another through inheritance.

In any case, these three elements are typically described as encapsulation, inheritance, and polymorphism.

This is the starting point of what we commonly refer to as object-oriented programming.

Object-Oriented Programming quickly became the dominant paradigm in programming, and from there emerged one of the great classics of the field.

That classic is none other than

the Gang of Four (GoF).

GoF

Design Patterns, written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, is an immortal classic of modern programming.

The GoF presents 23 design patterns and divides them into three categories.

The three categories are:

Creational Pattern

Structural Pattern

Behavioral Pattern

Creational Pattern

- Patterns related to object creation.

- Increase flexibility in the object creation process and make code easier to maintain.

Examples: Singleton, Abstract Factory, Builder, Factory Method, Prototype

Structural Pattern

- Patterns related to program structure.

- Useful for designing the structure of a program, including data structures and interface structures within it.

- Used to combine classes or objects to form larger structures.

Examples: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Behavioral Pattern

- Patterns that formalize recurring interactions between objects.

- Concerned with algorithms and the distribution of responsibility between objects.

- Minimize coupling (decoupling).

Examples: Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor

These examples exploded in popularity.

And they became a kind of template showing how to solve various problems in OOP.

But as these examples soared in popularity, overuse of these patterns became a problem,

and more and more people began treating them not as "the accumulated experience of seasoned programmers" but as outright axioms.

This post argues that programming patterns are not axioms;

they are highly versatile tools created by those who came before us.

Knowing patterns does increase the number of solutions available to you,

but what matters is how you solve the problem presented by the situation at hand,

not rote memorization of a pattern catalog.

(Refactoring using patterns)

Even in Joshua Bloch's acclaimed work on refactoring toward patterns, the following remark appears:

It seems you can’t overemphasize that a pattern’s Structure diagram is just an example, not a specification. It portrays the implementation we see most often. As such the Structure diagram will probably have a lot in common with your own implementation, but differences are inevitable and actually desirable. At the very least you will rename the participants as appropriate for your domain. Vary the implementation trade-offs, and your implementation might start looking a lot different from the Structure diagram. -John Vlissides

It was said by John Vlissides, one of the GoF authors.

What is the key point of this remark?

It is that even a single pattern we use can be implemented in multiple ways.

Let's start with a simple example.

// Strategy interface
interface IDiscountStrategy
{
    double ApplyDiscount(double price);
}
 
// Concrete strategy class 1
class FixedDiscountStrategy : IDiscountStrategy
{
    private double discountAmount;
    public FixedDiscountStrategy(double amount)
    {
        discountAmount = amount;
    }
    public double ApplyDiscount(double price)
    {
        return price - discountAmount;
    }
}
 
// Concrete strategy class 2
class PercentageDiscountStrategy : IDiscountStrategy
{
    private double discountRate;
    public PercentageDiscountStrategy(double rate)
    {
        discountRate = rate;
    }
    public double ApplyDiscount(double price)
    {
        return price * (1 - discountRate);
    }
}
 
// Context class
class ShoppingCart
{
    private IDiscountStrategy discountStrategy;
 
    public ShoppingCart(IDiscountStrategy strategy)
    {
        discountStrategy = strategy;
    }
 
    public double CalculateTotalPrice(double originalPrice)
    {
        return discountStrategy.ApplyDiscount(originalPrice);
    }
}
 
사용예시
ShoppingCart cart1 = new ShoppingCart(new FixedDiscountStrategy(1000));
ShoppingCart cart2 = new ShoppingCart(new PercentageDiscountStrategy(0.1));
 
Console.WriteLine($"Fixed Discount: {cart1.CalculateTotalPrice(10000)}"); //Fixed Discount:9000
Console.WriteLine($"Percentage Discount: {cart2.CalculateTotalPrice(10000)}"); //Percentage Discount: 9000

Here is the commonly used Strategy Pattern.

Now let's apply a lambda expression to it.

The interface and the class definitions that provide concrete implementations become unnecessary,

// Discount strategy delegate (function type definition)
delegate double DiscountStrategy(double price);
 
// Context class
class ShoppingCart
{
    private DiscountStrategy discountStrategy;
 
    public ShoppingCart(DiscountStrategy strategy)
    {
        discountStrategy = strategy;
    }
 
    public double CalculateTotalPrice(double originalPrice)
    {
        return discountStrategy(originalPrice);
    }
}
 
// Usage example (defining strategies directly with Lambda Expressions)
ShoppingCart cart1 = new ShoppingCart(price => price - 1000); // Fixed discount strategy (Lambda Expression)
ShoppingCart cart2 = new ShoppingCart(price => price * 0.9);   // Percentage discount strategy (Lambda Expression)
 
Console.WriteLine($"Fixed Discount (Lambda): {cart1.CalculateTotalPrice(10000)}"); //Fixed Discount:9000
Console.WriteLine($"Percentage Discount (Lambda): {cart2.CalculateTotalPrice(10000)}"); //Percentage Discount: 9000

the amount of code shrinks noticeably, various strategies can be defined easily with lambda expressions, and dynamic strategy switching becomes even more flexible when needed!

Next, let's try the Command Pattern using lambda expressions and method references.

// Command Interface
interface ICommand
{
    void Execute();
}
 
// Concrete Command Class 1
class LightOnCommand : ICommand
{
    private Light light;
    public LightOnCommand(Light light)
    {
        this.light = light;
    }
    public void Execute()
    {
        light.TurnOn();
    }
}
 
// Concrete Command Class 2
class LightOffCommand : ICommand
{
    private Light light;
    public LightOffCommand(Light light)
    {
        this.light = light;
    }
    public void Execute()
    {
        light.TurnOff();
    }
}
 
// Receiver Class
class Light
{
    public void TurnOn() { Console.WriteLine("Light On"); }
    public void TurnOff() { Console.WriteLine("Light Off"); }
}
 
// Invoker Class
class RemoteControl
{
    private ICommand onCommand;
    private ICommand offCommand;
 
    public RemoteControl(ICommand on, ICommand off)
    {
        this.onCommand = on;
        this.offCommand = off;
    }
 
    public void PressOnButton() { onCommand.Execute(); }
    public void PressOffButton() { offCommand.Execute(); }
}
 
// Usage Example
Light livingRoomLight = new Light();
RemoteControl remote = new RemoteControl(new LightOnCommand(livingRoomLight), new LightOffCommand(livingRoomLight));
 
remote.PressOnButton();  // Light On
remote.PressOffButton(); // Light Off

Now let's transform this with lambda expressions!

// Command delegate (void return, no parameters)
delegate void Command();
 
// Receiver class (same)
class Light
{
    public void TurnOn() { Console.WriteLine("Light On"); }
    public void TurnOff() { Console.WriteLine("Light Off"); }
}
 
// Invoker class
class RemoteControl
{
    private Command onCommand;
    private Command offCommand;
 
    public RemoteControl(Command on, Command off)
    {
        this.onCommand = on;
        this.offCommand = off;
    }
 
    public void PressOnButton() { onCommand(); }
    public void PressOffButton() { offCommand(); }
}
 
// Usage example (defining commands directly with Lambda Expressions and method references)
Light livingRoomLight = new Light();
RemoteControl remote = new RemoteControl(
    livingRoomLight.TurnOn,  // Method reference (replaces LightOnCommand)
    () => livingRoomLight.TurnOff() // Lambda Expression (replaces LightOffCommand)
);
 
remote.PressOnButton();  // Light On
remote.PressOffButton(); // Light Off

The line count drops again, various commands can be defined easily with method references,

and commands can now be changed dynamically as needed!

Now let's implement the Factory Pattern as well.

// Product Interface
interface IProduct
{
    void DoSomething();
}
 
// Concrete Product Class 1
class ConcreteProductA : IProduct
{
    public void DoSomething() { Console.WriteLine("Product A"); }
}
 
// Concrete Product Class 2
class ConcreteProductB : IProduct
{
    public void DoSomething() { Console.WriteLine("Product B"); }
}
 
// Factory Interface
interface IFactory
{
    IProduct CreateProduct();
}
 
// Concrete Factory Class 1
class ConcreteFactoryA : IFactory
{
    public IProduct CreateProduct() { return new ConcreteProductA(); }
}
 
//  Concrete Factory Class 2
class ConcreteFactoryB : IFactory
{
    public IProduct CreateProduct() { return new ConcreteProductB(); }
}
 
// Client Code
class Client
{
    public void UseProduct(IFactory factory)
    {
        IProduct product = factory.CreateProduct();
        product.DoSomething();
    }
}
 
// Usage Example
Client client = new Client();
client.UseProduct(new ConcreteFactoryA()); // Product A
client.UseProduct(new ConcreteFactoryB()); // Product B

Let's refactor this using lambda expressions and a dictionary!

// Product interface
interface IProduct
{
    void DoSomething();
}
 
// Concrete product class
class ConcreteProductA : IProduct
{
    public void DoSomething() { Console.WriteLine("Product A"); }
}
 
class ConcreteProductB : IProduct
{
    public void DoSomething() { Console.WriteLine("Product B"); }
}
 
// Factory delegate (returns product interface, no parameters)
delegate IProduct ProductFactory();
 
// Factory dictionary
class ProductFactoryDictionary
{
    private Dictionary<string, ProductFactory> factories = new Dictionary<string, ProductFactory>();
 
    public void RegisterFactory(string productName, ProductFactory factory)
    {
        factories.Add(productName, factory);
    }
 
    public IProduct CreateProduct(string productName)
    {
        if (factories.ContainsKey(productName))
        {
            return factories[productName]();
        }
        return null; // Exception handling
    }
}
 
// Client code
class Client
{
    public void UseProduct(ProductFactoryDictionary factoryDict, string productName)
    {
        IProduct product = factoryDict.CreateProduct(productName);
        if (product != null)
        {
            product.DoSomething();
        }
    }
}
 
// Usage example (registering factories with Lambda Expressions)
ProductFactoryDictionary factoryDict = new ProductFactoryDictionary();
factoryDict.RegisterFactory("A", () => new ConcreteProductA()); // Registering factories with Lambda Expressions
factoryDict.RegisterFactory("B", () => new ConcreteProductB()); // Registering factories with Lambda Expressions
 
Client client = new Client();
client.UseProduct(factoryDict, "A"); // Product A
client.UseProduct(factoryDict, "B"); // Product B

Using a dictionary makes it easy to register and manage factories dynamically at runtime, and reading factory information from a configuration file or external data source becomes straightforward, making the whole thing more flexible.

As people read through this, some will ask:

"Are you trying to argue that

using lambda expressions is the right answer?"

No.

Using lambda expressions is essentially adding syntactic sugar by borrowing from Functional Programming (FP) as a stopgap within conventional OOP,

and some might say to just use FP instead,

which is a fair point since there are other "orientations" beyond OOP for solving problems,

but that is not the core of what I am getting at.

Using lambda expressions also introduces another problem.

It can easily erode your team's code consistency.

When writing code, we fundamentally follow the rules of the software we are building.

That means adhering to coding conventions and code guidelines, and the basic principle of team code is this:

"It should look as if one person wrote it when someone else reads it." Code that follows this principle is generally considered well-written. (If you think of a program as a novel, isn't it better to have prose that reads as though a single author wrote it, rather than a book where the writing style shifts mid-way?)

But once you start writing lambdas the way shown above, you now face the problem of having to use lambda expressions throughout the rest of the codebase just to maintain consistency.

And Functional Programming is not always the right answer. It fundamentally introduces additional memory usage and non-trivial overhead. (Of course, using FP in asynchronous programming is one good way to prevent data races, but that is not what I am trying to discuss here.)

In practice, even within OOP, some patterns do evolve into better ones.

For instance, some uses of the Singleton Pattern are replaced by the relatively newer Service Locator Pattern,

(though both Singleton and Service Locator are often considered Anti-Patterns except in specific situations, so Dependency Injection (DI) is also widely used)

and the Builder Pattern has spawned a variant known as Fluent Builder.

These are variations that would never have emerged from rote memorization of patterns alone.

(Grady Booch, the person who helped me put food on the table)

So why are such variations and replacements even possible?

Earlier I mentioned that Alan Kay's OOP and Grady Booch's OOP were different things. If you look at the GoF's 23 design patterns, a significant number of them are solutions designed to work around the rigidity introduced by class inheritance hierarchies. The Strategy Pattern replaces inheritance with composition to swap out behavior, and the Decorator Pattern extends functionality without inheritance. In other words, GoF patterns can be described as "ways of solving problems created by Booch-style OOP, from within Booch-style OOP."

To put it differently, design patterns are empirical solutions to structural problems that recur within the specific paradigm of object-oriented programming; they are not universal truths that cut across all of programming. The reason we were able to transform those patterns using lambda expressions earlier is that we borrowed tools from a different paradigm, namely Functional Programming. And even within OOP itself, the fact that Singleton evolved into DI and Builder into Fluent Builder is not because the original patterns were flawed solutions, but because the context they needed to address had changed.

Ultimately, what I am trying to say is this:

"Always strive for a better answer within your team's conventions."

It is perhaps impossible to avoid being patterns happy on the road to learning patterns. In fact, most of us learn by making mistakes. I’ve been patterns happy on more than one occasion.

The true joy of patterns comes from using them wisely. Refactoring helps us do that by focusing our attention on removing duplication, simplifying code, and making code communicate its intention. When patterns evolve into a system by means of refactoring, there is less chance of over-engineering with patterns. The better you get at refactoring, the more chance you’ll have to find the joy of patterns.

Closing thoughts...

The true joy of patterns begins with using them wisely.

Why not take one more moment to think before reaching for a pattern?

You might just find your own new approach.

This is my personal belief,

but I think learning and practice increase the resolution of life.

A broader perspective starts with doubt, and doubt leads to new learning.

As new learning accumulates, we come to understand more and more of the world around us,

and the meaning that world offers reaches us more deeply.

The future ahead of us is uncertain and invisible, but if the scenery when we look back is rich in detail and beautiful, isn't that reason enough to find the journey worthwhile?

With that, I'll wrap up this post.