The main problem solved by dependency injection is unit-testing. Suppose you have a DoorOpeningService which depends on a NuclearPlant. To unit-test the DoorOpeningService, you would need to have a NuclearPlant (which is rather costly and hard to setup just to test opening doors).
If the code of DoorOpeningService is like the following:
public class DoorOpeningServiceImpl implements DoorOpeningService {
private NuclearPlant plant;
public DoorOpeningServiceImpl() {
this.plant = SomeNamingService.lookup("nuclearPlant");
}
public void openDoors() {
int electricity = plant.getSomeElectricity();
...
}
}
The DoorOpeningService is very hard to unit-test.
Dependency injection allows giving the NuclearPlant to the DoorOpeningService. So, instead of needing a real nuclear plant, you can give it a fake one, which always gives some electricity without needing all the real nuclear plant infrastructure. And the DoorOpeningService is thus much more easier to test:
public class DoorOpeningServiceImpl implements DoorOpeningService {
private NuclearPlant plant;
// The plant is injected by constructor injection
public DoorOpeningServiceImpl(NuclearPlant plant) {
this.plant = plant;
}
public void openDoors() {
int electricity = plant.getSomeElectricity();
...
}
}
Having a framework inject dependencies for you is easier, and also allows for additional aspects (interceptors if you prefer) to be added. For example, Spring can inject, instead of your NuclearPlant implementation, a proxy to this implementation that makes sure, for every call to the plant, that a transaction is open, or that the caller is authorized to call its methods, or that statistics are gathered to help diagnosing slow parts in the application.