Master Design Patterns: Chain, Template, Pub/Sub & Strategy in Java
This article explains the concepts, advantages, disadvantages, and real-world use cases of four essential design patterns—Chain of Responsibility, Template Method, Publish‑Subscribe, and Strategy—providing detailed Java and Spring code examples to help developers apply them effectively.
Introduction
This article shares the author’s practical experience with commonly used design patterns, covering four patterns: Chain of Responsibility, Template Method, Publish‑Subscribe, and Strategy.
Chain of Responsibility Template Method Publish‑Subscribe Strategy
Three Major Categories of Design Patterns
Creational Patterns : Abstract the object creation process, separating object creation from usage. Includes Factory Method, Abstract Factory, Singleton, Builder, and Prototype.
Structural Patterns : Focus on object composition and dependencies, combining classes or objects to form larger structures. Includes Adapter, Decorator, Proxy, Facade, Bridge, Composite, and Flyweight.
Behavioral Patterns : Concerned with object behavior and responsibility distribution. Includes Strategy, Template Method, Observer, Iterator, Chain of Responsibility, Command, Memento, State, Visitor, Mediator, and Interpreter.
Six SOLID Principles
Single Responsibility Principle : A class should have only one reason to change.
Open/Closed Principle : Software entities should be open for extension but closed for modification.
Liskov Substitution Principle : Subtypes must be substitutable for their base types without affecting correctness.
Interface Segregation Principle : Clients should not be forced to depend on interfaces they do not use.
Dependency Inversion Principle : High‑level modules should not depend on low‑level modules; both should depend on abstractions.
Law of Demeter : Talk only to your immediate friends; minimize knowledge of distant classes.
Benefits of Design Patterns
Enable code reuse and reduce duplication, improving maintainability.
Provide a common vocabulary for developers, facilitating communication.
Support the Open/Closed principle, allowing new features without altering existing structure.
Make refactoring easier and reduce the likelihood of errors.
Offer a robust architecture that adapts to change.
Save development time and increase efficiency.
1. Chain of Responsibility Pattern
Overview
The Chain of Responsibility pattern decouples the sender of a request from its receivers, allowing multiple objects a chance to handle the request as it passes along a chain.
Key roles:
Handler (Abstract Handler) : Defines the request handling interface and holds a reference to the next handler.
ConcreteHandler : Implements handling logic and forwards the request if it cannot process it.
Client : Creates handler objects and assembles the chain.
Advantages
Decouples request sender and receiver, enhancing flexibility.
Improves maintainability by allowing each handler to focus on its own logic.
Enables dynamic addition or removal of handlers.
Disadvantages
Increases system complexity and may affect performance if the chain is long.
Application Scenarios
Request processing pipelines.
Logging systems with multiple log levels.
Authentication and authorization checks.
Data filtering and transformation.
Error and exception handling.
Java Code Example (Interface‑Based)
public interface Handler {
void handleRequest(Request request);
}
public class Request {
private String type;
// getters and setters omitted
} public class ConcreteHandlerA implements Handler {
private Handler successor;
public void setSuccessor(Handler successor) { this.successor = successor; }
public void handleRequest(Request request) {
if (request.getType().equals("A")) {
// handle request
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
public class ConcreteHandlerB implements Handler {
private Handler successor;
public void setSuccessor(Handler successor) { this.successor = successor; }
public void handleRequest(Request request) {
if (request.getType().equals("B")) {
// handle request
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
public class ConcreteHandlerC implements Handler {
private Handler successor;
public void setSuccessor(Handler successor) { this.successor = successor; }
public void handleRequest(Request request) {
if (request.getType().equals("C")) {
// handle request
} else if (successor != null) {
successor.handleRequest(request);
}
}
} public class Client {
public static void main(String[] args) {
Handler handlerA = new ConcreteHandlerA();
Handler handlerB = new ConcreteHandlerB();
Handler handlerC = new ConcreteHandlerC();
handlerA.setSuccessor(handlerB);
handlerB.setSuccessor(handlerC);
Request request = new Request("A");
handlerA.handleRequest(request);
}
}Spring‑Based Implementation
Using Spring components to build an order processing chain:
public class Order {
private String orderNumber;
private String paymentMethod;
private boolean stockAvailability;
private String shippingAddress;
// getters and setters omitted
} public abstract class OrderHandler {
public abstract void handleOrder(Order order);
} @Component
public class CheckOrderHandler extends OrderHandler {
public void handleOrder(Order order) {
if (StringUtils.isBlank(order.getOrderNumber())) {
throw new RuntimeException("Order number cannot be empty");
}
if (order.getPrice().compareTo(BigDecimal.ONE) <= 0) {
throw new RuntimeException("Order amount must be greater than 0");
}
if (StringUtils.isBlank(order.getShippingAddress())) {
throw new RuntimeException("Shipping address cannot be empty");
}
System.out.println("Order parameters validated");
}
}
@Component
public class StockHandler extends OrderHandler {
public void handleOrder(Order order) {
if (!order.isStockAvailability()) {
throw new RuntimeException("Insufficient stock");
}
System.out.println("Stock deducted successfully");
}
}
@Component
public class AliPaymentHandler extends OrderHandler {
public void handleOrder(Order order) {
if (!order.getPaymentMethod().equals("Alipay")) {
throw new RuntimeException("Only Alipay is supported");
}
System.out.println("Alipay pre‑order successful");
}
} @Component
public class BuildOrderChain {
@Autowired private AliPaymentHandler aliPaymentHandler;
@Autowired private CheckOrderHandler checkOrderHandler;
@Autowired private StockHandler stockHandler;
private List<OrderHandler> list = new ArrayList<>();
@PostConstruct
public void init() {
list.add(checkOrderHandler);
list.add(stockHandler);
list.add(aliPaymentHandler);
}
public void doFilter(Order order) {
for (OrderHandler handler : list) {
handler.handleOrder(order);
}
}
} @SpringBootTest
@RunWith(SpringRunner.class)
public class OrderChainTest {
@Autowired private BuildOrderChain buildOrderChain;
@Test
public void test() {
Order order = new Order("123456", "Alipay", true, "Changsha", new BigDecimal("100"));
buildOrderChain.doFilter(order);
}
}In summary, the Chain of Responsibility pattern is suitable when multiple processing steps exist, each step has independent logic, and the processing sequence needs to be flexible and extensible.
2. Template Method Pattern
Overview
The Template Method pattern defines the skeleton of an algorithm in a base class while allowing subclasses to override specific steps without changing the overall structure.
Key Components
Abstract Class : Declares the template method and abstract steps.
Template Method : The concrete algorithm defined in the abstract class.
Concrete Subclass : Implements the abstract steps.
Advantages
Encapsulates invariant parts while allowing variation of specific steps.
Avoids code duplication by providing common implementations.
Facilitates easy extension of new behavior.
Disadvantages
May increase the number of classes.
High coupling between template and subclasses.
Application Scenarios
Framework development (e.g., Spring’s JdbcTemplate, RestTemplate).
Business logic where common workflow steps are shared.
Java Code Example (SMS Sending)
public abstract class SmsTemplate {
public void send(String mobile) throws Exception {
System.out.println("Check if SMS was sent within a minute, mobile:" + mobile);
if (checkUserReceiveInOneMinute(mobile)) {
throw new Exception("Please wait 1 minute before retrying");
}
String code = genCode();
if (manufacturer(mobile, code)) {
System.out.println("SMS sent successfully, mobile:" + mobile + ", code=" + code);
save2redis(mobile, code);
}
}
protected abstract boolean manufacturer(String mobile, String code);
public boolean checkUserReceiveInOneMinute(String mobile) { return false; }
public String genCode() { return "123456"; }
public void save2redis(String mobile, String code) { /* ... */ }
} public class AliyunSmsSend extends SmsTemplate {
@Override
protected boolean manufacturer(String mobile, String code) {
System.out.println("Read Aliyun SMS config");
System.out.println("Create Aliyun SMS client");
System.out.println("Aliyun SMS sent successfully");
return true;
}
}
public class TencentSmsSend extends SmsTemplate {
@Override
protected boolean manufacturer(String mobile, String code) {
System.out.println("Read Tencent SMS config");
System.out.println("Create Tencent SMS client");
System.out.println("Tencent SMS sent successfully");
return true;
}
} public class Main {
public static void main(String[] args) throws Exception {
SmsTemplate sms1 = new AliyunSmsSend();
sms1.send("13333333333");
System.out.println("---------------------------");
SmsTemplate sms2 = new TencentSmsSend();
sms2.send("13333333333");
}
}The Template Method pattern separates invariant workflow steps from variable ones, allowing flexible extension while keeping core logic stable.
3. Publish‑Subscribe Pattern
Overview
The Publish‑Subscribe pattern decouples publishers and subscribers, enabling asynchronous message distribution. It is a form of the Observer pattern widely used in event‑driven architectures.
Key Roles
Subject (Publisher) : Maintains a list of observers and provides methods to register/unregister and notify.
Observer (Subscriber) : Defines an update method to receive messages.
Advantages
Loose coupling between components.
Excellent extensibility; new subscribers can be added without changing publishers.
Supports asynchronous communication.
Disadvantages
Increased system complexity due to message brokers.
Performance overhead from message routing.
Reliance on middleware availability.
Harder to trace data flow.
Not suitable for synchronous operations.
Application Scenarios
Real‑time messaging systems (chat, notifications).
Event‑driven frameworks.
Distributed message queues (Kafka, RabbitMQ, Redis).
Push services (e.g., newsletters, social media updates).
Java Code Example
interface Subscriber {
void update(String message);
}
class Publisher {
private Map<String, List<Subscriber>> subscribers = new HashMap<>();
public void subscribe(String topic, Subscriber subscriber) {
subscribers.computeIfAbsent(topic, k -> new ArrayList<>()).add(subscriber);
}
public void unsubscribe(String topic, Subscriber subscriber) {
List<Subscriber> list = subscribers.get(topic);
if (list != null) list.remove(subscriber);
}
public void publish(String topic, String message) {
List<Subscriber> list = subscribers.get(topic);
if (list != null) {
for (Subscriber s : list) s.update(message);
}
}
}
class EmailSubscriber implements Subscriber {
private String email;
public EmailSubscriber(String email) { this.email = email; }
public void update(String message) {
System.out.println("Send email to " + email + ": " + message);
}
}
class SMSSubscriber implements Subscriber {
private String phoneNumber;
public SMSSubscriber(String phoneNumber) { this.phoneNumber = phoneNumber; }
public void update(String message) {
System.out.println("Send SMS to " + phoneNumber + ": " + message);
}
}
public class Main {
public static void main(String[] args) {
Publisher publisher = new Publisher();
Subscriber emailSub = new EmailSubscriber("[email protected]");
Subscriber smsSub = new SMSSubscriber("1234567890");
publisher.subscribe("news", emailSub);
publisher.subscribe("news", smsSub);
publisher.publish("news", "New message 1");
publisher.unsubscribe("news", smsSub);
publisher.publish("news", "New message 2");
}
}Spring implements the Publish‑Subscribe pattern through events, listeners, and publishers, allowing clean decoupling of business logic.
4. Strategy Pattern
Overview
The Strategy pattern defines a family of interchangeable algorithms, encapsulating each one and making them selectable at runtime.
Key Roles
Strategy Interface : Declares the operation method.
Concrete Strategies : Implement specific algorithms (e.g., Add, Subtract, Multiply, Divide).
Context : Holds a reference to a Strategy and delegates execution.
Advantages
Avoids complex conditional statements.
Promotes code reuse across similar algorithms.
Supports Open/Closed principle.
Enables dynamic algorithm switching.
Disadvantages
Clients must be aware of all strategy classes.
May increase the number of classes.
Application Scenarios
When multiple classes differ only in behavior.
When an algorithm needs to be selected at runtime.
When many conditional branches would otherwise be required.
Java Code Example (Calculator)
public interface Strategy {
int doOperation(int num1, int num2);
}
public class AddStrategy implements Strategy {
public int doOperation(int a, int b) { return a + b; }
}
public class SubtractStrategy implements Strategy {
public int doOperation(int a, int b) { return a - b; }
}
public class MultiplyStrategy implements Strategy {
public int doOperation(int a, int b) { return a * b; }
}
public class DivideStrategy implements Strategy {
public int doOperation(int a, int b) {
if (b == 0) throw new IllegalArgumentException("Divisor cannot be zero");
return a / b;
}
}
public class Context {
private Strategy strategy;
public Context(Strategy strategy) { this.strategy = strategy; }
public int executeStrategy(int a, int b) { return strategy.doOperation(a, b); }
}
public class StrategyTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter first number: ");
int n1 = scanner.nextInt();
System.out.print("Enter operator (+,-,*,/): ");
String op = scanner.next();
System.out.print("Enter second number: ");
int n2 = scanner.nextInt();
scanner.close();
Strategy strategy;
switch (op) {
case "+": strategy = new AddStrategy(); break;
case "-": strategy = new SubtractStrategy(); break;
case "*": strategy = new MultiplyStrategy(); break;
case "/": strategy = new DivideStrategy(); break;
default: System.out.println("Invalid operator"); return;
}
Context ctx = new Context(strategy);
int result = ctx.executeStrategy(n1, n2);
System.out.println(n1 + " " + op + " " + n2 + " = " + result);
}
}The Strategy pattern provides a clean way to swap algorithms without altering client code, enhancing flexibility and maintainability.
Conclusion
The four design patterns—Chain of Responsibility, Template Method, Publish‑Subscribe, and Strategy—each address specific architectural challenges, improve code reuse, and promote loose coupling. Understanding their structures, trade‑offs, and practical implementations equips developers to build more maintainable and extensible systems.
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.
