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.
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.