Mastering Java Design Patterns: From High Cohesion to Low Coupling in Spring
This article explains the core concepts of high cohesion and low coupling, introduces common Java design patterns, demonstrates how Spring applies these patterns such as Singleton, Factory, Proxy, Observer, Chain of Responsibility, and Template Method, and provides practical code examples for real‑world scenarios.
所谓 "解耦",就是代码和代码说分手。
Background
When discussing design patterns, you might wonder: what are they useful for? Even if you haven't consciously used them, they may already be present in your projects. Java has 23 design patterns, but only a few are frequently used in daily work. This chapter focuses on the most common patterns, explaining their concepts, contracts, and practical applications to benefit both interview preparation and real‑world development.
Article Outline
Design Philosophy
For Java developers, a basic programmer's creed can be summarized as:
Expose when needed, hide when needed
Encapsulate when needed
Everything is an object
Code must be robust
We can condense this into two core keywords: High Cohesion and Low Coupling 。
As a child I watched the animated "Nezha" where the hero has three heads and eight arms, symbolizing static and dynamic features. Mapping these ideas: Static features correspond to the three heads and eight arms, while Dynamic features correspond to the countless battles.
The concept of High Cohesion is illustrated above; the next question is what Low Coupling looks like.
If I were to write a myth called "The Legend of Nezha", I would give him three heads and nine arms, but ensure that adding a new weapon never affects the existing eight arms—this is the essence of low coupling, which also leads to robust and extensible code.
The ultimate design principle for patterns is to
encapsulate the changing parts while preserving the stable ones。
Design Principles
Design principles can be grouped into two major categories: Open‑Closed Principle (Liskov substitution, composition over inheritance, dependency inversion)
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, a component can change its behavior without altering its source code.
Single Responsibility (Interface Segregation, Law of Demeter)
A class should have only one responsibility and therefore only one reason to change.
Design Patterns in Spring
Spring extensively uses design patterns to provide powerful and flexible features. Below are several common patterns found in Spring's source code.
1. Singleton Pattern
In Spring, a Bean is singleton by default. The IoC container creates the instance and ensures that each Bean ID is instantiated only once. The class DefaultSingletonBeanRegistry manages singleton Beans.
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// ...
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// ...
@Override
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// ... omitted code
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
// ...
}2. Factory Pattern
Spring uses the factory pattern via BeanFactory and ApplicationContext interfaces to create and manage Bean objects. DefaultListableBeanFactory is the concrete implementation.
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
// ...
@Override
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
return doGetBean(name, requiredType, null, false);
}
@Override
public <T> T getBean(Class<T> requiredType) throws BeansException {
return doGetBean(null, requiredType, null, false);
}
// ... omitted code
}3. Proxy Pattern
Spring AOP is based on the proxy pattern. It creates proxy objects for target beans and weaves aspect logic into them. Core classes are JdkDynamicAopProxy and CglibAopProxy.
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
// ...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ... omitted code
final AdvisedSupport advised = this.advised;
// ...
List<Object> chain = advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// ...
return invokeJoinpointUsingReflection(target, method, args, targetClass, chain);
}
// ...
}4. Observer Pattern (Event Listener)
Spring's event handling mechanism implements the observer pattern. When an event occurs, all registered listeners are notified.
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
// ...
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
// invoke listener
invokeListener(listener, event);
}
}
private void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
} catch (Throwable err) {
errorHandler.handleError(err);
}
} else {
doInvokeListener(listener, event);
}
}
private void doInvokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
} catch (ClassCastException ex) {
// omitted handling
}
}
// ...
}5. Chain of Responsibility
Spring uses the chain of responsibility for request interception via HandlerInterceptor and HandlerInterceptorAdapter. Each interceptor processes the request in order until one returns false.
public interface HandlerInterceptor {
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}
public class CustomInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// custom pre‑handle logic
return true; // continue chain
}
// ... other methods
}6. Template Method
Classes such as JdbcTemplate and HibernateTemplate are classic examples of the template method pattern. They define the skeleton of an algorithm while allowing subclasses to customize specific steps.
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations, BeanFactoryAware {
// ...
public <T> T query(String sql, RowMapper<T> rowMapper) {
return query(sql, new Object[0], rowMapper);
}
public <T> T query(String sql, Object[] args, RowMapper<T> rowMapper) {
return query(sql, args, rowMapper, true);
}
// ... actual SQL execution and result‑set handling omitted
}
public class CustomRowMapper implements RowMapper<MyObject> {
@Override
public MyObject mapRow(ResultSet rs, int rowNum) throws SQLException {
return new MyObject(/* mapping logic */);
}
}Practical Application
Assume the following business requirements:
User login and authentication
Purchase product and create order
Validate order legality
Record request parameters
Send SMS notification after order confirmation
These can be split into two flows: User Process and Order Process.
User Login Process
The login flow often uses interceptors for authentication checks and logging, employing several design patterns.
Scenario 1 – User Authentication
Chain of Responsibility
Interceptors are executed in order; each decides whether to continue.
public interface Interceptor {
boolean intercept(AuthenticationContext context);
}
public class UserValidationInterceptor implements Interceptor {
@Override
public boolean intercept(AuthenticationContext context) {
if (isValidUser(context.getUser())) {
return true;
}
return false;
}
private boolean isValidUser(User user) {
// validation logic
return true;
}
}
public class AuthorizationInterceptor implements Interceptor {
@Override
public boolean intercept(AuthenticationContext context) {
if (isAuthorized(context.getUser(), context.getCredentials())) {
return true;
}
return false;
}
private boolean isAuthorized(User user, Credentials credentials) {
// authorization logic
return true;
}
}
public class InterceptorChain {
private List<Interceptor> interceptors = new ArrayList<>();
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public boolean execute(AuthenticationContext context) {
for (Interceptor interceptor : interceptors) {
if (!interceptor.intercept(context)) {
// stop chain
return false;
}
}
return true;
}
}Scenario 2 – Record User Login Info
Singleton Pattern
Loggers are typically singletons to ensure a single global instance.
Logger logger = LoggerFactory.getLoggerFactoryInstance().getLogger();
logger.log("This is a user log message");Order Process
The core order flow is illustrated below:
Scenario 3 – Order Validation
Factory Pattern
An ICheckOrderService interface defines validation, with two implementations: CountCheckOrder and ParamCheckOrder. A CheckOrderFactory creates the appropriate service.
public interface ICheckOrderService {
boolean checkOrder(Object order);
String getErrorMessage();
}
public class CountCheckOrder implements ICheckOrderService {
@Override
public boolean checkOrder(Object order) {
int quantity = ((Order) order).getQuantity();
return quantity > 0;
}
@Override
public String getErrorMessage() {
return "Purchase quantity must be greater than 0.";
}
}
public class ParamCheckOrder implements ICheckOrderService {
@Override
public boolean checkOrder(Object order) {
// parameter validation logic
return true;
}
@Override
public String getErrorMessage() {
return "Order parameter validation failed.";
}
}
public class CheckOrderFactory {
public static ICheckOrderService createCheckOrderService(String type) {
switch (type) {
case "count":
return new CountCheckOrder();
case "param":
return new ParamCheckOrder();
default:
throw new IllegalArgumentException("Unsupported validation type: " + type);
}
}
}Scenario 4 – SMS Notification
Observer Pattern (Event Listener)
Spring's ApplicationEvent and ApplicationListener are used to publish a PurchaseSuccessEvent and send an SMS.
public class PurchaseSuccessEvent extends ApplicationEvent {
private final String buyerPhoneNumber;
private final String orderId;
public PurchaseSuccessEvent(Object source, String buyerPhoneNumber, String orderId) {
super(source);
this.buyerPhoneNumber = buyerPhoneNumber;
this.orderId = orderId;
}
public String getBuyerPhoneNumber() { return buyerPhoneNumber; }
public String getOrderId() { return orderId; }
} @Component
public class SmsNotificationListener implements ApplicationListener<PurchaseSuccessEvent> {
@Override
public void onApplicationEvent(PurchaseSuccessEvent event) {
String message = "Dear buyer, your order " + event.getOrderId() + " was successful!";
sendSms(event.getBuyerPhoneNumber(), message);
}
private void sendSms(String phoneNumber, String message) {
// SMS sending logic
System.out.println("Sending SMS to " + phoneNumber + ": " + message);
}
} @Configuration
public class AppConfig {
@Bean
public ApplicationEventPublisher applicationEventPublisher() {
return new GenericApplicationContext();
}
} @Service
public class PurchaseService {
private final ApplicationEventPublisher applicationEventPublisher;
@Autowired
public PurchaseService(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void completePurchase(String buyerPhoneNumber, String orderId) {
// business logic for completing purchase
applicationEventPublisher.publishEvent(new PurchaseSuccessEvent(this, buyerPhoneNumber, orderId));
}
}Summary
Use design patterns to encapsulate change while preserving stability.
Benefits: improved extensibility, cleaner and more robust code.
Understanding how open‑source frameworks apply patterns enhances architectural insight.
Avoid over‑design; readability should not be sacrificed for pattern usage.
Think like a builder of a cathedral, not just a stone cutter.
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.
