Unlock Cleaner Code: Master 6 Essential Design Patterns with Real Java Examples
This article explores six core design patterns—Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton—explaining their real‑world business scenarios, underlying principles, and providing complete Java implementations to help developers write more maintainable, extensible, and principle‑compliant code.
Most of the time we write code in a linear pipeline style to implement business logic, but to make coding enjoyable we can apply design patterns to optimize our code.
1. Strategy Pattern
1.1 Business Scenario
In a big‑data system files are pushed in and need to be parsed differently based on type. A naïve implementation uses a long if‑else chain:
<code>if(type=="A"){
// parse A format
} else if(type=="B"){
// parse B format
} else {
// default parsing
}</code>This code becomes bloated, hard to maintain, and violates the Open‑Closed Principle and Single‑Responsibility Principle.
If branches increase, the code becomes bulky and unreadable.
Adding a new parsing type requires modifying existing code.
When you have multiple conditional branches that can be encapsulated and swapped, the Strategy pattern is appropriate.
1.2 Strategy Definition
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from the clients that use it.
Imagine dating different personalities: you need different strategies—movie, snack, shopping—to win their hearts. The strategies are interchangeable while the goal remains the same.
1.3 Strategy Usage
Define an interface or abstract class with two methods (type matching and executable logic).
Provide concrete implementations for each strategy.
Use the strategy via a context that selects the appropriate implementation.
1.3.1 Interface with Two Methods
<code>public interface IFileStrategy {
FileTypeResolveEnum gainFileType();
void resolve(Object objectParam);
}</code>1.3.2 Concrete Strategies
<code>@Component
public class AFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_A_RESOLVE; }
@Override
public void resolve(Object objectParam) {
logger.info("A type file, param: {}", objectParam);
// A‑type specific logic
}
}
@Component
public class BFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_B_RESOLVE; }
@Override
public void resolve(Object objectParam) {
logger.info("B type file, param: {}", objectParam);
// B‑type specific logic
}
}
@Component
public class DefaultFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_DEFAULT_RESOLVE; }
@Override
public void resolve(Object objectParam) {
logger.info("Default type file, param: {}", objectParam);
// default logic
}
}</code>1.3.3 Using the Strategy
Spring’s ApplicationContextAware can collect all IFileStrategy beans into a map and expose a
resolveFilemethod:
<code>@Component
public class StrategyUseService implements ApplicationContextAware {
private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
Map<String, IFileStrategy> beans = ctx.getBeansOfType(IFileStrategy.class);
beans.values().forEach(s -> iFileStrategyMap.put(s.gainFileType(), s));
}
public void resolveFile(FileTypeResolveEnum type, Object param) {
IFileStrategy strategy = iFileStrategyMap.get(type);
if (strategy != null) {
strategy.resolve(param);
}
}
}</code>2. Chain of Responsibility Pattern
2.1 Business Scenario
Order processing often involves parameter validation, security checks, blacklist verification, and rule interception. Using exceptions for flow control leads to tangled code and violates best practices.
【Mandatory】Do not use exceptions for normal control flow; they are meant for unexpected situations and are less efficient than condition checks.
2.2 Definition
The Chain of Responsibility creates a chain of handler objects where each handler can process a request or pass it to the next handler.
2.3 Usage
Define an abstract handler with a reference to the next handler.
Implement concrete handlers for each validation step.
Initialize the chain (e.g., via Spring) and invoke the first handler.
2.3.1 Abstract Handler
<code>public abstract class AbstractHandler {
private AbstractHandler nextHandler;
public void setNextHandler(AbstractHandler next) { this.nextHandler = next; }
public void filter(Request request, Response response) {
doFilter(request, response);
if (nextHandler != null) {
nextHandler.filter(request, response);
}
}
protected abstract void doFilter(Request request, Response response);
}</code>2.3.2 Concrete Handlers
<code>@Component @Order(1)
public class CheckParamFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request request, Response response) {
System.out.println("Non‑null parameter check");
}
}
@Component @Order(2)
public class CheckSecurityFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request request, Response response) {
System.out.println("Security check");
}
}
@Component @Order(3)
public class CheckBlackFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request request, Response response) {
System.out.println("Blacklist check");
}
}
@Component @Order(4)
public class CheckRuleFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request request, Response response) {
System.out.println("Rule check");
}
}</code>2.3.3 Chain Initialization and Execution
<code>@Component("ChainPatternDemo")
public class ChainPatternDemo {
@Autowired
private List<AbstractHandler> handlers;
private AbstractHandler head;
@PostConstruct
public void initChain() {
for (int i = 0; i < handlers.size(); i++) {
if (i == 0) {
head = handlers.get(0);
} else {
handlers.get(i - 1).setNextHandler(handlers.get(i));
}
}
}
public Response exec(Request req, Response resp) {
head.filter(req, resp);
return resp;
}
}</code>Running the demo prints:
<code>Non‑null parameter check
Security check
Blacklist check
Rule check</code>3. Template Method Pattern
3.1 Business Scenario
Different merchants (A, B, C) invoke a unified service that queries merchant info, signs the request, sends HTTP (via proxy or direct), and verifies the response. Repeating these steps in each merchant class leads to code duplication.
3.2 Definition
The Template Method defines the skeleton of an algorithm in an abstract class, allowing subclasses to override specific steps without changing the overall structure.
Analogy: courting a partner follows a fixed sequence—hand‑hold, hug, kiss—while the hand used can vary; the template is the sequence.
3.3 Usage
Abstract class defines the workflow.
Concrete subclasses implement variable steps.
3.3.1 Abstract Skeleton
<code>abstract class AbstractMerchantService {
abstract void queryMerchantInfo();
abstract void signature();
abstract void httpRequest();
abstract void verifySignature();
// template method
public Resp handleTemplate(Req req) {
queryMerchantInfo();
signature();
httpRequest();
verifySignature();
return new Resp();
}
// hook to decide proxy usage
protected abstract boolean isRequestByProxy();
}</code>3.3.2 Concrete Implementations
<code>public class CompanyAServiceImpl extends AbstractMerchantService {
@Override
public Resp handle(Req req) { return handleTemplate(req); }
@Override
protected boolean isRequestByProxy() { return true; }
// implement abstract steps …
}
public class CompanyBServiceImpl extends AbstractMerchantService {
@Override
public Resp handle(Req req) { return handleTemplate(req); }
@Override
protected boolean isRequestByProxy() { return false; }
// implement abstract steps …
}</code>4. Observer Pattern
4.1 Business Scenario
After a user registers, the system may need to send IM, email, SMS, or other notifications. Adding a new notification channel forces changes to the registration method, violating the Open‑Closed Principle.
4.2 Definition
The Observer pattern defines a one‑to‑many dependency: when the subject changes state, all registered observers are notified.
4.3 Usage
Subject (Observable) maintains a list of observers.
Observers implement a common interface.
Subject notifies observers when state changes.
4.3.1 Subject and Observers
<code>public class Observable {
private List<Observer> observers = new ArrayList<>();
private int state;
public void setState(int s) { this.state = s; notifyAllObservers(); }
public void addObserver(Observer o) { observers.add(o); }
public void removeObserver(Observer o) { observers.remove(o); }
private void notifyAllObservers() {
if (state != 1) return;
for (Observer o : observers) { o.doEvent(); }
}
}
public interface Observer { void doEvent(); }
public class IMMessageObserver implements Observer {
public void doEvent() { System.out.println("Send IM message"); }
}
public class MobileNoObserver implements Observer {
public void doEvent() { System.out.println("Send SMS"); }
}
public class EmailObserver implements Observer {
public void doEvent() { System.out.println("Send Email"); }
}</code>4.3.2 Guava EventBus (Practical Implementation)
Guava’s EventBus provides annotation‑based event publishing:
<code>public class EventBusCenter {
private static final EventBus bus = new EventBus();
public static void register(Object obj) { bus.register(obj); }
public static void unregister(Object obj) { bus.unregister(obj); }
public static void post(Object event) { bus.post(event); }
}
public class EventListener {
@Subscribe
public void handle(NotifyEvent e) {
System.out.println("IM:" + e.getImNo());
System.out.println("SMS:" + e.getMobileNo());
System.out.println("Email:" + e.getEmailNo());
}
}
public class NotifyEvent {
private String mobileNo; private String emailNo; private String imNo;
public NotifyEvent(String m, String e, String i) { this.mobileNo=m; this.emailNo=e; this.imNo=i; }
public String getMobileNo() { return mobileNo; }
public String getEmailNo() { return emailNo; }
public String getImNo() { return imNo; }
}</code>Demo:
<code>public class EventBusDemoTest {
public static void main(String[] args) {
EventListener listener = new EventListener();
EventBusCenter.register(listener);
EventBusCenter.post(new NotifyEvent("13372817283", "[email protected]", "666"));
}
}</code>Output:
<code>IM:666
SMS:13372817283
Email:[email protected]</code>5. Factory Pattern
5.1 Business Scenario
Factory pattern works together with Strategy to replace long if‑else or switch statements when creating objects based on file type.
5.2 Definition
A factory defines an interface for creating objects, while concrete factories produce specific implementations.
5.3 Usage
5.3.1 Factory Interface
<code>interface IFileResolveFactory { void resolve(); }</code>5.3.2 Concrete Factories
<code>public class AFileResolve implements IFileResolveFactory {
public void resolve() { System.out.println("File A type parsing"); }
}
public class BFileResolve implements IFileResolveFactory {
public void resolve() { System.out.println("File B type parsing"); }
}
public class DefaultFileResolve implements IFileResolveFactory {
public void resolve() { System.out.println("Default file type parsing"); }
}</code>5.3.3 Client Code
<code>IFileResolveFactory factory;
if (type.equals("A")) {
factory = new AFileResolve();
} else if (type.equals("B")) {
factory = new BFileResolve();
} else {
factory = new DefaultFileResolve();
}
factory.resolve();</code>6. Singleton Pattern
6.1 Business Scenario
Singleton ensures a class has only one instance, commonly used for I/O, database connections, or system managers.
6.2 Classic Implementations
Lazy (懒汉) – instance created on first request.
Eager (饿汉) – instance created at class loading.
Double‑checked locking – thread‑safe lazy initialization.
Static inner class – leverages class loading for thread safety.
Enum – simplest, serialization‑safe singleton.
Lazy (懒汉) Example
<code>public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}</code>Eager (饿汉) Example
<code>public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() { return INSTANCE; }
}</code>Double‑Checked Locking Example
<code>public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}</code>Static Inner Class Example
<code>public class InnerClassSingleton {
private static class Holder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); }
private InnerClassSingleton() {}
public static InnerClassSingleton getInstance() { return Holder.INSTANCE; }
}</code>Enum Example
<code>public enum SingletonEnum { INSTANCE; }
// Access via SingletonEnum.INSTANCE</code>macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.