Design Patterns in Java: Strategy, Chain, Template, Observer & Singleton
This article explores six classic design patterns—Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton—detailing their real‑world business scenarios, core principles, Java implementations with Spring integration, and how they improve code maintainability, extensibility, and adherence to SOLID principles.
Usually we write code in a linear pipeline fashion to achieve business logic, but to make coding enjoyable we can use design patterns to optimize our business code. Below are common design patterns and how to apply them in daily work.
1. Strategy Pattern
1.1 Business Scenario
Assume a big‑data system pushes files and, based on file type, different parsing methods are needed. Many developers write code like:
if(type=="A"){
// parse as A format
}
else if(type=="B"){
// parse as B format
}
else{
// default parsing
}This code can become 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 if...else branches that can be encapsulated and swapped, the Strategy pattern is a good fit.
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 clients that use it.
Imagine dating different personalities; you need different strategies—movie, snack, shopping—to win their hearts. The strategies are interchangeable, but the goal remains the same.
1.3 Using Strategy Pattern
Implementation steps:
Define an interface or abstract class with two methods (type matching and executable logic).
Create concrete strategy classes implementing the interface.
Use the strategy via a map initialized by Spring.
1.3.1 Interface with Two Methods
public interface IFileStrategy {
FileTypeResolveEnum gainFileType();
void resolve(Object objectParam);
}1.3.2 Concrete Strategies
A type strategy implementation:
@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 file, param: {}", objectParam);
// A type specific logic
}
}B type strategy implementation:
@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 file, param: {}", objectParam);
// B type specific logic
}
}Default strategy implementation:
@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 parsing file, param: {}", objectParam);
// Default parsing logic
}
}1.3.3 Using Strategy Pattern
Leverage Spring's lifecycle and ApplicationContextAware to populate a map of strategies, then call resolveFile to execute the appropriate strategy.
@Component
public class StrategyUseService implements ApplicationContextAware {
private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IFileStrategy> tempMap = applicationContext.getBeansOfType(IFileStrategy.class);
tempMap.values().forEach(service -> iFileStrategyMap.put(service.gainFileType(), service));
}
public void resolveFile(FileTypeResolveEnum fileTypeResolveEnum, Object objectParam) {
IFileStrategy strategy = iFileStrategyMap.get(fileTypeResolveEnum);
if (strategy != null) {
strategy.resolve(objectParam);
}
}
}2. Chain of Responsibility Pattern
2.1 Business Scenario
Order processing often involves parameter validation, security checks, blacklist checks, rule interception, etc. Using exceptions for flow control leads to problems and violates best practices.
public class Order {
public void checkNullParam(Object param) { 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 order = new Order();
try {
order.checkNullParam();
order.checkSecurity();
order.checkBackList();
order.checkRule();
System.out.println("order success");
} catch (RuntimeException e) {
System.out.println("order fail");
}
}
}Instead, the Chain of Responsibility pattern can decouple these checks.
2.2 Chain of Responsibility Definition
The pattern creates a chain of handler objects, each having a chance to process a request. If a handler processes it, it may pass the request to the next handler or stop.
2.3 Using Chain of Responsibility
Implementation steps:
Define an abstract handler with a reference to the next handler and a template method.
Implement concrete handlers for each validation step.
Initialize the chain (e.g., via Spring) and invoke the first handler.
2.3.1 Abstract Handler
public abstract class AbstractHandler {
private AbstractHandler nextHandler;
public void setNextHandler(AbstractHandler nextHandler) { this.nextHandler = nextHandler; }
public void filter(Request request, Response response) {
doFilter(request, response);
if (getNextHandler() != null) {
getNextHandler().filter(request, response);
}
}
public AbstractHandler getNextHandler() { return nextHandler; }
protected abstract void doFilter(Request request, Response response);
}2.3.2 Concrete Handlers
@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");
}
}2.3.3 Chain Initialization and Usage
@Component("ChainPatternDemo")
public class ChainPatternDemo {
@Autowired
private List<AbstractHandler> abstractHandleList;
private AbstractHandler abstractHandler;
@PostConstruct
public void initializeChainFilter() {
for (int i = 0; i < abstractHandleList.size(); i++) {
if (i == 0) {
abstractHandler = abstractHandleList.get(0);
} else {
AbstractHandler current = abstractHandleList.get(i - 1);
AbstractHandler next = abstractHandleList.get(i);
current.setNextHandler(next);
}
}
}
public Response exec(Request request, Response response) {
abstractHandler.filter(request, response);
return response;
}
}Running the demo prints:
Non‑null parameter check
Security check
Blacklist check
Rule check3. Template Method Pattern
3.1 Business Scenario
Different merchants invoke a third‑party service via HTTP. The process includes querying merchant info, signing the request, sending HTTP, and verifying the response. Some merchants use a proxy, others direct connection.
3.2 Template Method Definition
The pattern defines the skeleton of an algorithm in an abstract class, leaving some steps to subclasses to implement.
Dating analogy: first hold hands, then hug, then kiss— the steps are fixed, but the exact hand used can vary.
3.3 Using Template Method
Create an abstract class with the template method and abstract steps.
Implement common steps in the abstract class.
Let subclasses implement the variable steps.
3.3.1 Abstract Class Skeleton
abstract class AbstractMerchantService {
abstract void queryMerchantInfo();
abstract void signature();
abstract void httpRequest();
abstract void verifySignature();
public Resp handlerTemplate(Req req) {
queryMerchantInfo();
signature();
httpRequest();
verifySignature();
return resp;
}
abstract boolean isRequestByProxy();
}3.3.2 Concrete Implementations
class CompanyAServiceImpl extends AbstractMerchantService {
@Override
public Resp handler(Req req) { return handlerTemplate(req); }
@Override
public boolean isRequestByProxy() { return true; }
}
class CompanyBServiceImpl extends AbstractMerchantService {
@Override
public Resp handler(Req req) { return handlerTemplate(req); }
@Override
public boolean isRequestByProxy() { return false; }
}4. Observer Pattern
4.1 Business Scenario
After a user registers successfully, the system may need to send IM, SMS, email, etc. Adding new notification types should not modify the registration method, adhering to the Open/Closed Principle.
4.2 Observer Pattern Definition
The pattern defines a one‑to‑many dependency: when the subject changes state, all observers are notified and can react.
4.3 Using Observer Pattern
Implementation steps:
Define an observable class.
Define an observer interface.
Implement concrete observers (IM, SMS, Email).
Optionally use an event bus such as Guava for decoupled publishing.
4.3.1 Observable and Observers
public class Observable {
private List<Observer> observers = new ArrayList<>();
private int state;
public int getState() { return state; }
public void setState(int state) { this.state = state; notifyAllObservers(); }
public void addObserver(Observer observer) { observers.add(observer); }
public void removeObserver(Observer observer) { observers.remove(observer); }
public void notifyAllObservers() {
for (Observer o : observers) { o.doEvent(); }
}
}
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.2 EventBus (Guava) 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 event) {
System.out.println("Send IM: " + event.getImNo());
System.out.println("Send SMS: " + event.getMobileNo());
System.out.println("Send Email: " + event.getEmailNo());
}
}
public class NotifyEvent {
private String mobileNo;
private String emailNo;
private String imNo;
public NotifyEvent(String mobileNo, String emailNo, String imNo) {
this.mobileNo = mobileNo;
this.emailNo = emailNo;
this.imNo = imNo;
}
public String getMobileNo() { return mobileNo; }
public String getEmailNo() { return emailNo; }
public String getImNo() { return imNo; }
}
public class EventBusDemoTest {
public static void main(String[] args) {
EventListener listener = new EventListener();
EventBusCenter.register(listener);
EventBusCenter.post(new NotifyEvent("13372817283", "[email protected]", "666"));
}
}5. Factory Pattern
5.1 Business Scenario
Factory pattern is often combined with Strategy to replace long if…else or switch statements when creating objects based on type.
5.2 Using Factory Pattern
Define a factory interface.
Implement concrete factories for each type.
Use the factory to obtain the appropriate object.
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 Factory Usage
IFileResolveFactory factory;
if (fileType.equals("A")) { factory = new AFileResolve(); }
else if (fileType.equals("B")) { factory = new BFileResolve(); }
else { factory = new DefaultFileResolve(); }
factory.resolve();6. Singleton Pattern
6.1 Business Scenario
The Singleton pattern ensures a class has only one instance and provides a global access point. Commonly used for I/O, database connections, etc.
6.2 Classic Implementations
Lazy (lazy‑initialization) singleton.
Eager (hungry) singleton.
Double‑checked locking.
Static inner class.
Enum singleton.
6.2.1 Lazy Singleton
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) { instance = new LazySingleton(); }
return instance;
}
}6.2.2 Eager Singleton
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton 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; } }Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
