Search code examples
design-patternsoopanti-patternslaw-of-demeter

Wrappers/law of demeter seems to be an anti-pattern


I've been reading up on this "Law of Demeter" thing, and it (and pure "wrapper" classes in general) seem to generally be anti patterns. Consider an implementation class:

class FluidSimulator {
    void reset() { /* ... */ }
}

Now consider two different implementations of another class:

class ScreenSpaceEffects1 {
    private FluidSimulator _fluidDynamics;
    public FluidSimulator getFluidSimulator() { return _fluidDynamics; }
}

class ScreenSpaceEffects2 {
    private FluidSimulator _fluidDynamics;
    public void resetFluidSimulation() { _fluidDynamics.reset(); }
}

And the ways to call said methods:

callingMethod() {
   effects1.getFluidSimulator().reset(); // Version 1
   effects2.resetFluidSimulation();      // Version 2
}

At first blush, version 2 seems a bit simpler, and follows the "rule of Demeter", hide Foo's implementation, etc, etc. But this ties any changes in FluidSimulator to ScreenSpaceEffects. For example, if a parameter is added to reset, then we have:

class FluidSimulator {
    void reset(bool recreateRenderTargets) { /* ... */ }
}

class ScreenSpaceEffects1 {
    private FluidSimulator _fluidDynamics;
    public FluidSimulator getFluidSimulator() { return _fluidDynamics; }
}

class ScreenSpaceEffects2 {
    private FluidSimulator _fluidDynamics;
    public void resetFluidSimulation(bool recreateRenderTargets) { _fluidDynamics.reset(recreateRenderTargets); }
}

callingMethod() {
   effects1.getFluidSimulator().reset(false); // Version 1
   effects2.resetFluidSimulation(false);      // Version 2
}

In both versions, callingMethod needs to be changed, but in Version 2, ScreenSpaceEffects also needs to be changed. Can someone explain the advantage of having a wrapper/facade (with the exception of adapters or wrapping an external API or exposing an internal one).

EDIT: One of many real examples for which I ran into this rather than a trivial example.


Solution

  • The main difference is that in version 1, as provider of the Bar abstraction, you have no control on how Foo is exposed. Any change in Foo will be exposed to your clients, and they will have to bear with it.

    With version 2, as provider of abstraction Bar, you can decide if and how you want to expose the evolutions. It will depend only on the Bar abstraction, and not Foo's. In your example, your Bar abstraction may already know which integer to pass as argument, and thus you will be able to let your users transparently use the new version of Foo, with no change at all.

    Suppose now Foo evolves, and require the user to call foo.init() before any call to doSomething. With version 1, all users of Bar will need to see that Foo changed, and adapt their code. With version 2, only Bar has to be changed, its doSomething calling init if needed. This leads to less bugs (only the author of abstraction Bar has to know and understand abstraction Foo and less coupling between classes.