Обертки/закон диметра, кажется, антишаблон

Я читал на этом "Законе Demeter" вещь, и это (и чистые классы "обертки" в целом), кажется, обычно выступает против шаблонов. Рассмотрите класс реализации:

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

Теперь рассмотрите две различных реализации другого класса:

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

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

И способы звонить сказали методы:

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

На первый взгляд версия 2 кажется немного более простой, и следует "правилу Demeter", скрывается реализация Foo, и т.д., и т.д. Но это связывает любые изменения в FluidSimulator к ScreenSpaceEffects. Например, если параметр добавляется для сброса, то мы имеем:

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
}

В обеих версиях должен быть изменен callingMethod, но в Версии 2, также должен быть изменен ScreenSpaceEffects. Может кто-то объяснять преимущество наличия обертки/фасада (за исключением адаптеров или обертывания внешнего API или представления внутреннего).

Править: Один из многих реальных примеров, для которых я столкнулся с этим, а не тривиальным примером.

5
задан Robert Fraser 31 March 2010 в 10:29
поделиться

3 ответа

Основное различие заключается в том, что в версии 1, как поставщик абстракции Bar, вы не имеете контроля над тем, как Foo раскрывается. Любое изменение в Foo будет открыто для ваших клиентов, и им придется с этим смириться.

В версии 2, как поставщик абстракции Bar, вы можете решить, хотите ли вы раскрывать эволюции и как. Это будет зависеть только от абстракции Bar, а не от Foo. В вашем примере абстракция Bar может уже знать, какое целое число передавать в качестве аргумента, и таким образом вы сможете позволить своим пользователям прозрачно использовать новую версию Foo без каких-либо изменений.

Предположим, что теперь Foo развивается и требует от пользователя вызова foo.init() перед любым вызовом doSomething. В версии 1 все пользователи Bar должны будут увидеть, что Foo изменился, и адаптировать свой код. В версии 2 только Bar должен быть изменен, его doSomething вызывает init, если это необходимо. Это приводит к меньшему количеству ошибок (только автор абстракции Bar должен знать и понимать абстракцию Foo) и меньшей связи между классами.

14
ответ дан 18 December 2019 в 09:06
поделиться

Вопрос в том, нужно ли callMethod () знать, нужно ли воссоздавать таблицы рендеринга?

Предположим, что при выполнении некоторого заданного выполнения callMethod () требуется или не требуется воссоздавать таблицы отрисовки. В этом случае вы расширяете оболочку новым методом. Затем вам просто нужно вызвать новый метод только из соответствующих экземпляров callMethod () .

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

В качестве альтернативы решение о воссоздании может принадлежать совершенно другому месту ...

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

... в этом случае ничто в callMethod () вообще не нуждается в изменении.

2
ответ дан 18 December 2019 в 09:06
поделиться

Очевидно, что это искусственный пример. Во многих реальных случаях вызывающий метод (в реальной жизни может быть несколько вызывающих методов) может оставаться в неведении, что Foo.doSomething изменился, потому что Bar изолирует его. Например, если я использую стабильный API печати, мне не нужно беспокоиться о том, что в прошивку моего принтера добавят поддержку глянцевой печати. Мой существующий код черно-белой печати продолжает работать. Я полагаю, вы бы отнесли это к "адаптерам", которые, как мне кажется, встречаются гораздо чаще, чем вы предполагаете.

Вы правы, что иногда callingMethod тоже должен быть изменен. Но при правильном использовании закона Деметры это происходит крайне редко, обычно для того, чтобы воспользоваться преимуществами новой функциональности (в отличие от нового интерфейса).

EDIT: Кажется вполне возможным, что callingMethod не заботится о том, воссоздаются ли цели рендеринга (я предполагаю, что это вопрос производительности против точности). В конце концов, "Мы должны забыть о небольшой эффективности, скажем, около 97% времени" (Кнут). Поэтому ScreenSpaceEffects2 может добавить метод resetFluidSimulation(bool), но чтобы resetFluidSimulation() продолжал работать (без изменения callingMethod), вызывая _fluidDynamics.reset(true) за кулисами.

2
ответ дан 18 December 2019 в 09:06
поделиться
Другие вопросы по тегам:

Похожие вопросы: