A beginners guide to Dependency Injection [转载自TSS]

Scope

This article presents a high level overview of Dependency Injection (DI). It aims to present the overall concept of Dependency Injection to the junior developer within the context of how it could be used in a variety of DI containers.

Dependency Injection (DI) or Inversion of Control (IOC) ?

A lot of current literature often refers to the same design pattern as either Dependency Injection or Inversion of Control. I prefer Dependency Injection from my belief that it is a more specific name. I can imagine many scenarios where one would find evidence of Inversion of Control, though the resultant pattern is unlikely to be focusing on resolving dependencies (e.g. moving from a console application to a Windows Event Loop) . However should you prefer IOC, you can pretty much read the remainder of the article by replacing DI with IOC. Within IOC there are various types (creatively called as type 1, type 2, type 3). The differences within these types are not particularly relevant to the scope of this article and thus not entered into.

Also note that I have used the term Service extensively through this article. This should not be confused with Service Oriented Architectures. It simply became a lot easier to clarify the roles by identifying “Clients” and “Services” as opposed to “Client Component” and “Server Component or Supplier Component”

Simple Introduction to Dependency Injection

A simple introduction

Scenario 1

You work in an organization where you and your colleagues tend to travel a lot. Generally you travel by air and every time you need to catch a flight, you arrange for a pickup by a cab. You are aware of the airline agency who does the flight bookings, and the cab agency which arranges for the cab to drop you off at the airport. You know the phone numbers of the agencies, you are aware of the typical conversational activities to conduct the necessary bookings.

Thus your typical travel planning routine might look like the following :

  • Decide the destination, and desired arrival date and time
  • Call up the airline agency and convey the necessary information to obtain a flight booking.
  • Call up the cab agency, request for a cab to be able to catch a particular flight from say your residence (the cab agency in turn might need to communicate with the airline agency to obtain the flight departure schedule, the airport, compute the distance between your residence and the airport and compute the appropriate time at which to have the cab reach your residence)
  • Pickup the tickets, catch the cab and be on your way

Now if your company suddenly changed the preferred agencies and their contact mechanisms, you would be subject to the following relearning scenarios

  • The new agencies, and their new contact mechanisms (say the new agencies offer internet based services and the way to do the bookings is over the internet instead of over the phone)
  • The typical conversational sequence through which the necessary bookings get done (Data instead of voice).

Its not just you, but probably many of your colleagues would need to adjust themselves to the new scenario. This could lead to a substantial amount of time getting spent in the readjustment process.

Scenario 2

Now lets say the protocol is a little bit different. You have an administration department. Whenever you needed to travel an administration department interactive telephony system simply calls you up (which in turn is hooked up to the agencies). Over the phone you simply state the destination, desired arrival date and time by responding to a programmed set of questions. The flight reservations are made for you, the cab gets scheduled for the appropriate time, and the tickets get delivered to you.

Now if the preferred agencies were changed, the administration department would become aware of a change, would perhaps readjust its workflow to be able to communicate with the agencies. The interactive telephony system could be reprogrammed to communicate with the agencies over the internet. However you and your colleagues would have no relearning required. You still continue to follow exactly the same protocol as earlier (since the administration department did all the necessary adaptation in a manner that you do not need to do anything differently).

Dependency Injection ?

In both the scenarios, you are the client and you are dependent upon the services provided by the agencies. However Scenario 2 has a few differences.

  • You don't need to know the contact numbers / contact points of the agencies – the administration department calls you when necessary.
  • You don't need to know the exact conversational sequence by which they conduct their activities (Voice / Data etc.) (though you are aware of a particular standardized conversational sequence with the administration department)
  • The services you are dependent upon are provided to you in a manner that you do not need to readjust should the service providers change.

Thats dependency injection in “real life”. This may not seem like a lot since you imagine a cost to yourself as a single person – but if you imagine a large organization the savings are likely to be substantial.

Dependency Injection in a Software Context

Software components (Clients), are often a part of a set of collaborating components which depend upon other components (Services) to successfully complete their intended purpose. In many scenarios, they need to know “which” components to communicate with, “where” to locate them, and “how” to communicate with them. When the way such services can be accessed is changed, such changes can potentially require the source of lot of clients to be changed.

One way of structuring the code is to let the clients embed the logic of locating and/or instantiating the services as a part of their usual logic. Another way to structure the code is to have the clients declare their dependency on services, and have some "external" piece of code assume the responsibility of locating and/or instantiating the services and simply supplying the relevant service references to the clients when needed. In the latter method, client code typically is not required to be changed when the way to locate an external dependency changes. This type of implementation is considered to be an implementation of Dependency Injection and the "external" piece of code referred to earlier is likely to be either hand coded or implemented using one of a variety of DI frameworks.

Those attempting to map the above scenario to design idioms will notice that the analogy could be applied to three different design idioms – Dependency Injection, Factory and Program to an Interface and not an Implementation. As in this analogy these three idioms are often but not necessarily used together.

The conventional approach till a few years ago was to separate the interface from the implementation. The factory pattern even allowed for hiding the complexity of instantiation. However the mechanism to “locate” the services was often left to the clients. Moreover some parts of the software also needed to be aware of the dependencies between the various services themselves and thus implicitly had to work out the appropriate sequencing of such component initialization, and had to track and manage their life cycles.

As an example, while the J2EE specification uses JNDI as a mechanism to standardize the mechanism of locating object references, such implementations often require a lot of code change when say the clients need to work in much simpler environments where say JNDI is not available.

Usage of Dependency Injection requires that the software we write to declare the dependencies, and lets the framework or the container work out the complexities of service instantiation, initialization, sequencing and supplies the service references to the clients as required.

Some DI Frameworks

There are a number of frameworks available today to help the developer. The ones I have found useful are :

  • Spring Framework : A substantially large framework which offers a number of other capabilities apart from Dependency Injection.
  • PicoContainer : A fairly small tightly focused DI container framework.
  • HiveMind : Another DI container framework.
  • XWork : Primarily a command pattern framework which very effectively leverages Dependency Injection. While it is an independent framework in its own right, it is often used in conjunction with Webwork

In the following sections we shall take a scenario and implement the same first without using DI and then using the above frameworks respectively. This will allow us to get a flavor of how these frameworks can be leveraged. Note that I have tried to keep the code to the minimum with a strict focus on DI related functionalities.

Implemented Scenario

The attached source pretty much implements the scenario described earlier. The general flow (starting from the main method in each “*Harness” classes) is as follows :

  • The Harness class is instantiated and the runHarness() method called.
  • The runHarness() method (in AbstractHarness) first invokes the configure() method. The configure() method typically initializes the DI container and registers the implementation classes.
  • The runHarness() method subsequently invokes the getPlanner() method. This typically invokes the lookup method on the underlying DI container. What is not immediately apparent, is that the lookup actually triggers the DI container to instantiate the implementation classes for AirlineAgency, CabAgency and TripPlanner interfaces, and resolve the dependencies between them.
  • The runHarness() method subsequently invokes the planTrip() method which actually performs the ticket booking and cab pickup scheduling activities.

Some of the dependencies to be noted are :

  • AirlineAgency and CabAgency are interfaces for the respective services. CabAgency requires a reference to the AirlineAgency to be able to fulfill its expectations successfully.
  • TripPlanner is the interface for a service which can effectively plan out the trip in conjunction with the AirlineAgency and CabAgency. TripPlanner requires a reference to the AirlineAgency and CabAgency.

The UML diagram for the source is shown below :

The salient differences to be observed across different frameworks are

  • The configure() and getPlanner() method in each corresponding harness.
  • How the interface / implementor relationships are described (sometimes in java and sometimes in xml files)
  • How the dependencies between different services are declared (sometimes in xml, sometimes in the constructor and sometimes using a setter method)
  • Any additional requirements expectations (e.g. enabler interfaces in case of xwork)

NoContainer implementation :

There is no “typical” no container implementation. Some implementations would do a lookup from a service (e.g. JNDI) or interface with a set of factories (which perhaps may be implemented as a singleton). In this case I have chosen to read the class names from a property file and in turn instantiate the objects for the services using reflection.

The property file is as follows (simply contains the class names)

airline-agency-class = com.dnene.ditutorial.common.impl.SimpleAirlineAgency
cab-agency-class = com.dnene.ditutorial.common.impl.ConstructorBasedCabAgency

