10 Must-Have Design Patterns for Software Developers

指尖流年 2019-09-08 ⋅ 12 阅读

Design patterns are tried and tested solutions to common software design problems. They help developers build scalable, maintainable, and flexible software. In this blog post, we will explore ten must-have design patterns that every software developer should know.

1. Singleton Pattern

The Singleton pattern ensures that there is only one instance of a class in the entire application. It is commonly used to manage shared resources such as database connections, thread pools, or configuration settings.

public class Singleton {
    private static Singleton instance;

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

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2. Factory Pattern

The Factory pattern provides an interface for creating objects, but lets subclasses decide which class to instantiate. It encapsulates object creation logic, resulting in loose coupling between the client code and the object being created.

public interface Shape {
    void draw();
}

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

public class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a square");
    }
}

public class ShapeFactory {
    public static Shape createShape(String type) {
        if (type.equalsIgnoreCase("circle")) {
            return new Circle();
        } else if (type.equalsIgnoreCase("square")) {
            return new Square();
        }
        return null;
    }
}

3. Observer Pattern

The Observer pattern defines a one-to-many dependency between objects, where a change in one object will notify and update all its dependents. It is commonly used in event-driven systems or whenever one object needs to inform others about changes in its state.

public interface Observer {
    void update();
}

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();
        }
    }
}

public class ConcreteObserver implements Observer {
    @Override
    public void update() {
        System.out.println("Observer notified of the change");
    }
}

4. Strategy Pattern

The Strategy pattern defines a family of interchangeable algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently of the clients that use it.

public interface CompressionStrategy {
    void compress(String file);
}

public class ZipCompressionStrategy implements CompressionStrategy {
    @Override
    public void compress(String file) {
        System.out.println("Compressing file using ZIP algorithm: " + file);
    }
}

public class RarCompressionStrategy implements CompressionStrategy {
    @Override
    public void compress(String file) {
        System.out.println("Compressing file using RAR algorithm: " + file);
    }
}

public class CompressionContext {
    private CompressionStrategy strategy;

    public void setStrategy(CompressionStrategy strategy) {
        this.strategy = strategy;
    }

    public void compressFile(String file) {
        strategy.compress(file);
    }
}

5. Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.

public interface Coffee {
    double getCost();
    String getDescription();
}

public class BasicCoffee implements Coffee {
    @Override
    public double getCost() {
        return 1.0;
    }

    @Override
    public String getDescription() {
        return "Basic Coffee";
    }
}

public abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    @Override
    public double getCost() {
        return decoratedCoffee.getCost();
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.5;
    }

    @Override
    public String getDescription() {
        return super.getDescription() + " with Milk";
    }
}

6. Prototype Pattern

The Prototype pattern creates objects by cloning an existing object instead of creating new ones. It allows us to create new objects based on existing ones, reducing the complexity of object creation.

public abstract class Shape implements Cloneable {
    public abstract void draw();

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

public class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

public class ShapeCache {
    private static Map<String, Shape> shapeCache = new HashMap<>();

    public static Shape getShape(String shapeId) {
        Shape cachedShape = shapeCache.get(shapeId);
        return (Shape) cachedShape.clone();
    }

    public static void loadCache() {
        Circle circle = new Circle();
        circle.setId("1");
        shapeCache.put(circle.getId(), circle);

        Rectangle rectangle = new Rectangle();
        rectangle.setId("2");
        shapeCache.put(rectangle.getId(), rectangle);
    }
}

7. Proxy Pattern

The Proxy pattern provides a surrogate or placeholder object that controls access to another object. It can be used to add extra functionality, such as access control or caching, to the underlying object.

public interface Image {
    void display();
}

public class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    @Override
    public void display() {
        System.out.println("Displaying image: " + filename);
    }

    private void loadFromDisk() {
        System.out.println("Loading image: " + filename);
    }
}

public class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

8. Iterator Pattern

The Iterator pattern provides a way to access elements of a collection sequentially without exposing its underlying representation. It decouples the client code from the complexity of internal collection structure.

public interface Iterator<T> {
    boolean hasNext();
    T next();
}

public interface IterableCollection<T> {
    Iterator<T> createIterator();
}

public class NameCollection implements IterableCollection<String> {
    private String[] names;

    public NameCollection(String[] names) {
        this.names = names;
    }

    @Override
    public Iterator<String> createIterator() {
        return new NameIterator();
    }

    private class NameIterator implements Iterator<String> {
        private int index = 0;

        @Override
        public boolean hasNext() {
            return index < names.length;
        }

        @Override
        public String next() {
            return names[index++];
        }
    }
}

9. Template Method Pattern

The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to the subclasses. It allows subclasses to redefine certain steps without changing the overall algorithm's structure.

public abstract class Game {
    public final void play() {
        initialize();
        startGame();
        endGame();
    }

    protected abstract void initialize();
    protected abstract void startGame();
    protected abstract void endGame();
}

public class Chess extends Game {
    @Override
    protected void initialize() {
        System.out.println("Initializing chess game");
    }

    @Override
    protected void startGame() {
        System.out.println("Playing chess game");
    }

    @Override
    protected void endGame() {
        System.out.println("Ending chess game");
    }
}

10. Chain of Responsibility Pattern

The Chain of Responsibility pattern allows multiple objects to handle a request independently. Each object in the chain can decide whether to process the request or pass it to the next object in the chain.

public abstract class Logger {
    protected LogLevel logLevel;
    protected Logger nextLogger;

    public void setNextLogger(Logger nextLogger) {
        this.nextLogger = nextLogger;
    }

    public void logMessage(LogLevel level, String message) {
        if (this.logLevel.ordinal() <= level.ordinal()) {
            writeLog(message);
        }
        if (nextLogger != null) {
            nextLogger.logMessage(level, message);
        }
    }

    protected abstract void writeLog(String message);
}

public class InfoLogger extends Logger {
    public InfoLogger() {
        this.logLevel = LogLevel.INFO;
    }

    @Override
    protected void writeLog(String message) {
        System.out.println("Info Logger: " + message);
    }
}

public class ErrorLogger extends Logger {
    public ErrorLogger() {
        this.logLevel = LogLevel.ERROR;
    }

    @Override
    protected void writeLog(String message) {
        System.out.println("Error Logger: " + message);
    }
}

public enum LogLevel {
    INFO,
    WARN,
    ERROR
}

Design patterns are invaluable tools for software developers. They provide proven solutions to common problems, making code easier to understand, maintain, and extend. By understanding and utilizing these ten must-have design patterns, developers can write more robust and efficient software.


全部评论: 0

    我有话说: