Наследование классов: beginner, intermediate, advanced / Хабр

Содержание

Наследование и protected в C++ | Уроки С++

  Обновл. 6 Янв 2020  | 

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

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

Спецификатор доступа protected

Мы уже рассматривали спецификаторы доступа private и public, которые определяют, кто может иметь доступ к членам класса. В качестве напоминания: доступ к членам public открыт для всех. К членам private доступ имеют только члены того же класса, в котором находится член private. Это означает, что дочерние классы не могут напрямую обращаться к членам private родительского класса!

class Parent { private: int m_private; // доступ к этому члену есть только у других членов класса Parent и у дружественных классов/функций (но не у дочерних классов) public: int m_public; // доступ к этому члену открыт для всех объектов };

class Parent

{

private:

    int m_private; // доступ к этому члену есть только у других членов класса Parent и у дружественных классов/функций (но не у дочерних классов)

public:

    int m_public; // доступ к этому члену открыт для всех объектов

};

Всё просто.

Примечание: public = «открытый», private = «закрытый», protected = «защищённый».

В C++ есть третий спецификатор доступа, о котором мы ещё не говорили, так как он полезен только в контексте наследования. Спецификатор доступа protected открывает доступ к членам класса дружественным и дочерним классам

. Доступ к члену protected вне тела класса закрыт.

class Parent { public: int m_public; // доступ к этому члену открыт для всех объектов private: int m_private; // доступ к этому члену открыт только для других членов класса Parent и для дружественных классов/функций (но не для дочерних классов) protected: int m_protected; // доступ к этому члену открыт для других членов класса Parent, дружественных классов/функций, дочерних классов }; class Child: public Parent { public: Child() { m_public = 1; // разрешено: доступ к открытым членам родительского класса из дочернего класса m_private = 2; // запрещено: доступ к закрытым членам родительского класса из дочернего класса m_protected = 3; // разрешено: доступ к защищённым членам родительского класса из дочернего класса } }; int main() { Parent parent; parent.m_public = 1; // разрешено: доступ к открытым членам класса извне parent.m_private = 2; // запрещено: доступ к закрытым членам класса извне parent.m_protected = 3; // запрещено: доступ к защищённым членам класса извне }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

class Parent

{

public:

    int m_public; // доступ к этому члену открыт для всех объектов

private:

    int m_private; // доступ к этому члену открыт только для других членов класса Parent и для дружественных классов/функций (но не для дочерних классов)

protected:

    int m_protected; // доступ к этому члену открыт для других членов класса Parent, дружественных классов/функций, дочерних классов

};

class Child: public Parent

{

public:

    Child()

    {

        m_public = 1; // разрешено: доступ к открытым членам родительского класса из дочернего класса

        m_private = 2; // запрещено: доступ к закрытым членам родительского класса из дочернего класса

        m_protected = 3; // разрешено: доступ к защищённым членам родительского класса из дочернего класса

    }

};

int main()

{

    Parent parent;

    parent.m_public = 1; // разрешено: доступ к открытым членам класса извне

    parent.m_private = 2; // запрещено: доступ к закрытым членам класса извне

    parent.m_protected = 3; // запрещено: доступ к защищённым членам класса извне

}

В примере выше вы можете видеть, что член m_protected класса Parent напрямую доступен дочернему классу Child, но доступ к нему для членов извне — закрыт.

Когда следует использовать спецификатор доступа protected?

К членам protected родительского класса доступ открыт для членов дочернего класса, а это означает, что если вы позже измените что-либо в члене protected (тип данных, значение и т.д.), то вам придётся внести изменения как в родительский, так и во все дочерние классы.

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

Создание членов private предоставляет лучшую инкапсуляцию и изолирует родительские классы от изменений, вызванных дочерними классами. Но цена этому — создание открытого или защищённого интерфейса (способа взаимодействия других объектов с классами и их членами, т.е.

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

Типы наследований. Доступ к членам

Существует три типа наследований классов:

   public;

   private;

   protected.

Для определения типа наследования нужно просто указать нужное ключевое слово возле наследуемого класса:

// Открытое наследование class Pub: public Parent { }; // Закрытое наследование class Pri: private Parent { }; // Защищённое наследование class Pro: protected Parent { }; class Def: Parent// по умолчанию C++ устанавливает закрытое наследование { };

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

// Открытое наследование

class Pub: public Parent

{

};

// Закрытое наследование

class Pri: private Parent

{

};

// Защищённое наследование

class Pro: protected Parent

{

};

class Def: Parent// по умолчанию C++ устанавливает закрытое наследование

{

};

Если вы сами не определили тип наследования, то в C++ по умолчанию будет выбран тип наследования private (аналогично членам класса, которые по умолчанию являются private, если не указано иначе).

Это даёт нам 9 комбинаций: 3 спецификатора доступа (public, private и protected) и 3 типа наследования (public, private и protected).

Так в чём же разница между ними? Если вкратце, то при наследовании спецификатор доступа члена родительского класса может быть изменён в дочернем классе (в зависимости от типа наследования). Другими словами, члены, которые были public или protected в родительском классе, могут стать private в дочернем классе.

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

   Класс всегда имеет доступ к своим (не наследуемым) членам.

   Доступ к члену класса основывается на его спецификаторе доступа.

   Дочерний класс имеет доступ к унаследованным членам родительского класса на основе спецификатора доступа этих членов в родительском классе.

Наследование типа public

Открытое наследование является одним из наиболее используемых типов наследования. Очень редко вы увидите или будете использовать другие типы, поэтому основной упор следует уделить на понимание именно этого типа наследования. К счастью, открытое наследование является самым лёгким и простым из всех типов. Когда вы открыто наследуете родительский класс, то унаследованные члены public остаются public, унаследованные члены protected остаются protected, а унаследованные члены private остаются недоступными для дочернего класса. Ничего не меняется.

Спецификатор доступа в родительском классеСпецификатор доступа при наследовании типа public в дочернем классе
publicpublic
privateНедоступен
protectedprotected

Например:

class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; class Pub: public Parent // открытое наследование { // Открытое наследование означает, что: // члены public остаются public в дочернем классе; // члены protected остаются protected в дочернем классе; // члены private остаются недоступными в дочернем классе. public: Pub() { m_public = 1; // разрешено: доступ к m_public открыт m_private = 2; // запрещено: доступ к m_private в дочернем классе из родительского класса закрыт m_protected = 3; // разрешено: доступ к m_protected в дочернем классе из родительского класса открыт } }; int main() { Parent parent; parent.m_public = 1; // разрешено: m_public доступен извне через родительский класс parent.m_private = 2; // запрещено: m_private недоступен извне через родительский класс parent.m_protected = 3; // запрещено: m_protected недоступен извне через родительский класс Pub pub; pub.m_public = 1; // разрешено: m_public доступен извне через дочерний класс pub.m_private = 2; // запрещено: m_private недоступен извне через дочерний класс pub.m_protected = 3; // запрещено: m_protected недоступен извне через дочерний класс }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

class Parent

{

public:

    int m_public;

private:

    int m_private;

protected:

    int m_protected;

};

class Pub: public Parent // открытое наследование

{

    // Открытое наследование означает, что:

    // члены public остаются public в дочернем классе;

    // члены protected остаются protected в дочернем классе;

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

public:

    Pub()

    {

        m_public = 1; // разрешено: доступ к m_public открыт

        m_private = 2; // запрещено: доступ к m_private в дочернем классе из родительского класса закрыт

        m_protected = 3; // разрешено: доступ к m_protected в дочернем классе из родительского класса открыт

    }

};

int main()

{

    Parent parent;

    parent.m_public = 1; // разрешено: m_public доступен извне через родительский класс

    parent.m_private = 2; // запрещено: m_private недоступен извне через родительский класс

    parent.m_protected = 3; // запрещено: m_protected недоступен извне через родительский класс

    Pub pub;

    pub.m_public = 1; // разрешено: m_public доступен извне через дочерний класс

    pub.m_private = 2; // запрещено: m_private недоступен извне через дочерний класс

    pub.m_protected = 3; // запрещено: m_protected недоступен извне через дочерний класс

}

Правило: Используйте открытое наследование, если у вас нет веской причины делать иначе.

Наследование типа private

При закрытом наследовании все члены родительского класса наследуются как закрытые. Это означает, что члены private остаются недоступными, а члены protected и public становятся private в дочернем классе.

Обратите внимание, это не влияет на то, как дочерний класс получает доступ к членам родительского класса! Это влияет только на то, как другими объектами осуществляется доступ к этим членам через дочерний класс:

class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; class Priv: private Parent // закрытое наследование { // Закрытое наследование означает, что: // члены public становятся private (m_public теперь private) в дочернем классе; // члены protected становятся private (m_protected теперь private) в дочернем классе; // члены private остаются недоступными (m_private недоступен) в дочернем классе. public: Priv() { m_public = 1; // разрешено: m_public теперь private в Priv m_private = 2; // запрещено: дочерние классы не имеют доступ к закрытым членам родительского класса m_protected = 3; // разрешено: m_protected теперь private в Priv } }; int main() { Parent parent; parent.m_public = 1; // разрешено: m_public доступен извне через родительский класс parent.m_private = 2; // запрещено: m_private недоступен извне через родительский класс parent.m_protected = 3; // запрещено: m_protected недоступен извне через родительский класс Priv priv; priv.m_public = 1; // запрещено: m_public недоступен извне через дочерний класс priv.m_private = 2; // запрещено: m_private недоступен извне через дочерний класс priv.m_protected = 3; // запрещено: m_protected недоступен извне через дочерний класс }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

class Parent

{

public:

    int m_public;

private:

    int m_private;

protected:

    int m_protected;

};

class Priv: private Parent // закрытое наследование

{

    // Закрытое наследование означает, что:

    // члены public становятся private (m_public теперь private) в дочернем классе;

    // члены protected становятся private (m_protected теперь private) в дочернем классе;

    // члены private остаются недоступными (m_private недоступен) в дочернем классе.

public:

    Priv()

    {

        m_public = 1; // разрешено: m_public теперь private в Priv

        m_private = 2; // запрещено: дочерние классы не имеют доступ к закрытым членам родительского класса

        m_protected = 3; // разрешено: m_protected теперь private в Priv

    }

};

int main()

{

    Parent parent;

    parent.m_public = 1; // разрешено: m_public доступен извне через родительский класс

    parent.m_private = 2; // запрещено: m_private недоступен извне через родительский класс

    parent.m_protected = 3; // запрещено: m_protected недоступен извне через родительский класс

    Priv priv;

    priv.m_public = 1; // запрещено: m_public недоступен извне через дочерний класс

    priv.m_private = 2; // запрещено: m_private недоступен извне через дочерний класс

    priv.m_protected = 3; // запрещено: m_protected недоступен извне через дочерний класс

}

Итого:

Спецификатор доступа в родительском классеСпецификатор доступа при наследовании типа private в дочернем классе
publicprivate
privateНедоступен
protectedprivate

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

На практике наследование типа private используется редко.

Наследование типа protected

Этот тип наследования почти никогда не используется, за исключением особых случаев. С защищённым наследованием, члены public и protected становятся protected, а члены private остаются недоступными.

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

Спецификатор доступа в родительском классеСпецификатор доступа при наследовании типа protected в дочернем классе
publicprotected
privateНедоступен
protectedprotected

Финальный пример

class Parent { public: int m_public; private: int m_private; protected: int m_protected; };

class Parent

{

public:

int m_public;

private:

int m_private;

protected:

int m_protected;

};

Класс Parent может обращаться к своим членам беспрепятственно. Доступ к m_public открыт для всех. Дочерние классы могут обращаться как к m_public, так и к m_protected:

class D2 : private Parent // закрытое наследование { // Закрытое наследование означает, что: // члены public становятся private в дочернем классе; // члены protected становятся private в дочернем классе; // члены private недоступны для дочернего класса. public: int m_public2; private: int m_private2; protected: int m_protected2; };

class D2 : private Parent  // закрытое наследование

{

// Закрытое наследование означает, что:

// члены public становятся private в дочернем классе;

// члены protected становятся private в дочернем классе;

// члены private недоступны для дочернего класса.

public:

int m_public2;

private:

int m_private2;

protected:

int m_protected2;

};

D2 может беспрепятственно обращаться к своим членам. D2 имеет доступ к членам m_public и m_protected класса Parent, но не к m_private. Поскольку D2 наследует класс Parent закрыто, то m_public и m_protected теперь становятся закрытыми при доступе через D2. Это означает, что другие объекты не смогут получить доступ к этим членам через использование объекта D2, а также любые другие классы, которые будут дочерними классу D2, не будут иметь доступ к этим членам:

class D3 : public D2 { // Открытое наследование означает, что: // унаследованные члены public остаются public в дочернем классе; // унаследованные члены protected остаются protected в дочернем классе; // унаследованные члены private остаются недоступными в дочернем классе. public: int m_public3; private: int m_private3; protected: int m_protected3; };

class D3 : public D2

{

// Открытое наследование означает, что:

// унаследованные члены public остаются public в дочернем классе;

// унаследованные члены protected остаются protected в дочернем классе;

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

public:

int m_public3;

private:

int m_private3;

protected:

int m_protected3;

};

D3 может беспрепятственно обращаться к своим членам. D3 имеет доступ к членам m_public2 и m_protected2 класса D2, но не к m_private2. Поскольку D3 наследует D2 открыто, то m_public2 и m_protected2 сохраняют свои спецификаторы доступа и остаются public и protected при доступе через D3. D3 не имеет доступа к m_private класса Parent. Он также не имеет доступа к m_protected или m_public класса Parent, оба из которых стали закрытыми, когда D2 унаследовал их.

Заключение

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

   Во-первых, класс всегда имеет доступ к своим собственным не унаследованным членам (и дружественные ему классы также имеют доступ). Спецификаторы доступа влияют только на то, могут ли объекты вне класса и дочерние классы обращаться к этим членам.

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

Общая таблица спецификаторов доступа и типов наследования:

Спецификатор доступа в родительском классеСпецификатор доступа при наследовании типа public в дочернем классеСпецификатор доступа при наследовании типа private в дочернем классеСпецификатор доступа при наследовании типа protected в дочернем классе
publicpublicprivateprotected
privateНедоступенНедоступенНедоступен
protectedprotectedprivateprotected

Хотя в примерах выше мы рассматривали использование переменных-членов, эти правила выполняются для всех членов классов (и для методов, и для типов, объявленных внутри класса).

Оценить статью:

Загрузка…

Поделиться в социальных сетях:

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

В начале…

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

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

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

Мрачные были времена.

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

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



Увы, код (и интернет) говорит, что не так

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


Когда мантры вредят

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

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

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


Определения

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


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

Наследование фундаментально

Наследование — это фундаментальное понятие ООП. В языке программирования могут быть объекты и сообщения, но без наследования он не будет объектно-ориентированным (только основанным на объектах, но все еще полиморфным).


… как и композиция

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

(Инкапсуляция тоже вещь фундаментальная, но сейчас речь не о ней)


Так от чего весь сыр-бор?

Ну хорошо, и композиция, и наследование фундаментальны, в чем дело-то?

А дело в том, что можно подумать, что одно всегда может заменить другое, или что первое лучше или хуже второго. Разработка ПО — это всегда выбор разумного баланса, компромисс.

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


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

Как в биологии классификация таксонов организует их в иерархии, так наследование отражает иерархию понятий из предметной области. Упорядочивает их от общего к частному, собирает родственные идеи в ветви иерархического древа. Смысл (семантика) класса по большей части выражен в его интерфейсе — наборе сообщений, которые класс способен понять, но также определяется и теми сообщениями, которыми класс отвечает. Унаследовался от предка — будь добр не только понять все сообщения, которые мог понять предок, но также и уметь ответить как он (сохранить поведение предка — прим. пер.) И поэтому наследование связывает наследника с предком гораздо сильнее, чем если бы мы взяли просто экземпляр предка как компонент. Обратите внимание, даже если класс делает что-то совсем простое, почти не имеет логики, его имя несет существенную смысловую нагрузку, разработчик делает из него важные выводы о предметной области.


Наследование механическое

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

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


Как не надо наследовать. Пример 1

class Stack extends ArrayList {
    public void push(Object value) { … }
    public Object pop() { … }
}

Казалось бы, класс Stack, все хорошо. Но посмотрите внимательно на его интерфейс. Что должно быть в классе с именем Stack? Методы push() и pop(), что же еще. А у нас? У нас есть get(), set(), add(), remove(), clear() и еще куча барахла, доставшегося от ArrayList, которое стеку ну вообще не нужно.

Можно было бы переопределить все нежелательные методы, а некоторые (например, clear()) даже и адаптировать под наши нужды, но не многовато ли работы из-за одной ошибки в дизайне? На самом деле трех: одной смысловой, одной механической и одной комбинированной:


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

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


Как не надо наследовать. Пример 2

Частая ошибка при наследовании — это создать модель из предметной области, унаследовав ее от готовой реализации. Вот, скажем, нам надо выделить некоторых наших клиентов (класс Customer) в определенное подмножество. Легко! Наследуемся от ArrayList<Customer>, называем это CustomerGroup и понеслась.

Не тут-то было. Поступив так мы опять спутаем две предметные области. Старайтесь избегать этого:


  1. ArrayList<Customer> это уже наследник списка, утилиты типа «коллекция», готовой реализации.
  2. CustomerGroup это совсем другая штука — класс из предметной области (домена).
  3. Классы из предметной области должны использовать реализации, а не наследовать их.

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


Дело не в одиночном наследовании

Одиночное наследование пока остается самой популярной моделью ООП. Оно неизбежно влечет наследование реализации, которое приводит к сильному зацеплению (coupling — прим. пер.) между классами. Может показаться, что беда в том, что ветка наследования у нас только одна на обе потребности: и смысловую и механическую. Если использовали для одного, то для другого уже нельзя. А раз так, может быть множественное наследование все исправит?

Нет. Отношение наследования не должно пересекать границы между предметными областями: инструментальной (структуры данных, алгоритмы, сети) и прикладной (бизнес-логика). Если CustomerGroup будет наследовать ArrayList<Customer> и одновременно, скажем, DemographicSegment, то две предметные области переплетутся между собой, а «видовая принадлежность» объектов станет неочевидна.

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

От инструментов можно наследовать только другие инструменты.

Это очень частая ошибка новичков. Что не удивительно, ведь так просто взять и унаследовать. Редко где встретишь обсуждения, почему именно это неправильно. Еще раз: бизнес-сущности должны пользоваться инструментами, а не быть ими. Мухи (инструменты) — отдельно, котлеты (бизнес-модели) — отдельно.

Так когда же нужно наследование?


Наследуемся как надо

Чаще всего — и при этом с наибольшей отдачей — наследование применяют для описания объектов, незначительно отличающихся друг от друга (в оригинале используется термин «differential programming» — прим. пер.) Например, нам нужна особенная кнопка с небольшими дополнениями. Нормально, наследуемся от существующего класса Кнопка. Потому что наш новый класс, это все еще кнопка, а мы полностью наследуем API класса Кнопка, его поведение и реализацию. Новая функциональность только добавляется к существующему. А вот если в наследнике часть функциональности убирается, это повод задуматься, а нужно ли наследование.

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


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

В ситуации, когда вроде бы подходит и то и другое, взгляните на дизайн в двух плоскостях:


  1. Структура и механическое исполнение бизнес-объектов.
  2. Что они обозначают по смыслу и как взаимодействуют.

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

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

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

Наследуем, если:


  1. Оба класса из одной предметной области
  2. Наследник является корректным подтипом (в терминах LSP — прим. пер.) предка
  3. Код предка необходим либо хорошо подходит для наследника
  4. Наследник в основном добавляет логику

Иногда все эти условия выполняются одновременно:


  • в случае моделирования высокоуровневой логики из предметной области
  • при разработке библиотек и расширений для них
  • при дифференциальном программировании (автор снова использует термин «differential programming», очевидно, понимая под ним нечто, отличное от DDP — прим. пер.)

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

Надеюсь, эти правила помогут вам понять разницу между двумя подходами.

Приятного кодинга!


Послесловие

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




1

Первый официальный ОО-язык, SIMULA 67, появился в 1967 году.


2

Системные и прикладные программисты приняли на вооружение C++ в середине 1980-х, но перед тем, как ООП стал общепринятым, прошел еще десяток лет.


3

Я намеренно упрощаю, не говорю про паб/саб, делегатов и тому подобное, чтобы не раздувать статью.


4

На момент написание этого текста Амазон предлагает 24777 книг по ООП.


5

Поиск в гугле по фразе «объектно-ориентированное программирование» дает 8 млн результатов.


6

Поиск в гугле выдает 37600 результатов по запросу «наследование это зло».


7

Смысл (интерфейс) и механику (исполнение) можно разделить за счет усложнения языка. См. пример из спецификации языка D.


8

С грустью замечу, что в Java Stack унаследован от Vector.


9

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




Переводчик выражает благодарность ООП-чату в Telegram, без которого этот текст не смог бы появиться.
Еще раз о принципе подстановки Лисков, или семантика наследования в ООП / ХабрНаследование — один из столпов ООП. Наследование используется для того, чтобы переиспользовать общий код. Но не всегда общий код надо переиспользовать, и не всегда наследование — самый лучший способ для переиспользования кода. Часто получается, так, что есть похожий код в двух разных куска кода (классах), но требования к ним разные, т.е. классы на самом деле друг от друга наследовать может и не стоит.

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

Пусть есть у нас класс прямоугольник:

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

   def set_width(self, width):
        self._width = width 

   def set_height(self, height):
        self._height = height

   def get_area(self):
        return self._width * self._height
   ...

Теперь мы захотели написать класс Квадрат, но чтобы переиспользовать код вычисления площади, кажется, логичным отнаследовать Квадрат от Прямоугольника:
class Square(Rectangle):
    def set_width(self, width):
        self._width = width
        self._height = width

    def set_height(self, height):
        self._width = height
        self._height = height

Кажется, что код классов Square и Rectangle консистентен. Вроде бы Square сохраняет математические свойства квадрата, а т.е. и прямоугольника. А значит, мы можем передавать объекты класса Square вместо Rectangle.

Но если мы будем так делать, мы можем нарушить свойства поведения класса Rectangle:

Например, есть клиентский код:

def client_code(rect):
    rect.set_height(10)
    rect.set_width(20)
    assert rect.get_area() == 200

Если в качестве аргумента в эту функцию передать инстанс класса Square функция станет себя вести по-другому. Что является нарушением контракта на поведение класса Rectangle, потому что действия с объектом базового класса должны давать ровно такой же результат, как и над объектом класса потомка.

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

Исправить эту проблему, можно, например, так:

  1. сделать assert на точное соответствие классу, или сделать if, который будет работать для разных классов по-разному
  2. в Square сделать метод set_size() и переопределить методы set_height, set_width чтобы они бросали исключения
    и т.д и т.п.

Такой код и такие классы будут работать, в том смысле, что код будет рабочим.

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

С течением времени мы можем получить, что:

  • у класса потомка окажется переопределена большая часть методов
  • рефакторинг или добавление методов в базовый класс будет ломать код, использующий потомков
  • в коде, использующем объекты базового класса будут if-ы, с проверкой на класс объекта, а поведение для потомков и базового класса отличается

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

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

Собственно, это и есть принцип подстановки Лисков или принцип определения подтипа на основе поведения (strong behavioral typing) классов: если можно написать хоть какой-то осмысленный код, в котором замена объекта базового класса на объекта класса потомка, его сломает, то тогда не стоит их друг от друга-то наследовать. Мы должны расширять поведение базового класса в потомках, а не существенным образом изменять его. Функции, которые используют базовый класс, должны иметь возможность использовать объекты подклассов, не зная об этом. Фактически это семантика наследования в ООП.

И в реальном промышленном коде крайне рекомендуется этому принципу следовать и соблюдать описанную семантику наследования. И вот с этим принципом есть несколько тонкостей.

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

Если у класса Rectangle есть только два метода — вычисление площади и отрисовка, без возможности, перерисовки и изменения размеров, то в этом случае Square с переопределенным конструктором будет удовлетворять принципу замещения Лисков.

Т.е. такие классы удовлетворяют принципу подстановки:

class Rectangle:
    def draw():
        ...
    def get_area():
        ...

class Square(Rectangle):
    pass

Хотя конечно это не очень хороший код, и даже, наверное, антипаттерн проектирования классов, но с формальной точки зрения он удовлетворяет принципу Лисков.

Еще один пример. Множество — это подтип мультимножества. Это отношение абстракций предметной области. Но код можно написать так, что класс Set мы наследуем от Bag и принцип подстановки нарушается, а можно написать так, чтобы принцип соблюдался. С одной и той же семантикой предметной области.

В общем и целом, наследование классов можно рассматривать как реализацию отношения “IS”, но не между сущностями предметной области, а между классами. И является ли класс потомок подтипом базового класса определяется тем, какие ограничения и контракты поведения классов использует (и в принципе может использовать) клиентский код.

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

Любой осмысленный для объекта базового класса код не должен ломаться, если мы его заменим на объект класса потомка. Осмысленный код — это любой клиентский код, который использует объект базового класса (и его потомков) в рамках семантики и ограничений базового класса.

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

Например, у прямоугольника есть еще один метод, который возвращает представление в json

class Rectangle: 
   def to_dict(self):
       return {"height": self.height, "width": self.width}

А в Square мы его переопределяем:
class Square: 
   def to_dict(self):
       return {"size": self.height}

Если мы считаем базовым контрактом поведения класса Rectangle, что to_json должен иметь height и width, тогда код
r = rect.to_dict()
log(r['height'], r['width'])

будет осмысленным для объекта базового класса Rectangle. При замещении объекта базового класса на класс наследник Square код меняет свое поведение и нарушает контракт, и тем самым нарушает принцип подстановки Лисков.

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

Кстати, это хороший пример, разрушающий миф, что неизменяемость (immutability) спасает от нарушения принципа.

Формально, любое переопределение метода в классе-потомке опасно, также как и изменения логики в базовом классе. Например, довольно-таки часто классы потомки адаптируются к “неправильному” поведению базового класса, и когда в базовом классе исправляют баг, они ломаются.

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

Когда разработчик использует код, написанный другими разработчиками, он может понять, а какова же семантика класса только непосредственно по коду, названию методов, документации и комментариям. Но в любом случае семантика — это часто область человеческая, а значит и ошибкоемкая. Самое важное следствие: только по коду — синтаксически — проверить следование принципу Лисков невозможно, и нужно опираться на (часто) расплывчатую семантику. Формального (математического), значит, проверяемого и гарантированного, способа проверки strong behavioral typing — нет.

Поэтому часто вместо принципа Лисков используются формальные правила на предусловия и постусловия из контрактного программирования:

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

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

Важно не текущее поведение класса, а какие изменения класса подразумевает ответственность или семантика класса.

Код постоянно правится и изменяется. Поэтому если прямо сейчас код удовлетворяет принципу подстановки, это не значит, что правки в коде не изменят этого.

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

И в какой-то момент разработчик, отвечающий за бибилиотеку, добавил метод reshape или set_width/set_height в базовый класс Rectangle. С его точки зрения, просто произошло расширение базового класса. Но на самом деле, произошло изменение семантики и контрактов, на которые опирался класс потомок. Теперь классы уже не удовлетворяет принципу.

Вообще при наследовании в ООП, изменения в базовом классе, которые будут выглядеть, как расширение интерфейса — будет добавлен еще один метод или поле, могут нарушать предыдущие “естественные” контракты, и тем самым фактически менять семантику или ответственность. Поэтому добавление любого метода в базовый класс — опасно. Можно случайно ненароком изменить контракт.

И с практической точки зрения, в примере с прямоугольник и классом важно, не есть ли сейчас метод reshape или set_width/set_height. С практической точки зрения важно, насколько высока вероятность появления таких изменений в библиотечном коде. Подразумевает ли сама семантика или границы ответственности класса такие изменения. Если подразумевают, то вероятность ошибки и/или дальнейшей необходимости рефакторинга значительно повышается. И если даже небольшая возможность есть, вероятно лучше не наследовать такие классы друг от друга.

Поддерживать определения подтипа на основе поведения — сложно, даже для простых классов с понятной семантикой, что уж говорить про энтерпрайз со сложной бизнес-логикой. Несмотря на то, что базовый класс и класс наследник — это разные куски кода, для них нужно очень внимательно и хорошо продумать интерфейсы и ответственность. И даже при небольшом изменении семантики класса — чего никак не избежать, нам приходится смотреть код, связанных классов, проверять не нарушает ли новый контракт или инвариант то как уже сейчас написано(!) и используются. Почти при любом изменении в развесистой иерархии классов, мы должны посмотреть и проверить много другого кода.

Это одна из причин, почему классическое наследование в ООП некоторые не очень любят. И поэтому часто отдают предпочтение композиции классов, наследованию интерфейсов и т.д и т.п. вместо классического наследования поведения.

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

С помощью административных методов задача тоже решается не очень хорошо. Здесь можно почитать, как делал дядюшка Мартин в С++ и как это не сработало.

Но в реальном промышленном коде все же довольно-таки часто принцип Лисков нарушается, и это не страшно. Cледить за соблюдением принципа сложно, т.к. 1) ответственность и семантика класса часто не явна и не выразима в коде 2) ответственность класса может меняться — как в базовом классе, так и в классе потомке. Но это не всегда приводит к каким-то уж очень страшным последствиям. Самое частое, простое и элементарное нарушение — это переопределенный метод модифицирует поведение. Как в например, тут:

class Task: 
 def close(self):
    self.status = CLOSED
 ...

class ProjectTask(Task):
 def close(self):
    if status == STARTED:
        raise Exception("Cannot close a started Project Task")
    ...

Метод close у ProjectTask будет выкидывать исключение в тех случаях, в которых объекты класс Task нормально отработают. Вообще, переопределение методов базового класса очень часто приводит к нарушению принципа подстановки, но не становится проблемой.

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

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

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

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

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

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

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

Типы наследования

Простое наследование

Класс, от которого произошло наследование, называется базовым или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).

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

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

При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.

Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

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

Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

Единый базовый класс

В ряде языков программирования все классы явно или неявно наследуются от некого базового класса. Smalltalk был одним из первых языков, в которых использовалась эта концепция. К таким языкам относятся Objective-C (NSObject), Perl (UNIVERSAL), Eiffel (ANY), Java (java.lang.Object), C# (System.Object), Delphi (TObject).

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

Visual Basic

Наследование в Visual Basic:

Class A 'базовый класс
End Class
 
Class B : Inherits A    'наследование от A
End Class
 
Noninheritable Class C     'Класс, который нельзя наследовать (final в Java)
End Class
 
MustInherit Class Z 'Класс, который обязательно наследовать (абстрактный класс)
End Class

C++

Наследование в C++:

class A{    //базовый класс
};
 
class B : public A{    //public наследование
};
 
class C : protected A{    //protected наследование
};
 
class Z : private A{    //private наследование
};

В C++ существует три типа наследования: public, protected, private. Спецификаторы доступа членов базового класса меняются в потомках следующим образом:

ANSI ISO IEC 14882 2003

Если класс объявлен как базовый для другого класса со спецификатором доступа public, тогда public члены базового класса доступны как public члены производного класса, protected члены базового класса доступны как protected члены производного класса.

Если класс объявлен как базовый для другого класса со спецификатором доступа protected, тогда public и protected члены базового класса доступны как protected члены производного класса.

Если класс объявлен как базовый для другого класса со спецификатором доступа private, тогда public и protected члены базового класса доступны как private члены производного класса.

\ANSI ISO IEC 14882 2003

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

Эта интересная особенность открывает возможность динамической идентификации типа (RTTI).

Delphi (Object Pascal)

Для использования механизма наследования в Delphi необходимо в объявлении класса справа от слова class указать класс предок:

Предок:

TAncestor = class
private
protected
public
  // Виртуальная процедура
  procedure VirtualProcedure; virtual; abstract; 
  procedure StaticProcedure;
end;

Наследник:

TDescendant = class(TAncestor)
private
protected
public
  // Перекрытие виртуальной процедуры
  procedure VirtualProcedure; override;
  procedure StaticProcedure;
end;

Абсолютно все классы в Delphi являются потомками класса TObject. Если класс-предок не указан, то подразумевается, что новый класс является прямым потомком класса TObject.

Множественное наследование в Delphi частично поддерживается за счёт использования классов-помощников (Сlass Helpers).

Python

Python поддерживает как одиночное, так и множественное наследование. При доступе к атрибуту порядок просмотра производных классов называется порядком разрешения метода (англ. method resolution order)[1].

class Ancestor1(object):   # Предок 1
    def m1(self): pass
class Ancestor2(object):   # Предок 2
    def m1(self): pass
class Descendant(Ancestor1, Ancestor2):   # Наследник
    def m2(self): pass
 
d = Descendant()           # инстанциация
print d.__class__.__mro__  # порядок разрешения метода:
(<class '__main__.Descendant'>, <class '__main__.Ancestor1'>, <class '__main__.Ancestor2'>, <type 'object'>)

С версии Python 2.2 в языке сосуществуют «классические» классы и «новые» классы. Последние являются наследниками object. «Классические» классы будут поддерживаться вплоть до версии 2.6, но удалены из языка в Python версии 3.0.

Множественное наследование применяется в Python, в частности, для введения в основной класс классов-примесей (англ. mix-in).

PHP

Для использования механизма наследования в PHP необходимо в объявлении класса после имени объявляемого класса-наследника указать слово extends и имя класса-предка:

class Descendant extends Ancestor {
}

В случае перекрытия классом-наследником свойств и методов предка, доступ к свойствам и методам предка можно получить с использованием ключевого слова parent:

class A {
  function example() {
    echo "Вызван метод A::example().<br />\n";
  }
}
 
class B extends A {
  function example() {
    echo "Вызван метод B::example().<br />\n";
    parent::example();
  }
}

Objective-C

@interface MyNumber : NSObject { 
   int num;
}
- (int) num;
- (void) setNum: (int) theNum;
@end
 
@implementation
- (id) init { 
   self = [super init];
   return self;
}
 
- (int) num {
   return num;
}
 
- (void) setNum: (int) theNum {
   num = theNum;
}
@end

Переопределенные методы не нужно объявлять в интерфейсе.

Java

Пример наследования от одного класса и двух интерфейсов:

        public class A { }
        public interface I1 { }
        public interface I2 { }
        public class B extends A implements I1, I2 { }

Директива final в объявлении класса делает наследование от него невозможным.

C#

Пример наследования от одного класса и двух интерфейсов:

        public class A { }
        public interface I1 { }
        public interface I2 { }
        public class B : A, I1, I2 { }

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

        public class A<T>
        { }
        public class B : A<int>
        { }
        public class B2<T> : A<T>
        { }

Допустимо также наследование вложенных классов от классов, их содержащих:

    class A
    {
        public class B : A { }
    }

Директива sealed в объявлении класса делает наследование от него невозможным.[2]

Ruby

class Parent
 
  def public_method
    "Public method"
  end
 
  private
 
    def private_method
      "Private method"
    end
 
end
 
class Children < Parent
 
  def public_method
    "Redefined public method"
  end
 
  def call_private_method
    "Ancestor's private method: " + private_method
  end
 
end

Класс Parent является предком для класса Children, у которого переопределен метод public_method.

children = Children.new
children.public_method #=> "Redefined public method"
children.call_private_method #=> "Ancestor's private method: Private method"

Приватные методы предка можно вызывать из наследников.

JavaScript

var Parent = function( data ) {
    this.data = data || false;
    this.public_method = function() { return 'Public Method'; }
}
 
var Child = function() {
    this.public_method = function() { return 'Redefined public method'; }
    this.getData = function() { return 'Data: ' + this.data; }
}
 
Child.prototype = new Parent('test');
var Test = new Child();
 
Test.getData(); // => "Data: test"
Test.public_method(); // => "Redefined public method"
Test.data; // => "test"

Класс Parent является предком для класса Children, у которого переопределен метод public_method. В JavaScript используется прототипное наследование.

Конструкторы и деструкторы

В С++ конструкторы при наследовании вызываются последовательно от самого раннего предка до самого позднего потомка, а деструкторы наоборот — от самого позднего потомка до самого раннего предка.

class First
{
public:
    First()  { cout << ">>First constructor" << endl; }
    ~First() { cout << ">>First destructor" << endl; }
};
 
class Second: public First
{
public:
    Second()  { cout << ">Second constructor" << endl; }
    ~Second() { cout << ">Second destructor" << endl; }
};
 
class Third: public Second
{
public:
    Third()  { cout << "Third constructor" << endl; }
    ~Third() { cout << "Third destructor" << endl; }
};
 
// выполнение кода
Third *th = new Third();
delete th;
 
// результат вывода
/*
>>First constructor
>Second constructor
Third constructor
 
Third destructor
>Second destructor
>>First destructor
*/

См. также

Примечания

Ссылки

Наследование реализаций: закопайте стюардессу / Хабр

Как известно, классическое ООП покоится на трех китах:


  1. Инкапсуляция
  2. Наследование
  3. Полиморфизм

Классическая же реализация по умолчанию:


  1. Инкапсуляция — публичные и приватные члены класса
  2. Наследование — реализация функционала за счет расширения одного класса-предка, защищенные члены класса.
  3. Полиморфизм — виртуальные методы класса-предка.

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


Наследование ломает инкапсуляцию


  1. Классу-потомку доступны защищенные члены класса-предка. Всем остальным доступен только публичный интерфейс класса. Предельный случай взлома — антипаттерн Паблик Морозов;
  2. Реально изменить поведение предка можно только с помощью перекрытия виртуальных методов;
  3. Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку;
  4. Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода;
  5. Информация из пункта 4 зависит от реализации класса-предка, включая приватные члены и их код.

В теории мы уже имеем былинный отказ, но как насчет практики?


  1. Зависимость, создаваемая наследованием, чрезвычайно сильна;
  2. Наследники гиперчувствительны к любым изменениям предка;
  3. Наследование от чужого кода добавляет адскую боль при сопровождении: разработчики библиотеки рискуют получить обструкцию из-за поломанной обратной совместимости при малейшем изменении базового класса, а прикладники — регрессию при любом обновлении используемых библиотек.

Все, кто используют фреймворки, требующие наследования от своих классов (WinForms, WPF, WebForms, ASP.NET), легко найдут подтверждения всем трем пунктам в своем опыте.
Неужели все так плохо?


Влияние проблемы можно ослабить принятием некоторых конвенций:

1. Защищенные члены не нужны
Это соглашение ликвидирует пабликов морозовых как класс.

2. Виртуальные методы предка ничего не делают
Это соглашение позволяет сочетать знание о реализации предка с независимостью от нее реализации уже в потомке.

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

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

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

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

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



  1. Виртуальные методы-пустышки уже есть во многих языках и носят гордое звание абстрактных.
  2. Классы, экземпляры которых создавать нельзя, тоже есть во многих языках и даже имеют то же звание.
  3. Полное соблюдение указанных соглашений в языке C++ использовалось как паттерн для проектирования и реализации Component Object Model.
  4. Ну и самое приятное: в C# и многих других языках соглашения реализованы как первоклассный элемент «интерфейс».
    Происхождение названия очевидно — в результате соблюдения соглашений от класса остается только его публичный интерфейс. И если множественное наследование от обычных классов — редкость, то от интерфейсов оно доступно без всяких ограничений.


  1. Языки, где нет наследования от классов, но есть — от интерфейсов (например, Go), нельзя лишать звания объектно-ориентированных. Более того, такая реализация ООП правильнее теоретически и безопаснее практически.
  2. Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.
  3. Избегайте наследования реализаций без крайней необходимости.
  4. Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
  5. Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.

PS: Дополнения и критика традиционно приветствуются.

Почему наследование всегда было бессмысленным / ХабрЕсть три типа наследования.
  1. Онтологическое наследование указывает на специализацию: вот эта штука — специфическая разновидность той штуки (футбольный мяч — это сфера и у неё такой-то радиус).
  2. Наследование абстрактного типа данных указывает на замещение: у этой штуки такие же свойства, как у той штуки, и такое-то поведение (это принцип подстановки Барбары Лисков).
  3. Наследование реализации связано с совместным использованием кода: эта штука принимает некоторые свойства той штуки и переопределяет или дополняет их таким-то образом. Наследование в моей статье «О наследовании» именно такого и только такого типа.

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

Часто для наследования в ООП приводят контрпример отношений между квадратом и прямоугольником. Геометрически квадрат — это специализация прямоугольника: все квадраты — прямоугольники, но не все прямоугольники — квадраты. Все s в классе «Квадрат» являются прямоугольниками s, у которых длина равна ширине. Но в иерархии типов это отношение обратное: вы можете использовать прямоугольник везде, где используется квадрат (указав прямоугольник с одинаковой шириной и высотой), но нельзя использовать квадрат везде, где используется прямоугольник (например, вы не можете изменить длину и ширину).

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

Smalltalk и многие более поздние языки используют простое наследование для наследования реализации, потому что множественное наследование несовместимо с ним из-за проблемы ромба (типажи предоставляют надёжный способ объявить несовместимость, оставляя решение проблемы в качестве упражнения для читателя). С другой стороны, простое наследование несовместимо с онтологическим наследованием, поскольку квадрат является одновременно прямоугольником и равносторонним многоугольником.

Синяя книга по Smalltalk описывает наследование исключительно с точки зрения наследования реализации:

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

Наследование никогда не было проблемой: проблема в попытке использовать одно дерево для трёх разных концепций.

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

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

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

Я считаю, что в литературе должно быть написано следующее: «вот три типа наследования, сосредоточьтесь на одном из них». Я также считаю, что языки должны поддерживать это (очевидно, Smalltalk, Ruby и их друзья поддерживают это за счёт отсутствия каких-либо ограничений типов).

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

Ваша модель области распределения — это не объектная модель. Это не модель абстрактного типа данных. И объектная модель — не модель абстрактного типа данных.

Вот теперь наследование опять стало простым.

Наследование в Java Упс с примером

Guru99
  • Главная
  • Испытание

      • Назад
      • Agile тестирование
      • BugZilla
      • Огурцы
      • База данных Тестирование
      • ETL Тестирование
      • Jmeter
      • JIRA
      • Назад
      • JUnit
      • LoadRunner
      • Ручное тестирование
      • Мобильное тестирование
      • Mantis
      • Почтальон
      • QTP
      • Назад
      • 000000P000
      • 000000P000000000P000000000
      • 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
      • Управление тестированием
      • TestLink
  • SAP

      • Назад
      • 9 0004 ABAP
      • APO
      • Новичок
      • Основа
      • Bods
      • BI
      • BPC
      • CO
      • Назад
      • CRM
      • Crystal Reports
      • КУКИШ
      • HANA
      • HR
      • MM
      • QM
      • Заработная плата
      • Назад
      • 9000 9000 9000
      • 000
      • 000
      • 000000
      • 000
      • 000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000 9000
          • подписываются с отделениями
          • и получают
          • Apache
          • Android
          • AngularJS
          • ASP.Чистая
          • C
          • C #
          • C ++
          • CodeIgniter
          • СУБД
          • Назад
          • Java
          • JavaScript
          • JSP
          • Kotlin
          • M000 M000 js
          • Back
          • Perl
          • PHP
          • PL / SQL
          • PostgreSQL
          • Python
          • ReactJS
          • Ruby & Rails
          • Scala
          • SQL5000
          • SQL000
          • UML
          • VB.Net
          • VBScript
          • Веб-сервисы
          • WPF
      • Необходимо учиться!

          • Назад
          • Учет
          • Алгоритмы
          • Blockchain
          • Бизнес-аналитик
          • Сложение Сайт
          • CCNA
          • Cloud Computing
          • COBOL
          • Compiler Design
          • Embedded Systems
          • Назад
          • Ethical Hacking
          • Excel Учебники
          • Go Программирование
          • IoT
          • ITIL
          • Дженкинс
          • MIS
          • Networking
          • Операционная система
          • Prep
          • Назад
          • PMP
          • Photoshop Управление
          • Проект
          • Отзывы
          • Salesforce
          • SEO
          • Разработка программного обеспечения
          • VBA
      • Big Data

          • Назад
          • AWS
          • BigData
          • Cassandra
          • Cognos
          • Складирование данных
          • 000000000 HBB000500040005000 HB
          • MongoDB
          • NiFi
          • OBIEE
          • Pentaho
          • Назад
      ,

      11.2 — Базовое наследование в C ++

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

      Наследование в C ++ происходит между классами. В отношениях наследования (is-a) наследуемый класс называется родительским классом , базовым классом или суперклассом , а наследующий класс называется дочерним классом , производным классом или подкласс .

      На приведенной выше диаграмме Fruit — родитель, а Apple и Banana — дети.

      На этой диаграмме треугольник является одновременно дочерним (для фигуры) и родительским (для прямоугольного треугольника).

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

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

      A Класс человека

      Вот простой класс для представления общего человека:

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      160003

      140003

      14000000

      18

      #include

      class Person

      {

      // В этом примере мы делаем наши члены общедоступными для простоты

      public:

      std :: string m_name;

      int m_age;

      Person (std :: string name = «», int age = 0)

      : m_name (name), m_age (age)

      {

      }

      std :: string getName () const {return m_name; }

      int getAge () const {return m_age; }

      };

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

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

      A Класс BaseballPlayer

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

      Вот наш неполный класс бейсболистов:

      class BaseballPlayer

      {

      // В этом примере мы сделаем наших членов общедоступными для простоты

      public:

      double m_battingAverage;

      int m_homeRuns;

      BaseballPlayer (среднее значение ватина = 0.0, int homeRuns = 0)

      : m_battingAverage (battingAverage), m_homeRuns (homeRuns)

      {

      }

      };

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

