Designing to Interfaces

As a best practice, designing to interfaces is relatively ancient when compared to the seemingly unending stream of new best practices that emerge on a weekly basis; it is also one of the most enduring. One of the main design goals of Spring is to ease the development of applications that are designed and implemented around a set of well-defined interfaces. Before we can look at designing a Spring-based application in any detail, we should explore exactly why designing to interfaces is such a big thing and how Spring goes about making it easier.

Why Design to Interfaces?

There are many reasons you should design and code your applications to interfaces rather than to a concrete class hierarchy, but perhaps the biggest reason is to reduce coupling. If components in your application talk to each other in terms of interfaces rather than concrete types, it becomes very easy to swap out a component if it becomes problematic. This capability allows you to swap out one implementation for another, without having to touch the rest of the application code; indeed, it is possible for your application to be working with many different implementations of the same interface without even being aware that it is doing so.

Remember also that, in Java, a class only has one shot at concrete inheritance but can implement as many interfaces as necessary. By defining application components in terms of interfaces rather than classes, you are not going to constrain an implementing class to a particular base class unnecessarily.

One of the most important benefits gained by this loose coupling is the increase in testability. As the heads of a busy development team, we are constantly seeking new ways to improve the test coverage in the applications we produce as a direct way of improving the quality of the product we send to the customer. By designing to interfaces, we can swap out an implementation of an interface for a mock implementation, which allows us more flexibility when testing. For example, a requirement of the SpringBlog application is that each operation that results in a new posting or a post being modified should result in an audit message being written. This process should work under a transaction so that if the writing of the audit message fails, then blog modification is rolled back. Originally we had the audit log encapsulated as a private method of our implementation of BlogManager (the main business interface). The problem with this was that it made testing a rollback very difficult. To get around this, we refactored the auditing logic behind a new interface, AuditService, and then we created a MockAuditService implementation that threw an exception. This allows us to test that the transactional behavior was correct. This actual scenario is discussed in more detail in Chapter 12.

What we find when we design to interfaces is that we do not need to do huge amounts of up-front design. Typically, to start with, we lay out the main component interfaces as we see them, and then as the application progresses, we refactor certain pieces of functionality, introducing new interfaces as we find the code becoming unwieldy or hard to test.

The Factory Pattern

One of the key problems you will encounter when implementing an application where all the components are defined in terms of interfaces is how your application goes about locating instances of classes that implement the interfaces. A traditional solution to this is to use the Factory pattern. The Factory pattern defines a class whose responsibility is to provide application components with implementations of other application components; in this case, the available components are defined in terms of interfaces, not concrete implementations. Consider a system that has a business interface OrderService. Other components in the system want to obtain implementations of this interface without knowing ahead of time which implementation they need. To implement such a system, we can build a Factory class like the one shown in Listing 11-1.

Listing 11-1: A Basic Factory Implementation

image from book
package com.apress.prospring.ch11.factory;      public class BasicFactory {          private static final BasicFactory instance;          private OrderService orderService;          static {         instance = new BasicFactory();     }          public static BasicFactory getInstance() {         return instance;     }          public BasicFactory() {         this.orderService = new DefaultOrderService();     }          public OrderService getOrderService() {         return this.orderService;     } }
image from book

This is a simplistic Factory implementation, but it does illustrate the basic Factory approach. Now any application that wishes to get access to an implementation of OrderService simply uses the getOrderService() method of the BasicFactory class.

Drawbacks of the Basic Factory Pattern

In its basic form, the Factory pattern has three main drawbacks:

  • There is no way to change an implementing class without a recompile.

  • There is no way to make multiple implementations available transparently to different components. This is part of a larger problem in that the Factory class requires each component to have some knowledge of the Factory and which method on the Factory to invoke.

  • There is no way simply to switch instantiation models. In Listing 11-1, we maintained a singleton instance of DefaultOrderService, but if we wanted to return many instances, we would have to recompile the Factory class.

These drawbacks are discussed in detail in the next three sections.

Externally Configurable Factories

