Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does dependency inversion principle mean that I have to create an interface for every module?

If I want my code to follow SOLID principles, specifically the dependency inversion principle, does that mean that I have to create an interface (abstraction) for every module, even if it has only one implementation ?

In my opinion, and according to these posts:

http://josdejong.com/blog/2015/01/06/code-reuse/

http://blog.ploeh.dk/2010/12/02/Interfacesarenotabstractions/

creating an "abstraction" for every module is a code clutter and violates the YAGNI principle.

My rule of thumb is: do not use dependency injection, or create an interface for a module unless it has more than one implementation (the second implementation could be a mock class for unit testing in case of database/server/file modules).

Can somebody clear this up for me ? Does SOLID mean that I have to inject every module and abstract it ? If yes, isn't it just a lot of clutter we're simply not going to use most of the times ?

like image 230
michal.ciurus Avatar asked Feb 27 '15 20:02

michal.ciurus


1 Answers

The Dependency Inversion Principle states:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

In other words, each module that is depended upon (so that's everything except the entry point modules in your application) should be abstracted. Otherwise a high-level module will have to depend upon the low-level module directly, causing a violation of the DIP.

The number of implemenations an abstraction has is irrelevant to the DIP, because its goal is to make modules resistent to change. Without abstractions, it would become impossible to easily change implementations or add cross-cutting concerns without having to change or recompile a high-level component.

If, however, you find yourself defining many abstractions with just one implementation, you are violating the Reused Abstraction Principle, as Mark Seemann already stated in the article you are referencing:

Having only one implementation of a given interface is a code smell.

What this means, though, is not that you shouldn't define interfaces at all, but you need to take a good look at your design and spot the classes that are related in behavior. Those related classes can often be placed behind the same generic abstraction (generic interface) and this not only allows abstraction reuse, but makes applying cross-cutting concerns childs play.

Here are a few suggestions of functionality that you can place behind the same generic abstraction:

  • ICommandHandler<TCommand> for classes that make changes to the system on behalf of the user (use cases).
  • IQueryHandler<TQuery, TResult> as an abstraction for classes that query the database (or file system, web service, what ever) and return data.
  • IValidator<T> for classes that check report validation errors back to the user
  • ISecurityValidator<T> for classes that verify whether a user is allowed to execute a certain operation.
  • IAuthorizationFilter<T> for classes that allow applying row based security based on the user's permissions and roles.
  • IEventHandler<T> for classes that respond to a certain business event that has occurred.

These are just a few examples of abstractions. It highly depends on the application and design which generic abstractions you will get.

The applications I write make use of these generic abstractions and those applications just have a few interfaces with just one implementation. About 90% to 98% of the modules in the system implement one of those generic abstractions (depending a bit on the size of the application; the bigger the application, the higher the percentage).

These generic abstractions make it really easy to register all implementations in one line of code in your DI library (or at least, if you are using .NET), but more importantly, as I said before, applying cross-cutting concerns becomes really easy. For instance, without having to make sweeping changes to your application, you can run use cases in a database transaction, or apply a deadlock retry mechanism. Or you can apply caching of queries, without having to do sweeping changes throughout your application.

like image 178
Steven Avatar answered Oct 25 '22 01:10

Steven