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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With