zengr
zengr

Reputation: 38899

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?

Upvotes: 0

Views: 230

Answers (2)

ColinD
ColinD

Reputation: 110054

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.

Upvotes: 1

M. Maraist
M. Maraist

Reputation: 462

The part you are missing with inversion-of-control is that the application layer does not call the constructor directly.. It uses a factory (the IoC container) to populate the constructor parameter.

Whatever tool you use, guice / spring / picocontainer / singleton-factories, your application code should look something like:

@Controller
class MyController {
  @Resource // Some container knows about this annotation and wires you in
  MyBusinessLogic myBusinessLogic;

  @RequestMethod("/foo/bar.*")
  public MyWebResponse doService(Response resp, long id, String val) {
     boolean worked = myBusinessLogic.manipulatevalue(id, val);
     return new MyWebResponse(worked);
  }
}

Note, the myBusinessLogic could be registered in several ways - java's @Resource, MyBusinessLogicFactory.getMyBusinessLogic(), guice.get(MyBusinessLogic.class), etc.

A poor-mans solution would be:

package foo;
class MyBusinessLogicFactory {

   static volatile MyBusinessLogic instance; // package-scoped so unit tests can override
   public static MyBusinessLogic getInstance() {
       if (instance == null) {
           synchronized(MyBusinessLogicFactory.class) {
              instance = new MyBusinessLogic(MyDatabaseLayerFactory.getInstance());
           }
       }
       return instance;
   }
}

// repeat with MyDatabaseLayerFactory

Note the above singleton model is highly discouraged, since it has no scope. You COULD wrap the above inside a context - soething like

class Context {
   Map<Class,Object> class2Instance = new ConcurrentHashMap<>();
   public <T> T getInstance(Class<T> clazz) {
      Object o = class2Instance.get(clazz);
      if (o == null) { 
        synchronized(this) {
          o = class2Instance.get(clazz);
          if (o != null) return (T)o;
          o = transitivelyLoadInstance(clazz); // details not shown
          for (Class c : loadClassTree(clazz)) { // details not shown
            class2Instance.put(c, o);
          }
        }
      }
      return (T)o;
   } 
   ...
}

But at that point, picocontainer, guice and spring can solve the complexities of the above SOOO much better.

Further, things like spring that honor the java 6 annotations mean you can do things other than constructor injection, which is VERY useful if you have multiple configuration items of the same base data-type (e.g. strings).

Upvotes: 1

Related Questions