Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 4087 / 201 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Лекция 6:

Используйте наследование правильно

Множественные критерии и наследование видов

Вероятно, наиболее трудная проблема наследования возникает при наличии альтернативных критериев классификации.

Классификация при множественных критериях

Традиционная классификация в естественных науках использует единственный критерий (возможно, объединяющий несколько качеств) на каждом уровне: позвоночные или беспозвоночные, ветви, обновляемые один или несколько раз в год, и тому подобное. Результатом будет то, что называется иерархией единственного наследования, чье главное преимущество - простота классификации. Конечно, возникают проблемы, поскольку природа определенно не пользуется единственным критерием. Это очевидно для всякого, кто когда-либо пытался провести классификацию, вооружившись книгой по ботанике с традиционной классификацией Линнея.

При разработке ПО, где единый критерий кажется ограничительным, мы можем использовать все приемы множественного и особенно дублирующего наследования, которыми мы овладели при изучении предыдущих лекций. Рассмотрим, например, класс EMPLOYEE в системе управления персоналом. Предположим также, что у нас есть два различных критерия классификации служащих:

  • по типу контракта: временные или постоянные работники;
  • по типу исполняемой работы: инженерная, административная, управленческая.

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

В первой попытке введем все варианты на одном и том же уровне (рис. 6.12).

Для простоты на этой схеме имена классов сокращены. В реальной системе мы действуем более аккуратно и используем, как положено, полные имена, такие как PERMANENT_EMPLOYEE, ENGINEERING_EMPLOYEE и так далее.

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

Беспорядочная классификация

Рис. 6.12. Беспорядочная классификация

Наследование вида

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

Появились два вида служащих. Заметьте, имя CONTRACT_EMPLOYEE не означает служащего, имеющего контракт, а служащего, характеризуемого контрактом (он может не иметь контракта!). Имя класса для другого вида означает "служащий, характеризуемый своей специальностью".

То, что эти имена кажутся неестественными, отражает определенную сложность, характерную для наследования видов. При наследовании подтипов мы встречались с правилом, устанавливающим, что экземпляры наследников принадлежат непересекающимся подмножествам множества, заданного родителем. Здесь это правило неприменимо. Постоянный служащий имеет специальность и может быть инженером. Такая классификация подходит для дублирующего наследования: некоторые потомки классов, показанных на рисунке, будут иметь в качестве предков CONTRACT_EMPLOYEE и SPECIALTY_EMPLOYEE не напрямую, но через наследование от классов PERMANENT и ENGINEER. Такие классы будут дублируемыми потомками EMPLOYEE.

Эта форма наследования может быть названа наследованием видов: различные наследники некоторого класса представляют не непересекающиеся подмножества его экземпляров, но различные способы классификации экземпляров родителя. Заметьте, это имеет смысл только при условии, что родитель и наследники являются отложенными классами, говоря другими словами, классами, описывающими общие категории, а не полностью специфицированные объекты. Наша первая попытка классификации EMPLOYEE по видам (та, у которой все потомки на одном уровне) нарушает это правило, вторая ему удовлетворяет.

Классификация, использующая виды

Рис. 6.13. Классификация, использующая виды

Подходит ли нам наследование видов?

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

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

Альтернативой наследованию типов служит выбор одного критерия в качестве первичного, он и будет руководить построением иерархии. Для учета других критериев следует использовать специальные компоненты класса. Стоит отметить, что современные зоологи и ботаники используют именно такой подход: их основной критерий классификации основан на реконструкции эволюционной истории, включающей деление на роды и виды. Значит ли это, что мы всегда имеем единый, бесспорный стандарт, руководящий нами при создании программистских таксономий?

Чтобы в нашем примере придерживаться единого критерия, мы могли бы принять решение, что тип работы служащего является более важным фактором, а статус контракта задать компонентом. Рассмотрим первую попытку введения в класс EMPLOYEE такого компонента:

is_permanent: BOOLEAN

Но такое решение накладывает серьезные ограничения. Расширяя возможности, приходим к варианту:

Permanent: INTEGER is unique
Temporary: INTEGER is unique
Contractor: INTEGER is unique
...

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

if is_permanent then ... else ... end

или

inspect
    contract_type
when Permanent then
    ...
when ...
    ...
end

Как неоднократно говорилось, разбор случаев приводит к ряду проблем при появлении новых вариантов и нарушает важные принципы непрерывности, единого выбора, открытость-закрытость и так далее. Вместо этого мы поставляем класс WORK_CONTRACT с отложенными компонентами, представляющими операции, зависящие от типа контракта, которые по-разному будут реализованы потомками. Большинству из этих компонентов будет необходим аргумент типа EMPLOYEE, представляющий служащего, к которому применяется операция, примерами операций могут быть hire (приглашение на работу) and terminate (увольнение).

Результирующая структура показана на рис. 6.14.

Эта схема, как вы заметили, почти идентична образцу проектирования с описателями, изучаемому ранее в этой лекции.

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

Многокритериальная классификация с независимой иерархией, построенной для клиента

Рис. 6.14. Многокритериальная классификация с независимой иерархией, построенной для клиента

Критерии для наследования видов

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

Все же я нахожу наследование видов полезным при выполнении следующих трех условий:

  • Различные критерии классификации одинаково важны, так что выбор одного в качестве основного представляется спорным.
  • Многие возможные комбинации (такие как в примере: permanent supervisor, temporary engineer, permanent engineer и так далее) являются необходимыми.
  • Рассматриваемые классы настолько важны, что стоит потратить время на разработку лучшей из возможных структур наследования. Чаще всего речь идет в таких случаях о библиотечных классах повторного использования.

Примером приложения, удовлетворяющего этим критериям, является библиотека Base с ее структурой иерархии на верхних уровнях, описанная в последней лекции этой книги. Классы, полученные в результате этих усилий, в деталях описаны в [M 1994а]. Они построены в традиции естественных наук с применением таксономических принципов систематической классификации основных программистских структур. Верхняя часть этой иерархии выглядит так:

Классификация, основанная на видах фундаментальных программистских структур

Рис. 6.15. Классификация, основанная на видах фундаментальных программистских структур

Классификация на первом уровне ( BOX, COLLECTION, TRAVERSABLE ) основана на типах; уровень ниже (и многие другие, не показанные на рисунке) задают классификацию подтипов. Структура контейнера характеризуется тремя различными критериями:

  • COLLECTION определяет доступ к элементам. Класс SET позволяет определить сам факт присутствия элемента, в то время как BAG позволяет также посчитать число вхождений данного элемента. Дальнейшие уточнения включают такие абстракции доступа, как SEQUENCE (элементы доступны последовательно), STACK (элементы доступны в порядке, обратном их включению) и так далее.
  • BOX определяет представление элементов. Варианты включают конечные и бесконечные структуры. Конечные структуры могут быть ограниченными и не ограниченными. Ограниченные структуры могут быть фиксированными или изменяемого размера.
  • TRAVERSABLE определяет способы обхода структур.

Интересно отметить, что эта иерархия не начиналась, как иерархия видов. Начальная идея состояла в том, чтобы определить BOX, COLLECTION и TRAVERSABLE как несвязанные классы, каждый, задающий вершину своей независимой иерархии. Затем при описании реализации любой специальной структуры данных использовать множественное наследование с родителями из каждой иерархии. Например, связный список является конечным и неограниченным с последовательным доступом и линейным способом обхода.

Построение структуры данных комбинированием абстракций путем множественного наследования

Рис. 6.16. Построение структуры данных комбинированием абстракций путем множественного наследования

Но затем мы осознали, что независимые семейства классов BOX, COLLECTION и TRAVERSABLE не лучший способ: им всем потребовались некоторые общие компоненты, в частности has (тест на проверку членства) и empty (тест на отсутствие элементов). Все это указывало на необходимость иметь общего родителя - CONTAINER, где эти общие свойства теперь и появляются. Следовательно, структура, изначально спроектированная как чистое множественное наследование с тремя непересекающимися иерархиями, превратилась в структуру с наследованием типов, приводящую к дублируемому наследованию.

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