Loosen Up
Tame Your Software Dependencies for More Flexible Apps
James Kovacs
This article discusses:
|
This article uses the following technologies: .NET Framework |
Code download available at:DependencyInjection2008_03.exe(5408 KB)
Contents
The Inner Dependency Problem
Dependency Inversion
Dependency Injection
Containers
Full-Fledged IoC Containers
Lifetime Management
Auto-Wiring Dependencies
Loosen Up for a Change
Few would disagree that striving for a loosely coupled design is a bad thing. Unfortunately, the software we typically design is much more tightly coupled than we intend. How can you tell whether your design is tightly coupled? You can use a static analysis tool like NDepend to analyze your dependencies, but the easiest way to get a sense of the coupling in your application is to try to instantiate one of your classes in isolation.
Pick a class from your business layer, such as an InvoiceService, and just copy its code into a new console project. Try to compile it. You'll likely be missing some dependencies, such as an Invoice, InvoiceValidator, and so forth. Copy those classes to the console project and try again. You'll likely discover other missing classes. When you are finally able to compile, you may find that you have a significant fraction of your codebase in the new project. It's like pulling on a loose thread and watching your entire sweater fall apart. Every class is directly or indirectly coupled to every other class in your design. Making changes in this type of system is difficult at best because a change in any one class can have a ripple effect throughout the rest of the system.
Now the point is not to eschew coupling completely, as that would be impossible. For instance:
string name = "James Kovacs";
I've coupled my code to the System.String class in the Microsoft® .NET Framework. Is that a bad thing? I would argue not. The likelihood of the System.String class changing in undesirable ways is very low, as is the likelihood that requirements will change in such a way that would necessitate modifying how I interact with System.String. So I am comfortable with that coupling. The point of this thought exercise is not to eliminate coupling but to be aware of it and to make sure you choose your couplings wisely.
Let's take another example of code often found in the data layer of many applications:
SqlConnection conn = new SqlConnection(connectionString);
Or even this one:
XmlDocument settings = new XmlDocument(); settings.Load("Settings.xml");
How confident are you that your data layer will only talk to SQL Server® or that you'll always load your app settings from an XML document named Settings.xml? The intent here is not to build an infinitely extensible but hugely complex and unusable generic framework. It is about reversibility. How easily can you change your mind with respect to design decisions? Do you have an application architecture that responds well to change?
Why am I worried about change? Because practically the only constant in this industry is change. Requirements change, technologies change, developers change, and the business changes. Are you putting yourself in a position to respond to those changes? By creating loosely coupled designs, software can better respond to inevitable, and many times unforeseeable, changes.
The Inner Dependency Problem
Let's examine a typical highly coupled design that you'll find in your average layered application architecture (see Figure 1). A simple layering scheme has a UI layer that talks to a service (or business) layer that talks to a repository (or data) layer. The dependencies between these layers flow the same way down. The repository layer is unaware of the service layer, which is unaware of the UI layer.
Figure 1** Typical Layered Architecture **
Sometimes you have a few more layers, such as a presentation or workflow layer, but the pattern of layers being aware only of the layer beneath them is fairly typical. Layers as coherent clusters of responsibility are a good design technique. Direct coupling of upper layers to lower layers, however, increases coupling and makes the application difficult to test.
Why am I concerned with testability? Because testability is a good barometer of coupling. If you can't easily instantiate a class in a test, you have a coupling problem. For example, the service layer is intimately familiar with and dependent on the repository layer. We cannot test the service layer in isolation of the repository layer. On a practical level, this means that most tests access the underlying database, file system, or network. This leads to a variety of problems, including slow tests and high maintenance costs.
Slow Tests If tests can execute strictly in memory, time per test can be in the millisecond range. If tests access external resources, such as a database, file system, or network, time per test is often 100 milliseconds or more. Considering that a typical project with good test coverage has hundreds or thousands of tests, this can mean the difference between running your tests in a few seconds versus minutes or hours.
Poor Error Isolation A failure in a data layer component often causes tests of upper-layer components to fail, too. Rather than having a few tests fail, which helps you quickly isolate the problem, you have hundreds of failing tests, which makes finding the problem difficult and more time-consuming.
High Maintenance Costs Most tests require some initial data. If those tests touch the database, you must ensure that your database is in a known state before each test. In addition, you must ensure that each test's initial data is independent of other tests' initial data, or you can run into test ordering problems where certain tests fail if run out of order. Maintaining the database in a known good state is time-consuming and error prone.
Additionally, if you need to change the implementation of a lower layer, you're often forced to modify the upper layers as well due to implicit or explicit dependencies that those layers have on the lower layer. Although you have layered the application, you have not achieved loose coupling.
Let's take a look at a concrete example—a service that accepts invoices (see Figure 2). For InvoiceService.Submit to be able to accept an invoice submission, it depends on an AuthorizationService, InvoiceValidator, and InvoiceRepository, which are created in the class's constructor. You cannot unit test InvoiceService without its concrete dependencies. This means that before you run your unit tests, you must ensure that the database is in a state such that you will not cause any primary or unique key violations when InvoiceRepository inserts the new invoice, nor must InvoiceValidator report any validation failures. You must also ensure that the user running the unit tests has the correct permissions so that AuthorizationService will permit the "Submit" operation.
Figure 2 Invoice Service
public class InvoiceService { private readonly AuthorizationService authoriazationService; private readonly InvoiceValidator invoiceValidator; private readonly InvoiceRepository invoiceRepository; public InvoiceService() { authoriazationService = new AuthorizationService(); invoiceValidator = new InvoiceValidator(); invoiceRepository = new InvoiceRepository(); } public ValidationResults Submit(Invoice invoice) { ValidationResults results; CheckPermissions(invoice, InvoiceAction.Submit); results = ValidateInvoice(invoice); SaveInvoice(invoice); return results; } private void CheckPermissions(Invoice invoice, InvoiceAction action) { if(authoriazationService.IsActionAllowed(invoice, action) == false) { throw new SecurityException( "Insufficient permissions to submit this invoice"); } } private ValidationResults ValidateInvoice(Invoice invoice) { return invoiceValidator.Validate(invoice); } private void SaveInvoice(Invoice invoice) { invoiceRepository.Save(invoice); } }
That is a tall order. If there are problems in any of these dependent components, either code or data errors, the InvoiceService tests will fail unexpectedly. Even if the test passes, the total execution time will be a few hundred milliseconds between setting up the correct data in the database, executing the tests, and cleaning up any data created by the test. Even if you amortize the cost of the setup and cleanup by grouping the tests into a batch and then running the scripts before and after the batch, the execution time is still a lot longer than if you could have run the tests in-memory.
Additionally, there is an even subtler problem here. Supposing that you wanted to add auditing support for the InvoiceRepository, you would be forced to create an AuditingInvoiceRepository or modify InvoiceRepository itself. Because of the coupling between InvoiceService and its child components, you don't have many options with regard to introducing new functionality into the system.
Dependency Inversion
You can decouple your higher-level component, InvoiceService, from its lower-level dependencies by interacting with your dependencies via interfaces rather than their concrete classes:
public class InvoiceService : IInvoiceService { private readonly IAuthorizationService authService; private readonly IInvoiceValidator invoiceValidator; private readonly IInvoiceRepository invoiceRepository; ... }
This simple change to using interfaces (or an abstract base class) means that you can substitute an alternate implementation for any of the dependencies. Rather than creating an InvoiceRepository, you can create an AuditingInvoiceRepository (assuming that AuditingInvoiceRepository implements IInvoiceRepository). This also means that you can substitute fakes or mocks during testing. This design technique is known as programming to contract.
The principle I'm applying in decoupling my high-level and lower-level components is called the dependency inversion principle. As Robert C. Martin says in his article on the topic (objectmentor.com/resources/articles/dip.pdf), "High-level modules should not depend on low-level modules. Both should depend on abstractions."
In this case, both InvoiceService and InvoiceRepository now depend on the abstraction provided by IInvoiceRepository. However, I haven't completely solved the problem; I merely shifted it. Although the concrete implementations only depend on an interface, the question remains how the concrete classes "find" each other.
InvoiceService still needs concrete implementations of its dependencies. You could simply instantiate those dependencies in InvoiceService's constructor, but you wouldn't be much better off than before. If you wanted to use an AuditingInvoiceRepository, you would still have to modify InvoiceService to instantiate an AuditingInvoiceRepository. Additionally, you would need to modify every class that's dependent on IInvoiceRepository to instantiate an AuditingInvoiceRepository instead. There is no easy way to globally swap AuditingInvoiceRepository for InvoiceRepository.
One solution is to use a factory for creating IInvoiceRepository instances. This provides a central place to switch to AuditingInvoiceRepository by simply changing the factory method. Another name for this technique is service location, and the factory class responsible for managing instances is called a service locator:
public InvoiceService() { this.authorizationService = ServiceLocator.Find<IAuthorizationService>(); this.invoiceValidator = ServiceLocator.Find<IInvoiceValidator>(); this.invoiceRepository = ServiceLocator.Find<IInvoiceRepository>(); }
The functionality within the ServiceLocator could be based on data read from a configuration file or database, or it could be directly wired up with code. In either case, you now have centralized object creation for your dependencies.
Unit testing of isolated components can be accomplished by configuring the service locator with fake or mock objects rather than the real implementations. So for example, during testing, ServiceLocator.Find<IInvoiceRepository> could return a FakeInvoiceRepository, which assigned a known primary key to the invoice when it was saved but did not actually save the invoice to the database. You can eliminate your complex setup and teardown of the database and return known data from your fake dependencies (see the sidebar, "Is Faking Dependencies Wise?").
Service location has a few disadvantages, however. First, dependencies are hidden in the higher-level class. You cannot tell that InvoiceService depends on AuthorizationService, InvoiceValidator, or InvoiceRepository from its public signature—only by examining its code.
If you need to supply different concrete types for the same interface, you must resort to overloaded Find methods. This requires you to make decisions about whether an alternate type is required when you're implementing the factory class. For example, you can't reconfigure the ServiceLocator to substitute an AuditingInvoiceRepository at deployment time for specific IInvoiceRepository requests. But even with these disadvantages, service location is easy to understand and better than hardcoding your dependencies.
Dependency Injection
Is Faking Dependencies Wise?
You might be wondering whether or not it's dangerous to "fake" your dependencies. Won't you get false successes? Well, the reality is that you should already have tests that verify the proper functioning of your dependencies, like the real InvoiceRepository. These tests should talk to the actual database and verify that InvoiceRepository is behaving correctly.
If you know that InvoiceRepository.Save works, why do you need to test it again with every single test that depends on InvoiceRepository? You'll just slow down your higher-level tests by connecting to the database, and if there is a problem with InvoiceRepository, not only will your InvoiceRepository tests fail but so will your InvoiceService tests and any other components that depend on InvoiceRepository.
If an InvoiceService test failed, but InvoiceRepository did not, it means that you missed a test on InvoiceRepository. This type of defect is better caught by integration tests, which test the component with its concrete dependencies. These run more slowly but are run less frequently than unit tests with faked/mocked dependencies.
Now assuming that InvoiceRepository works because its unit tests pass, you have two choices. You can create and maintain complex scripts to ensure that the data in the database is correct so that InvoiceRepository returns the expected data for each InvoiceService test. Or, you can create a fake or mock implementation of InvoiceRepository that returns the expected data. This second option is much easier and works well in practice.
When unit testing a higher-level component, you want to provide fake or mock implementations for its dependencies. But rather than configuring a service locator with the fakes or mocks and then having the higher-level component look them up, you could just pass the dependencies directly to the higher-level component via a parameterized constructor. This technique is known as dependency injection. An example is shown in Figure 3.
Figure 3 Dependency Injection
[Test] public void CanSubmitNewInvoice() { Invoice invoice = new Invoice(); ValidationResults validationResults = new ValidationResults(); IAuthorizationService authorizationService = mockery.CreateMock<IAuthorizationService>(); IInvoiceValidator invoiceValidator = mockery.CreateMock<IInvoiceValidator>(); IInvoiceRepository invoiceRepository = mockery.CreateMock<IInvoiceRepository>(); using(mockery.Record()) { Expect.Call(authorizationService.IsActionAllowed( invoice, InvoiceAction.Submit)).Return(true); Expect.Call(invoiceValidator.Validate(invoice)) .Return(validationResults); invoiceRepository.Save(invoice); } using(mockery.Playback()) { IInvoiceService service = new InvoiceService(authorizationService, invoiceValidator, invoiceRepository); service.Submit(invoice); } }
In this example, I'm creating mock objects for InvoiceService's dependencies and then passing them to the InvoiceService constructor. (For more information on mock object frameworks, see Mark Seemann's "Unit Testing: Exploring the Continuum of Test Doubles" at msdn.microsoft.com/msdnmag/issues/07/09/MockTesting.) In sum, you specify the behavior of the InvoiceService by defining how it will interact with the mocks rather than verifying the state of the InvoiceService after the test is run.
By using dependency injection, you can easily supply your higher-level components with their dependencies in unit tests. However, you still have the problem of how to locate a class's dependencies outside of a unit test—either while running the application or in an integration test. It would be foolish to expect the UI layer to provide the service layer with its dependencies or the service layer to supply the repository layer with its dependencies. You would end up with an even worse problem than what you started with. But let's suppose that the UI layer was responsible for supplying the service layer with its dependencies:
// Somewhere in UI Layer InvoiceSubmissionPresenter presenter = new InvoiceSubmissionPresenter( new InvoiceService( new AuthorizationService(), new InvoiceValidator(), new InvoiceRepository()));
As you can see, the UI would have to be aware of not just its own dependencies but also the dependencies of its dependencies, ad infinitum, down to the data layer. This is obviously not an ideal situation. The easiest way to solve this dilemma is with a technique that we'll call the poor man's dependency injection.
Poor man's dependency injection uses the default constructor of the higher-level component to supply the dependencies:
public InvoiceService() : this(new AuthorizationService(), new InvoiceValidator(), new InvoiceRepository()) { }
Notice how I'm delegating to the most overloaded constructor. This ensures that the class's initialization logic is identical regardless of which constructor is used to create an instance. The only place the class is coupled to concrete dependencies is through the default constructor. The class remains testable because you still have the overloaded constructor that allows you to supply the class's dependencies during unit testing.
Containers
Now it's time to introduce inversion of control (IoC) containers, which provide a central place to manage dependencies. In reality, a container is nothing more than a fancy dictionary of interfaces versus implementing types. In its simplest form, an IoC container is just a service locator by a different name. Later on, I'll examine how a container can do much more than just service location.
Getting back to the problem at hand, you would like to completely decouple InvoiceService from concrete implementations of its dependencies. Like all problems in software, you can solve it by adding another layer of indirection. You introduce the notion of a dependency resolver, which maps an interface to a concrete implementation. You then use a generic method that takes an interface T and returns a type implementing that interface:
public interface IDependencyResolver { T Resolve<T>(); }
Let's implement the SimpleDependencyResolver, which uses a dictionary to store mapping information between interfaces and objects implementing those interfaces. We need a way to initially populate the dictionary, which is done by the Register<T>(object obj) method (see Figure 4). Note that the Register method does not need to be on the IDependencyResolver interface because only the creator of the SimpleDependencyResolver will register dependencies. Typically this is done by a helper class called in the Main method during application startup.
Figure 4 SimpleDependencyResolver
public class SimpleDependencyResolver : IDependencyResolver { private readonly Dictionary<Type, object> m_Types = new Dictionary<Type, object>(); public T Resolve<T>() { return (T)m_Types[typeof(T)]; } public void Register<T>(object obj) { if(obj is T == false) { throw new InvalidOperationException( string.Format("The supplied instance does not implement {0}", typeof(T).FullName)); } m_Types.Add(typeof(T), obj); } }
How does CompanyService find the SimpleDependencyResolver so that it can locate its dependencies? We could pass an IDependencyResolver into every class that needs it, but this quickly becomes cumbersome. The easiest solution is to place the configured SimpleDependencyResolver instance into a globally accessible location, which you can do using the static gateway pattern. (You could have also used the singleton pattern, but singletons are notoriously difficult to test. They are one of the biggest causes of tightly coupled code that's difficult to test, as they're little more than global variables in disguise. Avoid them if possible.)
Let's take a look at the static gateway, which I'll call IoC. (Another possible name is DependencyResolver, but IoC is shorter.) The static methods on IoC match the methods on IDependencyResolver. (Note that IoC doesn't implement IDependencyResolver because static classes cannot implement interfaces.) There is also an Initialize method that accepts the real IDependencyResolver. The IoC static gateway simply forwards all Resolve<T> requests to the configured IDependencyResolver:
public class IoC { private static IDependencyResolver s_Inner; public static void Initialize(IDependencyResolver resolver) { s_Inner = resolver; } public static T Resolve<T>() { return s_Inner.Resolve<T>(); } }
During application startup, you initialize IoC with the configured SimpleDependencyResolver. You can now replace poor man's dependency injection with IoC.Resolve in the default constructor:
public InvoiceService() : this(IoC.Resolve<IAuthorizationService>(), IoC.Resolve<IInvoiceValidator>(), IoC.Resolve<IInvoiceRepository>()) { }
Note that you do not need to synchronize access to the inner IDependencyResolver, as it is only read but never updated, after application startup.
The IoC class provides another benefit—it acts as an anticorruption layer in your application. If you want to use a different IoC container, you simply need to implement an adapter that implements IDependencyResolver. Even though IoC will be used extensively throughout your application, you have not coupled yourself to any particular container.
Full-Fledged IoC Containers
A simple IoC container such as SimpleDependencyResolver enables you to stitch together loosely coupled components. However, it lacks many of the features present in full-fledged IoC containers, including:
- Wider configuration options, such as XML, code, or script
- Lifetime management, such as singleton, transient, per-thread, or pooled
- Auto-wiring dependencies
- The ability to wire in new functionality
Let's discuss each of these features in more depth. I will use Castle Windsor, a widely used open source IoC container, as a concrete example. Many containers can be configured through an external XML file. For example, Windsor can be configured as follows:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <components> <component id="Foo" service="JamesKovacs.IoCArticle.IFoo, JamesKovacs.IoCArticle" type="JamesKovacs.IoCArticle.Foo, JamesKovacs.IoCArticle"/> </components> </configuration>
XML configuration is advantageous, as it can be modified without recompiling the application, though it does often require an application restart for the changes to be applied. It is not without its disadvantages though, as XML configuration tends to be very verbose; errors aren't detected until runtime; and generic types are declared using the CLR's backtick notation, rather than the more familiar C# generic notation. (Company.Application.IValidatorOf<Invoice> is written as Company.Application.IValidatorOf`1[[Company.Application.Invoice, Company.Application]], Company.Application.)
In addition to XML, you can configure Windsor using C# or any other Microsoft .NET Framework-compliant language. If you isolated your configuration code in a separate assembly, changing configuration would mean simply recompiling the configuration assembly and restarting the application.
You can script Windsor configuration using Binsor, which is a domain-specific language (DSL) built specifically for configuring Windsor. Binsor allows you to write your configuration files in Boo. (Boo is a statically typed CLR language that focuses on language and compiler extensibility, thus making it well suited for writing DSLs.) In Binsor, the previous XML configuration file could be rewritten as:
import JamesKovacs.IoCArticle Component("Foo", IFoo, Foo)
Things get more interesting when you realize that Boo is a full-fledged programming language, which means that you can use Binsor to automatically register types in Windsor without having to manually add component registrations, as you would with XML-based configuration:
import System.Reflection serviceAssembly = Assembly.Load("JamesKovacs.IoCArticle.IoCContainer") for type in serviceAssembly.GetTypes(): continue if type.IsInterface or type.IsAbstract or type.GetInterfaces().Length == 0 Component(type.FullName, type.GetInterfaces()[0], type)
Even if you're not familiar with Boo, the intent of the code should be clear. Simply by adding a new service to the JamesKovacs.IoCArticle.Services namespace, that service is automatically registered as the default implementation for its service interface. Let's say I create the following class:
public class AuthorizationService : IAuthorizationService { ... }
If any other class declares a dependency on IAuthorizationService by including it as a parameter to its constructor, Binsor will automatically wire it up without having to specify that dependency explicitly in a configuration file! You can find more information on Binsor at ayende.com/blog/2898/binsor-2-0 and Boo at boo.codehaus.org.
Lifetime Management
The SimpleDependencyResolver always returns the same instance that was registered for an interface, which effectively makes that instance a singleton. You could modify the SimpleDependencyResolver to register a concrete type rather than an instance. Then you could use different factories to create instances of the concrete type. A singleton factory would always return the same instance. A transient factory would always return a new instance. A per-thread factory would maintain one instance per requesting thread.
Your instancing strategy is only limited by your imagination. This is exactly what Windsor provides. By applying attributes in the XML configuration file, you can change which type of factory is used to create instances of a particular concrete type. By default, Windsor uses singleton instances. If you wanted to return a new Foo every time an IFoo was requested from the container, you would simply change the configuration to:
<component id="Foo" service="JamesKovacs.IoCArticle.IFoo, JamesKovacs.IoCArticle" type="JamesKovacs.IoCArticle.Foo, JamesKovacs.IoCArticle" lifestyle="transient"/>
Auto-Wiring Dependencies
Auto-wiring of dependencies means that the container can examine the dependencies of the requested type and create those dependencies without the developer having to supply a default constructor:
public InvoiceService(IAuthorizationService authorizationService, IInvoiceValidator invoiceValidator, IInvoiceRepository invoiceRepository) { ... }
When a client requests an IInvoiceService from the container, the container will notice that concrete type requires concrete implementations of IAuthorizationService, IInvoiceValidator, and IInvoiceRepository. It will look up the appropriate concrete types, resolve any dependencies that they might have, and construct them. It will then use these dependencies to create the InvoiceService. Auto-wiring obviates the need to maintain default constructors, thus simplifying code and removing many classes' dependency on the IoC static gateway.
By coding to contracts rather than concrete implementations and using a container, your architecture will be much more flexible and amenable to change. How could you implement configurable audit logging for the InvoiceRepository? In a tightly coupled architecture, you would have to modify InvoiceRepository. You would also need some application configuration setting to indicate whether audit logging was turned on.
In a loosely coupled architecture, is there a better way? You could implement an AuditingInvoiceRepositoryAuditor, which implements IInvoiceRepository. The auditor only implements auditing functionality and then delegates to the real InvoiceRepository, which is supplied in its constructor. This pattern is known as a decorator (see Figure 5).
Figure 5 Using the Decorator Pattern
public class AuditingInvoiceRepository : IInvoiceRepository { private readonly IInvoiceRepository invoiceRepository; private readonly IAuditWriter auditWriter; public AuditingInvoiceRepository(IInvoiceRepository invoiceRepository, IAuditWriter auditWriter) { this.invoiceRepository = invoiceRepository; this.auditWriter = auditWriter; } public void Save(Invoice invoice) { auditWriter.WriteEntry("Invoice was written by a user."); invoiceRepository.Save(invoice); } }
To turn on auditing, you configure the container to return an InvoiceRepository decorated with an AuditingInvoiceRepository when asked for an IInvoiceRepository. Clients will be none the wiser since they are still talking to an IInvoiceRepository. This approach has a number of benefits:
- Since InvoiceRepository has not been modified, there is no chance that you would have broken its code.
- AuditingInvoiceRepository can be implemented and tested independently of InvoiceRepository. Thus you can ensure that auditing works correctly regardless of whether you have an actual database.
- You can compose multiple decorators for auditing, security, caching, or other purposes without increasing the complexity of InvoiceRepository. In other words, the decorator approach in a loosely coupled system scales better when adding new functionality.
- Containers provide an interesting application extensibility mechanism. There is no reason that AuditingInvoiceRepository had to be implemented in the same assembly as InvoiceRepository or IInvoiceRepository. It could easily have been implemented in a third-party assembly that was referenced by the configuration file.
Loosen Up for a Change
Even though your software architecture is layered, you may still have tight coupling between your layers, which can impede testing and evolution of your app. But you can decouple the design. Through the use of dependency inversion and dependency injection, you can reap the benefits of coding to contract rather than concrete implementation. By introducing an inversion of control container, you can increase the flexibility of your architecture. In the end, your loosely coupled design will be more responsive to change.
James Kovacs is an independent architect, developer, trainer, and jack-of-all trades living in Calgary, Alberta, specializing in agile development using the .NET Framework. He is a Microsoft MVP for Solutions Architecture and received his master's degree from Harvard University. James can be reached at [email protected].