      У нас есть три варианта, как добавить имя и возраст в BaseballPlayer:
      1) Добавить имя и возраст в класс BaseballPlayer непосредственно в качестве членов. Вероятно, это худший вариант, так как мы дублируем код, который уже существует в нашем классе Person.Любые обновления Person также должны быть сделаны в BaseballPlayer.
      2) Добавить человека в качестве члена BaseballPlayer с помощью композиции. Но мы должны спросить себя, «есть ли у BaseballPlayer человек»? Нет, это не так. Так что это не правильная парадигма.
      3) Пусть BaseballPlayer наследует эти атрибуты от Person. Помните, что наследование представляет собой отношения. Является ли BaseballPlayer человеком? Да, это так. Так что наследование — хороший выбор здесь.

      Создание BaseballPlayer производного класса

      Чтобы BaseballPlayer наследовал от нашего класса Person, синтаксис довольно прост.После объявления класса BaseballPlayer мы используем двоеточие, слово «public» и имя класса, который мы хотим наследовать. Это называется публичное наследство . Мы поговорим подробнее о том, что означает публичное наследование, на следующем уроке.

      // BaseballPlayer, публично наследующий Person

      class BaseballPlayer: public Person

      {

      public:

      double m_battingAverage;

      int m_homeRuns;

      BaseballPlayer (среднее значение ватина = 0.0, int homeRuns = 0)

      : m_battingAverage (battingAverage), m_homeRuns (homeRuns)

      {

      }

      };

      При использовании деривационной диаграммы наше наследование выглядит следующим образом:

      Когда BaseballPlayer наследует от Person, BaseballPlayer получает функции-члены и переменные от Person. Кроме того, BaseballPlayer определяет два своих собственных члена: m_battingAverage и m_homeRuns.Это имеет смысл, поскольку эти свойства относятся только к BaseballPlayer, а не к любому Person.

      Таким образом, объекты BaseballPlayer будут иметь 4 переменных-члена: m_battingAverage и m_homeRuns из BaseballPlayer, а также m_name и m_age из Person.

      Это легко доказать:

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      140003

      160003

      14000000

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      9

      000 34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      #include

      #include

      класс Person

      {

      public:

      std :: string m_name;

      int m_age;

      Person (std :: string name = «», int age = 0)

      : m_name (name), m_age (age)

      {

      }

      std :: string getName () const {return m_name; }

      int getAge () const {return m_age; }

      };

      // PublicballPlayer, публично наследующий Person

      class BaseballPlayer: public Person

      {

      public:

      double m_battingAverage;

      int m_homeRuns;

      BaseballPlayer (среднее значение ватина = 0.0, int homeRuns = 0)

      : m_battingAverage (battingAverage), m_homeRuns (homeRuns)

      {

      }

      };

      int main ()

      {

      // Создать новый объект BaseballPlayer

      BaseballPlayer joe;

      // Назначаем имя (мы можем сделать это напрямую, потому что m_name является общедоступным)

      joe.m_name = «Joe»;

      // Распечатать имя

      std :: cout << joe.getName () << '\ n'; // используем функцию getName (), которую мы получили из базового класса Person

      return 0;

      }

      Который печатает значение:

      Джо
       

      Это компилируется и запускается, потому что joe является BaseballPlayer, а все объекты BaseballPlayer имеют переменную-член m_name и функцию-член getName (), унаследованные от класса Person.

      Сотрудник, производный класс

      Теперь давайте напишем еще один класс, который также наследуется от Person.На этот раз мы напишем класс Employee. Сотрудник «является» человеком, поэтому использование наследства уместно:

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      160003

      140003

      14000000

      // Сотрудник публично наследует от Person

      class Employee: public Person

      {

      public:

      double m_hourlySalary;

      длинный

      m_employeeID;

      Сотрудник (double hourlySalary = 0.0, длинный employeeID = 0)

      : m_hourlySalary (hourlySalary), m_employeeID (employeeID)

      {

      }

      недействительным printNameAndSalary () const

      <<

      << имя_счета_2_дней_2_2_дней :: имя_2_дней_2_дней_2_дней_2дней_стадии_2_дней_2: имя_файла_2000 :: {имя_файла_стадии_2000 :: {имя_файла}: имя_строка_2: имя_стандартного имени:

      {

      ) «<< m_hourlySalary << '\ n';

      }

      };

      Сотрудник наследует m_name и m_age от Person (а также две функции доступа) и добавляет еще две переменные-члена и собственную функцию-член.Обратите внимание, что printNameAndSalary () использует переменные как из класса, которому он принадлежит (Employee :: m_hourlySalary), так и из родительского класса (Person :: m_name).

      Это дает нам деривационную диаграмму, которая выглядит следующим образом:

      Обратите внимание, что Employee и BaseballPlayer не имеют никаких прямых отношений, даже если они оба наследуются от Person.

      Вот полный пример использования Employee:

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      140003

      160003

      14000000

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      9

      000 34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      44

      45

      #include

      #include

      класс Person

      {

      public:

      std :: string m_name;

      int m_age;

      std :: string getName () const {return m_name; }

      int getAge () const {return m_age; }

      Person (std :: string name = «», int age = 0)

      : m_name (name), m_age (age)

      {

      }

      };

      // Сотрудник публично наследует от Person

      class Employee: public Person

      {

      public:

      double m_hourlySalary;

      длинный

      m_employeeID;

      Сотрудник (double hourlySalary = 0.0, длинный employeeID = 0)

      : m_hourlySalary (hourlySalary), m_employeeID (employeeID)

      {

      }

      недействительным printNameAndSalary () const

      <<

      << имя_счета_2_дней_2_2_дней :: имя_2_дней_2_дней_2_дней_2дней_стадии_2_дней_2: имя_файла_2000 :: {имя_файла_стадии_2000 :: {имя_файла}: имя_строка_2: имя_стандартного имени:

      {

      ) «<< m_hourlySalary << '\ n';

      }

      };

      int main ()

      {

      Откровенный сотрудник (20.25, 12345);

      frank.m_name = «Frank»; // мы можем сделать это, потому что m_name является открытым

      откровенным.printNameAndSalary ();

      возврат 0;

      }

      Отпечатки:

      Франк: 20,25
       

      Цепи наследования

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

      Например, давайте напишем класс Supervisor. Руководитель — это Сотрудник, который является Лицом.Мы уже написали класс Employee, поэтому давайте использовать его в качестве базового класса, из которого можно извлечь Supervisor:

      Класс

      Супервайзер: публичный Сотрудник

      {

      публичный:

      // Этот супервайзер может контролировать до 5 сотрудников.

      Supervisor ()

      {

      }

      };

      Теперь наша деривационная диаграмма выглядит следующим образом:

      Все объекты Supervisor наследуют функции и переменные от Employee и Person и добавляют свои собственные переменные-члены m_overseesIDs.

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

      Почему этот вид наследования полезен?

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

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

      Заключение

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


      ,
      множественное наследование классов Python — переполнение стека Переполнение стека
      1. Товары
      2. Клиенты
      3. Случаи использования
      1. Переполнение стека Публичные вопросы и ответы
      2. Команды Частные вопросы и ответы для вашей команды
      3. предприятие Частные вопросы и ответы для вашего предприятия
      4. работы Программирование и связанные с ним технические возможности карьерного роста
      5. Талант Нанимать технический талант
      6. реклама Связаться с разработчиками по всему миру
      ,

      11,5 — Спецификаторы наследования и доступа

      Алекс 14 января 2008 г. | Последнее изменение: nascardriver 15 марта 2020 г.

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

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

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

      класс Base

      {

      private:

      int m_private; // могут быть доступны только членам Base и друзьям (не производным классам)

      public:

      int m_public; // может быть доступен любому

      };

      Это довольно просто, и к этому времени вы должны уже привыкнуть.

      Спецификатор защищенного доступа

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

      C ++ имеет третий спецификатор доступа, о котором нам еще только предстоит поговорить, потому что он полезен только в контексте наследования. Защищенный спецификатор доступа позволяет классу, к которому принадлежит член, друзьям и производным классам получить доступ к члену. Однако защищенные члены не доступны извне класса.

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      140003

      160003

      14000000

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      класс Base

      {

      public:

      int m_public; // может быть доступен любому

      protected:

      int m_protected; // могут быть доступны членам Base, друзьям и производным классам

      private:

      int m_private; // могут быть доступны только членам Base и друзьям (но не производным классам)

      };

      класс Производные: общедоступный База

      {

      общедоступный:

      производный ()

      {

      m_public = 1; // позволено: может получить доступ к открытым элементам базы из производного класса

      m_protected = 2; // позволено: может получить доступ к защищенным базовым элементам из производного класса

      m_private = 3; // не разрешено: не может получить доступ к частным базовым членам из производного класса

      }

      };

      int main ()

      {

      Базовая база;

      база.m_public = 1; // позволено: может получить доступ к открытым членам вне класса

      base.m_protected = 2; // не разрешено: не может получить доступ к защищенным членам извне class

      base.m_private = 3; // не разрешено: не может получить доступ к закрытым членам за пределами класса

      }

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

      Итак, когда я должен использовать спецификатор защищенного доступа?

      С помощью защищенного атрибута в базовом классе производные классы могут напрямую обращаться к этому члену.Это означает, что если вы позже измените что-либо об этом защищенном атрибуте (тип, значение и т. Д.), Вам, вероятно, потребуется изменить как базовый класс, так и все производные классы.

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

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

      Различные виды наследования и их влияние на доступ

      Во-первых, существует три различных способа наследования классов от других классов: открытый, защищенный и закрытый.

      Для этого просто укажите, какой тип доступа вы хотите при выборе класса для наследования:

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      160003

      140003

      14000000

      18

      // Открытое наследование от Base

      Pub

      class: public Base

      {

      };

      // Защищено от Base

      , класс Pro: защищено Base

      {

      };

      // Частное наследование от Base

      Класс Pri:

      : частный Base

      {

      };

      class Def: Base // По умолчанию для частного наследования

      {

      };

      Если вы не выберете тип наследования, в C ++ по умолчанию используется частное наследование (так же, как члены по умолчанию используют частный доступ, если не указано иное).

      Это дает нам 9 комбинаций: 3 спецификатора доступа члена (открытый, закрытый и защищенный) и 3 типа наследования (открытый, закрытый и защищенный).

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

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

      Придерживаясь следующих примеров, помните следующие правила:

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

      Общественное наследство

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

      Спецификатор доступа в базовом классе Спецификатор доступа при публичном наследовании
      Общественный Общественный
      Защищено Защищено
      Частный недоступный

      Вот пример, показывающий, как все работает:

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      140003

      160003

      14000000

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      9

      000 34

      35

      36

      37

      класс Base

      {

      public:

      int m_public;

      защищено:

      int m_protected;

      частный:

      int m_private;

      };

      класс Pub: public Base // примечание: публичное наследование

      {

      // Публичное наследование означает:

      // Публично унаследованные члены остаются публичными (поэтому m_public считается общедоступными)

      // Защищенные унаследованные члены оставаться защищенным (поэтому m_protected рассматривается как защищенное)

      // Частные унаследованные члены остаются недоступными (поэтому m_private недоступен)

      public:

      Pub ()

      {

      m_public = 1; // хорошо: m_public был унаследован как public

      m_protected = 2; // хорошо: m_protected был унаследован как защищенный

      m_private = 3; // не в порядке: m_private недоступен из производного класса

      }

      };

      int main ()

      {

      // Внешний доступ использует спецификаторы доступа к классу, к которому осуществляется доступ.

      Базовая база;

      base.m_public = 1; // хорошо: m_public является публичным в Base

      base.m_protected = 2; // не в порядке: m_protected защищено в Base

      base.m_private = 3; // не в порядке: m_private является частным в Base

      Pub pub;

      pub.m_public = 1; // хорошо: m_public общедоступно в Pub

      pub.m_protected = 2; // не в порядке: m_protected защищен в Pub

      pub.m_private = 3; // не в порядке: m_private недоступен в Pub

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

      Общественное наследование — это то, что вы должны использовать, если у вас нет особых причин не делать этого.

      Используйте публичное наследование, если у вас нет особых причин поступать иначе.

      Защищенное наследство

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

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

      Спецификатор доступа в базовом классе Спецификатор доступа при наследовании защищено
      Общественный Защищено
      Защищено Защищено
      Частный недоступный

      Частное наследство

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

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

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      140003

      160003

      14000000

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      9

      000 34

      35

      36

      37

      38

      39

      40

      41

      класс Base

      {

      public:

      int m_public;

      защищено:

      int m_protected;

      частный:

      int m_private;

      };

      class Pri: private Base // примечание: личное наследование

      {

      // Частное наследование означает:

      // Открытые унаследованные члены становятся частными (поэтому m_public считается частным)

      // Защищенные унаследованные члены стать частным (поэтому m_protected рассматривается как частное)

      // Частные унаследованные члены остаются недоступными (поэтому m_private недоступен)

      public:

      Pri ()

      {

      m_public = 1; // хорошо: m_public сейчас закрыт в Pri

      m_protected = 2; // хорошо: m_protected теперь приватно в Pri

      m_private = 3; // не в порядке: производные классы не могут получить доступ к закрытым членам базового класса

      }

      };

      int main ()

      {

      // Внешний доступ использует спецификаторы доступа к классу, к которому осуществляется доступ.

      // В этом случае указатели доступа к базе.

      Базовая база;

      base.m_public = 1; // хорошо: m_public является публичным в Base

      base.m_protected = 2; // не в порядке: m_protected защищено в Base

      base.m_private = 3; // не в порядке: m_private является частным в Base

      Pri pri;

      pri.m_public = 1; // не в порядке: m_public теперь закрыт в Pri

      pri.m_protected = 2; // не в порядке: m_protected теперь приватно в Pri

      pri.m_private = 3; // не в порядке: m_private недоступен в Pri

      return 0;

      }

      Подвести итог в виде таблицы:

      Спецификатор доступа в базовом классе Спецификатор доступа при частном наследовании
      Общественный Частный
      Защищено Частный
      Частный недоступный

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

      На практике частное наследование используется редко.

      Последний пример

      класс Base

      {

      public:

      int m_public;

      защищено:

      int m_protected;

      частный:

      int m_private;

      };

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

      , класс D2: частная база // примечание: частное наследование

      {

      // Частное наследование означает:

      // Публичные наследуемые члены становятся частными

      // Защищенные унаследованные члены становятся частными

      // Частные унаследованные члены остаются недоступный

      публичный:

      int m_public2;

      защищено:

      int m_protected2;

      частный:

      int m_private2;

      };

      D2 может получить доступ к своим членам без ограничений.D2 может получить доступ к членам Base m_public и m_protected, но не к m_private. Поскольку D2 унаследовал Base в частном порядке, m_public и m_protected теперь считаются закрытыми при доступе через D2. Это означает, что публика не может получить доступ к этим переменным при использовании объекта D2, равно как и классы, производные от D2.

      класс D3: общедоступный D2

      {

      // Публичное наследование означает:

      // Публично наследуемые члены остаются публичными

      // Защищенные унаследованные члены остаются защищенными

      // Частные унаследованные члены остаются недоступными

      публично:

      int m_public3;

      защищено:

      int m_protected3;

      частный:

      int m_private3;

      };

      D3 может получить доступ к своим членам без ограничений.D3 может обращаться к членам m2public2 и m_protected2, но не к m_private2. Поскольку D3 публично наследовал D2, m_public2 и m_protected2 сохраняют свои спецификаторы доступа при доступе через D3. D3 не имеет доступа к базе m_private, которая уже была закрытой в Base. Он также не имеет доступа к m_protected или m_public с Base, оба из которых стали частными, когда D2 унаследовал их.

      Резюме

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

      Во-первых, класс (и друзья) всегда могут получить доступ к своим собственным не наследуемым членам. Спецификаторы доступа влияют только на то, могут ли посторонние и производные классы иметь доступ к этим членам.

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

      Вот таблица всех комбинаций спецификатора доступа и типов наследования:

      Спецификатор доступа в базовом классе Спецификатор доступа при публичном наследовании Спецификатор доступа при частном наследовании Спецификатор доступа при наследовании защищено
      Общественный Общественный Частный Защищено
      Защищено Защищено Частный Защищено
      Частный недоступный недоступный недоступный

      В качестве заключительного замечания, хотя в приведенных выше примерах мы только показали примеры с использованием переменных членов, эти правила доступа сохраняются для всех членов (например,грамм. функции-члены и типы, объявленные внутри класса).


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

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

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