DEV Community

Saheed hussain
Saheed hussain

Posted on

Simply Explained : Top 5 Design Patterns

Introduction
Design patterns are reusable solutions to common programming problems. They provide proven approaches to solve recurring design challenges and promote code reuse, maintainability, and flexibility. In this blog, we will explore three popular design patterns, discussing their pros, cons, and providing code examples for better understanding.

1. Singleton Pattern:
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

Pros:

  • Allows a single instance of a class to be shared across the application.
  • Provides a global access point, making it easy to use and manage the instance.
  • Lazy initialization allows the instance to be created when needed.

Cons:

  • Can introduce tight coupling and make unit testing difficult.
  • Not suitable for scenarios where multiple instances of a class are required.

Code Example:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // Private constructor to prevent external instantiation
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Factory Pattern:
The Factory pattern provides an interface for creating objects, but delegates the responsibility of instantiation to subclasses.

Pros:

  • Encapsulates object creation, making it easier to manage and maintain.
  • Provides a flexible way to create objects without tightly coupling the client code to concrete classes.
  • Supports adding new product variants without modifying existing client code.

Cons:

  • Requires the creation of additional classes, which can increase code complexity.
  • May lead to a proliferation of factory classes if used excessively.

Code Example:

public interface Product {
    void doSomething();
}

public class ConcreteProduct1 implements Product {
    @Override
    public void doSomething() {
        // Implementation of doSomething method
    }
}

public class ConcreteProduct2 implements Product {
    @Override
    public void doSomething() {
        // Implementation of doSomething method
    }
}

public class ProductFactory {
    public Product createProduct(String type) {
       if (type.equalsIgnoreCase("Type1")) {
           return new ConcreteProduct1();
         } else if (type.equalsIgnoreCase("motorcycle")) {
            return new ConcreteProduct2();
        }
        throw new IllegalArgumentException("Invalid type: " + type);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Observer Pattern:
The Observer pattern establishes a one-to-many relationship between objects, so that when one object changes its state, all its dependents are notified and updated automatically. Its like a subscription to a newspaper or a magazine where all the subscribers are notified with the new version of the newspaper or magazine.

Pros:

  • Promotes loose coupling between objects, as the subject and observers interact through interfaces.
  • Supports the principle of open-closed design, allowing for easy addition or removal of observers.
  • Provides a clear separation between the subject and its observers, making the code more modular.

Cons:

  • Care must be taken to prevent memory leaks or excessive updates if not implemented carefully.
  • Overuse of the Observer pattern can lead to complexities and performance issues.

Code Example:

public interface Observer {
    void update();
}

public class ConcreteObserver implements Observer {
    @Override
    public void update() {
        // Update logic
    }
}

public interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

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

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Decorator Pattern:
The Decorator pattern allows for adding behavior to an object dynamically by wrapping it with one or more decorator objects.

Pros:

  • Provides a flexible alternative to subclassing for extending functionality.
  • Allows for adding or removing responsibilities at runtime.
  • Enhances the readability and maintainability of code by keeping classes focused on a single responsibility.

Cons:

  • Can result in a large number of small, specialized classes if used excessively.
  • Requires careful design to prevent unnecessary complexity and confusion.

Code Example:

public interface Component {
    void doSomething();
}

public class ConcreteComponent implements Component {
    @Override
    public void doSomething() {
        // Implementation of doSomething method
    }
}

public abstract class Decorator implements Component {
    protected Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void doSomething() {
        component.doSomething();
    }
}

public class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }

    @Override
    public void doSomething() {
        super.doSomething();
        // Additional behavior
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Strategy Pattern:
The Strategy pattern defines a family of interchangeable algorithms and encapsulates each one, allowing them to be easily swapped at runtime.

Pros:

  • Provides a flexible way to select and change algorithms dynamically.
  • Enhances code modularity and maintainability by encapsulating algorithms into separate classes.
  • Supports the open-closed principle, allowing new strategies to be added without modifying existing code.

Cons:

  • Requires the client to be aware of different strategies and select the appropriate one.
  • Can increase the number of classes if there are many strategies to be implemented.

Code Example:

public interface Strategy {
    void execute();
}

public class ConcreteStrategyA implements Strategy {
    @Override
    public void execute() {
        // Implementation of strategy A
    }
}

public class ConcreteStrategyB implements Strategy {
    @Override
    public void execute() {
        // Implementation of strategy B
    }
}

public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void executeStrategy() {
        strategy.execute();
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:
Design patterns provide proven solutions to common programming challenges. In this blog, we explored five popular Java design patterns: Singleton, Factory, Observer, Decorator, and Strategy. Each pattern has its pros and cons, and understanding when and how to apply them can greatly improve the quality, maintainability, and flexibility of your code. By using these design patterns effectively, you can enhance your software development skills and build more robust and scalable applications.

Top comments (0)