Design Better Software with the Inversion of Control Pattern

Design Better Software with the Inversion of Control Pattern

he Inversion of Control (IoC) pattern, also known as Dependency Injection, has recently become popular in the J2EE community. Several open source projects, including Spring, PicoContainer, and HiveMind, use the IoC pattern to develop lightweight J2EE Containers.

IoC is not a new concept, however. It has been around for several years now. Using object-oriented design principles and features such as interface, inheritance, and polymorphism, the IoC pattern enables better software design that facilitates reuse, loose coupling, and easy testing of software components. This article discusses IoC and demonstrates how to use this pattern in your software design without having to implement any of the open source frameworks.

IoC Design Pattern
Assume Class A has a relationship with Class B: it wants to use the services of Class B. The usual way to establish this relationship is to instantiate Class B inside Class A. Though this approach works, it creates tight coupling between the classes. You can’t easily change Class B without modifying Class A. To eliminate the coupling, you can have a Configurator inject the instance of Class B (Object “b”) to the instance of Class A (Object “a”). If you want to change the implementation of Class B later on, you simply change the Configurator object. So, the control of how Object “a” gets the reference of Object “b” is inverted. Object “a” is not responsible for getting the reference to Object “b”. Instead, the Configurator is responsible for it. This is the basis for the IoC design pattern.

Figure 1. Object “a” Directly Creates Object “b”

To demonstrate the benefits of the Configurator object in this case, the following examples show a simple design without IoC and one with IoC (click here to download the sample code for this article).

Listing 1 and Figure 1 are simple examples in which Class A uses Class B:

public class A{  private B b;  public A(){    b=new B();}Listing 1. Class A Directly Refers Class B 

Listing 1 assumes the following design decisions:

  1. Class A needs a reference to Class B.
  2. Class B is a concrete class that has a default constructor.
  3. Class A owns the instance of class B.
  4. No other class can access the instance of Class B.
Figure 2. Object “a” First Creates Object “c” and Then Object “b” by Passing Object “c”

If any one of the above design decisions change, then the code in Listing 1 must be modified. For example, if Class B changed to have a non-default constructor, which takes Class C (see Figure 2), then Listing 1 would change to Listing 2.

public class A{  private B b;  public A(){   C c=new C();    b=new B(c);}Listing 2. Class A Directly Refers Class B and Class C 

Once again, Listing 2 assumes certain design decisions. Now Object “a” owns both Object “b” and Object “c”. If Class B or Class C changes at all, then Class A needs to change as well. In essence, a simple design of a simple class with implicit design assumptions becomes a maintenance nightmare in the future. Consider how difficult making changes would be if you had this scenario in a typical application with several classes.

Alternatively, if you use a framework that uses the IoC pattern, the responsibility of creating Object “b” moves from Object “a” to the IoC framework, which creates and injects Object “b” into Object “a”. This insulates Class A from the changes in Class B. Before Object “a” is used, it needs the reference to Object “b”. The IoC framework will inject Object “b” into Object “a”.

Figure 3. IoC Framework Creates Object “b” and Sets It to Object “a”

Listing 3 shows Class A from the previous listings modified to use the IoC pattern.

public class A{  private B b;  public A(){  }   public setB(B b){   this.b=b; } }Listing 3. Class A Gets the Reference to Class B via setB 

Listing 3 assumes the following design decisions:

