Search code examples
dependency-injectionclean-architecturedependency-inversion

How to delay the adoption of a particular dependency injection framework during the early development phase?


Robert C. Martin in his book "Clean Architecture: A Craftsman's Guide to Software Structure and Design" mentions that a good architecture allows to delay decisions about details. One of those he mentions is:

It is not necessary to adopt a dependency injection framework early in development, because the high-level policy should not care how dependencies are resolved

What is a proper work methodology to achive that? How can you start developing in an efficient way without a particular dependency injection framework?


Solution

  • How can you start developing in an efficient way without a particular dependency injection framework?

    The short answer is:

    Just like you did before those DI frameworks exists!

    Even those frameworks have a lot of benefits it's a quite good idea to start without them to write your code in a framework independent way. This addresses the code smell immobility.

    Let's assume you want to setup a controller, a use case and repository.

     +------------+         +----------+        +------------+
     | Controller |  --->   | Use Case |  --->  | Repository |
     +------------+         +----------+        +------------+  
    

    Just use standard constructors and setters.

     Repository repo = new RepositoryImpl();
     UseCase useCase = new UseCaseImpl(repo);
     Controller controller = new ControllerImpl(useCase);
    

    Note: I don't prefer the Impl ending. It's just to distinguish between interface and implementation in this example.

    The piece of code I showed above is just the same as what a DI framework does.

    If you want to use a DI framework you can then wrap the code above in method that the DI framework uses. In java spring we would call such methods factory methods. E.g.

    @Bean
    public Controller controller(){
        Repository repo = new RepositoryImpl();
        UseCase useCase = new UseCaseImpl(repo);
        return new ControllerImpl(useCase);
    }
    

    Maybe the UseCase should be a singleton and therefore should be placed in a separate factory method.

    @Bean
    public UseCase useCase(DataSource ds){
        Repository repo = new RepositoryImpl(ds);
        repo.setCacheEnabled(true);
        return new UseCaseImpl(repo);
    }
    
    @Bean
    public Controller controller(UseCase useCase){
        ControllerImpl controller = new ControllerImpl(useCase);
        SomeConfig c = ...;
        controller.setSomeConfig(c);
        return controller;
    }
    

    The main point is that the controller, use case and repository in my example only tell what they need and not where it come from. It's a good idea to separate the setup code or bootstrap code from the other.