From the example in Listing 11-1, you can see that changing the implementation class means changing the BasicFactory class and recompiling. One of the benefits of interface-based design is that you can swap out implementations for new ones very easily. However, having to recompile the Factory removes some of the ease with which this can be done. In many projects in the past, before Spring was available, we created factories which allowed the implementation class for interfaces to be specified in an external configuration file. This solved the initial problem, but it added more development burden to our project and it did not really help out with the two remaining problems.

Supporting Multiple Implementations Transparently

Supporting multiple implementations transparently is perhaps the biggest drawback of the traditional Factory pattern. The basic method on the BasicFactory class, getOrderService(), can only return one particular implementation (or a random choice), but it cannot choose which implementation to return based on the caller. This leads, naturally, to an implementation such as that shown in Listing 11-2.

Listing 11-2: Basic Support for Multiple Implementations

image from book
package com.apress.prospring.ch11.factory;      public class MultiFactory {          private static final BasicFactory instance;          private OrderService orderService;     private OrderService superOrderService;          static {         instance = new BasicFactory();     }          public static BasicFactory getInstance() {         return instance;     }          public MultiFactory() {         this.orderService = new DefaultOrderService();         this.superOrderService = new SuperOrderService();     }          public OrderService getOrderService() {         return this.orderService;     }          public OrderService getSuperOrderService() {         return this.superOrderService;     } }
image from book

With this implementation, components that need to access the SuperOrderService implementation can call the getSuperOrderService() method. However, this approach just negates the benefit of a Factory. Although the components are not coupled by class to a particular implementation, they are coupled by the method they call on the Factory. Another drawback to this approach is that each new implementation requires a change to the Factory code and a change to the component that needs the new implementation. Having to add a new method for each new implementation makes this approach very difficult to configure externally.

Another implementation that tries to solve the problem of transparent support for multiple implementations requires components that invoke the getOrderService() method to pass in their class to the getOrderService() method so that the Factory can decide, based on the class of the caller, which implementation to return. This implementation suffers from numerous problems, not least of which is that it only works with classes, meaning two instances of the same class cannot have different implementations of the OrderService. You will also find that the implementation of the getOrderService() method quickly becomes messy when you have many components that need an OrderService implementation.

Yet another approach to this problem is to use a lookup-style approach by having each component look up the implementation with a key. So instead of calling getOrderService(), a component calls getOrderService("someKey"). The problem with this approach is that in order to maintain full flexibility, each component must use a separate key so that its implementation can be changed separately from the others. This in turn means that without some complex configuration logic, it is going to be difficult to share an instance of the same implementation class across components with different keys.

The root of this problem lies in the fact that a component actively has to ask for an implementation class, and to gain full flexibility, it must ask in a way that is different from all other instances of all other components. This is a problem that is not solved using the traditional Factory pattern.

Supporting Multiple Instantiation Modes

Another problem is supporting multiple instantiation modes of an implementation class for different components. This problem suffers from many of the issues discussed in the preceding section, and again, the core of this problem is that a component actively has to ask for an implementation class. This problem is also not solved using the traditional Factory pattern.

Thankfully, Spring solves all of these problems; we discuss how in the next section.

Impact of Spring on Interface-Based Design

Spring has a huge impact on applications that are designed using interfaces. Because Spring takes care of wiring all the components together, you no longer have to worry about creating Factory classes that consider every possible situation.

On the surface, when you are building interface-based applications, the biggest benefit from Spring is the reduction in glue code that you have to write. This benefit is further enhanced by the excellent, out-of-the-box support for external configuration of component dependencies. However, the biggest benefit comes from Spring's use of Dependency Injection. Because Spring removes the responsibility for dependency location from components themselves and simply asks that components allow it to provide them with the dependencies, Spring is able to solve the last two of the three problems discussed previously.

Dependency Injection means that Spring can provide any instance of any implementation class to any instance of any application component without requiring any special coding in the application component whatsoever. This is coupled with the fact that Spring can freely manage the lifecycle of any instance of any dependency that it is managing for an application component.

Basically this means that Spring has all the features that we need to design interface-based applications, and we do not have to worry about how we are going to glue the components together come implementation time.



Pro Spring
Pro Spring
ISBN: 1590594614
EAN: 2147483647
Year: 2006
Pages: 189

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net