The 30-second version

Invert the dependency on a Service, moving the new statement up one level in the call stack.1

Repeat for all dependencies on Services until the corresponding new statements arrive at your application’s entry point. The entry point now creates a large object graph of all your Services in its initialize function.

Remove duplication in EntryPoint.initialize():

Instantiate common objects only once, passing them into the necessary constructors, replacing any Singletons with plain objects.

Extract the choice of implementation for each Service interface into a lookup table mapping interface type to implementation type.

Externalise the lookup table to a file, if you like.

Now you have a customised Dependency Injection Container for your application. To go a little farther:

Remove duplication in EntryPoint.initialize() among three applications.

Now you have a generic Dependency Injection Container that probably provides 80% or more of the features you’ll ever need.

I recommend trying this incrementally. Think of the new statements flowing up the call stack, into the entry point, then changing from code into data. Nice, no?

This technique applies the Dependency Inversion Principle repeatedly to move the choice of implementation for an interface up the call stack. This way, concrete things depend on abstract things.

Removing duplication in the entry point respects the principle Abstractions in Code, Details in Data, but it does rely on reflection, which can cause some problems. All the better not to scatter this reflection throughout the code causing a serious cohesion problem. Using reflection like this, all in one place, helps balance using a powerful technique with a design that everyone can understand.

References

Loose Couplings, “Dependency Injection != using a DI Container”. A handy overview of using dependency injection containers, notably a reminder that the container ought to enchance your use of dependency injection, and not interfere with it.