Наследование в программировании: Наследование (программирование) | это… Что такое Наследование (программирование)?

Содержание

Наследование в объектно-ориентированном программировании. Курс «ООП на Kotlin»

Наследование – это одно из ключевых составляющих объектно-ориентированного программирования. В первую очередь оно позволяет

  • общую функциональность нескольких классов выносить в один общий суперкласс,

  • совместно обрабатывать объекты, созданные от разных, но родственных, классов.

Вспомним наш класс:

class NumInc(n: Int, gap: Int) {
    var number = n
    var step = gap
 
    fun inc() {
        number += step
    }
    fun dec() {
        number -= step
    }
}

Допустим, в программе должны быть объекты, поле number которых можно только увеличивать и уменьшать на величину шага. Также в программе нужны объекты, у которых number может изменяться не только добавлением/вычитанием шага, но также умножением на шаг. Конечно, мы можем написать еще один класс:

class NumMult(n: Int, gap: Int) {
    var number = n
    var step = gap
 
    fun inc() {number += step}
    fun dec() {number -= step}    
    fun mult() {number *= step}
}

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

Чтобы класс мог быть родительским перед его объявлением должно стоять ключевое слово open.

open class NumInc(n: Int, gap: Int) {
...

В свою очередь класс-наследник должен в своем заголовке иметь запись о родительском классе. В нашем случае определение класса NumMult будет выглядеть так:

class NumMult(num: Int, coef: Int): 
    NumInc(num, coef) {
    fun mult() {number *= step}
}

В заголовке после параметров первичного конструктора (если он есть) ставится двоеточие, после которого идет имя родительского класса. Поскольку конструктор родительского класса предусматривает два параметра, мы должны их туда передать.

После этого объекты NumMult будут обладать теми же свойствами и методами, что и объекты NumInc. У них тоже появятся свойства number и step, методы inc() и dec(). Однако помимо этого у них есть метод mult(), которого нет у объектов родительского класса.

Наследование может быть сложнее. Дочерний класс может стать родительским по отношению к другому дочернему. Для этого перед его объявлением также должно стоять слово open.

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

Давайте усложним наш пример, введя в дочерний класс третье свойство.

class NumMult(num: Int, gap: Int, coef: Int): 
    NumInc(num, gap) {
    var coefficient = coef
 
    fun mult() {number *= coefficient}
}

Теперь дочерний класс обладает не только дополнительным методом, но и дополнительным полем. Конструктору родительского мы по-прежнему передаем два аргумента. Больше он и не принимает.

При создании объекта от класса NumMult надо передавать три аргумента:

val b = NumMult(1, 3, 2)

Первые два будут присвоены полям number и step и использоваться в функциях inc() и dec(). Третий будет присвоен свойству coefficient и использоваться только в методе mult().

Теперь представим, что класс NumMult имеет два конструктора, а у NumInc он по прежнему один. В это случае вторичный конструктор NumMult должен делегировать к первичному своего же класса, а уже тот будет обращаться к конструктору родительского класса.

class NumMult(num: Int, gap: Int, coef: Int): 
    NumInc(num, gap) {
    var coefficient = coef
 
    constructor() : this(0, 1, 2)
 
    fun mult() {number *= coefficient}
}

Пример создания объекта через вторичный конструктор:

val c = NumMult()

Если у дочернего класса есть первичный конструктор, то все вторичные должны делегировать к нему. И только через него – к конструктору родительского класса. Однако если первичного конструктора нет, вторичные должны напрямую вызывать конструкторы родительского класса через ключевое слово super. Пример с двумя конструкторами как в основном, так и в дочернем классе при том, что в дочернем нет первичного:

open class NumInc(n: Int, gap: Int) {
    var number = n
    var step = gap
 
    constructor(): this(0, 1)
 
    fun inc() {number += step}
    fun dec() {number -= step}
}
class NumMult: NumInc {
    var coefficient = 2
 
