Unlock Cleaner Code: Master 6 Essential Design Patterns for Java Projects
This article introduces six classic design patterns—Strategy, Chain of Responsibility, Template Method, Observer, Factory, and Singleton—explaining their real‑world business scenarios, core principles, and providing clean Java implementations to help developers write more maintainable and extensible code.
Introduction
Hello, I am Su San . In daily development we often write straightforward pipeline code, but to make coding enjoyable we can use design patterns to optimize business logic. Below are the design patterns I frequently use.
1. Strategy Pattern
1.1 Business Scenario
When a big‑data system pushes files of different types, each type requires a different parsing logic. A naïve implementation uses multiple if...else branches, which quickly becomes bloated and violates the Open/Closed and Single Responsibility principles.
if(type == "A") {
// parse A format
} else if(type == "B") {
// parse B format
} else {
// default parsing
}1.2 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 personality types: you need different strategies—movie, snack, shopping—to win their hearts.
1.3 Usage
An interface or abstract class with two methods: one to identify the file type, another to perform the parsing.
Concrete strategy classes implement the interface.
A service loads all strategies into a map (using Spring) and selects the appropriate one at runtime.
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) { /* A‑type logic */ }
}
@Component
public class BFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_B_RESOLVE; }
@Override
public void resolve(Object objectParam) { /* B‑type logic */ }
}
@Component
public class DefaultFileResolve implements IFileStrategy {
@Override
public FileTypeResolveEnum gainFileType() { return FileTypeResolveEnum.File_DEFAULT_RESOLVE; }
@Override
public void resolve(Object objectParam) { /* default logic */ }
}
@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 strategy = iFileStrategyMap.get(type);
if (strategy != null) {
strategy.resolve(param);
}
}
}2. Chain of Responsibility Pattern
2.1 Business Scenario
Order processing often involves multiple 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 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");
}
}
}2.2 Definition
The pattern creates a chain of handler objects, each having a chance to process a request and optionally pass it to the next handler, thus decoupling sender and receiver.
Think of passing a love note through a row of classmates until it reaches the intended person.
2.3 Usage
An abstract handler with a reference to the next handler and a filter method.
Concrete handlers implement the specific check logic.
A demo component wires the handlers into a list and links them.
public abstract class AbstractHandler {
private AbstractHandler nextHandler;
public void setNextHandler(AbstractHandler next) { this.nextHandler = next; }
public void filter(Request req, Response resp) {
doFilter(req, resp);
if (nextHandler != null) {
nextHandler.filter(req, resp);
}
}
protected abstract void doFilter(Request req, Response resp);
}
@Component
@Order(1)
public class CheckParamFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request req, Response resp) { System.out.println("Non‑null param check"); }
}
@Component
@Order(2)
public class CheckSecurityFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request req, Response resp) { System.out.println("Security check"); }
}
@Component
@Order(3)
public class CheckBlackFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request req, Response resp) { System.out.println("Blacklist check"); }
}
@Component
@Order(4)
public class CheckRuleFilterObject extends AbstractHandler {
@Override
protected void doFilter(Request req, Response resp) { System.out.println("Rule check"); }
}
@Component("ChainPatternDemo")
public class ChainPatternDemo {
@Autowired
private List<AbstractHandler> handlers;
private AbstractHandler head;
@PostConstruct
public void initializeChain() {
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 void exec(Request req, Response resp) { head.filter(req, resp); }
}3. Template Method Pattern
3.1 Business Scenario
Different merchants require similar request processing steps (query merchant, sign request, send HTTP, verify response) but with variations such as proxy vs. direct HTTP.
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.
Dating analogy: the overall steps (hand‑hold, hug, kiss) stay the same, but you can choose left or right hand.
3.3 Usage
abstract class AbstractMerchantService {
// Template method
public void handleTemplate(Request req) {
queryMerchantInfo();
signature();
httpRequest();
verifySignature();
}
protected abstract void queryMerchantInfo();
protected abstract void signature();
protected abstract void httpRequest();
protected abstract void verifySignature();
protected abstract boolean isRequestByProxy();
}
class CompanyAServiceImpl extends AbstractMerchantService {
@Override protected void queryMerchantInfo() { /* A logic */ }
@Override protected void signature() { /* A logic */ }
@Override protected void httpRequest() { /* proxy request */ }
@Override protected void verifySignature() { /* A logic */ }
@Override protected boolean isRequestByProxy() { return true; }
}
class CompanyBServiceImpl extends AbstractMerchantService {
@Override protected void queryMerchantInfo() { /* B logic */ }
@Override protected void signature() { /* B logic */ }
@Override protected void httpRequest() { /* direct request */ }
@Override protected void verifySignature() { /* B logic */ }
@Override protected boolean isRequestByProxy() { return false; }
}4. Observer Pattern
4.1 Business Scenario
After a user registers, multiple notifications (IM, SMS, email) need to be sent. Adding a new notification channel should not modify existing registration code.
4.2 Definition
The Observer defines a one‑to‑many dependency so that when one object changes state, all its dependents are notified.
4.3 Usage
Observable class holds a list of observers.
Observer interface with a doEvent method.
Concrete observers implement specific notification logic.
Guava EventBus can be used as a ready‑made observable.
public class Observable {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer o) { observers.add(o); }
public void removeObserver(Observer o) { observers.remove(o); }
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"); } }
// Guava EventBus example
public class EventBusCenter {
private static final EventBus eventBus = new EventBus();
public static void register(Object obj) { eventBus.register(obj); }
public static void unregister(Object obj) { eventBus.unregister(obj); }
public static void post(Object event) { eventBus.post(event); }
}
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 final String mobileNo, emailNo, imNo;
public NotifyEvent(String mobile, String email, String im) { this.mobileNo = mobile; this.emailNo = email; this.imNo = im; }
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 helps replace long if…else or switch statements when creating objects based on file type.
5.2 Definition & Usage
Factory interface defines a creation method.
Concrete factories implement the interface to create specific objects.
Client code uses the factory to obtain instances without knowing concrete classes.
interface IFileResolveFactory { void resolve(); }
class AFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("File A type resolve"); } }
class BFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("File B type resolve"); } }
class DefaultFileResolve implements IFileResolveFactory { public void resolve() { System.out.println("Default file type resolve"); } }
public class FactoryDemo {
public static void main(String[] args) {
IFileResolveFactory factory;
String fileType = "A"; // example
if ("A".equals(fileType)) {
factory = new AFileResolve();
} else if ("B".equals(fileType)) {
factory = new BFileResolve();
} else {
factory = new DefaultFileResolve();
}
factory.resolve();
}
}6. Singleton Pattern
6.1 Business Scenario
Singleton ensures a class has only one instance and provides a global access point, useful for I/O, database connections, etc.
6.2 Classic Implementations
Lazy (lazy‑initialization) singleton.
Eager (hungry) singleton.
Double‑checked locking.
Static inner‑class holder.
Enum singleton.
// Lazy (lazy‑hand) 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 static class Holder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); }
private InnerClassSingleton() {}
public static InnerClassSingleton getInstance() { return Holder.INSTANCE; }
}
// Enum singleton
public enum EnumSingleton {
INSTANCE;
public void doSomething() { /* ... */ }
}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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.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.
