DRY против "предпочитают сдерживание над наследованием"

Существует общее правило проектирования OO, которое вы должны моделировать: отношения, использующие наследование, и имеющие отношения с использованием сдерживания/агрегации и пересылки/делегирования. Это еще более сузилось из предостережения от GoF, что вы, как правило, предпочитаете сдерживание над наследованием, предлагая, возможно, чтобы, если бы вы могли сделать убедительный аргумент для одного в конкретной ситуации, это сдерживание обычно должно получить одобрение из-за обслуживания Иногда может наследоваться проблемы.

Я понимаю причины этого мышления, и я не согласен с этим. Однако, когда я вижу класс с множеством методов, каждый из которых просто пересылает некоторую переменную экземпляра, я вижу форму дублирования кода. Кодирование дублирования, на мой взгляд, является окончательным запахом кода. Повторное выполнение огромного протокола методов только потому, что связь между двумя классами не является строго-а, похоже, излишним. Это дополнительный, ненужный код, добавленный в систему, код, который теперь нуждается в проверке и документировании, как и любая другая часть системы, - код, который вам, вероятно, не пришлось бы писать, если вы просто унаследовали.

Издержки, связанные с соблюдением этого принципа сдерживания над наследством, перевешивают его преимущества?

Ответы

Ответ 1

Стоимость практически любого может перевесить его преимущества. С жесткими и быстрыми правилами NO EXCEPTIONS всегда будут возникать проблемы с разработкой. В общем, это плохая идея использовать (если ваш язык/время исполнения поддерживает) отражение, чтобы получить доступ к переменным, которые не предназначены для видимости вашего кода. Это плохая практика? Как и все,

it depends.

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

Ни одно из этих понятий не является плохим, и люди должны осознавать тот факт, что иногда наше собственное отсутствие понимания или сопротивления изменениям может заставить нас создавать произвольные правила, которые определяют что-то как "плохое" без каких-либо реальных оснований для этого.

Ответ 2

Вы должны выбрать правильное решение проблемы. Иногда наследование лучше, чем сдерживание, иногда нет. Вы должны использовать свое мнение, и когда вы не можете понять, куда идти, напишите небольшой код и посмотрите, насколько он плох. Иногда писать какой-то код может помочь вам принять решение, которое не очевидно.

Как всегда: правильный ответ зависит от множества факторов, из-за которых вы не можете выполнять жесткие правила.

Ответ 3

Да, то, что вы видите, - это ужасное столкновение парадигмы дизайна из разных уголков вселенной: агрегация/состав GoF, влияющая на столкновение с "Законом Деметры".

Я уверен, полагая, что в контексте агрегирования и использования композиции Закон Деметры - это анти-шаблон.

В противоположность этому, я считаю, что конструкции вроде person->brain->performThought() абсолютно правильны и уместны.

Ответ 4

Я согласен с вашим анализом и предпочитаю наследование в этих случаях. Мне кажется, что это немного то же самое, что слепо внедрять глупых аксессуаров в наивную попытку обеспечить инкапсуляцию. Я думаю, что урок здесь состоит в том, что просто нет универсальных правил, которые всегда применяются.

Ответ 5

В то время как я согласен со всеми, кто сказал "это зависит", и что он также зависит от языка в определенной степени - я удивлен, что никто не упомянул об этом (в знаменитых словах Аллена Голуба) " extends is evil". Когда я впервые прочитал эту статью, я должен признать, что я немного отстранился, но он прав: независимо от языка, отношение is - это самая узкая форма связи. Высокие цепочки наследования являются отличным анти-шаблоном. Поэтому, хотя неправильно говорить, что вы всегда должны избегать наследования, его следует использовать экономно (для классов - наследование интерфейса рекомендуется). Моя объектно-ориентированная тенденция noob заключалась в том, чтобы моделировать все как цепочку наследования, и да, это уменьшает дублирование кода, но при очень реальной стоимости жесткой связи, что означает неизбежное обслуживание головных болей где-то в будущем.

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

С другой стороны, использование агрегации/состава заставляет хорошо инкапсулировать, делая систему намного менее жесткой. Группируйте, что многократно используемый код в класс утилиты, ссылка на него с отношениями "есть" и ваш клиентский класс теперь потребляют услугу, предоставляемую в соответствии с контрактом. Теперь вы можете реорганизовать класс утилиты на свой сердечный контент; пока вы соглашаетесь с интерфейсом, ваш клиентский класс может оставаться блаженно не осведомленным об изменении, и (что важно) его не нужно перекомпилировать.

