Last updated on September 4th, 2023
Design Patterns play a role in almost every aspect of the framework from its core. Operationally, Spring itself works using design patterns and supports your patterns in the process. In this post, I will go through Spring use cases where some well-known design patterns are applied. However, it won’t focus on the details implementation of each GOF. I’ll provide a brief overview and highlights of how Spring is using them.
Inversion of Control
IoC is not a Gang of Four. However, it is a core pattern applied in the Spring framework. You can explore the details of the IoC and Dependencies injection through this post.
Factory Pattern
Overview
The Abstract Factory and Factory method pattern belong to the creational design patterns category, which deals with object creation mechanisms.
The primary purposes of Factory patterns are to abstract object creation, and decoupling the client code from the concrete classes. This helps achieve Single Responsibility and Open-Closed Principles.
The main difference between them is the factory method is a method to create objects of a single type, while an abstract factory is an object to create families of objects.
Below is a visualization of the classes and objects participating:
- Factory Method pattern uses inheritance and relies on a subclass to handle the desired object instantiation.
- Abstract Factory pattern, a class delegates the responsibility of object instantiation to another object via composition.
To implement the Abstract Factory/ Factory method: You can find out more in this link or via the GitHub example.
Spring implementation
Spring Framework makes use of the Factory design patterns in several places and ways. The Factory Pattern’s main goal is to abstract object creation and provide a consistent way to instantiate, configure, and manage objects of various types or classes. Spring Framework achieves this goal by utilizing BeanFactory
, ApplicationContext
as factories, despite relying on reflection to instantiate the actual objects.
The Abstract Factory pattern is applied while creating an ApplicationContext
instance based on different configurations, such as XML, Java Config, or annotations, leading to the creation of different ApplicationContext
implementations. The primary abstract factory class for creating ApplicationContext
instances is org.springframework.context.support.AbstractApplicationContext
. Concrete ApplicationContext
implementations such as ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, and AnnotationConfigApplicationContext
providing their implementation of the factory method.
Spring’s Factory Pattern implementation integrates well with Dependency Injection. ApplicationContext
serves as an Inversion of Control
container and it wires beans together by injecting dependencies based on configuration. This approach encourages the separation of concerns. The application logic focuses on using beans, while the factory takes care of bean creation and wiring.
Adapter Pattern
Overview
Short version:
- Purpose: Allow incompatible interfaces to work together
- Use cases: integrate library code into the current code base without changing the code
- Advantages: Decoupling (have adaptor classes without modifying the original code)
- Implementation: Inheritance/ Composition
- Composition: adapter class holds an instance of one interface and implements the other
- Inheritance: The adapter inherits interfaces from both objects at once
Long version:
The Adapter Pattern is a structural design pattern that helps two incompatible interfaces work together by converting one interface into another.
It finds utility in integrating legacy/ third-party code that needs to fit into your code and helps to share functionality on objects to reduce code. It creates a bridge between the app’s interfaces and external code with minimal modifications to the existing codebase.
To implement the Adapter pattern: We can implement via inheritance or composition. You can find out more in this link or via the GitHub example.
- Object adapter: This implementation uses composition. The adapter serves as a wrapper class. This class holds an instance of one interface and implements the other.
- Class adapter: This implementation uses inheritance. The adapter inherits interfaces from both objects.
Basically, there’re 3 roles in the Adapter pattern:
- Target: The desired interface that the client expects
- Adapter: A class that implements the desired interface and adapts the Adaptee
- Adaptee: The underlying class/functionality that needs adaptation.
Spring implementation
The adapter pattern is essential in many Spring modules, as they allow us to plug in the custom implementations while maintaining a consistent interface for integrating different components without changing their code.
The main idea of the Adapter pattern is that Spring often provides multiple interfaces and abstract classes, which could be considered adapters and target classes in some cases. By implementing and extending these, we can create custom behaviour. By doing so, Spring simplifies the integration process, and we focus on application logic without worrying about low-level details. This simplifies complex interactions and provides flexibility in the overall architecture.
In Spring, we can find the Adapter pattern applied in various places. Below are some of them:
In Spring Data JDBC
Another example is JdbcTemplate
in Spring JDBC. Instead of the traditional approach, which handles many tedious tasks, such as creating a connection, and managing resources, this class simplifies the process by abstracting the low-level details and offering a more concise high-level API. JdbcTemplate acts as the Adapter that wraps the underlying JDBC API. We just need to provide an RowMapper
implementation, which is an interface for mapping rows of a ResultSet to the desired objects.
public class PersonRowMapper implements RowMapper<Person> {
@Override
public Person mapRow(ResultSet resultSet, int rowNum) throws SQLException {
Person person = new Person();
person.setId(resultSet.getInt("id"));
person.setName(resultSet.getString("name"));
person.setAge(resultSet.getInt("age"));
return person;
}
}
public class PersonDao {
private JdbcTemplate jdbcTemplate;
public PersonDao(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Person> findAll() {
String sql = "SELECT * FROM people";
return jdbcTemplate.query(sql, new PersonRowMapper());
}
}
- PersonRowMapper is the target interface implementation provided by the developer.
- JdbcTemplate is the adapter that translates method calls on the jdbcTemplate.query() into JDBC API calls.
- ResultSet serves as an adapted, which JdbcTemplate adapts to provide a better interface for working with query results.
In Spring AOP
The Adapter pattern is used internal operations of Spring AOP to integrate with various application components and extend the functionality. An example is AdapterAdvisor
, which serves as an adapter for bridging between different advice types (MethodBeforeAdvice
, AfterReturningAdvice
, …) and their corresponding MethodInterceptor
implementations (MethodBeforeAdviceInterceptor
, AfterReturningAdviceInterceptor
, …). This allows Spring AOP to handle each advice type consistently with the same execution logic and integrate different advice implementations into the proxy generation process.
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
@Override
public boolean supportsAdvice(Advice advice) {
return (advice instanceof MethodBeforeAdvice);
}
@Override
public MethodInterceptor getInterceptor(Advisor advisor) {
MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
return new MethodBeforeAdviceInterceptor(advice);
}
}
During the Spring AOP proxy generation process, it calls the getInterceptor(Advisor advisor)
method when creating the list of MethodInterceptors associated with each method in the target object. The main purpose of this method is to obtain the proper MethodInterceptor instance for a given Advisor
. So that when we call a method, instead of directly invoking the target object’s method, the AOP infrastructure wraps the target method with a chain of MethodInterceptors created from the applied aspects. The MethodInvocation
chain can be thought of as a linked list of MethodInterceptors, each having a reference to the next interceptor in the chain. This executes the aspects in a specific order determined by the configuration of the advisors. Each interceptor in the chain calls the invoke()
method of the next interceptor.
In Spring Security
One example is WebSecurityConfigurerAdapter
Spring Security allows customization of the security configuration. The primary purpose WebSecurityConfigurerAdapter
is to allow us to extend it and override methods to provide custom security configurations, rather than deal directly with low-level components. Its extended class helps to adapt between Spring security default implementation and our custom security configuration, such as HttpSecurity, AuthenticationManagerBuilder, …
And so on, there are many similar ones, so I won’t go into them all.
Proxy Pattern
Overview
Short version:
Long version:
A proxy pattern is a structural design pattern that allows one object – the proxy – to control access to another object. This pattern adds a level of indirection, allowing the application to control the flow of interactions and customize the behaviour of the target object without changing its code.
It comes into play in different situations, such as:
- Controlling the instantiation of costly objects. Proxies implement lazy loading, which postpones the loading of resources until they are required. This can save resources and improve the startup time of an application.
- Giving different access rights to an object. Proxies can implement access controls, enabling the system to validate requests and restrict access to certain features or methods based on permissions.
To implement the Proxy design pattern: create a lightweight object (proxy) that exposes the same interface as the target object. By doing so, the proxy can route client requests to the target object, implement additional behaviour, or decide whether the request should be allowed based on access restrictions.
Proxy Pattern provides a clear separation of concerns by introducing an intermediate layer between the client and the target object, resulting in a more maintainable and testable codebase. Additionally, it can help to improve performance by reducing the need for frequent interactions with the target object.
Spring implementation
Spring Framework is widely known for its use of Proxy design patterns. Since Spring 4, every bean created within the framework has a lightweight proxy object wrapped around it by default.
Spring also allows us to add custom proxies around beans, usually through annotations. Spring AOP leverages the Proxy pattern to add cross-cutting concerns, such as logging, security, or caching to an application without changing the existing code. For a better understanding of AOP, you can refer to my previous post.
Template Pattern
Overview
Short version:
- Use cases: common behaviour among classes, common use to integrate multiple third parties
- Implementation: defines a skeleton of an algorithm in a base class using abstract operations that subclasses override to provide concrete behaviour
- Advantages: avoid code duplication, achieve OCP
Long version:
The Template Method is a behavioral pattern that defines the basic steps of an algorithm or a process within a base class, allowing subclasses to override some of the steps without altering the overall structure of the algorithm. It is used when there’s a common behaviour among classes and can help maintain a consistent structure across different implementations.
To implement the Template design pattern: define a base class with a series of abstract operations that represent the steps of the algorithm. Subclasses then inherit from the base class and override the abstract operations with their concrete implementations.
The Template pattern helps to avoid code duplication by extracting the common behaviour into the base class, promoting code reusability and maintainability. It adheres to the OCP, by allowing the subclasses to provide new concrete implementations of the abstract operations without having to modify the overall algorithm structure in the base class. This makes it easy to add new variations of the algorithm without changing the existing code.
Spring implementation
The Spring uses the Template design patterns for remote calls such as JDBC, JMS, or REST. When dealing with remote calls, there is a set of common behaviours that need to be performed regardless of the specific remote technology being used. These common behaviours include creating and configuring connections, handling errors, dealing with transactions, and closing resources when they are no longer needed.
In Spring, this is achieved through template classes like JdbcTemplate
, JmsTemplate
, and RestTemplate
, which are for structuring the skeleton of the algorithm for performing a specific remote call, abstracting out the common steps and calling abstract methods for the implementation-specific details. By doing so, Spring not only reduces code duplication but also enforces consistency and best practices across different remote call implementations.
Below is a demonstration of the JDBC commands execution flow:
It is demonstrated below implementation in org.springframework.jdbc.core.JdbcTemplate
:
@Override
@Nullable
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
/**
* Callback to execute the query.
*/
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
@Nullable
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
return rse.extractData(rs);
} finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback(), true);
}
In this example, the query()
method contains a series of common steps to execute a SQL query and uses the ResultSetExtractor callback interface to provide variations in the way results are processed
Observer
Overview
Short version:
- Purposes: a change in one subject automatically updates the dependent objects (the observers)
- Use cases:
- The state change in one object needs to be reflected in other objects without keeping the objects tight coupled
- Code needs to be enhanced in future with new observers with minimal changes
- Implementation: an object maintains a list of observers & notification is sent to the observer when a state change occurs in the subject
- Advantages: oose coupling between event publishers and observers
Long version:
The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects, where a change in one subject automatically updates the dependent objects (the observers).
To implement the Observer design pattern: define a Subject (Publisher / Observable) to maintain a list of observers. Observers typically implement a common interface that defines the method called when the subject’s state changes. When a state change occurs in the subject, it iterates through the list of observers and notifies each observer by invoking the designated method.
Here’s a simple example in Java:
public interface Observer {
void update();
}
public class ConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("State change observed");
}
}
public class Subject {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
public void changeState() {
// Change the state of the subject
// Notify observers about the state change
notifyObservers();
}
}
The Observer pattern provides several benefits, including:
- Reduced coupling: By abstracting the dependency between the subject and observers through a common interface, the Observer pattern reduces coupling between objects.
- Dynamic relationships: The Observer pattern allows for dynamic relationships between subjects and observers during runtime. Observers can subscribe or unsubscribe from notifications as needed.
- Better code organization: The Observer pattern promotes the separation of concerns, as the subject focuses on its core logic while delegating the responsibility of notifying observers to the observer objects themselves. This makes the code more modular, maintainable, and easier to understand.
Spring Implementation
The Spring uses the Observer design patterns for many use cases such as ApplicationListener
or JMS Message Listener
.
The Spring Framework ApplicationContext makes use of the Observer pattern through its event-handling mechanism. ApplicationListener
is an interface that observers can implement to be notified when specific application events occur. When an event is published within the ApplicationContext, all registered ApplicationListener
that are capable of handling that event type will be notified by invoking their onApplicationEvent()
method. This mechanism allows for loose coupling between event publishers and subscribers, making it easy to add, modify, or remove event handlers without affecting the rest of the application. Here’s a simple example:
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
void onApplicationEvent(E event);
}
public class CustomEventListener implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
System.out.println("Event received: " + event);
}
}
Conclusion
This post has explored some of the use cases where the GOF design patterns are effectively applied in Spring. There are many similar ones, so I won’t go into them all. For the implementation of the details, you can explore various sources to gain a better understanding. Noticing how it is applied in every source in practice can help to further leverage your comprehension of Design Patterns. Happy coding!