Common Design Patterns in Java: Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton
This article introduces several classic Java design patterns—including Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton—explaining their business scenarios, core definitions, and providing complete code examples to demonstrate how each pattern improves code maintainability, extensibility, and adherence to SOLID principles.
1. Strategy Pattern
Business scenario: a big‑data system pushes files of different types that require different parsing logic. A naive implementation uses a long if...else chain, which becomes bulky, hard to maintain, and violates the Open/Closed and Single Responsibility principles.
if(type=="A"){
// parse as A
}
else if(type=="B"){
// parse as B
}
else{
// default parsing
}Solution: define a IFileStrategy interface and concrete strategy classes for each file type, then let Spring inject them into a map for runtime selection.
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); }
} @Component
public class StrategyUseService implements ApplicationContextAware {
private Map
iFileStrategyMap = new ConcurrentHashMap<>();
public void resolveFile(FileTypeResolveEnum enumVal, Object param) {
IFileStrategy strategy = iFileStrategyMap.get(enumVal);
if (strategy != null) { strategy.resolve(param); }
}
@Override
public void setApplicationContext(ApplicationContext ctx) {
ctx.getBeansOfType(IFileStrategy.class).values()
.forEach(s -> iFileStrategyMap.put(s.gainFileType(), s));
}
}2. Chain of Responsibility Pattern
Business scenario: an order‑processing method performs null‑check, security check, blacklist check, and rule interception, each implemented by throwing exceptions. This mixes control flow with error handling and makes the code hard to extend.
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"); }
}
}Solution: define an abstract handler with a reference to the next handler and a filter method that delegates to the concrete handler and then to the next one.
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);
}Concrete handlers (parameter check, security check, blacklist check, rule check) extend AbstractHandler . A Spring component assembles the chain at startup.
@Component("ChainPatternDemo")
public class ChainPatternDemo {
@Autowired private List
abstractHandleList;
private AbstractHandler head;
@PostConstruct
public void initializeChainFilter(){
for (int i = 0; i < abstractHandleList.size(); i++) {
if (i == 0) { head = abstractHandleList.get(0); }
else { abstractHandleList.get(i-1).setNextHandler(abstractHandleList.get(i)); }
}
}
public Response exec(Request req, Response res){ head.filter(req, res); return res; }
}3. Template Method Pattern
Business scenario: different merchants (A, B) need to execute a fixed sequence of steps—query merchant info, sign request, send HTTP request (proxy or direct), and verify response—but the HTTP transport varies per merchant, leading to duplicated code.
abstract class AbstractMerchantService {
abstract void queryMerchantInfo();
abstract void signature();
abstract void httpRequest();
abstract void verifySignature();
// template method
public void handleTemplate(Request req){
queryMerchantInfo();
signature();
httpRequest();
verifySignature();
}
abstract boolean isRequestByProxy();
}Concrete services implement the abstract methods and decide whether to use a proxy.
class CompanyAServiceImpl extends AbstractMerchantService {
@Override boolean isRequestByProxy(){ return true; }
// other abstract methods implemented here
} class CompanyBServiceImpl extends AbstractMerchantService {
@Override boolean isRequestByProxy(){ return false; }
// other abstract methods implemented here
}4. Observer Pattern
Business scenario: after a user registers, the system needs to send IM, email, and possibly SMS notifications. Adding a new notification type forces changes to the registration method, violating the Open/Closed principle.
public class Observerable {
private List
observers = new ArrayList<>();
private int state;
public void setState(int state){ this.state = state; notifyAllObservers(state); }
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(); }
}
}Observers implement a common interface.
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"); } }Guava EventBus can be used to simplify the implementation.
public class EventBusCenter {
private static final EventBus eventBus = new EventBus();
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());
}
}5. Factory Pattern
Factory pattern is often combined with strategy to replace long if...else or switch statements when creating objects based on a type.
interface IFileResolveFactory { void resolve(); }
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"); } } // 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();6. Singleton Pattern
Ensures a class has only one instance and provides a global access point. Commonly used for I/O, database connections, etc.
public class LanHanSingleton {
private static LanHanSingleton instance;
private LanHanSingleton() {}
public static LanHanSingleton getInstance(){
if(instance == null){ instance = new LanHanSingleton(); }
return instance;
}
}Other classic implementations include eager initialization, double‑checked locking, static inner‑class holder, and enum singleton.
public class EHanSingleton { private static final EHanSingleton INSTANCE = new EHanSingleton(); private EHanSingleton() {} public static EHanSingleton getInstance(){ return INSTANCE; } } public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance(){
if(instance == null){
synchronized(DoubleCheckSingleton.class){
if(instance == null){ instance = new DoubleCheckSingleton(); }
}
}
return instance;
}
} public class InnerClassSingleton {
private static class Holder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); }
private InnerClassSingleton() {}
public static InnerClassSingleton getInstance(){ return Holder.INSTANCE; }
} public enum SingletonEnum { INSTANCE; public SingletonEnum getInstance(){ return INSTANCE; } }These patterns improve code clarity, reduce duplication, and make systems easier to extend and maintain.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.