В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.


Beginner


Что такое наследование?


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


Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.


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


В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer. Однако их можно использовать, поскольку они унаследованы от базового класса.


Важное примечание: приватные переменные и методы не могут быть унаследованы.


#include <iostream>
using namespace std;

class Device {
    public:
        int serial_number = 12345678;

        void turn_on() {
            cout << "Device is on" << endl;
        }
    private:
        int pincode = 87654321;
};

class Computer: public Device {};

int main() {
    Computer Computer_instance;

    Computer_instance.turn_on();
    cout << "Serial number is: " << Computer_instance.serial_number << endl;
    // cout << "Pin code is: " << Computer_instance.pincode << endl;
    // will cause compile time error
    return 0;
}

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


В C ++ есть несколько типов наследования:


  • публичный (public)- публичные (public) и защищенные (protected) данные наследуются без изменения уровня доступа к ним;
  • защищенный (protected) — все унаследованные данные становятся защищенными;
  • приватный (private) — все унаследованные данные становятся приватными.

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


#include <iostream>
using namespace std;

class Device {
    public:
        int serial_number = 12345678;

        void turn_on() {
            cout << "Device is on" << endl;
        }
};

class Computer: private Device {
    public:
        void say_hello() {
            turn_on();
            cout << "Welcome to Windows 95!" << endl;
        }
};

int main() {
    Device Device_instance;
    Computer Computer_instance;

    cout << "\t Device" << endl;
    cout << "Serial number is: "<< Device_instance.serial_number << endl;
    Device_instance.turn_on();

    // cout << "Serial number is: " << Computer_instance.serial_number << endl;
    // Computer_instance.turn_on();
    // will cause compile time error

    cout << "\t Computer" << endl;
    Computer_instance.say_hello();
    return 0;
}

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


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


В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.


Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.


#include <iostream>
using namespace std;

class Device {
    public:
        // constructor
        Device() {
            cout << "Device constructor called" << endl;
        }
        // destructor
        ~Device() {
            cout << "Device destructor called" << endl;
        }
};

class Computer: public Device {
    public:
        Computer() {
            cout << "Computer constructor called" << endl;
        }
        ~Computer() {
            cout << "Computer destructor called" << endl;
        }
};

class Laptop: public Computer {
    public:
        Laptop() {
            cout << "Laptop constructor called" << endl;
        }
        ~Laptop() {
            cout << "Laptop destructor called" << endl;
        }
};

int main() {
    cout << "\tConstructors" << endl;
    Laptop Laptop_instance;
    cout << "\tDestructors" << endl;
    return 0;
}

Конструкторы: Device -> Computer -> Laptop.
Деструкторы: Laptop -> Computer -> Device.


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


Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.


#include <iostream>
using namespace std;

class Computer {
    public:
        void turn_on() {
            cout << "Welcome to Windows 95" << endl;
        }
};

class Monitor {
    public:
        void show_image() {
            cout << "Imagine image here" << endl;
        }
};

class Laptop: public Computer, public Monitor {};

int main() {
    Laptop Laptop_instance;
    Laptop_instance.turn_on();
    Laptop_instance.show_image();
    return 0;
}

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


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


#include <iostream>
using namespace std;

class Computer {
    private:
        void turn_on() {
            cout << "Computer is on." << endl;
        }
};

class Monitor {
    public:
        void turn_on() {
            cout << "Monitor is on." << endl;
        }
};

class Laptop: public Computer, public Monitor {};

int main() {
    Laptop Laptop_instance;
    // Laptop_instance.turn_on();
    // will cause compile time error
    return 0;
}

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


Intermediate


Проблема ромба



Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A, а класс D наследует B и C.


К примеру, классы A, B и C определяют метод print_letter(). Если print_letter() будет вызываться классом D, неясно какой метод должен быть вызван — метод класса A, B или C. Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

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


  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop).

#include <iostream>
using namespace std;

class Device {
    public:
        void turn_on() {
            cout << "Device is on." << endl;
        }
};

class Computer: public Device {};

class Monitor: public Device {};

class Laptop: public Computer, public Monitor {
    /*
    public:
        void turn_on() {
            cout << "Laptop is on." << endl;
        }
    // uncommenting this function will resolve diamond problem
    */
};

