Fundamentals 27 min read

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.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Common Design Patterns in Daily Development: Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton

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 check

3. 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

chain of responsibilitydesign patternsJavatemplate methodsingletonstrategyFactoryObserver
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.