EasyDI Example

| tagged as

This tutorial will show you the usage of Easy-DI. All files for this example can be found in the github-project in the package /src/test/java/eu/lestard/easydi/examples/coffee

Let's assume this class diagram for a coffee machine application:

uml-class-diagram

In this class diagram we can see the dependencies of each class: The CoffeeMachine has a reference to the WaterTank and the CoffeePowderProvider. The CoffeePowderProvider has a reference to the BeanContainer and to the Mill.

1. Write your classes

Let's start writing our classes. We begin with the ones that have no dependencies to other classes:

public class Mill {
    public void grind(){
        System.out.println("Mill: Let's start grinding");
    }
}

public class BeanContainer {
    public void getBeans(int amount){
        System.out.println("BeanContainer: here you have " + amount + " beans");
    }
}

public class WaterTank {
    public void getWater(){
        System.out.println("WaterTank:providing water");
    }
}

Now the rest. When a class needs an instance of another class we simply add a constructor parameter to the class:

public class CoffeePowderProvider {

    private final Mill mill;
    private final BeanContainer container;

    public CoffeePowderProvider(Mill mill, BeanContainer container){
        this.mill = mill;
        this.container = container;
    }

    public void getPowder(){
        System.out.println("CoffeePowderProvider: Start making coffee powder.");
        container.getBeans(10);
        mill.grind();
        System.out.println("CoffeePowderProvider: Here you have your coffee powder");
    }
}

public class CoffeeMachine {

    private final CoffeePowderProvider coffeePowderProvider;
    private final WaterTank waterTank;

    public CoffeeMachine(CoffeePowderProvider coffeePowderProvider, WaterTank waterTank ){
        this.coffeePowderProvider = coffeePowderProvider;
        this.waterTank = waterTank;
    }

    public void makeCoffee(){
        System.out.println("CoffeeMachine: Start making coffee");
        waterTank.getWater();
        coffeePowderProvider.getPowder();
        System.out.println("CoffeeMachine: I have all ingredients. Let's go");
        System.out.println("CoffeeMachine: ...");
        System.out.println("CoffeeMachine: Coffee is finished");
    }
}

Now we have all our "business" classes finished. The only thing that is missing is a class with a main method to start our program. In the main method we need to create all our instances and put them together:

public class CoffeeApp {
    public static void main(String...args){
        Mill mill = new Mill();
        WaterTank waterTank = new WaterTank();
        BeanContainer beanContainer = new BeanContainer();
        CoffeePowderProvider powderProvider = new CoffeePowderProvider(mill,beanContainer);
        CoffeeMachine coffeeMachine = new CoffeeMachine(powderProvider,waterTank);

        coffeeMachine.makeCoffee();
    }
}

When we run our example app we get this output: CoffeeMachine: Start making coffee WaterTank:providing water CoffeePowderProvider: Start making coffee powder. BeanContainer: here you have 10 beans Mill: Let's start grinding CoffeePowderProvider: Here you have your coffee powder CoffeeMachine: I have all ingredients. Let's go CoffeeMachine: ... CoffeeMachine: Coffee is finished

And this is it: This is dependency injection. You don't even need a framework for dependency injection! This is Inversion of Control. Instead of instantiating them by themselves, every class tells you what other classes it needs. As you can see there is no big magic in DI.

But wait! There are at least two problems with this code: First of all, when your application grows you will end with a big stack of constructor calls. And it's an annoying task to call constructors. Can't this be automated? Why can't a computer do this? Yes this is what a DI-Framework is doing.

But the second problem is even worse: Our CoffeeApp class has static references to all our classes. Every time you make a change to one of your classes (rename it, move it to another package) or the dependencies of one of your classes change the CoffeeApp has to be changed too.

So let's se what Easy-DI can do for us.

2. Use Easy-DI

Introducing Easy-DI to our example is ...easy. Instead of calling constructors you only create an instance of EasyDI and get an instance of the CoffeeMachine from it:

public static void main(String...args){
    EasyDI easyDI = new EasyDI();
    CoffeeMachine coffeeMachine = easyDI.getInstance(CoffeeMachine.class);

    coffeeMachine.makeCoffee();
}

This is all we have to do. No annoying constructor calls and no static dependencies to all classes anymore.