Я не предлагаю это как религию, просто лучшая практика. Разумеется, они должны быть разбиты, когда это необходимо, но, как правило, есть веская причина, что в этом термине "лучший".

Ответ 6

Это может не ответить на ваш вопрос, но есть что-то, что всегда беспокоило меня о Java и его стеке. Стек, насколько мне известно, должен быть очень простой (или, возможно, самой простой) структурой данных контейнера с тремя основными публичными операциями: pop, push и peek. Зачем вам нужно в Stack insertAt, removeAt, et al. Вид функциональности? (в Java, Stack наследует от Vector).

Можно сказать, что, по крайней мере, вам не нужно документировать эти методы, но почему есть методы, которые не должны быть там в первую очередь?

Ответ 7

В какой-то степени это вопрос языковой поддержки. Например, в Ruby мы можем реализовать простой стек, который использует массив внутри:

class Stack
  extend Forwardable
  def_delegators :@internal_array, :<<, :push, :pop
  def initialize() @internal_array = [] end
end

Все, что мы делаем здесь, объявляет, какое подмножество других функций класса мы хотим использовать. Там действительно не много повторений. Черт, если мы действительно хотели бы использовать все другие функциональные возможности класса, мы могли бы даже указать, что ничего не повторю:

class ArrayClone
  extend Forwardable
  def_delegators(:@internal_array, 
                  *(Array.instance_methods - Object.instance_methods))
  def initialize() @internal_array = [] end
end

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

Ответ 8

Вы можете подумать о реализации кода "decorator" в абстрактном базовом классе, который (по умолчанию) перенаправляет все вызовы методов на содержащийся объект. Затем подклассируем абстрактные декораторы и переопределяем/добавляем методы по мере необходимости.

abstract class AbstractFooDecorator implements Foo {
    protected Foo foo;

    public void bar() {
        foo.bar();
    }
}

class MyFoo extends AbstractFooDecorator {
    public void bar() {
        super.bar();
        baz();
    }
}

Это по крайней мере устраняет повторение кода "пересылки", если у вас много классов, обертывающих определенный тип.

Что касается того, является ли руководство полезным, я полагаю, что акцент должен быть сделан на слово "предпочитают". Очевидно, будут случаи, когда имеет смысл использовать наследование. Здесь пример, когда наследование не должно использоваться:

Класс Hashtable был расширен в JDK 1.2, чтобы включить новый метод, entrySet, который поддерживает удаление записей из Hashtable. Класс Provider не был обновлен, чтобы переопределить этот новый метод. Этот контроль позволил злоумышленнику обойти проверку SecurityManager, принудительно выполняемую в Provider.remove, и удалить сопоставления провайдеров, просто вызвав метод Hashtable.entrySet.

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

Ответ 9

GoF - старый текст к настоящему времени. Зависит от того, на какой OO-среде вы смотрите (и даже тогда очевидно, что вы можете встретить его в OO-недружественных частях, таких как C-с-классами).

Для среды исполнения у вас практически нет выбора, одиночного наследования. И любое обходное решение, предпринятое для того, чтобы обойти его ограничения, заключается в том, что, как бы сложной или "крутой" это не показалось.

Снова вы увидите, что это проявляется повсюду, включая С++ (наиболее способный), где он взаимодействует с обратными вызовами C (который достаточно распространен, чтобы заставить кого-либо обратить внимание). С++, однако, предлагает вам смешения и разработки на основе политик с функциями шаблонов, поэтому он может иногда помогать в жесткой инженерии.

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

Реальная проблема заключается в том, что компьютерный язык или инструмент моделирования должны дать вам возможность, что он должен делать независимо от выбора и как таковой сделать его менее подверженным ошибкам человека; но не многие люди модели, прежде чем позволить компьютерам писать программы для них + нет хороших инструментов вокруг (Osla, конечно, не один), или их окружающая среда толкает что-то глупое, как отражение, IoC и что нет. Это очень популярно и что в сам говорит много.

Когда-то был сделан кусок для древнего COM-технологии, который был назван Universal Delegator одним из лучших игроков Doom, но это не тот тип развития, который кто-то мог принять в наши дни. Для этого требовались интерфейсы (и а не реальное требование для общего случая). Идея проста, она предшествует всем способам прерывания обработки. Только аналогичные апробации дают вам лучшее из обоих миров, и они несколько очевидны в сценариях, выполненных в виде скриптов, как JavaScript и функциональное программирование (хотя и менее читаемое или выполняемое).