Поделиться через


Употребление наследования

Обновлен: Ноябрь 2007

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

Наследование лучше использовать в следующих случаях.

  • Иерархия наследования представляет собой отношения тождественности (is-a), а не отношения включения (has-a).

  • Допускается повторное использование кода базовых классов.

  • Требуется применение одинаковых классов и методов к различным типам данных.

  • Иерархия класса включает небольшое число уровней, и другие разработчики вряд ли будут добавлять в нее другие уровни.

  • Требуется внесение глобальных изменений в производные классы путем изменения базового класса.

Данные пункты далее рассматриваются по порядку.

Наследование и тождественность

В объектно-ориентированном программировании представлены два вида отношений между классами: отношения тождественности и включения. В отношениях тождественности производный класс явно является видом базового класса. Например, класс PremierCustomer (важный клиент) тождественен с базовым классом Customer (клиент), поскольку важный клиент является клиентом. Однако класс CustomerReferral (сведения о клиенте) включен в класс Customer, поскольку сведения о клиенте включают в себя понятие клиента, но не являются типом клиента.

Объекты в иерархии наследования должны иметь отношения тождественности с их базовым классом, так как они наследуют поля, свойства, методы и события, определенные в базовом классе. Классы, имеющие с другими классами отношения включения, не подходят для иерархий наследования, так как могут унаследовать неподходящие свойства и методы. Например, если класс CustomerReferral был получен из класса Customer (об это говорилось ранее), он может наследовать свойства, не имеющие смысла, такие как ShippingPrefs и LastOrderPlaced. Отношения включения должны быть представлены при помощи несвязанных классов или интерфейсов. Далее показан пример отношений тождественности и включения.

Сравнение отношений “Is a” и “Has a”

Базовые классы и повторное использование кода

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

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

Class CustomerInfo
    Protected PreviousCustomer As CustomerInfo
    Protected NextCustomer As CustomerInfo
    Public ID As Integer
    Public FullName As String

    Public Sub InsertCustomer(ByVal FullName As String)
        ' Insert code to add a CustomerInfo item to the list.
    End Sub

    Public Sub DeleteCustomer()
        ' Insert code to remove a CustomerInfo item from the list.
    End Sub

    Public Function GetNextCustomer() As CustomerInfo
        ' Insert code to get the next CustomerInfo item from the list.
        Return NextCustomer
    End Function

    Public Function GetPrevCustomer() As CustomerInfo
        'Insert code to get the previous CustomerInfo item from the list.
        Return PreviousCustomer
    End Function
End Class

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

Class ShoppingCartItem
    Protected PreviousItem As ShoppingCartItem
    Protected NextItem As ShoppingCartItem
    Public ProductCode As Integer
    Public Function GetNextItem() As ShoppingCartItem
        ' Insert code to get the next ShoppingCartItem from the list.
        Return NextItem
    End Function
End Class

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

Class ListItem
    Protected PreviousItem As ListItem
    Protected NextItem As ListItem
    Public Function GetNextItem() As ListItem
        ' Insert code to get the next item in the list.
        Return NextItem
    End Function
    Public Sub InsertNextItem()
        ' Insert code to add a item to the list.
    End Sub

    Public Sub DeleteNextItem()
        ' Insert code to remove a item from the list.
    End Sub

    Public Function GetPrevItem() As ListItem
        'Insert code to get the previous item from the list.
        Return PreviousItem
    End Function
End Class

Класс ListItem требует только однократной отладки. Затем можно строить классы, использующие данный класс, не заботясь больше об управлении списками. Пример.

Class CustomerInfo
    Inherits ListItem
    Public ID As Integer
    Public FullName As String
End Class
Class ShoppingCartItem
    Inherits ListItem
    Public ProductCode As Integer
End Class

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

Взаимозаменяемые производные классы

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

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

Sub Draw(ByVal Shape As DrawingShape, ByVal X As Integer, _
    ByVal Y As Integer, ByVal Size As Integer)

    Select Case Shape.type
        Case shpCircle
            ' Insert circle drawing code here.
        Case shpLine
            ' Insert line drawing code here.
    End Select
End Sub

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

Большинство из этих проблем решается при помощи наследования. Хорошо разработанные базовые классы оставляют реализацию отдельных методов производным классам, что позволяет применять любые фигуры. Другие разработчики могут реализовывать методы в производных классах, используя документацию базовых классов. Другие члены (например координаты x и y) могут встраиваться в базовый класс, так как они используются всеми производными классами. Например Draw может быть методом MustOverride:

MustInherit Class Shape
    Public X As Integer
    Public Y As Integer
    MustOverride Sub Draw()
End Class

Затем можно добавлять к данному классу функциональные возможности для вычерчивания разных фигур. Например, для класса Line может требоваться только поле Length:

Class Line
    Inherits Shape
    Public Length As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for this shape.
    End Sub
End Class

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

Class Rectangle
    Inherits Line
    Public Width As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for the Rectangle shape.
    End Sub
End Class

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

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

Неполные иерархии классов

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

Глобальные изменения производных классов через базовый класс

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

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

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

См. также

Основные понятия

Употребление интерфейсов

Изменение структуры базовых классов после развертывания