Login | Register   
RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


The Zen of Inversion of Control : Page 4

Make your classes more useful by minimizing how concretely they specify external dependencies.


The Rise (and Fall) of the Factory Method

One way out of this problem is to refactor the GUI logic so that Scanner instantiation and configuration is done somewhere else. The home for this logic would be called a factory method. I am introducing a level of indirection here. The logic in the GUI class would call an intermediate "broker" or "factory" method. The factory method would instantiate the Scanner class, configure it as needed, and return it to the GUI class. The GUI class would invoke the Scanner Process method with the path to the initial file directory. All of the "just one more interface" changes would happen in the factory method rather than in the GUI class. No more muddy boot prints on the GUI carpet.

This arrangement is clearly better than having the GUI class handle the instantiation. You can nail the GUI class down and say with some truth that it is truly done. The GUI class has a single responsibility, as does the factory method. However, you're not yet in a state of nirvana. The factory method has too much knowledge of the innards of the Scanner class and of the classes that the Scanner class uses. I've focused on the Scanner class and how to inject dependencies into that class. It's time to extend that awareness to the cast of dependent classes. It is quite possible that each of the dependent classes also have dependencies that need injection treatment. Rinse and repeat for as many levels of dependencies that your program has. In a real-world system, you could have several dozen such dependencies, spread over a dozen different assemblies/components.

You can make it even more complicated. Suppose that you have three different formatting classes, F1, F2, and F3, each of which implements the IFormatOutput interface. Suppose that F1 is self-contained and has no dependencies, that F2 needs dependencies Q1 and Q2, and F3 needs dependencies Z1, Z2, and Z3. If this is not complicated enough for you, suppose that each of the classes Q1, Q2, Z1, Z2, and Z3 have different dependencies. Trust me on this! I can keep going until your head explodes. The factory method has become way too complicated. The GUI class is content, but now the factory method calls out, "There must be a better way!"

A Primitive Broker

There is a better way! Let's take a look at what the factory method (or the GUI class before it) is trying to do. It is instantiating the classes that implement the interfaces and it is injecting those interfaces into the classes that have dependencies on these interfaces. Suppose that you split those responsibilities apart: one for instantiation and a second for injection.

Let's look at the injection responsibility first. Each class that has a dependency knows that it has a dependency. The overall goal is not to pretend that the dependency does not exist but to keep the class from knowing the details of how the dependency is satisfied. What if the class with the dependency could just "ask" for an instance of a class that implements the interface—and just get it without having to know how it was instantiated? I put the "ask" action in quotes to suggest a metaphorical inquiry rather than a necessarily explicit request. For example, the constructor parameters described above could easily be interpreted as requests for the various interfaces. The class could mark certain setter methods to indicate that the setter represented a dependency. Finally, the class could make an explicit request. Each approach has both positive and negative aspects, but all serve to decouple a class with dependencies from the classes that might be used to satisfy those dependencies.

Let's assume that each class makes an explicit request for each dependent interface to a "broker" class that has the responsibility of instantiating objects that implement the specified interface. A very primitive broker might consist of a big case/switch statement that instantiates a hard-coded class for each type of interface. This is admittedly crude but still it is effective. A little less primitive broker might use reflection to find the parameters in the constructor or the setters of the newly instantiated class that are marked as dependencies and recursively call itself to satisfy these dependencies. You could write this in roughly 100 lines of code and debug in a matter of hours.

While not a perfect solution, this does go a long way toward simplifying the logic. Each class is responsible for injecting its own direct dependencies. None of the classes needs to know how to instantiate its dependencies. The broker class needs to know how to instantiate classes to satisfy the various interfaces but does not need to know who might need the dependencies. The broker certainly does not need to know about the hierarchy of dependencies.

Remember that this is a technique, not a technology. There is nothing to download, nothing to install, and nothing to add to the references. All that you have to do is to change the way that you think about dependencies. Oh yes, there is the business about disciplining the development process to prevent direct instantiation of dependent classes that you might want to switch out sometime in the future. Simple!

A Clever Broker

Even though you have decoupled the Scanner class from the means of satisfying its dependencies, there are other problems to solve. The primitive broker is still hard-wired. The application is still strongly coupled to the broker. This becomes a problem if you want to be able to instantiate different implementations of the various interfaces depending upon the execution context.

In addition to production, one of the most obvious contexts is unit testing. In unit testing you want to isolate the logic of each class so that you can test the functionality of that class without all of the baggage of the dependent classes. Since the Scanner class is dependent upon interfaces (rather than concrete classes), the unit test context could satisfy these dependencies with "fake" versions of the interfaces. The unit test context could do that if the Scanner class was not tightly coupled to a specific broker.

So here you want to insert "yet another layer of indirection" that will allow you to further decouple the Scanner class from the broker. The first thing that you have to do is to introduce another interface:

Public Interface IResolveDependencies Sub RegisterImplementationOf(Of T)( _ ByVal component As T) Function GetImplementationOf(Of T)() As T End Interface

This interface defines the key functions of the broker. The RegisterImplementationOf method allows the caller to supply an instance of a class that implements a specified interface. The GetImplementationOf method allows the caller to retrieve an instance of a class that implements the specified interface.

Next, you must create a "container" to hold the current broker. Different environments would store different versions of the broker in this container. Each class that wanted to retrieve one of its dependencies would ask the container to resolve the dependency by calling the GetImplementationOf method of the container. The container, in turn, would pass the request on to the inner broker.

Public Module DependencyResolver Private _resolver As IResolveDependencies Public Sub RegisterResolver(ByVal _ resolver As IResolveDependencies) _resolver = resolver End Sub Public Sub RegisterImplementationOf(Of T)( _ ByVal component As T) _resolver.RegisterImplementationOf( _ Of T)(component) End Sub Public Function GetImplementationOf( _ Of T)() As T Return _resolver.GetImplementationOf( Of T)() End Function End Module

Each environment (e.g., production, Q/A, and Unit Test) would call the RegisterResolver method to supply the particular broker that would handle that environment.

Author's Note: I make no claim to having invented this technique. James Kovacs wrote a very nice MSDN Magazine article on how to use this technique.

Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.