And unlike some other DI-frameworks we didn't have to do any changes to our business classes. There was no need to add annotations or other configuration (at least for this simple example).

To get an idea what EasyDI is doing under the hood we could add a sysout (System.out.println) to all our constructors like this:

public class Mill {
    public Mill(){
        System.out.println("new Mill()");
    }

    ...
}

If we do this with all our classes and run the example we get this output:

new Mill()
new BeanContainer()
new CoffeePowderProvider(...)
new WaterTank()
new CoffeeMachine(...)
CoffeeMachine: Start making coffee
WaterTank:providing water
CoffeePowderProvider: Start making coffee powder.
BeanContainer: here you have 10 beans
Mill: Let's start grinding
CoffeePowderProvider: Here you have your coffee powder
CoffeeMachine: I have all ingredients. Let's go
CoffeeMachine: ...
CoffeeMachine: Coffee is finished

As you can see, EasyDI is doing the same as we have done by hand before: It's starting to instantiate the classes that don't have dependencies.

3. Singletons

Our coffee machine is working. Let's implement some new features like a milk frother. The milk frother will use hot steam to froth the milk. Therefore it needs a reference to the WaterTank:

public class MilkFrother {

    private final WaterTank waterTank;

    public MilkFrother(WaterTank waterTank){
        System.out.println("new MilkFrother(...)");
        this.waterTank = waterTank;
    }

    public void makeMilkFroth(){
        waterTank.getWater();
        // heat the water up to get steam
        System.out.println("MilkFrother: making milk froth");
    }
}

Our CoffeeMachine now looks like this:

public class CoffeeMachine {

    private final CoffeePowderProvider coffeePowderProvider;
    private final WaterTank waterTank;
    private final MilkFrother frother;

    public CoffeeMachine(CoffeePowderProvider coffeePowderProvider, WaterTank waterTank, MilkFrother frother ){
        System.out.println("new CoffeeMachine(...)");
        this.coffeePowderProvider = coffeePowderProvider;
        this.waterTank = waterTank;
        this.frother = frother;
    }

    public void makeCoffee(){
        System.out.println("CoffeeMachine: Start making coffee");
        waterTank.getWater();
        coffeePowderProvider.getPowder();
        frother.makeMilkFroth();

        System.out.println("CoffeeMachine: I have all ingredients. Let's go");
        System.out.println("CoffeeMachine: ...");
        System.out.println("CoffeeMachine: Coffee is finished");
    }
}

Let's try this out: new Mill() new BeanContainer() new CoffeePowderProvider(...) new WaterTank() new WaterTank() new MilkFrother(...) new CoffeeMachine(...) CoffeeMachine: Start making coffee ... MilkFrother: making milk froth ... CoffeeMachine: Coffee is finished

But what is this? The constructor of the WaterTank is called two times? That's bad. The reason is that by default EasyDI will always create a new instance when one is needed. We have now two classes that depend on a water tank: The CoffeeMachine and the MilkFrother.

To fix this we need to tell EasyDI that there should only be exactly one instance of the WaterTank. The water tank should be a Singleton. There are two ways of doing this:

1. Add @Singleton annotation to WaterTank

import javax.inject.Singleton;

@Singleton
public class WaterTank {
...
}

When EasyDI finds out that a class is annotated with javax.inject.Singleton it will only create one instance and reuse it everywhere it is needed.

2. Use the EasyDI.markAsSingleton() method.

You can mark a class as singleton with the method EasyDI.markAsSingleton(<yourclass>.class);:

public static void main(String...args){
    EasyDI easyDI = new EasyDI();
    easyDI.markAsSingleton(WaterTank.class);

    CoffeeMachine coffeeMachine = easyDI.getInstance(CoffeeMachine.class);

    coffeeMachine.makeCoffee();
}

The result is the same in both cases:

new Mill()
new BeanContainer()
new CoffeePowderProvider(...)
new WaterTank()
new MilkFrother(...)
new CoffeeMachine(...)
CoffeeMachine: Start making coffee
...
MilkFrother: making milk froth
...
CoffeeMachine: Coffee is finished

When should I use @Singleton, when markAsSingleton?

In general I would recommend to use the @Singleton annotation. This annotation is part of the JSR-330 specification. If you choose to switch to another DI framework in the future it's likely that it will support this annotation as well and you probably won't have to change anything to get your singleton working again.

The markAsSingleton method can be useful if you can't add the annotation for some reason. For example when the class is part of a third-party library or you don't have access to the source files.

4. Interfaces

Our coffee machine business grows and now we like to add another variant of the coffee machine to our catalogue. The new variant won't have a simple water tank anymore. Instead it will have a direct water supply.

To make our application flexible we add an interface WaterSupply with two implementations: The old WaterTank and our new class DirectWaterSupply.

public interface WaterSupply {
    void getWater();
}

@Singleton
public class WaterTank implements WaterSupply{
    ...
    @Override
    public void getWater(){
        System.out.println("WaterTank:providing water");
    }
}

@Singleton
public class DirectWaterSupply implements WaterSupply {
    ...
    @Override
    public void getWater() {
        System.out.println("DirectWaterSupply: get water from the water tap");
    }
}

Additionally we change the type of the constructor parameters in MilkFrother and CoffeeMachine: ```java public class CoffeeMachine { public CoffeeMachine(CoffeePowderProvider coffeePowderProvider, WaterSupply waterSupply, MilkFrother frother ){ ... } ... }

public class MilkFrother { public MilkFrother(WaterSupply waterSupply){ ... } ... } ```

But when we run our application like this we will get this exception:

java.lang.IllegalStateException:
EasyDI can't create an instance of the class [interface WaterSupply].
It is an interface and there was no implementation class mapping defined for this type.
Please use the 'bindInterface' method of EasyDI to define what
implementing class should be used for a given interface.

As the exception says, EasyDI can't know which of the implementing classes it should use when an WaterSupply is requested.

To define this use the method EasyDI.bindInterface:

public static void main(String...args){
    EasyDI easyDI = new EasyDI();
    easyDI.bindInterface(WaterSupply.class, WaterTank.class);

    CoffeeMachine coffeeMachine = easyDI.getInstance(CoffeeMachine.class);

    coffeeMachine.makeCoffee();
}

Now we get the expected output again: ... CoffeeMachine: Coffee is finished

5. Multiple Constructors

EasyDI is expecting to find exactly one public constructor when it has to create an instance of a class. If for some reason your class has more then one constructor, EasyDI can't know which one to use.

Look at this example:

public class BeanContainer {
    public BeanContainer(){
        System.out.println("new BeanContainer()");
    }

    public BeanContainer(int initialAmount){

    }
    ...
}

When you run the example you will get this exception: java.lang.IllegalStateException: EasyDI can't create an instance of the class [class BeanContainer]. There are more than one public constructors so I don't know which to use. Fix this by either make only one constructor public or annotate exactly one constructor with the javax.inject.Inject annotation. You have to provide this information with the annotation javax.inject.Inject like this:

import javax.inject.Inject;

public class BeanContainer {
    @Inject
    public BeanContainer(){
        System.out.println("new BeanContainer()");
    }

    public BeanContainer(int initialAmount){

    }
    ...
}

If there are more then one public constructors EasyDI will use the one that is annotated with @Inject. Of cause you may not annotate more then one constructor.

6. Provider Methods

Sometimes the shown features aren't sufficient for your use case. For example when you like to use a class from a third-party library that has multiple constructors and you can't add an annotation to one of them.

Or the only way to get an instance of the third-party class is to use a factory method.

For these use cases you can define a Provider for a given type:

import javax.inject.Provider;

public static void main(String...args){
    EasyDI easyDI = new EasyDI();

    easyDI.bindProvider(BeanContainer.class, new Provider<BeanContainer>() {
            @Override
            public BeanContainer get() {
                final BeanContainer beanContainer = new BeanContainer();
                beanContainer.setAmount(100);
                return beanContainer;
            }
        });

    CoffeeMachine coffeeMachine = easyDI.getInstance(CoffeeMachine.class);

    coffeeMachine.makeCoffee();
}

With java 8 lambdas this can be written in a shorter way:

easyDI.bindProvider(BeanContainer.class, () -> {
        final BeanContainer beanContainer = new BeanContainer();
        beanContainer.setAmount(100);
        return beanContainer;
    });
}

With this configuration EasyDI will always call the given provider instance when an instance of BeanContainer is requested.