    constructor(num: Int, gap: Int, coef: Int): 
        super(num, gap) {
        coefficient = coef
    }
 
    constructor(): super()
 
    fun mult() {number *= coefficient}
}

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

Рассмотрим другое преимущество наследования в ООП. В Kotlin мы можем присвоить переменной более общего типа объект дочернего типа, а не только своего собственного.

val a: NumInc = NumInc(2, 1)
val b: NumInc = NumMult(1, 3, 2)

Однако, поскольку переменная

b имеет тип NumInc через нее нельзя получить доступ к свойствам и методам, которых нет в NumInc. Объект NumMult приводится к типу NumInc с потерей своих дополнительных свойств и методов.

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

Таким образом, объекты разных дочерних классов одного родительского, или дочернего и родительского, могут обладать одними и теми же методами. Это позволяет производить групповую обработку таких разноклассовых объектов. Например, мы можем создать список объектов NumInc и NumMult, дальше в цикле перебрать список, вызывая один и тот же метод для всех объектов.

fun main() {
    val a: List<NumInc> = listOf(
        NumMult(),NumMult(3,4,3),
        NumInc(10, 3), NumInc(5, 1))
 
    for(i in a) {
        i.
inc() println(i.number) } }

Результат выполнения программы:

1
7
13
6

Практическая работа:

Переделайте последний пример классов из урока так, чтобы родительский класс содержал только один конструктор, а дочерний – два.

PDF-версия курса с ответами к практическим работам

Приложение для Android «Kotlin. Курс»


Наследование. Урок 4 курса «Объектно-ориентированное программирование на Python»

Наследование – важная составляющая объектно-ориентированного программирования. Так или иначе мы уже сталкивались с ним, ведь объекты наследуют атрибуты своих классов. Однако обычно под наследованием в ООП понимается наличие классов и подклассов. Также их называют супер- или надклассами и классами, а также родительскими и дочерними классами.

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

Простое наследование методов родительского класса

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

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

class Table:
    def __init__(self, l, w, h):
        self.length = l
        self.width = w
        self.height = h
 
 
class DeskTable(Table):
    def square(self):
        return self.width * self.length
 
 
t1 = Table(1.5, 1.8, 0.75)
t2 = DeskTable(0.8, 0.6, 0.7)
print(t2.square())  # вывод: 0.48

В данном случае у класса DeskTable нет своего конструктора, поэтому он наследует его от родителя. При создании объектов передавать аргументы необходимо в обоих случаях. Попытка вызова DeskTable с пустыми скобками приведет к ошибке.

С другой стороны, экземпляры надкласса Table, согласно неким родственным связям, не наследуют метод square своего подкласса.

В этом смысле терминология «родительский и дочерний класс» не совсем верна. Наследование в ООП – это скорее аналог систематизации и классификации наподобие той, что есть в живой природе. Все млекопитающие имеют четырехкамерное сердце, но только носороги – рог.

Полное переопределение метода надкласса

Рассмотрим вариант программы с «цепочкой наследования». Пусть дочерний по отношению к Table класс DeskTable в свою очередь выступит родительским по отношению к ComputerTable (компьютерные столы):

class Table:
    def __init__(self, l, w, h):
        self.length = l
        self.width = w
        self.height = h
 
 
class DeskTable(Table):
    def square(self):
        return self.width * self.length
 
 
class ComputerTable(DeskTable):
    def square(self, monitor=0.0):
        return self.width * self.length - monitor
 
 
t3 = ComputerTable(0.8, 0.6, 0.7)
print(t3.square(0.3))  # вывод: 0.18

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

Определив в дочернем классе метод, одноименный методу родительского, мы тем самым переопределяем метод родительского класса. При вызове square на экземпляры ComputerTable будет вызываться метод из этого класса, а не из родительского класса DeskTable.

В то же время ComputerTable наследует конструктор класса от своей «бабушки» – класса Table.

Дополнение, оно же расширение, метода

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

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

class Table:
    def __init__(self, l, w, h):
        self.length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    def __init__(self, l, w, h, p):
        self.length = l
        self.width = w
        self.height = h
        self.places = p
 
 
t4 = KitchenTable(1.5, 2, 0.75, 6)

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

class Table:
    def __init__(self, l, w, h):
        self.length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    def __init__(self, l, w, h, p):
        Table.__init__(self, l, w, h)
        self.places = p
 
 
t4 = KitchenTable(1.5, 2, 0.75, 6)

Здесь в теле конструктора KitchenTable мы вызываем метод __init__ через объект-класс Table, а не через объект-экземпляр. Вспомним, что в таких случаях метод вызывается как обычная функция (объект, к которому применяется метод, не передается в качестве первого аргумента). Поэтому в конструктор надкласса мы «вручную» передаем текущий экземпляр (self), записывая его перед остальными аргументами.

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

В Python c целью улучшения так называемой обслуживаемости кода можно использовать встроенную в язык функцию super. Наиболее распространенным вариантом ее применения является вызов метода родительского класса из метода подкласса:

class KitchenTable(Table):
    def __init__(self, l, w, h, p):
        super().__init__(l, w, h)
        self.places = p

В данном случае аргумент self в скобках вызываемого родительского метода указывать явно не требуется.

Параметры со значениями по умолчанию у родительского класса

Рассмотрим случай, когда родительский класс имеет параметры со значениями по умолчанию, а дочерний – нет:

class Table:
    def __init__(self, l=1, w=1, h=1):
        self. length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    def __init__(self, p, l, w, h):
        Table.__init__(self, l, w, h)
        self.places = p

При таком определении классов можно создать экземпляр от Table без передачи аргументов для конструктора:

t = Table()

Можем ли мы создать экземпляр от KitchenTable, передав значение только для параметра p? Например, вот так:

k = KitchenTable(10)

Возможно ли, что p будет присвоено число 10, а l, w и h получат по единице от родительского класса? Невозможно, будет выброшено исключение по причине несоответствия количества переданных аргументов количеству требуемых конструктором:

...
    k = KitchenTable(10)
TypeError: __init__() missing 3 required
 positional arguments: 'l', 'w', and 'h'

Когда создается объект от дочернего класса, сначала вызывается его конструктор, если он есть. Интерпретатор еще не знает, что в теле этого конструктора будет вызван конструктор родительского класса. Ведь это не обязательно. Значит, если все параметры дочернего конструктора не имеют значений по умолчанию, при построении объекта все значения должны передаваться.

Поэтому, если требуется допустить создание объектов от дочернего класса без передачи аргументов, придется назначить значения по умолчанию также в конструкторе дочернего класса.

class Table:
    def __init__(self, l=1, w=1, h=1):
        self.length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    def __init__(self, l=1, w=1, h=0.7, p=4):
        Table.__init__(self, l, w, h)
        self.places = p

Параметр p, которого нет у родительского класса, мы делаем последним не просто так. Бывает, объекты разных родственных классов создаются или обрабатываются в одном цикле, то есть по одному алгоритму. При этом у них должны быть одинаковые «интерфейсы», то есть одинаковое количество передаваемых в конструктор аргументов.

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

Другой вариант – отказаться от конструктора в дочернем классе, а значение для поля places устанавливать отдельным вызовом метода:

class Table:
    def __init__(self, l=1, w=1, h=1):
        self.length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    places = 4
 