The code to instantiate the agencies and trip planner is as follows :

 
None.gif AirlineAgency airlineAgency  =   null ;
None.gifCabAgency cabAgency 
=   null ;
None.gifTripPlanner tripPlanner 
=   null ;
None.gif
None.gif
//  Get class names from property file
None.gif
InputStream propertyFile  =  getClass().getClassLoader().
None.gif    getResourceAsStream(
" nocontainer-agency.properties " );
None.gifProperties properties 
=   new  Properties();
None.gifproperties.load(propertyFile);
None.gif        
None.gifClass airlineAgencyClass 
=  Class.forName
None.gif        (properties.getProperty(
" airline-agency-class " ));
None.gifClass cabAgencyClass 
=  Class.forName
None.gif        (properties.getProperty(
" cab-agency-class " ));
None.gifClass tripPlannerClass 
=  Class.forName
None.gif        (properties.getProperty(
" trip-planner-class " ));
None.gif        
None.gif
if  (AirlineAgency. class .isAssignableFrom(airlineAgencyClass))
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
// Instantiate airline agency
InBlock.gif
    airlineAgency = (AirlineAgency) airlineAgencyClass.newInstance();
ExpandedBlockEnd.gif}

None.gif
if  (CabAgency. class .isAssignableFrom(cabAgencyClass))
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
// Instantiate CabAgency, and satisfy its dependency on an airlineagency.
InBlock.gif
            
InBlock.gif    Constructor constructor 
= cabAgencyClass.getConstructor
ExpandedSubBlockStart.gifContractedSubBlock.gif        (
new Class[]dot.gif{AirlineAgency.class});
InBlock.gif    cabAgency 
= (CabAgency) constructor.newInstance
ExpandedSubBlockStart.gifContractedSubBlock.gif        (
new Object[]dot.gif{airlineAgency});
ExpandedBlockEnd.gif}
    
None.gif
if  (TripPlanner. class .isAssignableFrom(tripPlannerClass))
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    Constructor constructor 
= tripPlannerClass.getConstructor
ExpandedSubBlockStart.gifContractedSubBlock.gif        (
new Class[]dot.gif{AirlineAgency.class,CabAgency.class});
InBlock.gif    tripPlanner 
= (TripPlanner) constructor.newInstance
ExpandedSubBlockStart.gifContractedSubBlock.gif        (
new Object[]dot.gif{airlineAgency,cabAgency});
ExpandedBlockEnd.gif}

None.gif
return  tripPlanner;
None.gif

Note that the reference to the AirlineAgency is passed to the constructor of the CabAgency (to reflect the dependencies between the services).

The code to actually conduct the trip planning is in the AbstractHarness and AbstractTripPlanner and is reused across all harnesses.

AbstractHarness.java

 
None.gif public   void  runHarness() throws Exception
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
this.configure();
InBlock.gif    
this.getPlanner().planTrip(
InBlock.gif        
"1, Acme Street",
InBlock.gif        
"11111",
InBlock.gif        
"22222",
InBlock.gif        
new Date(System.currentTimeMillis() + 7200000));
ExpandedBlockEnd.gif}

None.gif

AbstractTripPlanner.java

 
None.gif public   void  planTrip(String departingAddress, String departingZipCode, 
None.gif        String arrivalZipCode, Date preferredArrivalTime)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    FlightSchedule schedule 
= airlineAgency.bookTicket
InBlock.gif            (departingZipCode,arrivalZipCode,preferredArrivalTime);        
InBlock.gif    cabAgency.requestCab(departingAddress ,schedule.getFlightNumber());
ExpandedBlockEnd.gif}

None.gif

PicoContainer Implementation

Container instantiation

This requires a simple instantiation of the DefaultPicoContainer class.

 
None.gif pico  =   new  DefaultPicoContainer();
Service Registration

The implementation classes are registered with PicoContainer

 
None.gif pico.registerComponentImplementation(SimpleAirlineAgency. class );
None.gifpico.registerComponentImplementation(ConstructorBasedCabAgency.
class );
None.gifpico.registerComponentImplementation(ConstructorBasedTripPlanner.
class );
None.gif
Dependency Declaration

Dependencies are declared by using a constructor containing the list of the (interface) classes the service is dependent upon. In the following case the ConstructorBasedCabAgency service is dependent upon the AirlineAgency interface.

 
None.gif public  ConstructorBasedCabAgency(AirlineAgency airlineAgency)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
this.airlineAgency = airlineAgency;
ExpandedBlockEnd.gif}

None.gif

Similarly the ConstructorBasedTripPlanner implicitly declares its dependencies on the AirlineAgency and the CabAgency through its constructor.

Dependency Resolution

Pico resolves dependencies by looking at the constructors. The service classes are registered with PicoContainer by registering the implementation class names. The client however requests for the services using the interface class names. PicoContainer is smart enough to traverse the interface implementation hierarchy and return the appropriate implementation as requested. (Note that the no container implementation discussed earlier also instantiated the implementation using the class names).

