15

Say we have 1001 clients that construct their dependencies directly rather than accept injections. Refactoring the 1001 is not an option according to our boss. We're actually not even allowed access to their source, just the class files.

What we are supposed to do is "modernize" the system that these 1001 clients go through. We can refactor that all we like. The dependencies are part of that system. And some of those dependencies we're supposed to change to have a new implementation.

What we'd like to do is have the ability to configure different implementations of dependencies to satisfy this multitude of clients. Sadly, DI doesn't seem an option since the clients don't accept injections with constructors or setters.

The options:

1) Refactor the implementation of the service that the clients use so that it does what the clients need now. Bang we're done. Not flexible. Not complex.

2) Refactor the implementation so that it delegates it's work to yet another dependency that it acquires through a factory. Now we can control which implementation they all use by refactoring the factory.

3) Refactor the implementation so that it delegates it's work to yet another dependency that it acquires through a service locator. Now we can control which implementation they all use by configuring the service locator which might simply be a hashmap of strings to objects with a little casting going on.

4) Something I haven't even thought of yet.

The objective:

Minimize the design damage being caused by dragging the old poorly designed client code into the future without adding pointless complexity.

Clients shouldn't know or control the implementation of their dependencies but they insist on building them with new. We can't control the new but we do control the class they're building.

My question:

What have I failed to consider?


Questions from Doc Brown

do you really need a possibility to configure between different implementations? For what purpose?

Agility. Lot of unknowns. Management wants potential for change. Only lose dependency on the outside world. Also testing.

do you need need a run time mechanics, or just a compile time mechanics to switch between different implementations? Why?

Compile time mechanics is likely enough. Except for testing.

which granularity do you need to switch between implementations? All at once? Per module (each containing a group of classes)? Per class?

Of the 1001 only one is run through the system at any one time. Changing what all clients use at once is likely fine. Individual control of the dependencies is likely important though.

who needs to control the switch? Only your/your developer team? An administrator? Each client on his own? Or the maintenance developers for the client's code? So how easy/robust/foolproof does the mechanics need to be?

Dev for testing. Administrator as external hardware dependencies change. It needs to be easy to test and configure.


Our goal is to show that the system can be remade quickly and modernized.


actual use case for the implementation switch?

One is, some data will be provided by software until the hardware solution is ready.

3
  • 1
    I don't feel you will achieve much by storing factories or locators in global state. Why bother? Are you going to test clients substituting the dependency?
    – Basilevs
    Feb 2, 2017 at 4:37
  • Consider using custom class loader.
    – Basilevs
    Feb 2, 2017 at 4:38
  • Just out of curiosity, what's this system do?
    – Prime
    Jul 4, 2017 at 0:40

3 Answers 3

7
+200

Well, I am not sure I understand the technical details and their exact differences of your supported solutions completely, but IMHO you first need to find out which kind of flexibility you really need.

The questions you have to ask yourself are:

  • do you really need a possibility to configure between different implementations? For what purpose?

  • do you need need a run time mechanics, or just a compile time mechanics to switch between different implementations? Why?

  • which granularity do you need to switch between implementations? All at once? Per module (each containing a group of classes)? Per class?

  • who needs to control the switch? Only your/your developer team? An administrator? Each client on his own? Or the maintenance developers for the client's code? So how easy/robust/foolproof does the mechanics need to be?

Having the answers to these questions in mind, pick the most simple solution you can think of which provides you with the required flexibility. Don't implement any flexibility you are not sure about "just in case" if it means additional effort.

As a reply to your answers: if you have at least one real use case at hand, use it to validate your design decisions. Use that use case to find out which kind of solution works best for you. Just try out if a "factory" or a "service locator" provides you with you what you need, or if you need something else. If you think both solutions are equally good for your case, throw a dice.

7
  • Good questions! Please see update. Feb 2, 2017 at 11:28
  • Updated with actual use case. Feb 2, 2017 at 12:34
  • @CandiedOrange: I cannot give you some fish, I can only try to help you fishing by yourself :-)
    – Doc Brown
    Feb 2, 2017 at 12:40
  • Understood. I'm just staring at the water wondering if I need better bait or a different fishing hole. I'd love to do proper DI but this situation doesn't seem to allow for it. Feb 2, 2017 at 12:43
  • 1
    @CandiedOrange Don't get hung up on a desire to do DI because it's "good" or "better" than anything else. DI is one particular solution to a problem (or sometimes several). But, it's not always the most "fit" solution. Don't be in love with it ... treat it like it is. It's a tool.
    – svidgen
    Feb 2, 2017 at 18:30
2

Just to make sure I'm getting this right. You have some service made of some classes, say C1,...,Cn and a bunch of clients that directly call new Ci(...). So I think I agree with the general structure of your solution which is to create a new internal service with some new classes D1,...,Dn that are nice and modern and inject their dependencies (permitting such dependencies through the stack) and then rewriting Ci to do nothing but instantiate and delgate to Di. The question is how to do it and you suggested a couple ways in 2 and 3.

To give a similar suggestion to 2. You can use dependency injection throughout Di and internally and then create a normal composition root R (using a framework if you deem it appropriate) that is responsible for assembling the object graph. Shove R behind a static factory and let each Ci get it's Di through that. So for example you might have

public class OldClassI {
    private final NewClassI newImplementation;

    public OldClassI(Object parameter) {
        this.newImplementation = CompositionRoot.getInstance().getNewClassI(parameter);
    }
}

Essentially this is your solution 2, but it collects all your factories into one location along with the rest of the dependency injection.

2
  • I agree with this. Just not sure how this isn't the same as the service locator from option 3. Are you expecting to build a new instance with each call to getInstance()? Feb 2, 2017 at 11:39
  • @CandiedOrange This is probably just a clash in usage, for me a service locator is very specifically something that's essentially just a hashmap from types to objects, whereas this the composition root is just an object with a bunch of methods that construct other objects.
    – walpen
    Feb 2, 2017 at 15:04
1

Start with simple, nothing-fancy, singular implementations.

If you later need to create additional implementations, it's a question of when the implementation decision occurs.

If you need "install-time" flexibility (each client installation uses a single, static implementation), you still don't need to do anything fancy. You just offer different DLL's or SO's (or whatever your lib format is). The client just needs to put the right one in the lib folder ...

If you need runtime flexibility, you'll just need a thin adapter and an implementation-chooser mechanism. Whether you use a factory, locator, or IoC container is mostly moot. The only significant differences between an adapter and a locator are A) Naming and B) Whether the returned object is a singleton or a dedicated instance. And the big difference between an IoC container an a factory/locator is who calls who. (Often a matter of personal preference.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.