    def set_places(self, p):
        self.places = p

Здесь у всех кухонных столов по-умолчанию будет 4 места. Если мы хотим изменить значение поля places, можем вызвать метод set_places(). Хотя в случае Python можем сделать это напрямую, присвоив полю. При этом у экземпляра появится собственное поле places.

k = KitchenTable()
k.places = 6

Поэтому метод set_places() в общем-то не нужен.

В любом случае произвольное количество мест будет устанавливаться не в конструкторе, а отдельно. Если все же требуется указывать места при создании объекта, это можно сделать и в конструкторе родителя:

class Table:
    def __init__(self, l=1, w=1, h=1):
        self.length = l
        self.width = w
        self.height = h
        if isinstance(self, KitchenTable):
            p = int(input("Сколько мест: "))
            self.places = p

С помощью функции isinstance() проверяется, что создаваемый объект имеет тип KitchenTable. Если это так, то у него появляется поле places.

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

Практическая работа

Разработайте программу по следующему описанию.

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

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

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

Отправьте одного из солдат первого героя следовать за ним. Выведите на экран идентификационные номера этих двух юнитов.

Курс с примерами решений практических работ:
pdf-версия, android-приложение


Наследование и полиморфизм — Основы программирования

Дэйв Брауншвейг

Обзор

В объектно-ориентированном программировании наследование — это механизм базирования объекта или класса на другом объекте (прототипное наследование) или классе (наследование на основе классов) с сохранением аналогичной реализации. В большинстве объектно-ориентированных языков, основанных на классах, объект, созданный посредством наследования («дочерний объект»), приобретает все свойства и поведение родительского объекта (за исключением конструкторов, деструкторов, перегруженных операторов и дружественных функций базового класса). Наследование позволяет программистам создавать классы, основанные на существующих классах, задавать новую реализацию, сохраняя при этом то же поведение (реализуя интерфейс), повторно использовать код и независимо расширять исходное программное обеспечение с помощью общедоступных классов и интерфейсов. [1]

Обсуждение

Наследование  – это способ упорядочивания объектов в иерархии от наиболее общего к наиболее конкретному. Объект, который наследует от другого объекта, считается подтипом этого объекта. Пример может включать Instructor и Student, каждый из которых наследуется от Person. Когда мы можем описать связь между двумя объектами, используя фразу is-a , эта связь является наследованием.

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

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

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

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

Ключевые термины

наследство
Объект или класс, основанный на другом объекте или классе, использующий ту же реализацию или определяющий новую реализацию для сохранения того же поведения. [2]
полиморфизм
Предоставление единого интерфейса для сущностей разных типов. [3]

Каталожные номера