int main() {
    Laptop Laptop_instance;

    // Laptop_instance.turn_on();
    // will produce compile time error
    // if Laptop.turn_on function is commented out

    // calling method of specific superclass
    Laptop_instance.Monitor::turn_on();

    // treating Laptop instance as Monitor instance via static cast
    static_cast<Monitor&>( Laptop_instance ).turn_on();
    return 0;
}

Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on(), приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on().


Проблема ромба: Конструкторы и деструкторы


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


#include <iostream>
using namespace std;

class Device {
    public:
        Device() {
            cout << "Device constructor called" << endl;
        }
};

class Computer: public Device {
    public:
        Computer() {
            cout << "Computer constructor called" << endl;
        }
};

class Monitor: public Device {
    public:
        Monitor() {
            cout << "Monitor constructor called" << endl;
        }
};

class Laptop: public Computer, public Monitor {};

int main() {
    Laptop Laptop_instance;
    return 0;
}

Виртуальное наследование


Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.


#include <iostream>
using namespace std;

class Device {
    public:
        Device() {
            cout << "Device constructor called" << endl;
        }
        void turn_on() {
            cout << "Device is on." << endl;
        }
};

class Computer: virtual public Device {
    public:
        Computer() {
            cout << "Computer constructor called" << endl;
        }
};

class Monitor: virtual public Device {
    public:
        Monitor() {
            cout << "Monitor constructor called" << endl;
        }
};

class Laptop: public Computer, public Monitor {};

int main() {
    Laptop Laptop_instance;
    Laptop_instance.turn_on();
    return 0;
}

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device {}; ).


Абстрактный класс


В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.


#include <iostream>
using namespace std;

class Device {
    public:
       void turn_on() {
            cout << "Device is on." << endl;
        }
        virtual void say_hello() = 0;
};

class Laptop: public Device {
    public:
       void say_hello() {
            cout << "Hello world!" << endl;
        }
};

int main() {
    Laptop Laptop_instance;
    Laptop_instance.turn_on();
    Laptop_instance.say_hello();

    // Device Device_instance;
    // will cause compile time error
    return 0;
}

Интерфейс


С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).


#include <iostream>
using namespace std;

class Device {
    public:
        virtual void turn_on() = 0;
};

class Laptop: public Device {
    public:
       void turn_on() {
            cout << "Device is on." << endl;
        }
};

int main() {
    Laptop Laptop_instance;
    Laptop_instance.turn_on();

    // Device Device_instance;
    // will cause compile time error
    return 0;
}

Advanced


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


Наследование от реализованного или частично реализованного класса


Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.


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


Интерфейс


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


Интерфейс: Пример использования


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


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


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


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


Заключение


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


А SOLID это хорошо.

