Tom Quarendon
Tom Quarendon

Reputation: 5716

Dependency inversion and pervasive dependencies

I'm trying to get dependency inversion, or at least understand how to apply it, but the problem I have at the moment is how to deal with dependencies that are pervasive. The classic example of this is trace logging, but in my application I have many services that most if not all code will depend on (trace logging, string manipulation, user message logging etc).

None of the solutions to this would appear to be particularly palatable:

Does anyone have any other suggestions for how to structure these kinds of dependencies, or indeed any experience of any of the above solutions?

Note that I don't have a particular DI framework in mind, in fact we're programming in C++ and would be doing any injection manually (if indeed dependencies are injected).

Upvotes: 5

Views: 1245

Answers (3)

Robert Martin
Robert Martin

Reputation: 2933

class Base {
 public:
  void doX() {
    doA();
    doB();
  }

  virtual void doA() {/*does A*/}
  virtual void doB() {/*does B*/}
};

class LoggedBase public : Base {
 public:
  LoggedBase(Logger& logger) : l(logger) {}
  virtual void doA() {l.log("start A"); Base::doA(); l.log("Stop A");}
  virtual void doB() {l.log("start B"); Base::doB(); l.log("Stop B");}
 private:
  Logger& l;
};

Now you can create the LoggedBase using an abstract factory that knows about the logger. Nobody else has to know about the logger, nor do they need to know about LoggedBase.

class BaseFactory {
 public:
  virtual Base& makeBase() = 0;
};

class BaseFactoryImp public : BaseFactory {
 public:
  BaseFactoryImp(Logger& logger) : l(logger) {}
  virtual Base& makeBase() {return *(new LoggedBase(l));}
};

The factory implementation is held in a global variable:

BaseFactory* baseFactory;

And is initialized to an instance of BaseFactoryImp by 'main' or some function close to main. Only that function knows about BaseFactoryImp and LoggedBase. Everyone else is blissfully ignorant of them all.

Upvotes: 2

zumalifeguard
zumalifeguard

Reputation: 9016

The Dependency Inversion principle is part of the SOLID Principles and is an important principle for among other things, to promote testability and reuse of the higher-level algorithm.

Background: As indicated on Uncle Bob's web page, Dependency Inversion is about depend on abstractions, not on concretions.

In practice, what happens is that some places where your class instantiates another class directly, need to be changed such that the implementation of the inner class can be specified by the caller.

For instance, if I have a Model class, I should not hard code it to use a specific database class. If I do that, I cannot use the Model class to use a different database implementation. This might be useful if you have a different database provider, or you may want to replace the database provider with a fake database for testing purposes.

Rather than the Model doing a "new" on the Database class, it will simply use an IDatabase interface that the Database class implements. The Model never refers to a concrete Database class. But then who instantiates the Database class? One solution is Constructor Injection (part of Dependency Injection). For this example, the Model class is given a new constructor that takes an IDatabase instance which it is to use, rather than instantiate one itself.

This solves the original problem of the Model no longer references the concrete Database class and uses the database through the IDatabase abstraction. But it introduces the problem mentioned in the Question, which is that it goes against Law of Demeter. That is, in this case, the caller of Model now has to know about IDatabase, when previously it did not. The Model is now exposing to its clients some detail about how it gets its job done.

Even if you were okay with this, there's another issue that seems to confuse a lot of people, including some trainers. There's as an assumption that any time a class, such as Model, instantiates another class concretely, then it's breaking the Dependency Inversion principle and therefore it is bad. But in practice, you can't follow these types of hard-and-fast rules. There are times when you need to use concrete classes. For instance, if you're going to throw an exception you have to "new it up" (eg. threw new BadArgumentException(...)). Or use classes from the base system such as strings, dictionaries, etc.

There's no simple rule that works in all cases. You have to understand what it is that you're trying to accomplish. If you're after testability, then the fact that the Model classes references the Database class directly is not itself a problem. The problem is the fact that the Model class has no other means of using another Database class. You solve this problem by implementing the Model class such that it uses IDatabase, and allows a client to specify an IDatabase implementation. If one is not specified by the client, the Model can then use a concrete implementation.

This is similar to the design of the many libraries, including C++ Standard Library. For instance, looking at the declaration std::set container:

template < class T,                        // set::key_type/value_type
           class Compare = less<T>,        // set::key_compare/value_compare
           class Alloc = allocator<T> >    // set::allocator_type
           > class set;

You can see that it allows you to specify a comparer and an allocator, but most of the time, you take the default, especially the allocator. The STL has many such facets, especially in the IO library where detailed aspects of streaming can be augmented for localization, endianness, locales, etc.

In addition to testability, this allows the reuse of the higher-level algorithm with entirely different implementation of the classes that the algorithm internally uses.

And finally, back to the assertion I made previously with regard to scenarios where you would not want to invert the dependency. That is, there are times when you need to instantiate a concrete class, such as when instantiating the exception class, BadArgumentException. But, if you're after testability, you can also make the argument that you do, in fact, want to invert dependency of this as well. You may want to design the Model class such that all instantiations of exceptions are delegated to a class and invoked through an abstract interface. That way, code that tests the Model class can provide its own exception class whose usage the test can then monitor.

I've had colleagues give me examples where they abstract instantiation of even system calls, such as "getsystemtime" simply so they can test daylight savings and time-zone scenarios through their unit-testing.

Follow the YAGNI principle -- don't add abstractions simply because you think you might need it. If you're practicing test-first development, the right abstractions becomes apparent and only just enough abstraction is implemented to pass the test.

Upvotes: 2

Steven
Steven

Reputation: 172835

Service locator pattern just drives the dependencies underground, Singleton services are, well, Singletons, and also serve to hide the dependencies

This is a good observation. Hiding the dependencies doesn't remove them. Instead you should address the number of dependencies a class needs.

Using constructor dependency injection would mean that most of the constructors would have several, many, standard injected dependencies because most classes explicitly require those dependencies

If this is the case, you are probably violating the Single Responsibility Principle. In other words, those classes are probably too big and do too much. Since you are talking about logging and tracing, you should ask yourself if you aren't logging too much. But in general, logging and tracing are cross-cutting concerns and you should not have to add them to many classes in the system. If you correctly apply the SOLID principles, this problem goes away (as explained here).

Upvotes: 2

Related Questions