Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 801
Robert Fraser Avatar asked Mar 31 '10 06:03

Robert Fraser


2 Answers

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.

like image 164
tonio Avatar answered Oct 21 '22 05:10

tonio


This is obviously an artificial example. In many real cases, callingMethod (in real life, there can multiple callingMethods) can remain blissfully unaware that Foo.doSomething has changed, because Bar insulates it. For example, if I use a stable printing API, I don't have to be concerned about my printer's firmware adding support for glossy printing. My existing black-and-white printing code keeps on working. I suppose you would group this under "adapter", which I think it much more common than you imply.

You are right that sometimes callingMethod must be changed too. But when the Law of Demeter is used properly, this will only occur rarely, usually to take advantage of new functionality (as distinct from a new interface).

EDIT: It seems quite possible that callingMethod does not care whether render targets are recreated (I'm assuming this is a question of performance v. accuracy). After all, "We should forget about small efficiencies, say about 97% of the time" (Knuth). So ScreenSpaceEffects2 could add a resetFluidSimulation(bool) method, but have resetFluidSimulation() keep working (without change to callingMethod) by calling _fluidDynamics.reset(true) behind the scenes.

like image 21
Matthew Flaschen Avatar answered Oct 21 '22 04:10

Matthew Flaschen