  1. A needs a reference to B, and it doesn’t need to know how B is instantiated.
  2. B can be an interface, an abstract class, or a concrete class.
  3. Before the instance of Class A is used, it needs a reference to the instance of Class B.

From the above design decisions, you can see that there is no tight coupling between Class A and Class B. Both can be changed independently without affecting each other. Of course, if there is any change in the public methods of Class B, Class A needs to change as well. But how Object “b” is created and managed is not decided in the implementation of Object “a”. Instead, the IoC framework uses the setB() method in Object “a” to inject Object “b” (see Figure 3).

IoC Frameworks
Several open source IoC frameworks like Spring, PicoContainer, and HiveMind support the IoC pattern. While the general IoC principle is simple, each of these frameworks supports different implementations and offers different benefits. The IoC pattern can be implemented in three ways: setter based, constructor based, and interface based. This section briefly discusses each of them. For more in-depth details, refer to the frameworks’ home pages.

Setter-Based IoC
This type of IoC uses a setter method to inject the referenced object into a referring object. This is the most common type of IoC, and both Spring and PicoContainer implement it. Setter-based IoC is good for objects that take optional parameters and objects that need to change their properties many times during their lifecycles. Its main disadvantage is that you need to expose all the properties of an object to the outside world when using a setter method. This breaks one of the key OO principles: data encapsulation and information hiding.

Constructor-Based IoC
This type of IoC uses a constructor to set the reference of the object. Its main advantage is that only the creator knows about the referenced object. Once the object is created, the client code that uses the object is unaware of the referenced object. This approach may not work in all types of applications, however. For example, if you use an external API that needs a default constructor, then you need to go with a setter-based IoC. A constructor-based IoC is the main type of implementation in Spring.

Interface-Based IoC
In this type of IoC, objects implement a specific interface from the IoC framework, which the IoC framework will use to properly inject the objects. One of the main advantages of this type is that it doesn’t need an external configuration file to configure the object references. Since you need to use the IoC framework’s marker interface, the IoC framework knows how to glue the objects together. It is similar to using EJB. Any EJB container knows how to instantiate your objects and hook them with itself. The main disadvantage of this approach is that the marker interface ties your application to a specific IoC framework. Apache Avalon is based on this approach but this project has been closed.

An Example of IoC
If you are starting a new project, you can choose any of the open source IoC frameworks based on your needs. If you want to use the IoC pattern in your existing project then you need to write your own classes that support IoC. Though the open source frameworks offer off-the-shelf components and may provide many more features than your own implementation, you can still develop a set of classes that support the IoC pattern. This section demonstrates how.

Say you want to write a CustomerService object to process customer data. Assume customer data is stored in two different places: a relational database and an XML file. You also want to have CustomerService create Customer objects by reading data from either the database or the XML file. Now, say you want to accomplish this without changing the source code of CustomerService to switch between the database and XML file.

First, design an interface (see Listing 4) and define common methods, which you will use to retrieve and store customer data. Both XMLDataSource and JDBCDataSource classes implement this interface.

public interface DataSource {	public Object retrieveObject();	public void setDataSourceName(String name);	public String getDataSourceName();	public void storeObject(Object object);}Listing 4. Common Interface to Customer Data 

Listing 5 shows a class that implements DataSource and provides an implementation that uses an XML file to retrieve and store customer data.

public class XMLDataSource implements DataSource {	private String name;	/**	 * Default Constructor	 */	public XMLDataSource() {		super();	}	/**	 * Retrieve Customer data from XML Source and construct	 * Customer object	 */	public Object retrieveObject() {		//get XML data, parse it and then  construct		//Customer object 		return new Customer("XML",10);	}		/**	 * Set the DataSource name	 */	public void setDataSourceName(String name) {;			}	/**	 * Return DataSource name	 */	public String getDataSourceName() {		return name;	}	/**	 * Store Customer into XML file	 */	public void storeObject(Object object) {		//Retrieve customer data and store it in 		//XML file			}}Listing 5. Class to Retrieve Customer Data from XML File 

The class in Listing 6 also implements DataSource and provides the same implementation as XMLDataSource with only one difference: this class retrieves and stores customer data using a relational database.

public class RelationalDataSource implements DataSource {	private String name;		/**	 * Default constructor	 */	public RelationalDataSource() {		super();	}	/**	 * Using the DataSource retrieve data for Customer and build a 	 * Customer object to return it to the caller	 */	public Object retrieveObject() {		//get data for Customer object from DB and create a 		//Customer object		return new Customer("Relational",10);	}	/**	 * Set the DataSource name	 */	public void setDataSourceName(String name) {;			}	/**	 * Return the name of the DataSource	 */	public String getDataSourceName() {		return name;	}	/**	 * Store Customer into relational DB	 */	public void storeObject(Object object) {		//store the customer data into Relational DB			}}Listing 6. Class to Retrieve Customer Data from Relational DB 

The simple class in Listing 7 holds customer data, which either the XMLDataSource or RelationalDatSource object will construct.

public class Customer {	private String name;	private int age;		/**	 * Default Constructor	 */	public Customer(String name, int age) {;		this.age=age;	}	/**	 * @return Returns the age.	 */	public int getAge() {		return age;	}	/**	 * @param age The age to set.	 */	public void setAge(int age) {		this.age = age;	}	/**	 * @return Returns the name.	 */	public String getName() {		return name;	}	/**	 * @param name The name to set.	 */	public void setName(String name) { = name;	}}Listing 7. Class That holds Customer Data 

This class in Listing 8 has a reference to DataSource, which it uses to retrieve and store Customer objects. The actual implementation of DataSource will be either XMLDataSource or RelationalDataSource, which will be injected into the CustomerService object. The constructor of the CustomerService object accepts a concrete implementation of the DataSource object and uses it to retrieve and store customer data.

public class CustomerService {	private DataSource dataSource;	private Customer customer;		/**	 * Constructor in which DataSource object is injected. Based on the 	 * this object can either refer to RelationlDataSource or 	 * XMLDataSource	 */	public CustomerService(DataSource dataSource) {		super();		this.dataSource=dataSource;		customer=(Customer)dataSource.retrieveObject();	}	/**	 * Modify Customer name 	 * @param name	 */	public void updateCustomerName(String name) {		customer.setName(name);	}		/**	 * Modify Customer age	 * @param age	 */	public void updateCustomerAge(int age){		customer.setAge(age);	}		/**	 * 	 * @return Customer name	 */	public String getCustomerName(){		return customer.getName();	}		/**	 * 	 * @return Customer age	 */	public int getCustomerAge(){		return customer.getAge();	}	}Listing 8. CustomerService Class That Gets DataSource Reference via ServiceConfigurator 
Figure 4. IoC Using ServiceConfigurator

The ServiceConfigurator in Listing 9 is a main class that uses the IoC pattern to inject the concrete implementation of DataSource into the CustomerService object. It reads the configuration parameters (see Figure 4) from the file and decides to create either an XMLDataSource or a RelationalDataSource object. The DataSource object is created using its default Constructor, and the name of the DataSource is set using its setter method. The created DataSource object is injected into CustomerService using its constructor, which accepts a DataSource reference. The created CustomerService object is stored in the ServiceConfigurator registry so that it will be returned when a subsequent request comes for the CustomerService object.

Figure 4 shows the inner workings of ServiceConfigurator, and Figure 5 shows the UML diagram for all of the classes explained previously. You can switch between XMLDataSource and RelationalDataSource just by editing the file.

public class ServiceConfigurator {	public static final String IoC_CONFIG_FILE="";	private Properties props;	private HashMap serviceRegistery;	private static ServiceConfigurator thisObject;		/**	 * This method first checks if there is a ServiceConfigurator instance	 * exist, if not creates a one, stored it and returns it	 * @return	 */	public static ServiceConfigurator createServiceConfigurator(){		if(thisObject==null){			thisObject=new ServiceConfigurator();		}		return thisObject;	}		/**	 * Private Constructor makes this class singleton	 */	private ServiceConfigurator() {			props = new Properties();			serviceRegistery=new HashMap();					loadIoCConfig();			createServices();	}		/**	 * Load the IoC_CONFIG_FILE properties file	 *	 */	public void loadIoCConfig(){		InputStream is = this.getClass().getClassLoader().getResourceAsStream(IoC_CONFIG_FILE);        try {            props.load(is);            is.close();        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {        	e.printStackTrace();        }	}		/**	 * Create the CustomerService by getting the DataSource name from the	 * properties file. The CustomerService object is stored in the	 * serviceRegistery so that it will be retrieved when requested. 	 * During the construction of CustomerService the DataSource object	 * is injected into it. So the CustomerService can access the DataSource	 * to retrieve the Customer.	 *	 */	public void createServices(){		String dataSourceName=props.getProperty("dataSource");		DataSource dataSource=null;		if(dataSourceName!=null){			try {				dataSource=(DataSource)Class.forName(dataSourceName).newInstance();			} catch (InstantiationException e) {				e.printStackTrace();			} catch (IllegalAccessException e) {				e.printStackTrace();			} catch (ClassNotFoundException e) {				e.printStackTrace();			}		}		//name of the DataSource is retrieved from the properties file		dataSource.setDataSourceName(props.getProperty("name"));		CustomerService customerService=new CustomerService(dataSource);		serviceRegistery.put(CustomerService.class,customerService);	}	/**	 * Stored Service is retrieved from serviceRegistery for the given	 * Class object	 * 	 * @param classObj	 * @return	 */	public Object getService(Class classObj){		return serviceRegistery.get(classObj);	}	}Listing 9. ServiceConfigurator that Uses IoC to Inject DataSource into CustomerService 
Figure 5. ServiceConfigurator UML Class Diagram

The ServiceConfigurator is a simple class that supports the IoC pattern. You can modify it to add more features such as simultaneously supporting multiple DataSource objects, dynamically injecting the DataSource into CustomerService, and life cycle management.

The class in Listing 10 is a JUnit test that uses CustomerService. It uses ServiceConfigurator to retrieve the CustomerService object, which is used to change the Customer name and age.

public class CustomerServiceTester extends TestCase{	private ServiceConfigurator serviceConfig;		/**	 *Default Constructor 	 */	public CustomerServiceTester() {		super();	}		/**	 * Create ServiceConfigurator	 */	public void setUp() throws Exception{		super.setUp();		serviceConfig=ServiceConfigurator.createServiceConfigurator();	}	/**	 * Test CustomerService and check for Customer	 * @throws Exception	 */	public void testCustomerService() throws Exception{		CustomerService custService=(CustomerService)serviceConfig.getService(CustomerService.class);		assertNotNull(custService);		custService.updateCustomerAge(30);		custService.updateCustomerName("Mani");		assertEquals(30,custService.getCustomerAge());		assertEquals("Mani",custService.getCustomerName());			}Listing 10. JUnit Test that Uses CustomerService 

Service Configurator vs. Service Locator
The implementation of ServiceConfigurator in Listing 9 may lead you to think that there is no difference between the ServiceConfigurator class that uses IoC and one that uses the Service Locator pattern. In fact, you can modify the CustomerService in Listing 8 to use a Service Locator, which will give the CustomerService class either an XMLDataSource or a RelationalDataSource object, based on the Service Locator’s internal implementation. By changing the internal implementation of Service Locator, you can switch the DataSource from which CustomerService gets its Customer. In this context, both Service Locator and ServiceConfigurator provide the same functionality to CustomerService. The main difference is how CustomerService gets the DataSource reference. In Service Locator, CustomerService must get the DataSource reference directly from Service Locator, while in ServiceConfigurator, CustomerService gets the DataSource indirectly through ServiceConfigurator.

So, what is the advantage of using Service Locator instead of ServiceConfigurator? ServiceConfigurator frees CustomerService from either acquiring or instantiating the DataSource directly. This makes CustomerService easy to test: simply inject the appropriate DataSource into it. More over, as explained previously in Listing 1 and Listing 2, because you do not make any assumptions about how CustomerService will get the DataSource reference, you can reuse the CustomerService object in different applications. This is the main advantage of using Service Configurator over Service Locator.

What to Inject?
In Figure 5, you see that CustomerService has a relationship with both the DataSource and the Customer classes. Yet this discussion has been about how to inject DataSource into CustomerService. Why not inject Customer into CustomerService? In fact, it is perfectly legitimate to make the ServiceConfigurator inject the Customer object into CustomerService by creating an interface for Customer and referencing it in CustomerService. It all depends on your application requirements and how you want to design the CustomerService class. The previous example made DataSource responsible for creating and storing the Customer object and created a tight coupling between DataSource and Customer. In the future, any change in the Customer class will require changes to all of the implementations of DataSource. However, the design decision was made with the assumption that the public methods of Customer would not undergo any changes, and if any changes occurred in the internals of Customer, then they will not affect the DataSource.

The only anticipated change is how the Customer data is retrieved and stored. Currently, the Customer data is stored in a relational database or in an XML file. Perhaps in the future, it might be stored in an object database or retrieved via a Web service. In either scenario, you can have new classes retrieve and store Customer data. So, based on these assumptions, the example injects only the DataSource into CustomerService, which is used to retrieve and store Customer data.

Add on to Simple Example
This article discussed the IoC pattern and briefly discussed open source frameworks that use it. It also discussed how you could incorporate the IoC pattern in your existing applications using the ServiceConfigurator example. Though simple, the ServiceConfigurator class supports the IoC pattern and provides all of its benefits. You can modify the ServiceConfigurator to add more features as you need for your applications.


About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist