Bolaji
Bolaji

Reputation: 566

Grasping Dependency Injection in the context of two simple classes

I have been having issues grasping Dependency Injection(or let me say its benefit). So I decided to write two simple pieces of code of one without DI and the other with it.

So I have a class A

public class A {
    public void foo(){
        B b = new B();
        b.fooB();
    }
}

as can be seen above A depends on B, B which is

public class B {
    public void fooB(){
        Log.e("s", "y");
    }
}

and we can use A like

public void do(){
    A a = new A();
    a.foo();
}

But it's said that A should not simply initialize B because it depends on it, however we should have have a service that have some sort of contracts between the two classes. For Example, please if I am wrong kindly let me know

So lets have an interface BService

public interface BService {
    void fooB();
}

And B becomes DiB

public class DiB implements BService {
    @Override
    public void fooB(){
        Log.e("s", "y");
    }
}

And A becomes DiA

public class DiA {
    BService bService;

    public DiA(BService bService){
        this.bService = bService;
    }

    public void foo(){
        bService.fooB();
    }
}

and we can use A like

public void dIdo(){
        BService service = new diB();
        diA a = new diA(service);
        a.foo();
}

So I read benefits of DI are :

  1. Testable codes : Because I can actually test both codes in JUnit(I dont want to post the test here to avoid long question)
  2. Decoupling: Its said that if class B changes then A shouldn't be affected, and I cant grasp that because If i change fooB() in class B to fooB2(), i will have to change the override method in BService which in turn means i will have to change it in class A

Both codes seems to work fine and I cant fathom benefit of one over the other, only that the other is more complex. So please can you enlighten me more on the benefits in the context of this simple A and B classes. What am I not getting?

Upvotes: 4

Views: 60

Answers (2)

Bohdan Levchenko
Bohdan Levchenko

Reputation: 3561

Decoupling: Its said that if class B changes then A shouldn't be affected, and I cant grasp that because If i change fooB() in class B to fooB2(), i will have to change the override method in BService which in turn means i will have to change it in class A

I guess once your understand that, you'll understand the whole concept.

Try to think about the interfaces you provide as a contracts between different components of your system.

By declaring BService with method fooB() you're saying that any component which respects this contract (e.g implements interface) is able to do declared work in its own way as long as it not violating the contract.

Component A shouldn't be interesting in how BService doing it's job, for A is sufficient enough to know the work will be done.

Then you will be able to create another implementation of BService which could do necessary work totally differently. Your can reconfigure your IoC to inject your new implementation into A and that is it. You haven't changed your A but you have changed the way how it works.

Let's use another example:

Suppose you have a Repository an interface which can store/retrieve anything by some string identifier (for simplicity).

interface Repository {
    Object retrieve(String identifier);
    void store(String identifier, Object content);
}

And you might have a couple of components which are using this repository to manipulate some data:

class DocumentStorage {
    private int seqNo = 1;
    private Repository repository;

    public void saveMyDocuments(Iterable<Document> documents) {
         for (Document document : documents) {
             repository.store("DocumentStorage" + ++seqNo, document);
         }
    }
}

And

class RuntimeMetrics {
   private Repository repository;

   public void saveFreeMemoryAmount() {
       repository.store("MEM", Runtime.getRuntime().freeMemory());
   }
}

Now, this components have no idea how repository will save the documents, they just know it will.

And you can implement in-memory repository:

class InMemoryRepository implements Repository {
    private final java.util.Map<Integer, Object> mem = new java.util.HashMap<>();
    @Override
    Object retrieve(Integer identifier) {
        return mem.get(identifier);
    }

    @Override
    void store(Integer identifier, Object content) {
        mem.put(identifier, content);
    }
}

and live with that.

Now in some point in time you can decide that documents are too important to store in memory and you have to store them in the file or database or some other place.

You are implementing DatabaseRepository according to the Repository contract, reconfigure your DI container and BOOM, your documents are now lives in the database. You haven't changed anything in the DocumentStorage, RuntimeMetrics still using InMemoryRepository for managing its data.

In the similar way you can test DocumentStorage by replacing Repository with a fake implementation instead of firing up the whole database server.

This is the main benefit of the DI.

Upvotes: 1

Michael
Michael

Reputation: 44150

You don't need the interfaces you've created for it to be classified as dependency injection. This class is using dependency injection:

public class A {
    private final B b;

    public A(B b) {
        this.b = b;
    }

    public void foo(){
        b.fooB();
    }
}

Don't overthink it. 'Dependency injection' sounds like a complicated concept when you don't understand it but the name actually describes the concept perfectly and concisely. Let's break it down:

Dependency: Things that something relies upon
Injection: The act of putting something external inside something else

In the above example, are we putting the things we rely upon inside our class from the outside? Yes we are, so we're using dependency injection. Whether our class implements an interface or not is irrelevant.

There are good reasons why implementing classes to interfaces is a good idea but that is tangential to dependency injection. Don't get these things confused and don't think that it's a requirement.


Addressing testability: yes, in your non-dependency injected version we can test A and B. We can't test A in isolation from B but so what? Who says that we'd want to? What benefit would that give us?

Well, suppose B is not so trivial. Suppose B reads from a database and returns some value. We don't want our unit tests for A to rely on a database because A doesn't care about databases, it just cares about being able to fooB. Unfortunately, if A is the one in charge of creating B then we have no way to change that behaviour. It can only do one thing and in our production code we need it to create a B that talks to a database, so that's what we're stuck with.

However, if we were injecting the dependency then we could do this in our real code:

new A(new DatabaseB());

and inject a 'fake' or a 'mock' in our tests that acts like it's talking to a database without actually doing so:

new A(mockB);
new A(fakeB);

This allows us to use A in two different ways: with and without a database; for production code, and for test code. It gives us the flexibility to choose.

Upvotes: 3

Related Questions