Взгляд на класс как на модуль
Взгляд на класс как на модуль
С этой точки зрения наследование особенно эффективно в качестве метода повторного использования.
Модуль это множество служб, предлагаемых внешнему миру. Без наследования каждому новому модулю пришлось бы самому определять все предоставляемые им службы. Конечно, реализации этих служб могут основываться на службах, предоставляемых другими модулями: это и есть цель отношения "быть клиентом". Но единственным способом определить новый модуль является добавление новых служб к ранее определенным модулям.
Наследование предоставляет эту возможность. Если B является наследником A, то все службы (компоненты) A автоматически доступны в B, и их не нужно в нем явно определять. В соответствии со своими целями B может добавить новые компоненты. Дополнительная гибкость обеспечивается переопределением, позволяющим B по-разному использовать реализации, предлагаемые A: некоторые из них не меняются, а другие переделываются в более подходящие для данного класса версии.
Это приводит к такому стилю разработки ПО, при котором вместо попытки решать каждую новую задачу с нуля поощряется ее решение, основанное на предыдущих достижениях и на расширении их результатов. Его смысл состоит в экономии - зачем повторять то, что уже однажды было сделано? - и в скромности, в духе известного замечания Ньютона, что он смог достичь таких высот только потому, что стоял на плечах гигантов.
Полное преимущество этого подхода лучше всего понимается в терминах принципа Открыт-Закрыт, введенного в одной из предыдущих лекций. (Стоило бы перечитать этот раздел в свете только что введенных понятий.) Этот принцип утверждает, что хорошая структура модуля должна быть и закрытой, и открытой.
[x]. Закрытой, поскольку клиентам для выполнения их собственной разработки нужны службы модуля и, будучи один раз зафиксированы в некоторой его версии, они не должны изменяться при введении новых служб, в которых клиент не нуждается.
[x]. Открытой, так как нет никакой гарантии, что с самого начала в модуль были включены все службы, потенциально необходимые некоторому клиенту.
Эти два требования представляют дилемму, и классическая структура модулей не дает ключа к ее разгадке. Но наследование эту проблему решает. Класс закрыт, так как он может компилироваться, заноситься в библиотеку и использоваться классами-клиентами. Но он также открыт, поскольку любой новый класс может его использовать в качестве родителя, добавляя новые компоненты и меняя объявления некоторых унаследованных компонентов, при этом совершенно не нужно изменять исходный класс и беспокоить его клиентов. Это фундаментальное свойство при применении наследования к построению повторно используемого расширяемого ПО.
Если бы довести эту идею до предела, то каждый класс просто добавлял бы один компонент к его родителям! Конечно, это не рекомендуется. Решение завершить класс не следует принимать легковесно, оно должно основываться на осознанном заключении о том, что класс в его нынешнем состоянии уже обеспечивает логически последовательный набор служб - стройную абстракцию данных - для потенциальных клиентов.
Следует помнить, что принцип Открыт-Закрыт не отменяет последующей переделки неадекватных служб. Если плохой результат явился следствием неверной спецификации компонента, то мы не сможем модифицировать класс так, чтобы это не отразилось на его клиентах. Однако, благодаря переопределению, принцип Открыт-Закрыт все еще применим, если вводимое изменение согласовано с объявленной спецификацией.
Одним из самых трудных вопросов, связанных с проектированием повторно используемых структур модулей, была необходимость использовать преимущества большой общности, которая может существовать у разных однотипных групп абстракций данных - у всех хеш-таблиц, всех последовательных таблиц и т. п. Используя структуры классов, связанных наследованием, можно получить выигрыш, зная логические соотношения между разными реализациями. Внизу на диаграмме представлен грубый и частичный набросок возможной структуры библиотеки для работы с таблицами. В этой схеме естественно используется множественное наследование, которое будет детально обсуждаться в следующей лекции.
Рис. 14.12. Набросок структуры библиотеки таблиц
Эта диаграмма наследования представляет только набросок, хотя на ней показаны типичные для этих структур связи по наследованию. Систематическую классификацию таблиц и других контейнеров, основанную на наследовании, см. в [M 1994a].При таком взгляде требование повторного использования можно выразить весьма точно: идея состоит в том, чтобы передвинуть определение каждого компонента как можно выше в иерархии наследования так, чтобы он мог наследоваться максимально возможным числом классов-потомков. Можно представлять этот процесс как игру переиспользования, в которую играют на доске, представляющей иерархии наследования (такие, как на рис. 14.12), фигурами, представляющими компоненты. Выигрывает тот, кто сможет в результате открытия абстракций более высокого уровня передвинуть как можно больше компонентов как можно выше, и по пути, благодаря обнаружению общих свойств, сможет слить наибольшее число фигур.