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.
本文来自极简博客,作者:指尖流年,转载请注明原文链接:10 Must-Have Design Patterns for Software Developers