Since the TripPlanner is dependent upon AirlineAgency and CabAgency and the AirlineAgency and CabAgency is also dependent upon the AirlineAgency, PicoContainer will automatically instantiate the AirlineAgency first, pass its reference while constructing the CabAgency, and pass both the agency references while constructing the TripPlanner. This entire sequence gets executed upon the following request being made.

 
None.gif pico.getComponentInstanceOfType(TripPlanner. class );
None.gif

HiveMind

XML Declarations

In this example, both the interface and implementation classes are declared in the xml file along. Note that the dependency between the services is not stated in the xml file, but follows later in the code. A “service-point” element in the xml file documents the data necessary for each service.

 
None.gif xml version="1.0" ?>
None.gif
< module  id ="com.dnene.ditutorial.hivemind"  version ="1.0.0" >
None.gif  
< service-point  id ="AirlineAgency"  interface ="com.dnene.ditutorial.common.AirlineAgency" >
None.gif    
< create-instance  class ="com.dnene.ditutorial.common.impl.SimpleAirlineAgency" />
None.gif  
service-point>
None.gif  
<service-point id="CabAgency" interface="com.dnene.ditutorial.common.CabAgency">
None.gif    
<invoke-factory>
None.gif      
<construct class="com.dnene.ditutorial.common.impl.SetterBasedCabAgency"/>
None.gif    
invoke-factory>
None.gif   
service-point>
None.gif   
<service-point id="TripPlanner" interface="com.dnene.ditutorial.common.TripPlanner">
None.gif    
<invoke-factory>
None.gif      
<construct class="com.dnene.ditutorial.common.impl.SetterBasedTripPlanner"/>
None.gif    
invoke-factory>
None.gif   
service-point>  
None.gif
module>
None.gif
Container Initialization

This will automatically configure the registry based on the xml specified earlier.

 
None.gif Registry registry  =  RegistryBuilder.constructDefaultRegistry();
Dependency Declaration

The fact that the CabAgency requires the AirlineAgency is implicitly made clear to HiveMind by declaring the associated setter method.

 
None.gif public   void  setAirlineAgency(AirlineAgency airlineAgency)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
this.airlineAgency = airlineAgency;
ExpandedBlockEnd.gif}
Dependency Resolution

Again the dependency resolution is automatically performed when the reference to the trip planner is requested as shown in the following snippet. Hivemind is able to traverse the dependencies and instantiates AirlineAgency, CabAgency and TripPlanner in that order and invokes setAirlineAgency() on the cab agency, and setAirlineAgency() and setCabAgency() on the TripPlanner to resolve the dependencies across them.

 
None.gif registry.getService(TripPlanner. class );

Spring Framework

XML Declaration

The implementations that need to be instantiated are declared in the XML. Dependencies between services are also declared as “property” element. Spring framework will use this information to invoke the corresponding “setter” method.

 
None.gif < beans >
None.gif    
< bean  id ="AirlineAgency"  class ="com.dnene.ditutorial.common.impl.SimpleAirlineAgency"  singleton ="true" />
None.gif    
< bean  id ="CabAgency"  class ="com.dnene.ditutorial.common.impl.SetterBasedCabAgency"  singleton ="true" >
None.gif        
< property  name ="airlineAgency" >
None.gif            
< ref  bean ="AirlineAgency" />
None.gif        
property>
None.gif    
bean>
None.gif    
<bean id="TripPlanner" class="com.dnene.ditutorial.common.impl.SetterBasedTripPlanner" singleton="true">
None.gif        
<property name="airlineAgency">
None.gif            
<ref bean="AirlineAgency"/>
None.gif        
property>
None.gif        
<property name="cabAgency">
None.gif            
<ref bean="CabAgency"/>
None.gif        
property>
None.gif    
bean>
None.gif
beans>
None.gif
Container Initialization

The container initialization typically requires a reference to the xml file and an instantiation of the BeanFactory.

 
None.gif ClassPathResource res  =   new  ClassPathResource( " spring-beans.xml " );
None.gifBeanFactory factory 
=   new  XmlBeanFactory(res);
None.gif
Dependency Resolution

References to the services are retrieved based on the 'id' specified in the xml (not the interface class). Again all the services are implicitly instantiated in the appropriate order and the setters called to resolve their dependencies.

 
None.gif factory.getBean( " TripPlanner " );

XWork Implementation

XWork is actually a command pattern framework with some amount of DI functionality. It is in all likelihood the least sophisticated of all DI frameworks. However I have found it a useful framework especially in situations where one has already decided to use webwork, more specifically when injecting dependencies into the action classes.