  • Прочитайте документы: объектно-ориентированное программирование в Python
  • Викиверситет: Компьютерное программирование

  1. Википедия: Наследование (объектно-ориентированное программирование) ↵
  2. Википедия: Наследование (объектно-ориентированное программирование) ↵
  3. Википедия: Полиморфизм (информатика) ↵

Композиция или наследование: как выбрать?

В начале.

..

… не было ни наследования, ни композиции, только код.

И код был громоздким, повторяющимся, блочным, несчастливым, многословным и усталым.

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

Темное время было.

Затем мир осветил свет объектно-ориентированного программирования (ООП)… И мир практически игнорировал его в течение нескольких десятилетий 1 . До графических пользовательских интерфейсов 2 , которые, как оказалось, очень и очень нуждаются в ООП. Когда вы нажимаете кнопку в окне, что может быть проще для создания соответствующих ответов, чем отправить этой кнопке (или ее заменителю) сообщение Click 3 ?

После этого полетел ООП. Было написано множество 4 книг и выпущено бесчисленное количество 5 статей. Итак, теперь все в деталях разбираются в объектно-ориентированном программировании, верно?

К сожалению, код (и Интернет) говорят нет.

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

Мантры считаются вредными

Как эвристика, «предпочесть композицию наследованию» — это нормально, однако я не фанат мантр. Хотя они часто содержат зерно истины, люди слишком легко слышат лозунг, не понимая его источника или контекста, и, таким образом, избегают думать самостоятельно, а это никогда не заканчивается хорошо.

Я также не являюсь поклонником нелепых кликбейтных заголовков, таких как «Наследование — это зло» 6 , особенно когда автор пытается подкрепить такое возмутительное утверждение ненадлежащим использованием наследования… а затем обвиняет его. Как плотник, заявляющий, что молотки бесполезны, потому что они плохо закручивают шурупы.

Начнем с основ.

Определения

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

  • Класс: именованное понятие в доменном пространстве с необязательным суперклассом, определенным как набор полей и методов.
  • Поле: именованное свойство некоторого типа, которое может ссылаться на другой объект (см. состав)
  • Метод: именованная функция или процедура с параметрами или без них, реализующая некоторое поведение класса.
  • Наследование: класс может наследовать — использовать по умолчанию — поля и методы своего суперкласса. Наследование является транзитивным, поэтому класс может наследовать от другого класса, который наследуется от другого класса, и так далее, вплоть до базового класса (обычно Object, возможно, неявного/отсутствующего). Подклассы могут переопределять некоторые методы и/или поля, чтобы изменить поведение по умолчанию.
  • Состав: когда тип поля является классом, поле будет содержать ссылку на другой объект, тем самым создавая ассоциативное отношение между ними. Не вдаваясь в нюансы разницы между простой ассоциацией, агрегацией и композицией, давайте интуитивно определим композицию как случай, когда класс использует другой объект для обеспечения части или всей своей функциональности.
  • Инкапсуляция: взаимодействуя с объектами, а не напрямую с реализацией методов и полей, мы скрываем и защищаем реализацию класса. Если потребитель ничего не знает об объекте, кроме его открытого интерфейса, он не может полагаться на какие-либо детали внутренней реализации.

Наследование является основным

Наследование является фундаментальным для объектно-ориентированного программирования. Язык программирования может иметь объекты и сообщения, но без наследования он не является объектно-ориентированным (просто «объектно-ориентированным», но все же полиморфным).

…а также Состав

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

(Инкапсуляция тоже фундаментальна, но мы не будем здесь подробно о ней говорить.)

Итак, о чем спор?

И композиция, и наследование фундаментальны, так в чем же дело?

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

Композицию довольно легко понять — мы можем видеть композицию в повседневной жизни: у стула есть ножки, стена состоит из кирпичей и раствора и так далее. Хотя определение наследования простое, оно может стать сложной и запутанной вещью при неразумном использовании. Наследование — это скорее абстракция, о которой мы можем только говорить, а не касаться напрямую. Хотя во многих ситуациях можно имитировать наследование с помощью композиции, часто это оказывается громоздким. Цель композиции очевидна: сделать из частей целое. Цель наследования немного сложнее, потому что наследование служит двум целям: семантике и механике.

Семантика наследования

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

Механика наследования

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

Двойное назначение наследования 7 в большинстве современных ООП-языков, как мне кажется, является причиной наибольшей путаницы. Многие думают, что «повторное использование кода» — основная цель наследования, но это не единственная его цель. Чрезмерный акцент на повторном использовании может привести к трагически ошибочным проектам. Давайте посмотрим на пару примеров.

Как неправильно использовать наследование — пример 1

Начнем с простого и чрезвычайно распространенного примера неправильного использования наследования:

 class Stack extends ArrayList {
    public void push(значение объекта) {…}
    общедоступный объект pop() { … }
}
 

Этот класс будет функционировать как стек, но его интерфейс сильно раздут. Открытый интерфейс этого класса не только push и pop, как можно было бы ожидать от класса с именем Stack, он также включает в себя получение, установку, добавление, удаление, очистку и кучу других сообщений, унаследованных от ArrayList, которые не подходят для класса Stack. Куча.

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

