Decorating Spring @Components

It is a common situation when an existing Spring component requires some extensions. We have to add new functionality to the existing application. I would like to share my experience on how to deal with such situations in order to keep the application code clean and maintainable.

Adding small extensions

The first thought may be: “I will just add a single if to the existing component, an additional couple of lines of code or another dependency”. It is very tempting to do it this way because it is easy and straightforward. If the preexisting method or component is implemented cleanly and methods are short it does not seem to be a bad idea. The final solution is still not that bad.

But what about existing tests? Our change may require an addition of another mocked dependency to keep existing tests green. Sometimes we have to take into account new corner cases to verify if old functionality works fine. It may seem to somebody that this is not a bad solution either. The tests are only a bit more complicated, the number of corner cases which should be verified is just a bit higher.

In my opinion, the fact that the code seems only a bit more complicated than before the change is the most dangerous. The quality of the code is decreasing slowly, so we do not feel very bad about it. The problem is that after a couple of rounds of such enhancements, the code is much worse than at the beginning. How can we avoid it?

Maintaining high quality

The open-closed principle can be a good starting point here. We need a solution which enables us to easily extend existing functionalities (“open for extension”), but will not require modification of the existing implementation and tests (“closed for modifications”). There are some classical design patterns which can be used in this situation. One of them is decorator pattern. I will present some examples how decorator pattern can be applied to Spring components.

A typical usage example of decorator pattern is adding transaction handling functionality to existing code. It is a cliche, though, Spring developers dealt with it years ago and usually there is no need to revisit it. In real life, most of the enhancements will not be so generic and commonly used and it is not worth to prepare annotations and annotation processors or aspect-oriented approach to deal with them. Let’s take a look at the examples below.

Let’s assume that we have a simple service which is responsible for saving products in a persistent storage.

public class BasicProductService implements ProductService {

	private final ProductRepository productRepo;
	
	public BasicProductService(ProductRepository productRepo) {
		this.productRepo = productRepo;
	}

	@Override
	public Product createProduct(String productName) {
		// simplified implementation for brevity
		Product product = new Product(productName);
		productRepo.save(product);
		return product;
	}

}

Due to some requirements change we may need to introduce a full-text search capability for products stored in the system. Adding this functionality to ProductService will break a single responsibility rule and will increase the number of mocked services in tests. Please take a look at the unit test before and after adding new functionality:

public class BasicProductServiceTest {

	private ProductService productService;
	private ProductRepository productRepo;

	@Before
	public void setup() {
		productRepo = mock(ProductRepository.class);
		productService = new BasicProductService(productRepo);
	}

	@Test
	public void createsProductWithGivenName() throws Exception {
		String productName = "new product";
		Product product = productService.createProduct(productName);
		
		assertThat(product.getName()).isEqualTo(productName);
		verify(productRepo, times(1)).save(product);
	}

}
public class BasicProductServiceTest {

	private ProductService productService;
	private ProductRepository productRepo;
	private FullTextSearchIndexService fullTextSearchIndexService;

	@Before
	public void setup() {
		productRepo = mock(ProductRepository.class);
		fullTextSearchIndexService = mock(FullTextSearchIndexService.class);
		// creation of mocked FullTextSearchIndexService is required
		// to keep old tests working
		productService = new BasicProductService(
            productRepo, 
            fullTextSearchIndexService
        );
	}

	@Test
	public void createsProductWithGivenName() throws Exception {
		String productName = "new product";
		Product product = productService.createProduct(productName);
		
		assertThat(product.getName()).isEqualTo(productName);
		verify(productRepo, times(1)).save(product);
	}
	
	@Test
	public void indexesProductDuringCreation() throws Exception {
		// it's assumed that the same setup of mocks works fine
		// for both tests, more complex example may require
		// separate setup for each test
		String productName = "new product";
		Product product = productService.createProduct(productName);
		
		assertThat(product.getName()).isEqualTo(productName);
		verify(fullTextSearchIndexService, times(1)).index(product);
	}

}

The example is very simplified, but you can easily imagine what will happen after adding more and more functionalities toBasicProductService. I have presented the example of unit tests only. In case of integration tests, the increase of complexity caused by adding new dependency will be much higher.

We can easily decorate the original ProductService in order to decouple the two functionalities (persistence and full-text search). We can even separate the indexing functionality itself from its usage while creating Product. In addition, existing tests may remain unchanged.