XML Declaration
 
None.gif < components >
None.gif    
< component >
None.gif        
< scope > application scope>
None.gif        
<class>com.dnene.ditutorial.common.impl.SimpleAirlineAgencyclass>
None.gif        
<enabler>com.dnene.ditutorial.xwork.AirlineAgencyAwareenabler>
None.gif    
component>
None.gif    
<component>
None.gif        
<scope>applicationscope>
None.gif        
<class>com.dnene.ditutorial.common.impl.SetterBasedCabAgencyclass>
None.gif        
<enabler>com.dnene.ditutorial.xwork.CabAgencyAwareenabler>
None.gif    
component>
None.gif    
<component>
None.gif        
<scope>applicationscope>
None.gif        
<class>com.dnene.ditutorial.common.impl.SetterBasedTripPlannerclass>
None.gif        
<enabler>com.dnene.ditutorial.xwork.TripPlannerAwareenabler>
None.gif    
component>
None.gif
components>
None.gif

Note that XWork depends upon a notion called “enabler interface”. This is an interface which basically defines a setter method for the service. eg.

 
None.gif public   interface  CabAgencyAware
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
public void setCabAgency(CabAgency cabAgency);
ExpandedBlockEnd.gif}

None.gif

Unlike other containers, it requires us to define interfaces AirlineAgencyAware and CabAgencyAware and TripPlannerAware and implement them appropriately. To that extent this requires tends to be a bit more intrusive than other containers.

Container Initialization

This tends to be a little bit more complex

 
None.gif ComponentConfiguration config  =   new  ComponentConfiguration();
None.gifInputStream 
in   =  
None.gif    XworkHarness.
class .getClassLoader().getResourceAsStream( " components.xml " );
None.gifconfig.loadFromXml(
in );
None.gifcm 
=   new  DefaultComponentManager();
None.gifconfig.configure(cm,
" application " );
None.gif
Dependency Declaration

Classes declare their dependencies by implementing the necessary “enabler interfaces”. Thus each service declares its dependencies on other services by implementing the corresponding enabler interfaces. XWork uses the enabler interfaces to resolve the dependencies.

 
ExpandedBlockStart.gif ContractedBlock.gif public   class  SetterBasedCabAgency extends AbstractCabAgency implements AirlineAgencyAware  dot.gif dot.gif }
None.gif
Dependency Resolution

No surprises here. Usually a simple lookup from the component manager and all dependencies are automatically resolved.

 
None.gif cm.getComponent(TripPlannerAware. class );
None.gif
Xwork and Webwork

XWork is often used in conjunction with Webwork. In such scenarios, all you need to do is define and implement the enabler interfaces and create the xml file and use the component interceptor. The component manager interactions are all implemented by Webwork so that no coding to the ComponentManager is required so long as the dependencies are being resolved for the Action classes only. In addition it is very easy to setup different component managers for different scopes (e.g. Application, Session and Request).

Conclusion

As you can see, each DI framework does implement its features a little bit differently. In all fairness they could be used a little bit differently as well (eg. PicoContainer can be used with Setter based Dependency Injection, whereas Spring Framework can be used with Constructor based Dependency Injection). I have just chosen to document one of the supported mechanisms.

Its a little hard to be able to clearly declare one better than the other in a comprehensive sense. I guess you need to make up your mind what suits you the best in your context.

What should've become fairly clear with the above scenarios is that using any DI container substantially reduces the complexity of instantiating various services and resolving various dependencies between them.

Additional Features :

Note that I have primarily focused on only two features viz.

  • Resolving Service dependencies for clients and
  • Resolving Service dependencies within services themselves.

DI Frameworks do offer more functionality than just that – primarily in the area of managing the configuration and lifecycle of the services. You should read up the associated documentation to find more about these features. Dependency Injection alone is likely to suffice for most applications of simple to moderate complexities. However you would perhaps want to exploit the other features as your application (and services) start getting more complex. Some of the resources listed at the end are likely to be good starting points for getting more information :

Some Contentious Topics

There are a few areas where there has been a fair degree of debate (often between the authors/supporters of the specific DI frameworks). Some of these are :

Constructor Based Dependencies OR Setter Based Dependencies

This refers to whether it is better to declare dependencies using constructors or by using setter methods. I am not sure if there is a clear case that can be made in favor of one or the other. I have however observed that setter based dependencies do seem simpler to manage as as the overall dependency structures start getting more complex.

XML OR Java
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值