Master Java Design Patterns: Strategy, Chain, Template, Observer, Factory & Singleton
This article explains the core concepts, typical business scenarios, definitions, and concrete Java implementations of six fundamental design patterns—Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton—showing how they improve code maintainability, extensibility, and adherence to SOLID principles.
1. Strategy Pattern
Business scenario
When a large‑data system pushes files of different types, using multiple
if...elsebranches leads to bloated, hard‑to‑maintain code that violates the Open/Closed and Single Responsibility principles.
Encapsulating each parsing logic behind a common interface and selecting the appropriate implementation at runtime solves this problem.
<code>if(type=="A"){ /* parse A */ }
else if(type=="B"){ /* parse B */ }
else{ /* default parse */ }</code>Definition
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from clients.
Implementation
An interface
IFileStrategywith methods
gainFileType()and
resolve(Object).
Concrete strategies
AFileResolve,
BFileResolve, and
DefaultFileResolveimplement the interface.
A service loads all strategy beans into a map and invokes the appropriate one based on the file type.
<code>public interface IFileStrategy {
FileTypeResolveEnum gainFileType();
void resolve(Object objectParam);
}
@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: {}", objectParam); }
}
// BFileResolve and DefaultFileResolve are similar</code> <code>@Component
public class StrategyUseService implements ApplicationContextAware {
private Map<FileTypeResolveEnum, IFileStrategy> iFileStrategyMap = new ConcurrentHashMap<>();
@Override public void setApplicationContext(ApplicationContext ctx) {
ctx.getBeansOfType(IFileStrategy.class).values()
.forEach(s -> iFileStrategyMap.put(s.gainFileType(), s));
}
public void resolveFile(FileTypeResolveEnum type, Object param) {
IFileStrategy s = iFileStrategyMap.get(type);
if(s != null) s.resolve(param);
}
}</code>2. Chain of Responsibility Pattern
Business scenario
Order processing often requires sequential checks such as parameter validation, security verification, blacklist filtering, and rule enforcement. Using exceptions for flow control makes the code hard to extend and violates best practices.
Definition
The pattern builds a chain of handler objects where each handler decides whether to process the request or pass it to the next handler.
Implementation
An abstract class
AbstractHandlerholds a reference to the next handler and defines the template method
filter.
Concrete handlers (
CheckParamFilterObject,
CheckSecurityFilterObject,
CheckBlackFilterObject,
CheckRuleFilterObject) implement
doFilterwith specific logic.
A demo component assembles the chain at startup.
<code>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);
}
protected abstract void doFilter(Request req, Response res);
}
@Component @Order(1)
public class CheckParamFilterObject extends AbstractHandler {
@Override protected void doFilter(Request req, Response res) { System.out.println("Param check"); }
}
// Other handlers are similar, with different @Order values
</code> <code>@Component("ChainPatternDemo")
public class ChainPatternDemo {
@Autowired private List<AbstractHandler> handlers;
private AbstractHandler head;
@PostConstruct
public void init() {
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; }
}
</code>3. Template Method Pattern
Business scenario
Different merchants require slightly different request handling (proxy vs direct HTTP). Duplicating the whole workflow for each merchant violates the Open/Closed principle.
Definition
The pattern defines the skeleton of an algorithm in an abstract class, leaving some steps to subclasses.
Implementation
An abstract class
AbstractMerchantServicedeclares abstract steps and provides a concrete template method
handlerTemplate.
Subclasses
CompanyAServiceImpland
CompanyBServiceImplimplement the variable step
isRequestByProxy.
<code>public abstract class AbstractMerchantService {
public void handlerTemplate(Request req) {
queryMerchantInfo();
signature();
httpRequest();
verifySignature();
}
protected abstract void queryMerchantInfo();
protected abstract void signature();
protected abstract void httpRequest();
protected abstract void verifySignature();
}
public class CompanyAServiceImpl extends AbstractMerchantService {
@Override protected void httpRequest() { /* proxy request */ }
// other methods implemented similarly
}
public class CompanyBServiceImpl extends AbstractMerchantService {
@Override protected void httpRequest() { /* direct request */ }
// other methods implemented similarly
}
</code>4. Observer Pattern
Business scenario
After a user registers, the system may need to send IM messages, emails, SMS, etc. Adding new notification types should not require modifying the registration method.
Definition
The pattern defines a one‑to‑many dependency so that when the subject changes state, all observers are automatically notified.
Implementation
A subject class maintains a list of
Observerinstances and notifies them.
Concrete observers implement
doEvent()to perform specific actions.
Guava’s
EventBuscan be used as a ready‑made event dispatcher.
<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); }
private void notifyAllObservers() { observers.forEach(Observer::doEvent); }
}
public interface Observer { void doEvent(); }
public class IMMessageObserver implements Observer { public void doEvent() { System.out.println("Send IM"); } }
// EmailObserver, MobileObserver are similar
</code> <code>public class EventBusCenter {
private static final EventBus bus = new EventBus();
public static void register(Object o) { bus.register(o); }
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());
}
}
</code>5. Factory Pattern
Business scenario
Creating objects based on file type often results in repetitive
if…elseor
switchstatements. A factory centralises object creation.
Implementation
An interface
IFileResolveFactorydefines a
resolve()method.
Concrete factories (
AFileResolve,
BFileResolve,
DefaultFileResolve) implement the interface.
Client code selects the appropriate factory based on the file type.
<code>interface IFileResolveFactory { void resolve(); }
class AFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("File A parsing"); } }
class BFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("File B parsing"); } }
class DefaultFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("Default parsing"); } }
// Usage
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
Business scenario
Ensuring a class has only one instance is essential for resources such as thread pools, configuration managers, or database connections.
Implementations
Lazy (lazy‑initialised) singleton.
Eager (hungry) singleton.
Double‑checked locking.
Static inner‑class holder.
Enum‑based singleton.
<code>// Lazy singleton
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if(instance == null) instance = new LazySingleton();
return instance;
}
}
// Eager singleton
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() { return INSTANCE; }
}
// Double‑checked locking
public class DCLSingleton {
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if(instance == null) {
synchronized(DCLSingleton.class) {
if(instance == null) instance = new DCLSingleton();
}
}
return instance;
}
}
// Static inner‑class
public class InnerClassSingleton {
private InnerClassSingleton() {}
private static class Holder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); }
public static InnerClassSingleton getInstance() { return Holder.INSTANCE; }
}
// Enum singleton
public enum EnumSingleton { 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.