  1. Семантически утверждение «Стек — это ArrayList» неверно; Stack не является правильным подтипом ArrayList. Предполагается, что стек обеспечивает соблюдение принципа «последним пришел — первым вышел» — ограничение, которое легко удовлетворяется интерфейсом push/pop, но не обеспечивается интерфейсом ArrayList.
  2. Механически наследование от ArrayList нарушает инкапсуляцию; использование ArrayList для хранения коллекции объектов стека — это выбор реализации, который должен быть скрыт от потребителей.
  3. Наконец, реализация стека путем наследования от ArrayList представляет собой междоменное отношение: ArrayList — коллекция с произвольным доступом; Стек — это концепция организации очереди со специально ограниченным (неслучайным) доступом 8 . Это разные области моделирования.

Последняя проблема важна, но немного тонка, поэтому давайте рассмотрим ее на другом примере.

Как неправильно использовать наследование — пример 2

Создание класса предметной области путем наследования от класса реализации является распространенным злоупотреблением наследованием. Например, предположим, что мы хотим что-то сделать с определенным сегментом наших клиентов. Самый простой и очевидный способ — создать подкласс ArrayList, назвать его CustomerGroup и начать программировать, верно?

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

  1. ArrayList уже является подклассом списка, набор утилит — класс реализации .
  2. CustomerGroup — это еще один подкласс — класс домена .
  3. Классы домена должны использовать классы реализации , а не наследовать от них.

Пространство реализации должно быть невидимым на уровне домена. Когда мы думаем о том, что делает наше программное обеспечение, мы работаем на уровне предметной области; мы не хотим отвлекаться на детали того, как он работает. Если мы сосредоточимся только на «повторном использовании кода» через наследование, мы будем постоянно попадать в эту ловушку.

Одиночное наследование не проблема

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

. Отношения наследования не должны пересекать границы домена (домен реализации и домен приложения). Наследование CustomerGroup от ArrayList, а также (скажем) от DemographicSegment запутывает два субдомена, запутывая таксономию.

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

Если вы не создаете класс реализации, вы не должны наследовать от класса реализации.

Это одна из самых распространенных проблем новичков — потому что это так удобно! — а причины, по которым это неправильно, нечасто обсуждаются в литературе по программированию, поэтому я повторю еще раз: ваши классы предметной области должны использовать классов реализации, а не быть одним. Разделяйте эти таксономии/домены.

Итак, когда и как мы должны использовать наследование?

Использование скважины наследства

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

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

Как решить: состав или наследство?

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

  1. Представление/реализация концепций вашей предметной области является одним измерением
  2. Семантика понятий вашей предметной области и их связь друг с другом — это второе измерение

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

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

Нет никакой замены объектному моделированию и критическому проектному мышлению. Но если вам нужны какие-то рекомендации, рассмотрите их —

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

  1. Оба класса находятся в одном логическом домене
  2. Подкласс является правильным подтипом надкласса
  3. Реализация суперкласса необходима или подходит для подкласса
  4. Улучшения, сделанные подклассом, в основном аддитивны.

Бывают моменты, когда все эти вещи сходятся:

  • Моделирование предметной области более высокого уровня
  • Фреймворки и расширения фреймворков
  • Дифференциальное программирование

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

Надеемся, что эти рекомендации помогут вам заметить разницу.

Удачного кодирования!

Приложение

Особая благодарность следующим мыслителям за ценный вклад и комментарии: Питу Ходжсону, Тиму Брауну, Скотту Робинсону, Мартину Фаулеру, Минди Ор, Шону Ньюэму, Сэму Гибсону и Махендре Кария.


1. Первый официально объектно-ориентированный язык SIMULA 67 родился в 1967 году. Объектно-ориентированному программированию исполнилось 48 лет!
2. Программисты систем и приложений приняли C++ в середине 1980-х годов, но повсеместному распространению ООП пришлось ждать еще десять лет.
3. да, я упрощаю, игнорируя слушателей/делегатов событий/и т.д.; стараясь сделать эту статью короткой!
4. Amazon утверждает, что на момент написания этой статьи было 24 777 книг по теме объектно-ориентированного программирования
5.

Оставить комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *