Типичный пример обобщения (generalization) включает индивидуального и корпоративного клиентов некоторой бизнес-системы. Несмотря на определенные различия, у них много общего. Одинаковые свойства можно поместить в базовый класс Customer (Клиент, супертип), при этом класс Personal Customer (Индивидуальный клиент) и класс Corporate Customer (Корпоративный клиент) будут выступать как подтипы.
Этот факт служит объектом разнообразных интерпретаций в моделях различных уровней. На концептуальном уровне мы можем утверждать, что Корпоративный клиент представляет собой подтип Клиента, если все экземпляры класса Корпоративный клиент по определению являются также экземплярами класса Клиент. Таким образом, класс Корпоративный клиент представляет собой частную разновидность класса Клиент. Основная идея заключается в следующем: все, что нам известно о классе Клиент (ассоциации, атрибуты, операции), справедливо также и для класса Корпоративный клиент.
С точки зрения программного обеспечения очевидная интерпретация наследования выглядит следующим образом: Корпоративный клиент является подклассом класса Клиент. В основных объектно-ориентированных языках подкласс наследует всю функциональность суперкласса и может переопределять любые методы суперкласса.
|
|
Важным принципом эффективного использования наследования является заменяемость. Мне необходимо иметь возможность подставить Корпоративного клиента в любом месте программы, где требуется Клиент, и при этом все должно прекрасно работать. По существу это означает, что когда я пишу программу в предположении, что у меня есть Клиент, то я могу свободно использовать любой подтип Клиента.
Вследствие полиморфизма Корпоративный клиент может реагировать на определенные команды не так, как другой Клиент, но вызывающий не должен беспокоиться об этом отличии. (Дополнительную информацию можно найти в главе «Liskov Substitution Principle (LSP)» (Принцип замещения Лисков) книги [30]).
Наследование представляет собой мощный механизм, но оно несет с собой много такого, что не всегда является необходимым для достижения замещаемости. Вот хороший пример: на заре существования языка Java многим разработчикам не нравилась реализация встроенного класса Vector (Вектор), и они хотели заменить его чем-нибудь полегче. Однако единственным способом получения класса, способного заменить Vector, было создание его подкласса, что означало наследование множества нежелательных данных и поведения.
Замещаемые классы можно создавать при помощи массы других механизмов. Поэтому многие разработчики предпочитают различать создание подтипа, то есть наследование интерфейса, и создание подкласса, или наследование реализации. Класс - это подтип, если он может замещать свой супертип, в независимости от того, использует он наследование или нет. Создание подкласса используется как синоним обычного наследования.
Существует достаточное количество других механизмов, позволяющих создавать подтипы без создания подклассов. Примером может служить реализация интерфейса (стр. 96) и множество стандартных шаблонов разработки [21].