Common Design Patterns in Daily Development: Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton
This article introduces several classic design patterns—Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton—explaining their business scenarios, core definitions, Java implementations, and how to apply them in real-world projects to improve code maintainability and extensibility.
Preface
Hello, I am the "Boy Who Picks Snails". In daily coding we often write straightforward pipeline code, but to make development more enjoyable we can use design patterns to optimise business logic. Below are the design patterns I frequently use in my work.
1. Strategy Pattern
1.1 Business Scenario
When a big‑data system pushes files of different types, we need different parsing logic. A typical implementation uses a long if...else chain, which quickly becomes bloated and violates the Open/Closed Principle and Single‑Responsibility Principle.
if(type=="A"){
// parse A format
}
else if(type=="B"){
// parse B format
}
else{
// default parsing
}To avoid this, we can encapsulate each parsing algorithm as a separate strategy.
1.2 Strategy Pattern Definition
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from the client.
Imagine dating different personalities: you choose a movie, a snack, or shopping based on the person’s preference—each choice is a strategy.
1.3 Strategy Pattern Usage
Implementation steps:
A common interface or abstract class with two methods: one to identify the file type, another to perform the parsing.
Concrete strategy classes implementing the interface.
A service that loads all strategies into a map (using Spring's ApplicationContextAware ) and provides a resolveFile method.
1.3.1 Interface
public interface IFileStrategy {
FileTypeResolveEnum gainFileType();
void resolve(Object objectParam);
}1.3.2 Concrete Strategies
@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 parsing, param:{}", objectParam); }
}
@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 parsing, param:{}", objectParam); }
}
@Component
public class DefaultFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_DEFAULT_RESOLVE; }
@Override
public void resolve(Object objectParam) { logger.info("Default parsing, param:{}", objectParam); }
}1.3.3 Service Using Strategies
@Component
public class StrategyUseService implements ApplicationContextAware {
private Map
iFileStrategyMap = new ConcurrentHashMap<>();
public void resolveFile(FileTypeResolveEnum type, Object param) {
IFileStrategy strategy = iFileStrategyMap.get(type);
if (strategy != null) {
strategy.resolve(param);
}
}
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
Map
beans = ctx.getBeansOfType(IFileStrategy.class);
beans.values().forEach(s -> iFileStrategyMap.put(s.gainFileType(), s));
}
}2. Chain of Responsibility Pattern
2.1 Business Scenario
Order processing often requires a series of checks (null, security, blacklist, rule). Using exceptions for flow control leads to tangled code and violates best practices.
public class Order {
public void checkNullParam(Object p){ throw new RuntimeException(); }
public void checkSecurity(){ throw new RuntimeException(); }
public void checkBackList(){ throw new RuntimeException(); }
public void checkRule(){ throw new RuntimeException(); }
public static void main(String[] args){
Order o = new Order();
try {
o.checkNullParam();
o.checkSecurity();
o.checkBackList();
o.checkRule();
System.out.println("order success");
} catch (RuntimeException e) {
System.out.println("order fail");
}
}
}Replacing this with a Chain of Responsibility separates each check into its own handler.
2.2 Definition
The pattern creates a chain of handler objects, each having a chance to process the request and optionally pass it to the next handler.
Imagine passing a note through a row of classmates until someone finally handles it.
2.3 Usage
An abstract handler with a reference to the next handler and an abstract doFilter method.
Concrete handlers implementing specific checks.
A component that wires the handlers into a chain (using @PostConstruct ).
2.3.1 Abstract Handler
public abstract class AbstractHandler {
private AbstractHandler nextHandler;
public void setNextHandler(AbstractHandler next){ this.nextHandler = next; }
public void filter(Request req, Response res){
doFilter(req, res);
if (nextHandler != null) { nextHandler.filter(req, res); }
}
public AbstractHandler getNextHandler(){ return nextHandler; }
abstract void doFilter(Request req, Response res);
}2.3.2 Concrete Handlers
@Component @Order(1)
public class CheckParamFilterObject extends AbstractHandler {
@Override
public void doFilter(Request r, Response s){ System.out.println("Non‑null param check"); }
}
@Component @Order(2)
public class CheckSecurityFilterObject extends AbstractHandler {
@Override
public void doFilter(Request r, Response s){ System.out.println("Security check"); }
}
@Component @Order(3)
public class CheckBlackFilterObject extends AbstractHandler {
@Override
public void doFilter(Request r, Response s){ System.out.println("Blacklist check"); }
}
@Component @Order(4)
public class CheckRuleFilterObject extends AbstractHandler {
@Override
public void doFilter(Request r, Response s){ System.out.println("Rule check"); }
}2.3.3 Chain Initialization & Execution
@Component("ChainPatternDemo")
public class ChainPatternDemo {
@Autowired
private List
handlers;
private AbstractHandler head;
@PostConstruct
public void initializeChainFilter(){
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 res){
head.filter(req, res);
return res;
}
}Running the demo prints:
Non‑null param check
Security check
Blacklist check
Rule check3. Template Method Pattern
3.1 Business Scenario
Different merchants require slightly different request flows (query merchant info, sign request, send HTTP, verify response). Implementing each flow separately leads to duplicated code.
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.
3.3 Usage
Abstract class with the template method and abstract steps.
Concrete subclasses implement the variable steps (e.g., whether to use a proxy).
3.3.1 Abstract Class
abstract class AbstractMerchantService {
abstract void queryMerchantInfo();
abstract void signature();
abstract void httpRequest();
abstract void verifySignature();
Resp handlerTemplate(Req req){
queryMerchantInfo();
signature();
httpRequest();
verifySignature();
return new Resp();
}
abstract boolean isRequestByProxy();
}3.3.2 Concrete Implementations
public class CompanyAServiceImpl extends AbstractMerchantService {
@Override
public boolean isRequestByProxy(){ return true; }
// other abstract methods implemented accordingly
}
public class CompanyBServiceImpl extends AbstractMerchantService {
@Override
public boolean isRequestByProxy(){ return false; }
// other abstract methods implemented accordingly
}4. Observer Pattern
4.1 Business Scenario
After a user registers, we may need to send IM, SMS, and email notifications. Adding a new notification type forces us to modify the registration method, violating the Open/Closed Principle.
4.2 Definition
The Observer pattern defines a one‑to‑many dependency so that when one object changes state, all its dependents are notified and updated.
4.3 Usage
A subject (Observable) maintains a list of observers.
Observers implement a common interface.
Guava EventBus can be used as a ready‑made event bus.
4.3.1 Observable Class
public class Observable {
private List
observers = new ArrayList<>();
private int state;
public int getState(){ return state; }
public void setState(int s){ this.state = s; notifyAllObservers(s); }
public void addObserver(Observer o){ observers.add(o); }
public void removeObserver(Observer o){ observers.remove(o); }
public void notifyAllObservers(int state){
if(state != 1){ System.out.println("Not the notifying state"); return; }
for(Observer o : observers){ o.doEvent(); }
}
}4.3.2 Observer Implementations
interface Observer { void doEvent(); }
class IMMessageObserver implements Observer {
public void doEvent(){ System.out.println("Send IM message"); }
}
class MobileNoObserver implements Observer {
public void doEvent(){ System.out.println("Send SMS message"); }
}
class EmailObserver implements Observer {
public void doEvent(){ System.out.println("Send Email message"); }
}4.3.3 Guava EventBus Example
public class EventBusCenter {
private static final EventBus eventBus = new EventBus();
private EventBusCenter(){}
public static EventBus getInstance(){ return eventBus; }
public static void register(Object obj){ eventBus.register(obj); }
public static void unregister(Object obj){ eventBus.unregister(obj); }
public static void post(Object obj){ eventBus.post(obj); }
}
public class EventListener {
@Subscribe
public void handle(NotifyEvent e){
System.out.println("Send IM " + e.getImNo());
System.out.println("Send SMS " + e.getMobileNo());
System.out.println("Send Email " + e.getEmailNo());
}
}
public class NotifyEvent {
private String mobileNo, emailNo, imNo;
public NotifyEvent(String m, String e, String i){ this.mobileNo=m; this.emailNo=e; this.imNo=i; }
// getters omitted
}
public class EventBusDemoTest {
public static void main(String[] args){
EventListener listener = new EventListener();
EventBusCenter.register(listener);
EventBusCenter.post(new NotifyEvent("13372817283","[email protected]","666"));
}
}Output:
Send IM 666
Send SMS 13372817283
Send Email [email protected]5. Factory Pattern
5.1 Business Scenario
Factory pattern helps replace long if...else or switch statements when creating objects based on a type, such as selecting the appropriate file‑parsing strategy.
5.2 Definition
A factory defines an interface for creating objects, while concrete factories decide which class to instantiate.
5.3 Usage
Factory interface.
Concrete factories implementing the interface.
Client code selects the appropriate factory.
5.3.1 Factory Interface
interface IFileResolveFactory { void resolve(); }5.3.2 Concrete Factories
class AFileResolve implements IFileResolveFactory { public void resolve(){ System.out.println("File A type parsing"); } }
class BFileResolve implements IFileResolveFactory { public void resolve(){ System.out.println("File B type parsing"); } }
class DefaultFileResolve implements IFileResolveFactory { public void resolve(){ System.out.println("Default file type parsing"); } }5.3.3 Client Code
IFileResolveFactory factory;
if(fileType.equals("A")) { factory = new AFileResolve(); }
else if(fileType.equals("B")) { factory = new BFileResolve(); }
else { factory = new DefaultFileResolve(); }
factory.resolve();In Spring, the StrategyUseService shown earlier acts as a factory by storing all IFileStrategy beans in a map.
6. Singleton Pattern
6.1 Business Scenario
Singleton ensures a class has only one instance and provides a global access point, commonly used for I/O, database connections, or system components like a task manager.
6.2 Classic Implementations
Lazy ("lazy‑han") singleton with optional synchronized for thread safety.
Eager singleton (instance created at class loading).
Double‑checked locking.
Static inner‑class holder.
Enum singleton.
6.2.1 Lazy (Lazy‑Han) Singleton
public class LanHanSingleton {
private static LanHanSingleton instance;
private LanHanSingleton(){}
public static LanHanSingleton getInstance(){
if(instance == null){ instance = new LanHanSingleton(); }
return instance;
}
}6.2.2 Eager Singleton
public class EHanSingleton {
private static final EHanSingleton instance = new EHanSingleton();
private EHanSingleton(){}
public static EHanSingleton getInstance(){ return instance; }
}6.2.3 Double‑Check Locking
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;
}
}6.2.4 Static Inner‑Class
public class InnerClassSingleton {
private static class Holder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); }
private InnerClassSingleton(){}
public static InnerClassSingleton getInstance(){ return Holder.INSTANCE; }
}6.2.5 Enum Singleton
public enum SingletonEnum {
INSTANCE;
public SingletonEnum getInstance(){ return INSTANCE; }
}Recommended Reading
Tencent Interview: Does Redis support ACID transactions?
Understanding Redis Source Code – 7 Key Insights
Cache‑Database Consistency Issues
Understanding Distributed Multi‑Active Architecture
Distributed Locks – Redis vs Redisson
MVCC Principle Explained
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
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.