public class FullTextSearchIndexedDecorator implements ProductService {

	private final ProductService wrappedProductService;
	private final FullTextSearchIndexService fullTextSearchIndexService;
	
	public FullTextSearchIndexedDecorator(ProductService wrappedProductService, 
			FullTextSearchIndexService fullTextSearchIndexService) {
		this.wrappedProductService = wrappedProductService;
		this.fullTextSearchIndexService = fullTextSearchIndexService; 
	}

	@Override
	public Product createProduct(String productName) {
		Product product = wrappedProductService.createProduct(productName);
		fullTextSearchIndexService.index(product);
		return product;
	}
	
}

FullTextSearchIndexService is responsible for creating indexes and full-text searching and FullTextSearchIndexedDecorator is responsible for indexing product during creation.

Building @Components with decorators

There are multiple ways to organize @Components with decorators in Spring environment. I would like to present some of them.

Multiple implementations of an interface in a single Spring context

One of the options is to create multiple implementations of a base interface. It would be one basic implementation and a set of required decorated ones. In such a case each component which requires the implementation of the interface has to define which one should be used. It can be achieved with @Qualifier annotation as follows:

@Configuration
public class AppConfiguration {
	
	@Bean(name = "basicService")
	ProductService basicService(ProductRepository productRepository) {
		return new BasicProductService(productRepository);
	}

	@Bean(name = "decoratedService")
	ProductService decoratedService(
			@Qualifier("basicService") ProductService basicProductService,
			ElkFullTextSearchIndexService indexService) {
		return new FullTextIndexedDecorator(basicProductService, indexService);
	}
	
	@Bean
	ServiceRequiringDecoratedService serviceRequiringDecoratedService(
			@Qualifier("decoratedService") ProductService productService) {
		return new ServiceRequiringDecoratedService(productService);
	}
	
	@Bean
	ServiceRequiringBasicService serviceRequiringBasicService(
			@Qualifier("basicService") ProductService productService) {
		return new ServiceRequiringBasicService(productService);
	}

}
Before we create a configuration with multiple beans implementing an interface in Spring context, it’s worth to think if we really need both of them to be managed by Spring. We may encounter some surprising problems in complex and hierarchical configurations. You can find more details in “When Two Beans Collide” post.

A single implementation of an interface in Spring context

Another option is to provide just a single implementation of the interface in the Spring context. It can be some base implementation which can be later decorated in a component which uses an interface. Please take a look at examples:

  • only the basic implementation is created as a bean, other components can use base implementation and decorate it if needed
@Configuration
public class AppConfiguration {
	
	@Bean
	ProductService basicService(ProductRepository productRepository) {
		return new BasicProductService(productRepository);
	}

	@Bean
	ServiceRequiringDecoratedService serviceRequiringDecoratedService(
			ProductService productService,
			ElkFullTextSearchIndexService indexService) {
		ProductService decoratedProductService = 
				new FullTextIndexedDecorator(productService, indexService);
		return new ServiceRequiringDecoratedService(decoratedProductService);
	}
	
	@Bean
	ServiceRequiringBasicService serviceRequiringBasicService(
            ProductService productService) {
		return new ServiceRequiringBasicService(productService);
	}

}
  • only the decorated implementation is created as a bean, all other components which are using auto-wiring will get the decorated implementation
@Configuration
public class AppConfiguration {

	@Bean
	ProductService decoratedService(ProductRepository productRepository,
			ElkFullTextSearchIndexService indexService) {
		ProductService productService = basicService(productRepository);
		return new FullTextIndexedDecorator(productService, indexService);
	}
	
	@Bean
	ServiceRequiringDecoratedService serviceRequiringDecoratedService(
			ProductService productService) {
		return new ServiceRequiringDecoratedService(productService);
	}

}

Summary

This approach – decorating services with additional functionality – is extremely useful when the number of those additional functionalities is rising. We can easily combine and dynamically add new decorators to existing base functionalities. The big advantage of such solution is that we do not modify existing code. It lowers the risk that some new bugs can occur after a change. At the same time, each decorator can be easily tested separately and independently to the others. Of course, the whole system was modified and requires additional integration and end-to-end tests. Regardless of the approach chosen, such tests must still be added.

In my opinion, using decorators in Spring environment is a good way to develop reliable and maintainable systems. It is worth to remember about this classical design pattern and apply it in everyday work. Good luck!

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *