5

Having recently discovered dependency injection I'm now trying to get to grips with how often and how far to take it.

For example, let us say that I have a dialog which prompts a user for their registration details - first name, last name, telephone number, a serial number - that sort of thing. The data should be validated in various ways (e.g. that the first and last name aren't empty, that the serial number is a certain length). Once validated, it should be cached on the local machine and also sent to a registration server. The dialog should only close once all of those things succeed, or the user cancels.

So that's maybe four things we're trying to achieve here (responsibilities): UI, validation, local caching, sending data to a non-local server.

What is the dialog's responsibility and what should be injected? Obviously the dialog does the UI, but should validation, caching, and data sending all be injected? I'm thinking they do, otherwise the dialog class has to know about the logic behind the data fields in order to do validation, it has to know how and where to cache data, and also how to send the data somewhere. If so, this can lead to some hefty code at the caller's end (assuming we're doing injection via the constructor, which I think is preferable to setter functions), e.g.

MyDialog dlg(new validator(), new cacher(), new sender());

But perhaps that's ok? It does look a little alien to me right now after years of seeing code where things like the dialog do everything. But I can also see how this escalates quickly - what if there are all sorts of other small things it needs to do - how many things being injected becomes "too many"?

Please don't try to pick holes in the example scenario, I'm just using it for illustration. I'm more interested in the principle of DI and at what point you may be taking it too far.

1
  • There are many advantages to DI and it really depends on your project. I've worked on a project before where every single dependency was injected and on other projects where it was half and half. If you are writing unit tests then you would need DI so that you can pass mock objects when testing. Commented Mar 11, 2016 at 10:34

2 Answers 2

1

Well you certainly can do that. Injecting validation makes a lot of sense because then you can write unit tests around your validation code which don't have to fire up any GUI components in order to work. Injecting caching makes sense because then the dialog doesn't have to know anything about the caching system beyond its interface. Injecting a sender makes a lot of sense because your dialog doesn't have to have the foggiest idea where anything's going.

I have a habit of splitting things out quite heavily, because I like the single responsibility principle and I like writing code that's as pure as possible.

The problem is when you inject interfaces which are too big, so you no longer have any reasonable idea which bits of those interfaces the thing you're injecting into might actually need to call, and the interactions get complex and your unit tests start relying on precisely what gets done with the dependencies because you can't be bothered to mock out the whole interface when you know 75% of it won't be used.

So, do inject things which are clearly separate responsibilities, but make sure you design their interfaces in a suitably constrained manner. Classes can implement multiple interfaces simultaneously, so it's not like you can't slice up interfaces into small bits but implement them all with the same object if you want to. The dependent code never has to know!

As for when you're taking it too far... difficult to say really, but I don't think you get to that point until you're injecting something using an interface which adds nothing at all. I would always want to inject things which have side effects, because that's a massive aid to unit testing and to keeping things more reasonable. If you can split out business logic into pure classes and inject those you're going to have an awesome time writing unit tests for it, so that's probably worth doing.

I use a test something like this:

  1. does it do I/O and I'm not already inside an I/O providing class? Inject it.
  2. does it provide self-contained processing that I don't need to know the details of? Inject it.
  3. does it do something which isn't part of my single responsibility? Inject it.

Your mileage may vary.

Sign up to request clarification or add additional context in comments.

Comments

0

You have stumbled upon one of the confusing parts of DI that many struggle with. When using constructor injection, there is a natural tendency to push all of the top-level services into the entry point of the application.

This anti-pattern is called constructor over-injection. When a class has more than 3 or 4 dependencies, it is a code smell that it (in this case, your form) is likely violating the single responsibility principle. When this happens, you should consider creating facade services that combine related functionality.

Although your validator, cacher and sender are separate services, their functionality is clearly related. In fact, their functionality probably overlaps in several method calls.

For example, in this particular instance perhaps it would make sense to use a decorator pattern for cacher and sender, since you will be caching the data upon read from the sender (which I would consider a receiver as well - response/request) and you may also need to write data directly to the cacher and the sender from the UI so you don't need to reload the cache after writing your data to a persistent store.

public interface IDataService
{
    IData ReadData(int id);
    void WriteData(IData data);
}

public class Sender: IDataService
{
    public IData ReadData(int id)
    {
        // Get data from persistent store
    }

    public void WriteData(IData data)
    {
        // Write data to persistent store
    }
}

public class Cacher : IDataService
{
    public readonly IDataService innerDataService;
    public readonly ICache cache;

    public Cacher(IDataService innerDataService, ICache cache)
    {
        if (innnerDataService == null)
            throw new ArgumentNullException("innerDataService");
        if (cache == null)
            throw new ArgumentNullException("cache");

        this.innerDataService = innerDataService;
        this.cache = cache;
    }

    public IData ReadData(int id)
    {
        IData data = this.cache.GetItem(id);
        if (data == null)
        {
            data = this.innerDataService.ReadData(id);
            this.cache.SetItem(id, data);
        }
        return data;
    }

    public void WriteData(IData data)
    {
        this.cache.SetItem(id, data);
        this.innerDataService.WriteData(data);
    }
}

Usage

MyDialog dlg = new MyDialog(new validator(), new cacher(new sender()));

Depending on how the validation is required, it may also make sense to make your validation another IDataService.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.