Question

I am wondering about how to make lower-level code reusable when the Dependency Inversion Principle (DIP) is used.

In the book Clean Architecture by Robert C. Martin, the DIP is described such that the higher-level components define the interfaces they need, and then the lower-level components implement these interfaces, as in

+-----------------------------------+
|                                   |
|  Application ------> Service <I>  |
|                         ^         |
+-------------------------|---------+
                          |
                     +----|--------+
                     | ServiceImpl |
                     +-------------+

That way,

  • the application code is protected from changes in the lower-level components
  • the application does not depend on the source of the ServiceImpl
  • "the source code dependencies are inverted against the flow of control".

Maybe the image in my head is wrong, but I always imagine that the Service in the picture above may be something like a logging service, so maybe the interface has methods like debug(), info() etc., and then the implementation could log to a file, to a database, or whatever.

I find two things a bit weird about this approach:

  1. The general-purpose logging code in ServiceImpl that doesn't know anything about the data it is logging needs to import an interface from the big valuable application module, is that right? Somewhere in my lower-level code I have a line that looks like from high_level_app import ServiceInterface, that doesn't feel right. It feels like I am creating an artificial dependency to the higher-level module here.
  2. From a more semantic viewpoint, obviously the functionality in my lower-level logging implementation could be reused across the whole organization, but since the higher-level code defines the interface and (as per the point above) I probably have a source code dependency on the higher-level code, does that limit the reuse?

So I would like to ask:

  • Does the Dependency Inversion Principle in general inhibit the reuse of lower-level libraries? Or is my example of the logging service flawed, because that's not what the DIP is aiming at?
  • When using the DIP, how would I go and create reusable lower-level components, rather than only making them usable for the particular higher-level component I am dealing with right now?
Was it helpful?

Solution

There's some detail missing in that picture. The Service interface isn't a general-purpose interface; instead it has methods geared specifically towards the needs of your application. Now, the idea with DI used in this way is that you want to be able to independently change, or even entirely replace, the implementation of the service. ServiceImpl packaged separately is essentially a plug in to your application.

To actually implement the service, you are likely going to use some general-purpose 3rd party library. This 3rd party library is reusable, and it solves a specific problem that's common enough in one form or another across different domains. Naturally, it has no idea about your application, and it certainly doesn't depend on it.

How do you apply DI then?

You introduce a wrapper/adapter - a component that you own that adapts this 3rd party component to the Service interface required by the application.

This wrapper then implements the interface, but behind the scenes, it calls into the reusable general-purpose library.

So, the dependency structure is like this:

|---------------- code you own (your application) -------|----- frameworks/libs -----|
|------ core application --------|-- gateways/adapters --|                           |

[Application]----->[Service]<|--------[Wrapper]----------->[Reusable Generic Library]

The Wrapper implements a Use Case–specific service (e.g., an operation that supports a specific need of the Use Case) by calling and orchestrating a couple of general-purpose operations of the reusable library. Now, logging is perhaps not the best example, because it's mostly a cross-cutting concern, but suppose there's a requirement to keep an audit log for business purposes; then, a Log method on the Service interface will be expressed in terms of domain concepts, e.g. LogCustomerInfo, and may take parameters that represent domain elements (e.g., a Customer object). Compare that with the general-purpose library: it has methods like LogInfo(string) or LogWarning(string).

Does the Dependency Inversion Principle in general inhibit the reuse of lower-level libraries?

As you see, the answer is no. A lower-level library is really its own thing; DI as applied here just provides a mechanism to structurally isolate the application from a lower-level library.

If the lower-level, general-purpose library is your own, rather then a 3rd party one, the code for it doesn't go into the Wrapper, as it can't depend on, or know about, your application (you basically make the same decisions as if you were a 3rd party supplier).

When using the DIP, how would I go and create reusable lower-level components, rather than only making them usable for the particular higher-level component I am dealing with right now?

It usually takes some time, and several projects before you truly get code that's valuable enough to be separated out into a reusable component, but in general, you identify the reusable parts (generalize them if necessary, do some restructuring, code cleanup, etc.), and separate them out in the same way as described above.

OTHER TIPS

There is a reason why the hexagonal architecture proposed by Robert Martin is also called "ports and adapters".

The low level implementation of your high level interfaces are most of the time (very straightforward) adapters from your application requirements (high-level policies) to concrete infrastructure.

If these adapters get complex, I recommend factoring out all generic logic into functions and classes. These functions and classes will eventually depend on no com.myapp packages (eg. interfaces etc) and voila: you created generic reusable code, that can be carried over to the next project or other adapters.

If you use logging as an example, then yes, you are right that if it was implemented strictly like you you DIP is described. But I would argue that there are two "classes" of infrastructure problems. Logging, Configuration, and Enumerations are examples of problems that have already been solved and rarely require DIP as you describe. But when it comes to actual application logic and behavior, strict DIP can be highly valuable to separate the high-level policies and low-level implementation details, provide better testability and modularity.

Licensed under: CC-BY-SA with attribution
scroll top