Комментарии (62)


  1. geher
    01.04.2019 13:37
    +1

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

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


    1. KaterynaBondarenko Автор
      01.04.2019 14:10

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


  1. Antervis
    01.04.2019 13:39

    я думал тут в advanced будет вплоть до crtp


    1. AxisPod
      01.04.2019 14:40

      И стратегии к примеру, в смысле шаблон проектирование, на базе шаблонов.


  1. NickViz
    01.04.2019 15:39

    Ошибка:
    «Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.»
    class Laptop: public Computer, public Monitor {};

    у объекта Laptop будет 2 разных объекта Device. И для каждого будет вызван конструктор.


    1. KaterynaBondarenko Автор
      01.04.2019 15:47

      У объекта Laptop будет один subobject (к сожалению не знаю точного перевода термина) класса Computer и один subobject класса Monitor, а те в свою очередь будут иметь по subobject`у класса Device. Как известно,

      при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов

      и как результат
      возникает и другая проблема: конструктор базового класса Device будет вызван дважды


      1. NickViz
        01.04.2019 16:27

        мне всё равно не понятно, почему вы пишите в единственном числе о базовом классе. будут два объекта, два раза вызовется конструкторы. в чем проблема? в том что вы хотели один объект Device? ну так для этого virtual наследование и изобрели.

        «возникает и другая проблема: конструктор базового класса Device будет вызван дважды.» в контексте вашего кода — «конструктор базового класса Device будет вызван дважды.» для _разных_ объектов. и «проблемой» это не является. «проблема» в наличии этих двух объектов.


  1. loginsin
    01.04.2019 16:11

    В C ++ конструкторы и деструкторы не наследуются.

    Не все так просто.
    В дополнение


    1. pvsur
      01.04.2019 18:58
      +1

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


    1. KaterynaBondarenko Автор
      01.04.2019 22:10

      Да, вы абсолютно правы. Честно говоря, материала обрабатывала много, и про них просто забыла. Завтра статью дополню, спасибо


  1. berez
    01.04.2019 17:02

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


    1. pvsur
      01.04.2019 19:05

      Фабрика классов где?


    1. KaterynaBondarenko Автор
      01.04.2019 22:19

      То, что вы описываете, по моему мнению вплотную подходит к понятию «полиморфизм». Поскольку эта статья, как исходит из названия, о наследовании, я старалась не освещать смежные понятия


      1. berez
        02.04.2019 16:02

        Поскольку эта статья, как исходит из названия, о наследовании, я старалась не освещать смежные понятия

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


        1. KaterynaBondarenko Автор
          03.04.2019 11:06

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


  1. mcroitor
    01.04.2019 17:17

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


    1. domix32
      01.04.2019 20:23

      Но ведь наследование и есть помещение объекта-родителя в потомка. Или имеется ввиду относительно последовательности конструирования?


      1. mcroitor
        02.04.2019 09:21

        Нет. Нельзя (то есть крайне не рекомендуется) поместить объект родителя в потомка, потому что все свойства потомка, которых нет в родителе, останутся непроинициализированными, и, соответственно, результат их использования непредсказуем.
        Отвлеченно:
        image

        • Пусть Y — множество (класс-родитель), X — его подмножество (класс-потомок).
        • Существует объект (d) множества Y, которое не принадлежит X.
        • Любой объект (r, e, a) множества X, которое принадлежит Y.


        1. domix32
          02.04.2019 10:42

          Очень странные схемы вы рисуете. Класс-потомок наследует часть множества родителя исключая приватные поля — собственно то самое множество d. У потомка есть своё множество о которых гарантированно ничего не известно классу родителю — свои приватные, пубичные и переопределенные методы. Учитывая, что даже структурно класс-потомок ссылается на виртуальную таблицу методов — все же родитель помещается в потомка и из потомка же триггерит конструирование родителя, а не наоборот. Или вы имели ввиду вот такую ситуацию с указателями на объекты
          Derive * x = new Base();?


          1. mcroitor
            02.04.2019 11:07

            Именно это я и имею в виду.


  1. EvgeniiR
    01.04.2019 21:03

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

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

    И наследование не является основопологающей идеей ООП. В первой версии SmallTalk его, кстати, вообще не было.


    1. KaterynaBondarenko Автор
      01.04.2019 22:36

      Я сожалею о том, что вас огорчает то, что эта статья о наследовании. Также, если под LSP вы подразумеваете Liskov substitution principle, то смею подчеркнуть, что в статье не упомянуты и SRP, OCP, ISP, DIP, зато еще во вступлении сказано, что в статье нет ни слова про SOLID.

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

      Напоследок хочу сказать, что наследование все же принято считать одним из основополагающих принципов ООП (автором того же SOLID, к примеру)


      1. EvgeniiR
        01.04.2019 23:36

        принято считать одним из основополагающих принципов ООП (автором того же SOLID, к примеру)

        Не говорил Дядя Боб такого, он высмеивал эту терминологию упоминая что «Инкапсуляция Наследование и Полимофизм» достижимы и в Си, который вроде как не ООП.
        Если вы про книгу «Clean Architecture»(«Чистая Архитектура»), то вам определенно стоит внимательно её перечитать. Мне жаль тех новичков кто наткнется на вашу статью, потому что умением фильтровать информацию они, к сожалению, пока что особо не обладают.

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

        В русской Википедии тоже всё плохо. Смысл тащить это ещё и на Хабр?

        что в статье не упомянуты и SRP, OCP, ISP, DIP,

        Если вы обдумаете эти принципы, вы поймете почему я упомянул именно LSP

        P.s.
        (автором того же SOLID, к примеру)

        Я конечно понял что вы про Дядюшку Боба, но он не автор принципов из SOLID, он лишь аббревиатуру красивую придумал


        1. mcroitor
          02.04.2019 09:32

          Не представляю, как в С можно реализовать наследование. Не могли бы вы привести пример?
          Касательно автора принципов SOLID, а также аббревиатуры:
          en.wikipedia.org/wiki/Robert_C._Martin
          en.wikipedia.org/wiki/SOLID
          Принципиальны не статьи в википедии, но библиография к статьям.
          Воббще-то, я считаю, что статья замечательная. Невозможно уместить в рамках статьи объемы энциклопедии. А рекомендации по использованию наследования — это тема отдельной статьи, возможно даже вашей.


          1. MaxVetrov
            02.04.2019 09:58

            1. mcroitor
              02.04.2019 10:19

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


          1. EvgeniiR
            02.04.2019 10:05

            На счёт наследования в Си уже ответили выше.
            Подменять понятия и выдавать ложные фразы пытаясь прикрыть их громким именем "Автора SOLID", абсолютно не разбираясь в теме, не может быть хорошо.
            Нужно различать статьи которые покрывают лишь часть материала, и статьи которые откровенно вредят.


            1. mcroitor
              02.04.2019 10:24

              Абсолютно согласен. Давайте почитаем источник 1995 года:
              tinyurl.com/84emx
              Я ничего не говорил о вашей компетентности, так что прошу не высказываться о моей.


            1. KaterynaBondarenko Автор
              02.04.2019 11:38

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


              1. EvgeniiR
                02.04.2019 13:01

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


                1. KaterynaBondarenko Автор
                  03.04.2019 11:11

                  Не знаю какой реакции вы ожидаете. Вы — большой молодец раз написали такой большой и красивый комментарий


          1. KaterynaBondarenko Автор
            02.04.2019 11:23

            Во-первых, спасибо:) Во-вторых, тема применения ООП принципов в С поднималась не раз, к примеру OOP with ANSI-C тут, и тот же Мартин в Clean Architecture. По большому счету, написана и масса статей, и их можно найти просто погуглив


            1. EvgeniiR
              02.04.2019 11:32

              Во-вторых, тема применения ООП принципов в С поднималась не раз, к примеру OOP with ANSI-C тут, и тот же Мартин в Clean Architecture

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

              1.
              Инкапсуляция упоминается как часть определения ОО потому, что языки
              ОО поддерживают простой и эффективный способ инкапсуляции данных
              и функций. Как результат, есть возможность очертить круг связанных
              данных и функций. За пределами круга эти данные невидимы и доступны
              только некоторые функции. Воплощение этого понятия можно наблюдать
              в виде приватных членов данных и общедоступных членов-функций класса.
              Эта идея определенно не уникальная для ОО. Например, в языке C имеется
              превосходная поддержка инкапсуляции. Рассмотрим простую программу
              на C:

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


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

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


              3.
              Была ли возможность реализовать полиморфное поведение до появления
              языков ОО? Конечно!


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

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


      1. eefadeev
        02.04.2019 12:16

        И наследование не является основопологающей идеей ООП.

        Наследование является одним из четырёх основополагающих принципов ООП.


        1. EvgeniiR
          02.04.2019 12:43

          Наследование является одним из четырёх основополагающих принципов ООП.

          То есть, получается, что Си Объектно-Ориентированный язык, а первые версии SmallTalk — нет?)

          Если обратиться к определнию Алана Кея, основополагающие идеи ООП — messaging, information hiding, late static binding.

          Классы+инстансы классов != ООП.
          В ином случае термин ООП просто не имеет смысла, потому что каждый понимат его по своему.


          1. mcroitor
            02.04.2019 13:46

            Напишите, пожалуйста, статью о вашем видении ООП, и там мы это обсудим. Согласны?


          1. eefadeev
            02.04.2019 14:29

            То есть, получается, что Си Объектно-Ориентированный язык, а первые версии SmallTalk — нет?)


            Если в Си есть наследование, инкапсуляция, полиморфизм и абстракции — то да. Я последний раз писал на Си лет 25 назад, тогда этого там, насколько я помню, не было. Но возможно я просто не в курсе.

            Про SmallTalk не скажу, не знаю. Опять же, насколько я помню он считается первым ООП языком (не считая Симулы-67) и в нём был тот самый messaging. Но если в нём не было наследования он, пожалуй, не полноценная реализация ЯООП.

            Классы+инстансы классов != ООП


            Если ООП = OOP — то да. Если ООП = OOD, то вполне


            1. EvgeniiR
              02.04.2019 14:58

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

              Если ООП = OOP — то да.

              ООП = Объектно-Ориентированное Программирование. Термин который ввёл Алан Кей, бакалавр молекулярной биологии, и ввел он его позже чем появилась Симула.
              Идеи, которые он вкладывал в это понятие — вовсе не «Наследование, инкаспуляция и полиморфизм»( wiki.c2.com/?AlanKaysDefinitionOfObjectOriented ), иначе его парадигма ничем бы не отличалась от уже существующих приёмов написания кода, хоть в той же Симуле.

              Вобщем печально что изначальные идеи ООП утерялись в глубинах истории.


              1. eefadeev
                02.04.2019 16:09

                Слушайте, я не знаю какие смыслы в термин ООП вкладывал Алан Кей. Но я начинал программировать на языках, про некоторые из которых вы, вполне вероятно, даже не слышали и в них не было даже намёка на ООП, классы и т.п. И когда в мой мир пришла парадигма ООП (а конкретно для меня она началась с Borland TurboPascal 5.5) это потребовало очень серьёзной перестройки мышления. Потому что это действительно была принципиально иная парадигма. И она вполне корректно описана в той же Википедии (и, кстати, там в определении ООП от Алана Кея п.6 значится именно наследование).
                А если следовать вашей логике (и слегка довести её до абсурда — простите, профдеформация), то получается что даже обычный Бейсик вполне себе объектный язык, если я могу подключить к нему какую-нибудь библиотеку для работы с сообщениями — ведь в этот момент у него появится messaging и он станет «почти как SmallTalk».


                1. EvgeniiR
                  02.04.2019 16:24

                  Бейсик вполне себе объектный язык, если я могу подключить к нему какую-нибудь библиотеку для работы с сообщениями — ведь в этот момент у него появится messaging и он станет «почти как SmallTalk».

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

                  Ну и да, мессаджинг утерян и сейчас лишь немного возврождается в Actor Model (Scala(Acca) / Erlang), либо даже в виде микросервисах, если подходить к ним грамотно.

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

                  А вообще, в ООП от Алана Кея, был ещё один важный момент — «Всё — объект», наподобие клеток в организме, обменивающимися сообщениями, и именно эта часть, например, совсем не прижилась в современных языках программирования.


                  1. eefadeev
                    02.04.2019 17:10

                    В смысле «пришли»? Я регулярно вижу написанный на вполне ООП-шных языках во вполне себе процедурном стиле код. А на собеседованиях на вопрос «Назовите основные концепции ООП» чуть ли не половина больше чем «Наследование» и, в лучшем случае «Инкапсуляция» вспомнить не могут.
                    Но это не имеет никакого отношения к самой парадигме. Я, например, вообще — разработчик БД (казалось бы — где SQL и где ООП?!), но тем не менее мыслю во вполне объектной парадигме.


                    1. EvgeniiR
                      02.04.2019 17:16

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

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


                      1. eefadeev
                        03.04.2019 11:03

                        Да у меня-то нет никаких проблем ни с ООП, ни с ООД :)

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


                        1. EvgeniiR
                          03.04.2019 11:59

                          Но наследование, таки, входит в «набор элементов ООП»

                          Нет. Ещё раз, ООП это конкретный термин введенный конкретным человеком. Его определение хоть и изначально было размыто, но никаким наследованием и стрелочками там и не пахнет. Наследование возможно как с «ООП» которое вы предлагаете, так и без него. Типичное «наследование инкапсуляция и полиморфизм» реализуемы и без объектов в понимании «объект = инстантс класса», и по сути ничем не отличается от структурного программирования с сокрытием состояния в каком-то скоупе, взять ту же инкапсуляцию в Си.

                          если наследования нет, то, практически автоматом, «выбывают» абстракции и полиморфизм.

                          Да и пусть выбывают, эти штуки существуют как с объектами, так и без, поэтому основопологающими принципами ОО-парадигмы быть не могут в принципе. Оригинальный термин ООП был не о добавлении ключевого слова object/class в язык программирования, а о децентрализации, о том как эти объекты будут между собой общаться.

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


                          1. eefadeev
                            03.04.2019 12:57

                            Тут вот какое дело: любую (я подчёркиваю — любую!) программу, реализованную на любом (хоть ООП, хоть нет) ЯП можно реализовать и на ассемблере. Который ни разу ни структурный, ни объектный и вообще, практически, машинный. Следует ли из этого что ассемблер — ООП язык?
                            Вот так и со структурным и объектным. Всё дело в конкретной реализации.
                            А про определение Алана Кея я вам ещё вчера написал (см. п.6 в Википедии).

                            P.S. И да, я читал «Чистую архитектуру» (в том числе). И да, я прекрасно понимаю то, что там написано :)


                            1. EvgeniiR
                              03.04.2019 13:42

                              Тут вот какое дело: любую (я подчёркиваю — любую!) программу, реализованную на любом (хоть ООП, хоть нет) ЯП можно реализовать и на ассемблере.

                              А кто спорит то?)
                              Все парадигмы это просто разные способы делать одни и те же вещи, где-то удобнее, где-то нет.

                              А про определение Алана Кея я вам ещё вчера написал (см. п.6 в Википедии).

                              Я ни в русской ни в Английской версии википедии не нашел упоминания определения от Алана Кея в п.6

                              Честно говоря, в русской википелии в п.6 вообщее какие-то вообще не связанные с ООП вещи и описание недостатков наследования и иерархии классов, никоим образом к Объектно-ориентированной парадигме не относящиеся. Мы можем сделать иерархию структур в том же Си, и можем писать на Объектно-ориентированном языке и не встретиться с такой проблемой если в нем не будет наследования.

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

                              Из каких частей состоит система;
                              В чём состоит ответственность каждой из её частей.

                              Конкретно этот кусок вообще не понятно как там оказался, когда мы пишем процедуры, или когда мы реализовываем инкапсуляцию в Си, да и вообще, в тру-ФП языках аля Хаскель, нам всё так же нужно думать из каких частей состоит система, и какие у этих частей ответственности


                  1. geher
                    02.04.2019 18:49

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

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


                    1. EvgeniiR
                      02.04.2019 18:55

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

                      Сообщения эти механизмы в различных ООП языках вполне себе эмулируют.

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


                      1. geher
                        02.04.2019 19:06

                        Эмуляция не обязана воспроизводить процесс полностью. Достаточно только интересующей части. Соответственно, каждый ООП программист реализует ту часть процесса передачи сообщений, которую посчитал достаточной для своей модели.
                        Причем большинство ООП языков вообще не реализуют конкретного метода передачи сообщений между объектами, отдавая этот процесс на откуп программистам. Все конкретные способы эмуляции передачи сообщений обычно мы получаем уже на уровне библиотек (VCL, QT). Как говорится, все в ваших руках.


                        1. EvgeniiR
                          02.04.2019 19:42

                          Вобщем. Разговор уходит не туда. Вернувшись к теме данного поста, наследовние — явно не основопологающий принцип ООП, и в принципе вещь сомнительная и опасная.


                          1. geher
                            03.04.2019 08:49

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


                1. EvgeniiR
                  02.04.2019 16:42

                  когда в мой мир пришла парадигма ООП (а конкретно для меня она началась с Borland TurboPascal 5.5) это потребовало очень серьёзной перестройки мышления.

                  Кстати, мне правда интересно, что нового для вас внесла эта парадигма придя в виде классов и экземпляров классов? Ну то есть… В чем принципиальное отличие от того же процедурного программирования, что пришлось серьёзно перестраивать мышление?

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


              1. geher
                02.04.2019 18:58

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


                1. EvgeniiR
                  02.04.2019 19:39

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

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


  1. udattsk
    01.04.2019 21:25
    +1

    "Предпочитайте композицию наследованию"
    34 правило Александреску / Саттера.


  1. Mazenrab
    02.04.2019 11:49

    В С++, класс в котором существует хотя бы один виртуальный метод принято считать абстрактным.


    Если обратиться к первоисточникам, то:
    An abstract class is a class that can be used only as a base class of some other class; no objects of an abstract class can be created except as subobjects of a class derived from it. A class is abstract if it has at least one pure virtual function.


    Хотя я в C++ не разбираюсь )


    1. KaterynaBondarenko Автор
      02.04.2019 11:50

      Да, вы правы, исправила буквально за минуту до того как вы написали:)


  1. MaxVetrov
    02.04.2019 23:28

    Меня смущает вот это утверждение:

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


    1. geher
      03.04.2019 08:58

      Более узкое понимание наследования. Т.е. наследуется только то, что "видно" снаружи или внутри класса.


      1. MaxVetrov
        03.04.2019 10:44

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


        1. KaterynaBondarenko Автор
          03.04.2019 11:18

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


          1. MaxVetrov
            03.04.2019 11:45

            Все верно .)

            Всё должно быть изложено так просто, как только возможно, но не проще.
            Лучше недосказать, чем сказать что-то неверное.