Refactoring 7 Duplicate Notification Services Using Template Method Pattern in Spring Boot
The article demonstrates how to correctly apply the DRY principle by refactoring seven duplicated notification service classes in Spring Boot 3.5.0 using the template method pattern and a NotificationMessage value object, consolidating 40 lines of repeated validation, logging, and error handling into a single abstract base class.
Problem: Copy-Paste Duplication Across Notification Services
The article begins with a clean EmailNotificationService that validates order and user, logs, builds a message, sends via an email client, and handles errors. When SMS notifications are required, the class is copied and slightly modified — changing the contact field to phone number, the client to SmsClient, and log messages. This pattern repeats for push, WeChat, in-app, and other channels, resulting in seven services each containing roughly 40 lines of nearly identical code.
The duplication is not merely syntactic; it represents the same business knowledge: "an order notification requires a valid order and a valid user with a valid contact method." When this rule changes, all seven copies must be updated, and missing one leads to inconsistent production behavior.
Understanding DRY Correctly
DRY (Don't Repeat Yourself) is often misunderstood as avoiding duplicate lines of code. The authors (Andy Hunt and Dave Thomas) define it as ensuring every piece of knowledge has a single, authoritative, unambiguous representation. If two methods share a three-line validation but represent different business concepts, extracting them creates harmful coupling. However, when the same validation appears in seven notification services and embodies the same rule, each copy is redundant.
Refactoring Step 1: Extract a Common Abstract Base Class
The invariant parts across all services are:
Validate order and user
Validate channel-specific contact method
Log send attempt
Build order message
Send via channel
Log success or failure
Wrap errors in a domain exception
The variant parts are:
Specific contact field (email, phone, device token)
Specific send mechanism (email client, SMS client, push client)
Minor log message details (channel name)
This is a classic Template Method pattern scenario. The overall algorithm is fixed; only the "validate contact" and "deliver" steps differ.
Abstract Base Class
public abstract class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
public void sendOrderNotification(Order order, User user) {
validateOrderAndUser(order, user);
validateContactMethod(user);
log.info("正在为用户 {} 发送订单 {} 的通知", user.getId(), order.getId());
String message = buildOrderMessage(order, user);
try {
deliver(user, message);
log.info("订单 {} 的{}发送成功", order.getId(), getChannelName());
} catch (Exception e) {
log.error("订单 {} 的{}发送失败", order.getId(), getChannelName(), e);
throw new NotificationException(getChannelName() + "发送失败", e);
}
}
private void validateOrderAndUser(Order order, User user) {
if (order == null) {
throw new IllegalArgumentException("订单不能为空");
}
if (user == null) {
throw new IllegalArgumentException("用户不能为空");
}
}
private String buildOrderMessage(Order order, User user) {
return String.format("您好 %s,您的订单 #%s 已收到,总金额:%s",
user.getName(), order.getId(), order.getTotalAmount());
}
protected abstract void validateContactMethod(User user);
protected abstract void deliver(User user, String message);
protected abstract String getChannelName();
}Concrete Implementations
Each concrete service now implements only three small methods:
public class EmailNotificationService extends NotificationService {
private final EmailClient emailClient;
public EmailNotificationService(EmailClient emailClient) {
this.emailClient = emailClient;
}
@Override
protected void validateContactMethod(User user) {
if (user.getEmail() == null || user.getEmail().isBlank()) {
throw new IllegalArgumentException("用户邮箱不能为空");
}
}
@Override
protected void deliver(User user, String message) {
emailClient.send(user.getEmail(), "您的订单已提交", message);
}
@Override
protected String getChannelName() {
return "email";
}
} public class SmsNotificationService extends NotificationService {
private final SmsClient smsClient;
public SmsNotificationService(SmsClient smsClient) {
this.smsClient = smsClient;
}
@Override
protected void validateContactMethod(User user) {
if (user.getPhoneNumber() == null || user.getPhoneNumber().isBlank()) {
throw new IllegalArgumentException("用户手机号不能为空");
}
}
@Override
protected void deliver(User user, String message) {
smsClient.send(user.getPhoneNumber(), message);
}
@Override
protected String getChannelName() {
return "sms";
}
}Adding a new channel now requires implementing only three methods instead of copying 40 lines.
Refactoring Step 2: Introduce a NotificationMessage Value Object
The buildOrderMessage method returns a plain String, but different channels need different structures: email has a subject, SMS has only a body, push has both title and body. Returning a single string forces each channel to parse the body to extract a title.
A NotificationMessage record encapsulates title and body:
public record NotificationMessage(String title, String body) {
public static NotificationMessage orderPlaced(Order order, User user) {
String body = "您好 %s, 您的订单 #%s 已收到,总金额: %s".formatted(
user.getName(), order.getId(), order.getTotalAmount());
return new NotificationMessage("Order Confirmation", body);
}
}Updated Abstract Class
public abstract class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
public void sendOrderNotification(Order order, User user) {
validateOrderAndUser(order, user);
validateContactMethod(user);
log.info("正在为用户 {} 发送订单 {} 的通知", user.getId(), order.getId());
NotificationMessage message = NotificationMessage.orderPlaced(order, user);
try {
deliver(user, message);
log.info("订单 {} 的{}发送成功", order.getId(), getChannelName());
} catch (Exception e) {
log.error("订单 {} 的{}发送失败", order.getId(), getChannelName(), e);
throw new NotificationException(getChannelName() + "发送失败", e);
}
}
// ... validateOrderAndUser, abstract methods unchanged
protected abstract void deliver(User user, NotificationMessage message);
}Now each channel receives a structured message and can use title, body, or both as appropriate.
Conclusion
By applying the DRY principle correctly — focusing on single source of knowledge rather than line-level duplication — the article shows how to eliminate 40 lines of repeated code across seven services using the Template Method pattern and a domain-specific value object. The result is a maintainable, extensible design where adding a new notification channel requires minimal, focused changes.
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.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.
