Which effects does the Dependency Inversion Principle have to a project structure?
Asked Answered
H

1

8

In case I want to use the DIP to develop a hypothetical modular C++ project. Because of the modularity I choose to implement one specific feature completely in one library A. Another library B (or two, or three ...) is using this feature (e.g. a logging mechanism):

class ILogger
{
    virtual void log(const std::string& s) = 0;
};

Where should I put this interface physically? Some bloggers seem to suggest, that because the interface belongs to its users (because of DIP) you shall put the interface on the user side (or here). This would also improve testability, because you don't need any implementation to be linked to the test.

This would mean, that the library A itself will not compile, because it lacks the interface. It will also mean, that if a library C will also use the logging facility it will also bring in an interface ILogger, which will break the ODR?! This could be solved by introducing an extra package layer library D which contains only the interface. But the main problem remains:

Where to put the interface? I read the original paper about DIP, but I cannot agree with the interpretation, that I should not put the interfaces into the library. I have the feeling, that the this paper is meant as a guideline of how to think of development (as "the users are defining the interface not the implementators"). Is this correct? How do you use the Dependency Inversion Principle?

Hermy answered 27/8, 2014 at 4:31 Comment(2)
what about putting the interface in a separate library? Then you can include that library both from A and B. In the end, it's was is advised to for for DIP...Debunk
For most of the use cases this would be overexaggerated. And the most problematic case for me is: If you package the interface with the user together and the implementation in another package, then it will add a build dependency from the package with the implementation to the package with the user and the interface. This is IMHO simply unreasonable only to gain a small testability advantage.Hermy
U
2

Software can be viewed as a combination of different layers:

  • One layer is the implementation level (rougthly, the function level)

  • Another one is the way data structures interact together (the class level, which is primarily where the DIP should apply)

  • And another one the way that components should interact together (the package layer). We also would like to apply some kind of DIP here if possible. Robert C. Martin insist on the fact that this layer is primarily business-dependent (whatever that means) and so principles are a little different: Stable-Dependencies Principle and Stable-Abstractions Principle (see Martin's Principle Pattern and Practice)

Now what should also be emphathized about principles in software engineering, is that you should apply them only when you have to solve the problem they solve. As long as you don’t have the problem, do not use them.

At the class level, you should use the DIP if you have good reasons to believe that your logging mechanism will be implemented by several classes. If you believe that there will be only one logging mechanim for the time being, then it is perfectly fine not to use the DIP because there is no problem to solve.

Now the same kind of choice should be made at the package level. However, the guide for your packaging choice is deployment. Here:

class ILogger {
    virtual void log(const std::string& s) = 0;
};
class A : public ILogger {
    …
};
class A2 : public ILogger {
    …
};
  1. If you believe (for business reasons) that it makes sense to release A without A2, then make 4 libraries : one for ILogger, one for the user-class B, one for A, one for A2.
  2. If for some reasons A and A2 be should be released together, then make only one library for ILogger, A and A2. If later on they should be released separatly, then break your library but not now because remember : YAGNI.
  3. If you have only one dependency to ILogger, then it could also make sense to create only one library with everything.
  4. Do not release a lib with ILogger and B, and another one with A because then you have no advantage compared to solution 3, this is more complex and could furthermore violate another principle for packages: the Acyclic-Dependencies Principle.

Anyway, this decision is mostly business dependent. Also remember that packaging should be done bottom-up: only create a new package when you have many classes you want to organize. Until you don’t have this many classes, don’t try to make early decisions because you will almost certainly be wrong.

Umont answered 4/9, 2014 at 0:3 Comment(2)
Thx for the answer. It is clear, that I have to add an extra layer, if there are 2 implementations of an interface in different libraries, but DIP seems to insist, that you need the abstraction, if there are more than one uses, because the interface belongs to the user?! Your 4th entry in the enumeration seems to violate the DIP interpretation of the sites I linked? Does R. C. Martin anywhere insist on the packaging of the interface with its users?Hermy
Martin does not tell to package the interface with its client. How would you package it if you have more than one client? So at the package level, the interface definitely does not belong to the user. Even at the class level, "belong to the client" really means "the client depend upon it". If you really don't want to depend on possible changes due to another client of ILogger, make another interface, say ILoggerForB using the Adapter design pattern. Since ILoggerForB is created for B only, its only client is B, so you can package it with B.Ecclesiolatry

© 2022 - 2024 — McMap. All rights reserved.