Обертки/закон Деметры кажутся антипаттерном

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

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 или раскрытия внутреннего).

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


person Robert Fraser    schedule 31.03.2010    source источник
comment
Вы имеете в виду, что версия 2 кажется немного проще?   -  person Matthew Flaschen    schedule 31.03.2010
comment
Да, извините, изменится   -  person Robert Fraser    schedule 31.03.2010
comment
Версия 1 не следует правилу Деметры. Опечатка?   -  person Corwin    schedule 31.03.2010


Ответы (3)


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

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

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

person tonio    schedule 31.03.2010
comment
Я не уверен, что это стоит дополнительной сложности (особенно если классы уже связаны), но все равно спасибо за ответ :-). - person Robert Fraser; 02.04.2010
comment
Почти каждый раз, когда что-то меняется, что-то еще должно измениться. Лучшее, что можно сделать, это попытаться устроить все так, чтобы те части, которые вы хотите оставить неизменными, могли оставаться постоянными, даже если вещи, которые, вероятно, изменятся, изменятся. Каждый из представленных проектов сможет легче приспосабливаться к некоторым типам потенциальных будущих изменений API, чем другой; какой дизайн лучше, в некоторой степени зависит от того, как человек оценивает вероятность различных возможных изменений в будущем. - person supercat; 12.11.2013

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

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

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

person Matthew Flaschen    schedule 31.03.2010
comment
На самом деле, я задаю этот вопрос, потому что он слишком много раз возникал в моем хобби-проекте. - person Robert Fraser; 31.03.2010
comment
Что случилось? Может реальный пример, вместо всех Foo этого и Bar этого? - person Matthew Flaschen; 31.03.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() менять не нужно.

person APC    schedule 31.03.2010