DEV Community

Cover image for When to Use a Pattern
CodeCraft Diary
CodeCraft Diary

Posted on • Edited on • Originally published at codecraftdiary.com

When to Use a Pattern

Have you ever stared at a messy codebase and wondered, “There must be a better way to organize this”? That’s exactly when design patterns come into play.

Design patterns are proven solutions to recurring problems in software design. They help you write code that is more maintainable, readable, and scalable. But there’s a catch: patterns are tools, not rules. Using them blindly can create unnecessary complexity. Knowing when and why to use a pattern is just as important as knowing the pattern itself.

In this article, we’ll explore how to recognize situations that call for patterns, which patterns to consider, and practical tips for applying them effectively.

Why Patterns Exist

Patterns exist because developers keep running into similar problems. By codifying these solutions, patterns save you from reinventing the wheel.

Consider the Singleton pattern. It ensures that only one instance of a class exists. For example, you might use it for a logging system or configuration manager. Without Singleton, multiple instances could be created, causing inconsistent logging or settings.

class Logger {
    private static $instance;

    private function __construct() {}

    public static function getInstance(): Logger {
        if (!self::$instance) {
            self::$instance = new Logger();
        }
        return self::$instance;
    }

    public function log($message) {
        echo "[LOG]: $message\n";
    }
}

$logger = Logger::getInstance();
$logger->log("This is a log message.");
class Logger {
    private static $instance;

    private function __construct() {}

    public static function getInstance(): Logger {
        if (!self::$instance) {
            self::$instance = new Logger();
        }
        return self::$instance;
    }

    public function log($message) {
        echo "[LOG]: $message\n";
    }
}

$logger = Logger::getInstance();
$logger->log("This is a log message.");
Enter fullscreen mode Exit fullscreen mode

While powerful, Singleton is often overused. If you don’t truly need a single instance, it’s better to keep your code simple. This is the first lesson: patterns are for recurring, concrete problems, not hypothetical ones.

Signs You Might Need a Pattern

Not every piece of messy code requires a pattern. Here are some signs your code might benefit from one:

1. Repeated Logic

When you find yourself copying and pasting the same code multiple times, a Strategy or Template pattern can help.

Example: Different discount calculations based on customer type:

// Before
function getDiscount($customerType) {
    if ($customerType === 'vip') return 0.3;
    if ($customerType === 'regular') return 0.1;
    return 0;
}// Before
function getDiscount($customerType) {
    if ($customerType === 'vip') return 0.3;
    if ($customerType === 'regular') return 0.1;
    return 0;
}PHP
Enter fullscreen mode Exit fullscreen mode

This works, but adding a new type means editing the function. Using Strategy, each discount type becomes its own class:

interface DiscountStrategy { public function getDiscount(): float; }

class VIPDiscount implements DiscountStrategy { public function getDiscount() { return 0.3; } }
class RegularDiscount implements DiscountStrategy { public function getDiscount() { return 0.1; } }

function getDiscountForType(string $type) {
    $strategies = ['vip' => new VIPDiscount(), 'regular' => new RegularDiscount()];
    return ($strategies[$type] ?? new RegularDiscount())->getDiscount();
}
interface DiscountStrategy { public function getDiscount(): float; }

class VIPDiscount implements DiscountStrategy { public function getDiscount() { return 0.3; } }
class RegularDiscount implements DiscountStrategy { public function getDiscount() { return 0.1; } }

function getDiscountForType(string $type) {
    $strategies = ['vip' => new VIPDiscount(), 'regular' => new RegularDiscount()];
    return ($strategies[$type] ?? new RegularDiscount())->getDiscount();
}
Enter fullscreen mode Exit fullscreen mode

Adding new customer types is now simple and safe.

2. Complex Conditional Logic

If you have a forest of if and switch statements, consider Polymorphism or Strategy.

// Before
function calculateShipping(order) {
    if (order.type === 'standard') return 5;
    if (order.type === 'express') return 15;
    return 0;
}// Before
function calculateShipping(order) {
    if (order.type === 'standard') return 5;
    if (order.type === 'express') return 15;
    return 0;
}PHP
Enter fullscreen mode Exit fullscreen mode
// After (Strategy)
class StandardShipping { cost() { return 5; } }
class ExpressShipping { cost() { return 15; } }

const shippingStrategies = { standard: new StandardShipping(), express: new ExpressShipping() };
return (shippingStrategies[order.type] ?? new StandardShipping()).cost();// After (Strategy)
class StandardShipping { cost() { return 5; } }
class ExpressShipping { cost() { return 15; } }

const shippingStrategies = { standard: new StandardShipping(), express: new ExpressShipping() };
return (shippingStrategies[order.type] ?? new StandardShipping()).cost();
Enter fullscreen mode Exit fullscreen mode

This makes it easy to add new shipping methods without touching existing code.

3. Fragile Code That Breaks Easily

When changing one module causes bugs elsewhere, your code might need Observer or Dependency Injection.

Example: multiple components need to react when a user updates their profile. Using Observer, subscribers automatically get notified:

class User:
    def __init__(self):
        self._observers = []

    def subscribe(self, observer):
        self._observers.append(observer)

    def notify(self, event):
        for obs in self._observers:
            obs.update(event)

    def update_profile(self, name):
        # Update profile logic...
        self.notify(f"Profile updated: {name}")class User:
    def __init__(self):
        self._observers = []

    def subscribe(self, observer):
        self._observers.append(observer)

    def notify(self, event):
        for obs in self._observers:
            obs.update(event)

    def update_profile(self, name):
        # Update profile logic...
        self.notify(f"Profile updated: {name}")
Enter fullscreen mode Exit fullscreen mode

Now, any component that subscribes gets notified automatically — no tight coupling.

4. Multiple Responsibilities in One Class or Function

If a class or function is doing too much, think about Facade, Adapter, or Command patterns. Splitting responsibilities makes the code more modular, easier to test, and easier to extend.

5. Frequently Changing Requirements

Patterns help minimize the impact of changes. For example, Open/Closed Principle patterns like Strategy or Decorator let you extend behavior without modifying existing code — safer for long-term maintenance.

Practical Guidelines for Using Patterns

Refactor first, pattern later: Start by cleaning up duplicates, long methods, and unclear responsibilities. Only introduce patterns if problems recur.

Favor simplicity: A simple solution is better than a pattern you don’t fully need.

Write tests: Patterns are easier to implement safely when you can verify behavior automatically.

Document your choices: Explain why a pattern is applied, so future developers understand the reasoning.

Common Pitfalls

Over-engineering: Adding patterns where simple code would suffice.

Misunderstanding the pattern: Applying a pattern incorrectly can make code harder to read.

Obfuscation: Patterns can hide intent if overused or applied to trivial problems.

Conclusion

Design patterns are powerful tools, not rules. The key question isn’t “Which pattern should I use?” — it’s:

“Does this pattern make my code easier to understand, maintain, or extend?”

Start by identifying recurring problems, refactor incrementally, and introduce patterns only when they solve a real problem. Used wisely, patterns will make your codebase cleaner, safer, and more adaptable — and you, a happier developer.

Patterns are like seasoning: just the right amount enhances the dish, too much ruins it. Learn to spot when they

Top comments (0)