javainversion-of-controlclass-designguicetestability

Correct design of classes built for testability using constructor injection


Say I have these 3 layers of my code:
1. Database layer (ORM)
2. BusinessLogic
3. Application

Now, I write my code as follows:

  1. Database layer:
    This mainly has CURD operations over database.

    class MyDatabaseLayer {
        public int findValue(int k) {
            // find v
        }
    
        public void insertValue(int k, int v) {
            // Insert v
        }
    }
    
  2. BusinessLogic:
    This holds the actual logic to call database layer and do stuff.

    class MyBusinessLogic {
        private MyDatabaseLayer dbLayer;
        public MyBusinessLogic(MyDatabaseLayer dbLayer) {
            this.dbLayer  = dbLayer;
        }
    
        public int manipulateValue(int k) {
            dbLayer.findValue(k);
            //do stuff with value
        }
    }
    
  3. Application layer:
    This calls the business logic and displays the data

    MyBusinessLogic logic = new MyBusinessLogic(new MyDatabaseLayer ()); //The problem
    logic.manipulateValue(5);
    

Now if you see in the application layer, it knows about the database layer, which is wrong. It knows too much.

Misko Hevery says: Constructor injection is good. But if I follow that, how will I achieve abstraction? And how can Google Guice help me here?


Solution

  • Note that for the testability Misko refers to, you ideally want to make interfaces for MyDatabaseLayer, MyBusinessLogic, etc. and have the constructors take those interfaces rather than concrete classes so that in testing you can easily pass in fake implementations that don't actually use the database, etc.

    With Guice, you would bind the interfaces to concrete classes in a Module or Modules. You would then create an Injector using those Modules and get some root object (for example, your application object) from the Injector.

    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override protected void configure() {
        bind(MyDatabaseLayer.class).to(MyDatabaseLayerImplementation.class);
        // etc.
    });
    MyApplicationLayer applicationLayer = injector.getInstance(MyApplicationLayer.class);
    

    In MyApplicationLayer, you'd inject the business logic:

    @Inject
    public MyApplicationLayer(MyBusinessLogic logic) {
      this.logic = logic;
    }
    

    This is of course a very simple example and there are much more complex things you can do. In a web application, for example, you can use constructor injection on servlets using Guice Servlet rather than getting an object directly out of the Injector after creating it.