Inversion of Control (IoC) and Dependency Injection (DI) are patterns that draw a lot of attention (see Resources). They are mostly used within so-called IoC containers, which inject dependencies into a component in the form of other components. However, the patterns don't define the design of these dependency components' methods. In a classic design, value objects or data transfer objects in those methods are used as method parameters and return values when complex objects are required.
This article shows you that you can also use IoC on the method signatures to decouple the methods from the value objects. You do this by replacing the value objects in the method signatures with interfaces. I show you scenarios where this approach can be useful. I use this pattern frequently and find that it helps me better separate the concerns between the components. And in the runtime, it reduces object-creation and copying efforts.
Value objects as method parameters
Much has been written about IoC, so I'll describe only its general principle: a component "outsources" the configuration, localization, and life-cycle aspects of the components it uses. For example, a data access bean -- instead of looking up a JDBC datasource (configuration and localization) and perhaps handling the connection pooling (life cycle) itself -- "gets" the JDBC connection from somewhere and simply uses it. In an IoC setup, these aspects are normally handled by an IoC container that injects the dependencies into the component by calling a setter method, for example.
IoC focuses on handling the life cycle of components. This article focuses not on the component but on the method parameters of operations that a component provides.
Take a look at the dependency graph for a typical setup of a component with two dependencies and the method parameters used (see Figure 1). These method parameters are defined as value objects; that is, objects with no logic that hold only data values.
Figure 1. Dependency graph for components with value objects as method parameters
In Figure 1, Component1 depends on Component2 and Component3 (in terms of IoC), and calls method2 and method3, respectively. It's irrelevant at this point if Component1 "knows" Component2 or Component3 directly or only via interfaces that Component2 and Component3 implement. However, it's essential that normally the method parameters are objects, not interfaces.
In this setup, when Component1 calls method2, it must instantiate Value Object 2 and fill it with values. Likewise, if Component1 calls method3, it then must instantiate Value Object 3 and fill it with values.
Now assume that Component1 needs to call method2 and method3 with the same input data to get different output data. For example, Component1 could be an order-preparation component, method2 a method to determine lead times, and method3 a method to determine prices. Both methods need the same input and provide different output.
In this case, using value objects as method parameters requires that Component1 create the value objects for each method call and actively copy the values required into the value objects. Also, each of the value objects must be instantiated, which is not as expensive as it was with early Java™ versions, but it still requires resources. All this reduces performance. The following sections show how you can optimize performance instead.
The goal is to avoid copying values between the various value objects. You can do this by defining the method parameters as interfaces. This way, the calling component can use any object it likes as a method parameter, as long as the object implements the interface.
Figure 2 shows the new dependency graph:
Figure 2. Dependency graph for components with interfaces as method parameters
The dependency methods define the method parameters as interfaces. The calling component instantiates an object -- deliberately not called a value object -- that implements these interfaces and uses this object as a method parameter in both method calls.
The following examples highlight some advantages of this approach.
Example: Pricing and lead times
Again, let's assume that method1 determines lead times for an order and method2 determines prices. A simple definition of these components and methods could be as shown in Listing 1:
Listing 1. Sample components using interfaces as method parameters
interface LeadtimeComponent {
void getLeadtimes(List<LeadtimeItem> items) throws LeadtimeException;
}
interface LeadtimeItem {
Long getArticleId();
BigDecimal getQuantity();
String getQuantityUnit();
void setLeadtimeInDays(Integer leadtime);
}
interface PricingComponent {
void getPrices(List<PriceItem> items) throws PricingException;
}
interface PriceItem {
Long getArticleId();
BigDecimal getQuantity();
String getQuantityUnit();
void setPrice(BigDecimal price);
void setPriceUnit(String currency);
}
|
Note that both interfaces define the very same method signatures for the methods to retrieve the article data: getArticleId(), getQuantity(), and getQuantityUnit(). Also note that the component methods have no return values; they modify the objects given "in place" by calling the setter methods on the parameter objects (that is, interfaces) to set the prices and lead times.
This approach simplifies the implementation of the pipeline pattern (see Resources), where data is "piped" from one component to the next, and where one stage of the pipeline (component) uses data provided by previous steps in the pipeline. Figure 3 shows a sequence diagram that uses the pipeline pattern:
Figure 3. Sequence diagram for order preparation, using the pipeline pattern
See the full figure here.
In this example, the order-preparation process first reads the article IDs from the shopping cart. Then it adds more information from the catalog database itself, retrieves lead times and prices (where prices need lead times), and stores the additional information in the cart so it can be used in finalizing the order. If the item objects returned from reading the shopping cart implement the interfaces required by the catalog, lead times, and prices methods, no copying at all is necessary in the process.
You might wonder about the OrderDB component in Figure 3 and its readCart() method. Indeed, this is a special case. In the preceding example, all objects that were modified by the dependency method (such as getPrices(...)) were already passed as method parameters. This is impossible when the component is reading data from a database because in this case, the number of items in the shopping cart is unknown before it's read.
The solution here is to provide a method parameter with a factory method for the items to read, as shown in Listing 2:
Listing 2: Using factory methods in the method parameter
interface OrderDBComponent {
void readCart(Cart cart) throws OrderDBException;
}
interface Cart {
Long getCartId();
CartItem newItem();
void addItem(CartItem item);
}
interface CartItem {
void setArticleId(Long articleId);
...
}
|
With this definition, the OrderDB component reads the items from the database and for each item it reads, it gets a new item object (CartItem) from the Cart object using the newItem() method. After filling in the values read from the database, CartItem is added to the cart with the addItem() method. Note that adding the item to the cart only after filling it with values keeps the cart consistent at all times.
The approach I describe works well if the parameter interfaces defined by multiple dependency components are compatible in that they can be implemented by the very same object. Incompatibilities can arise when, for example, two interfaces define the same method with different return value types. You must take care in designing the methods so you don't introduce such incompatibilities. Also, when designing the methods, you should make sure the semantic meaning of methods defined by the method parameter interfaces is the same when the method signatures are the same in different interfaces.
However, even if incompatibilities exist, all is not lost! Adapter objects can transform an object into one that implements the required interface, without copying the data around. Although you must instantiate the adapter object with this approach, it still avoids copying the data values.
Here's another example that shows the flexibility of this approach. I wrote an editor for a certain object model but wanted to keep the model implementation separate from the editor implementation. So, I let the editor define interfaces for a model that it can edit. Then the actual model implementation implemented the interface in Listing 3:
Listing 3. Model editor interface example
interface ModelEditor {
void edit(Model model);
}
interface Model {
ModelElement newElement();
ModelElement addElement(ModelElement element);
}
|
In this (vastly) simplified definition, you can see that the addElement() method on the model not only takes a ModelElement as a parameter but also returns an instance of ModelElement. The returned ModelElement is the model element that is being replaced by the newly added model element, or NULL if no element is being replaced. The returned value is then stored in an undo command so that the model can be restored easily by calling addElement() again. Also, the addElement() method implements model consistency checks and rejects invalid changes.
This article showed a specific form of IoC that applies to the parameters of the component methods and not to components. Using interfaces as method parameters is (in IoC terms) a form of context IoC, applied on a dependency of the caller. Just as a dependency component (such as PriceComponent) is injected into a caller component (such as OrderPrepareComponent), the caller component injects its dependency object (the implementation of the method parameter interface) into the dependency component's method. Because the called component is restricted to the methods defined in the parameter interface, the interface can ensure that the object given as a parameter is kept consistent. Carefully minimizing the interface to the functionally necessary methods reduces the coupling between the components.
Learn
-
"Inversion of Control Containers and the Dependency Injection pattern" (Martin Fowler, martinfowler.com, January 2004): Fowler digs into how the IoC pattern works and contrasts it with the Service Locator alternative.
-
Apache Excalibur: The Apache Software Foundation's Excalibur project is based on IoC.
-
"Secrets of lightweight development success, Part 4: A comparison of lightweight containers" (Bruce Tate, developerWorks, August 2005): Learn about three lightweight containers that can dramatically loosen the coupling between your system's major components.
-
Open Source Inversion of Control Containers: Check out this comprehensive list of open source IoC containers.
-
Value Object: Read more about value objects.
-
Data Transfer Object: Martin Fowler's Patterns of Application Architecture Catalog describes data transfer objects.
-
PipelineProcessing Design Pattern: Read about the pipeline pattern.
-
The Java technology zone: Hundreds of articles about every aspect of Java programming.
Discuss
-
Check out developerWorks
blogs and get involved in the developerWorks community.
André Fachat is an IT Architect in the Enterprise Java community within IBM Global Business Services in Germany. Although he still knows his first computer's machine language by heart, his areas of expertise now include Web and Enterprise Java application architectures, SOA and Web services, distributed computing, and modeling. He holds a Ph.D. in theoretical physics from the Technical University of Chemnitz, Germany, where he investigated stochastic optimization algorithms on parallel computers. He joined IBM in 1999 and has since conducted various project engagements, including production references. He has worked for IBM in such areas as solution architecture, application development, and consulting.
Comments (Undergoing maintenance)





