Обертки / закон Деметры, кажется, против паттерна - PullRequest
5 голосов
/ 31 марта 2010

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

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 кажется немного проще и следует «правилу Деметры», скрывает реализацию 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
}

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

РЕДАКТИРОВАТЬ: Один из многих реальных примеров, для которых я столкнулся с этим, а не тривиальным примером.

Ответы [ 3 ]

14 голосов
/ 31 марта 2010

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

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

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

2 голосов
/ 31 марта 2010

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

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

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() ); }
}

... в этом случае ничего в callingMethod() вообще не нужно менять.

2 голосов
/ 31 марта 2010

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

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

РЕДАКТИРОВАТЬ: Кажется вполне вероятным, что вызываетMethod не волнует, воссозданы ли цели рендеринга (я предполагаю, что это вопрос производительности против точности). В конце концов, «Мы должны забыть о малой эффективности, скажем, в 97% случаев» (Кнут). Таким образом, ScreenSpaceEffects2 мог бы добавить метод resetFluidSimulation(bool), но resetFluidSimulation() продолжал работать (без изменения вызывающего метода), вызывая _fluidDynamics.reset(true) за кулисами.

...