A friend of mine recently watched my Dependency Injection On-Ramp course on Pluralsight and sent me a few questions. In my experience, if one person has a question, there are probably other people with the same question. So, here are the questions and answers.
Note: these same questions/answers apply to my live presentation: Dependency Injection: A Practical Introduction.
Interfaces vs. Abstract Classes
Q: I see that interfaces are used with DI. Is there a reason abstract classes aren't used? Just wondering.
In the example, we create a repository interface so that we can add a layer of abstraction. Then we use constructor injection to get the repository into our view model class. Here's that code:
With this code, our view model does not need to know anything about the concrete repository types (so we can eliminate the coupling between the view model and those repositories). But there is nothing about Dependency Injection that limits us to using interfaces. In fact, in another part of the example, we inject a concrete class into our view:
Here we are injecting a concrete view model (MainWindowViewModel) into our view (MainWindow). For this, we do not get the same loose-coupling as we do with the repository example above. But Dependency Injection here still gives us some good advantages, namely adhering to the separation of concerns (SoC) and single responsibility principle (SRP). These principles often go hand in hand.
In this case, even though we need a compile time reference to the view model class, we have still freed the view from the responsibility of creating and managing the lifetime of the view model object. This means that creating the view model is not the responsibility of the view (separation of concerns) and the view can concentrate on what it does best -- providing the user interface for the application (it's single responsibility).
I guess I haven't answered the question yet (well, I've sort of answered it).
A: We can use whatever abstractions we like with Dependency Injection.
The type of abstraction is completely separate from dependency injection itself. That means we can use interfaces, abstract classes, or base classes. To make the decision, we'll just look at the pros and cons of using these types just like we do in our other code.
Specifically, interfaces are good where we do not have any shared code in the implementations (thus, no duplication). We can implement as many interfaces as we want, so we are not limited by single inheritance when we apply interfaces. And we're free to descend from another class if we want.
Abstract classes are good where we have shared code in the implementations. This lets us put the shared code into the abstract class and all of the child classes can use it (thus, no duplication). We are limited by single inheritance, so we cannot descend from an abstract class and another class at the same time.
If you want more information on how to make a decision between interfaces and abstract classes, you can check out my C# Interfaces course on Pluralsight, or reference the materials for my live presentation: IEnumerable, ISaveable, IDontGetIt: Understanding .NET Interfaces.
Client Applications vs. Server Applications
Q: All of your examples are with a client app. Can this be used server side, too? I'm assuming yes.
In the examples, we use Dependency Injection to break the coupling between the layers in a WPF (desktop client) application. Specifically, we break the coupling between our view model (presentation) and repository (data access) so that we can more easily extend and test our application.
And we can get these same benefits server-side as well. So, the answer is...
A: Yes, of course.
Another of my presentations uses an ASP.NET MVC project as an example: Shields Up! Defensive Coding in C#.
This example has a custom controller factory that uses Ninject. Here's that code:
The details aren't all that important here. What we're doing is creating a custom ASP.NET MVC controller factory. This class uses Ninject to inject the repositories for the objects in our application. This allows us to easily swap out the repository (similar to our WPF example above).
For this particular example, I'm showing the dangers of SQL injection. So I have a SQL repository that is vulnerable to SQL injection and another repository that uses parameterized SQL (and thus, is not vulnerable to this specific attack). Using the IRepository abstraction along with Ninject allows me to swap this out very quickly in my demo.
Okay, so you're probably saying that isn't a "real" server application (although, the code is running on the IIS server, not the client machine). So, here's another example. This is taken from a lab for an upcoming Education Day (more info on this soon).
This is an application that is designed to run inside an automated job. It loads a file, parses the data, and outputs any errors to a log file. In this case, we want to isolate the data parsing code to make it easier to unit test. So instead of having a logger hard-coded in the parser, we use constructor injection:
This lets us use the file logger when we're running this in production on our server, but it's easy to swap out the logger for a mock during our unit testing. (In fact, this particular lab is all about how to make code easier to test.)
So we can get the benefits from Dependency Injection (extensibility, testability, maintainability, late binding, parallel development) whether we are working with client applications or server-side code.
More Advanced Ninject
Q: How would you use the CachingRepository decorator with a container such as Ninject?
In the example code, we start out pretty simply: we manually wire our application pieces together in the composition root of our application. Here's the example using the repository that uses a WCF SOAP service:
This is pretty straight forward since we're using constructor injection all the way down. We create a repository, pass it to the view model, and then pass the view model to the view.
Using a container eliminates some of these steps. Rather than wiring everything together, we just create a binding between the abstraction (IPersonRepository) and the concrete type (PersonServiceRepository):
So we tell Ninject that if someone asks for an IPersonRepository, we want to return a PersonServiceRepository (and the InSingletonScope means we only want one of these that gets re-used throughout our application). We let the dependency injection container (Ninject) take care of figuring everything else out.
But in the example, we also created this really cool caching repository. This wraps any existing repository and adds a client-side cache to it. This was pretty easy to wire up manually:
We start with our concrete repository (PersonServiceRepository), pass that to our caching repository, then pass that to our view model, then pass that to our view. We just snap the pieces together in a different order.
But what about using a container? We can't just associate the IPersonRepository with the CachingPersonRepository because it also needs an IPersonRepository as a parameter. It seems like we'd end up in an endless loop.
A: Ninject (and other DI containers) provide lots of advanced features to handle complex situations.
When we configure our container, we just need to supply a little more information:
The first part of the configuration is the same (Bind...To...InSingletonScope), but then we add some further instructions. In this case, we specify that the CachingPersonRepository has a constructor parameter called "personRepository". When Ninject comes across this parameter, it should supply a PersonServiceRepository.
This has the same effect as our manual code: it takes a PersonServiceRepository and wraps it in the caching repository.
What this shows is that it pays to spend some time with a particular DI container. Learn the ins-and-outs of what is available. There are tons of options.
Wrap Up
Dependency Injection is still one of my favorite topics. It's a powerful tool that can give us some good advantages in many situations. Like every tool, it is not appropriate everywhere. But if we understand what it is good for, then we can make sure we use it in the right situations.
I'm always open to answering questions about the topics that I speak about. There is no such thing as a stupid question. This is how we learn.
Happy Coding!
Thanks Jeremy, super answer
ReplyDelete