На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: Qraizer, Hsilgos
  
> Наследование, Композиция, Интерфейсы ... , тема о расширябельности функционала
    Всем привет!

    В последнее время немного интересовался более другими языками, кроме как С++ и у каждого свои фишки по сабжу. Ну если более предметно, очень понравился Rust и Dart. Второй наверное даже более интересен. Ну это так, типа "взгляда ребёнка", которому показали коробочку с хэлоу ворлдами. Но! Естественно, первое знакомство с этими ЯП волей-не-волей сформировало некий фидбэк к С++ и Perl, которые я очень уважаю. А еще я познакомился и выучил наизусть догмы из S.O.L.I.D, еще одно дополнительное измерение. В общем - в голове какая-то каша от всего этого. Ничего не понимаю - но очень интересно!

    Хочется все эти знания-понятия как-то логически разложить по логическим полочкам в разрезе С++ возможностей. Вопросов куча, и скорее не "как?", а "для каких случаев лучше?".
    Понятное дело, что приветствие в начале поста было всем ... а просьба показать и рассказать "на пальцах" сами-знаете-кому :lol:

    Уверен, если оценить сабж с точки зрения описания темы - будет суперский материал для FAQ C++, ну и нам, недотепам - материал для утренних намазов.

    P.S. Всех страждущих прошу не скромничать! Тема будет полезна всем.
      Цитата Majestio @
      а просьба показать и рассказать "на пальцах" сами-знаете-кому
      Он с нескрываемым интересом послушает.
        Вредина! :lol: Давай делись тайными знаниями! :lol:
          В смысле? Чё ты от меня хошь? Что мульён раз уже говорено? Я думал, счас кто-то что-то скажет, и у меня вдруг случится сдвиг мозга.
            Цитата Qraizer @
            В смысле? Чё ты от меня хошь? Что мульён раз уже говорено? Я думал, счас кто-то что-то скажет, и у меня вдруг случится сдвиг мозга.

            Что-то я запамятовал, где такое обсуждалось. Ну а сказать, что кто-то скажет ... сложно, осталось совсем мало собеседников.
              Оставлю тут, из небольшой беседы с ChatGPT :)

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

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

              Композиция

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

              Агрегация

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

              Интерфейсы

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

              Преимущества и недостатки
              • Наследование обеспечивает повторное использование кода, но может привести к жесткой связанности между классами
              • Композиция и агрегация позволяют создавать более гибкие и модульные системы, но могут привести к увеличению сложности взаимодействия между объектами
              • Интерфейсы обеспечивают гибкость и расширяемость, но требуют более тщательного планирования и проектирования программы
                Majestio, так, вернулся, ещё раз перечитал, и у меня главный вопрос: ты чего хотел-то? Если учебник по терминам, то сорри, это ж азы, и вряд ли у тебя с этим проблемы. Если какое это отношение имеет к Плюсам, так ответ очевиден: прямое, т.к. Плюсы поддерживают парадигму ООП. Если как это в Плюсах реализуется, то это опять же учебник, только по языку, а не программированию.
                  На самом деле, мне интересны "Преимущества и недостатки", т.е. способы применения. Когда что лучше, когда хуже, и почему. Чисто академический интерес.
                    Хм. Ну, GPT в целом тебя не обманул. Но есть нюанс: описал скорее следствия преимуществ и недостатков, нежели сами преимущества и недостатки.
                    • Наследование не представляет собой средство повторного использования и уменьшения дублирования, для этого есть другие методы. Да, повторное использование и уменьшение дублирования имеет место, но это в целом лишь бонус. Наследование само по себе призвано решать другие задачи: декомпозиция сложной сущности на более простые и – при условии, что исходная сущность изначально была спроектирована из расчёта на это – расширение возможностей сущности.
                    Важно не забывать, что наследование создаёт сильную зависимость нового класса от предков. Любые изменения в них отражаются на производном классе, т.к. они являются его частью. Это недостаток, ибо чем слабее связи между сущностями, тем проще управлять комплексом в целом, однако – гораздо реже, чем поначалу кажется студентам, впрочем – бывает так, что выгода с лихвой этот недостаток перебивает. Типичная ситуация использования: когда целью изначально был этот самый производный класс, и его деление на базовые оказалось просто удобным средством его декомпозиции на подобъекты. Ирония в том, что в таких случаях наследованию нет необходимости быть публичным, т.к. базовые классы не предназначаются для использования отдельно от производного и даже могут не иметь публичного интерфейса вовсе. Хороший пример – классы (не ассоциативных) контейнеров std::vector<>, std::deque<>, std::basic_string. Им всем нужен примерно одинаковый функционал по управлению своим хранилищем, который не привязан к свойствам конкретных объектов, которые эти контейнеры будут хранить. Нужно уметь проверять доступное место в хранилище, нужно уметь его расширять/уменьшать, нужно уметь его перераспределять без потери уже имеющейся в нём информации. Почему бы не выделить этот функционал в отдельную сущность? Контейнеры его далее смогут просто использовать. Да, тут будет иметь место устранение дублирования и повторное использование, но даже притом, что если бы был только один тип контейнера, std::vector<>, например, то это всё равно выгодно, так как мы декомпозировали сложный объект на два простых, где каждый занимается своим делом и не лезет в дела другого. Ожин занимается хранилищем, другой push_back()чит, insert()ит, []сит итд. Так что я легко вижу тут некий std::storage, который приватно наследуется и std::vector<>, и std::deque<>, и std::basic_string. При этом std::storage запросто может оказаться без публичного интерфейса вовсе, т.к. бесполезен вне этих (ну или нами самописных подобных, мало ли) контейнеров.
                    Второе свойство наследования – расширяемость – тоже студентами зачастую понимается превратно. Типа, у-у-у! как здорово, у нас есть std::string, а давайте добавим ему связи с std::locale и научим case insensitive и up/down cases, и фигачат
                    ExpandedWrap disabled
                      template <typename Ch, typename Tr = std::char_traits<Ch>, typename Al = std::allocator<Ch>>
                      class basic_locale_string: public std::basic_string<Ch, Tr, Al>, public std::locale;
                    Увы, но так это не работает. Во-первых, std::basic_string<> не проектировался быть базовым для чего-либо, так что простое наследование вызовет кучу проблем для basic_locale_string, когда его пользователю понадобится в нём функционал обычного std::basic_string. Главное правило: если ваш производный класс не способен собою заменить базовый везде, где этот базовый ожидается, то наследование, как минимум публичное, не подходит. Расширение функционала среди прочего подразумевает, что прежний никуда не делся и может использоваться с нашим новым классом как будто он всё ещё старый. Просто потому, что вообще-то так и есть: производный класс полностью и без исключений включает в себя базовый. И если вдруг в некую void foo(const std::string&) передать наш locale_string, он обязан обеспечить этой foo() отработать с собой как с std::string. Внезапно выясняется, что это очень нелегко обеспечить, придётся чуть ли не на каждый чих городить прокси. Во-вторых, научить case insensitive и up/down cases – это здравая идея, но вот беда: тут сам locale_string решает, что оно нужно, и вот оно нужно всегда и точка. В итоге получается, что какие-нибудь find(), operator<() и иже с ними будут работать не так, как с std::string. Т.е. мы не только расширили функционал, но и местами изменили. А вот это уже никуда не годится. Если мы хотим расширить, то это не подразумевает замену существующего, так что исходные find(), operator<() итп нужно в locale_string оставить как есть, а для нового функционала завести новые методы. Ну и в итоге от наследования просто ничего не остаётся. Достаточно будет просто сделать новый namespace и нагрузить его дополнительным контентом, так что отдельного класса даже и вовсе не понадобится.
                    to be continued
                    Сообщение отредактировано: Qraizer -
                      continue is here
                      • Композиция представляет собой термин, выходящий за рамки ООП. В контексте же ООП он, насколько я помню, применяется нечасто, и причиной тому то, что методик композиции классов далеко не одна. В частности наследование тоже одна из них. Но не простое наследование, а в специфически спроектированной архитектуре. А именно: предварительная декомпозиция на подклассы с последующим их объединением. Причём подразумевается, что одна и та же декомпозированная сущность может быть реализована по-разному.
                      Пример: паттерн "стратегия" в одной из своих возможных вариантах архитектуры. Ну, кто читал Александреску, те понимают хорошо. При этом наследование тут в общем-то не требуется, хотя в зависимости от конкретики оно может быть полезно. В std, например, таковым паттерном является концепция аллокаторов, и наследования там не применяется. (Впрочем, строгого запрета на это нет, просто оно не нужно.)
                      В качестве примера реализации композиции с наследованием а-ля Александреску могу привести свою библиотечку сокетов. Там что-то вроде:
                      ExpandedWrap disabled
                        // служебные классы
                        template        <int = AF_INET> class SocketBase;
                        template        <int = AF_INET> class ConnectBase;
                        template <class, int = AF_INET> class SocketRead;
                        template        <int = AF_INET> class SocketWrite;
                        template        <int = AF_INET> class SocketServer;
                        template        <int = AF_INET> class SocketClient;
                        template        <int = AF_INET> class SocketUDP;
                        template        <int = AF_INET> class BroadcastWrite;
                        // классы для использования
                        template        <int = AF_INET> class BroadcastServer;  // слушатель броадкастов
                        template        <int = AF_INET> class BroadcastClient;  // рассыльщик броадкастов
                        template        <int = AF_INET> class SocketServerUDP;  // читатель датаграм
                        template        <int = AF_INET> class SocketClientUDP;  // писатель датаграм
                        template        <int = AF_INET> class SocketServerTCP;  // точка подключения для серверов
                        template        <int = AF_INET> class SocketClientTCP;  // клиент для подключения к серверу
                      Выглядит жутковато, но право, тому умнику из Беркли, который спроектировал архитектуру сокетов в их виде, я желаю отдельного котла, причём оплаченного им же самим ещё при жизни. Вот кусочки определений (кроме AF_INET, реализаций ещё нет и вряд ли будет):
                      ExpandedWrap disabled
                        /*
                           Служебные классы. Не имеют публичного интерфейса, в их задачи входит
                           предоставление конкретного функционала, запрашиваемого теми или иными
                           аспектами функциональности сокетов.
                        */
                         
                        /* Базовый класс сокета. RAII над сокетом.
                           Занимается только созданием, удалением и хранением состояния последней операции */
                        template <int TYPE>
                        class SocketBase                                                                                  {/*...*/};
                         
                        /* Базовый класс соединения.
                           Занимается только подключением к серверу. Броадкаст также возможен. */
                        template <>
                        class ConnectBase<AF_INET>: virtual public SocketBase<AF_INET>                                    {/*...*/};
                         
                        /* Класс стратегии чтения из сокета. Приём на конкретном сконнекченном сокете. */
                        template <int TYPE>
                        class SocketReadRaw: virtual public SocketBase<TYPE>                                              {/*...*/};
                         
                        /* Класс стратегии чтения из сокета. Приём по броадкасту. Сохраняет последнего клиента. */
                        template <int TYPE>
                        class BroadcastReadRaw: virtual public SocketBase<TYPE>                                           {/*...*/};
                         
                        /* Сокет для чтения.
                           Raw является стратегией чтения (непосредственный клиент/броадкаст). */
                        template <class Raw, int TYPE>
                        class SocketRead: virtual public SocketBase<TYPE>, public Raw                                     {/*...*/};
                         
                        /* Сокет для записи.
                           Занимается только записью в сокет. */
                        template <int TYPE>
                        class SocketWrite: virtual public SocketBase<TYPE>                                                {/*...*/};
                         
                        /* Групповой сокет для записи.
                           Предназначен для броадкастов. Броадкасты не маршрутизируются. */
                        template <>
                        class BroadcastWrite<AF_INET>: virtual public SocketBase<AF_INET>                                 {/*...*/};
                         
                        /* Сокет для серверов. Ориентирован на приём данных.
                           Собирается из классов сокета, коннекта и чтения с политикой коннекта. */
                        template <int TYPE>
                        class SocketServer: virtual public ConnectBase<                     TYPE>,
                                                    public SocketRead <SocketReadRaw<TYPE>, TYPE>                         {/*...*/};
                         
                        /* Сокет для клиентов. Ориентирован на передачу данных конкретному серверу.
                           Собирается из классов сокета и конкретной записи. */
                        template <>
                        class SocketClient<AF_INET>: public SocketWrite<AF_INET>                                          {/*...*/};
                         
                        /* Базовый класс для UDP сокетов. Обрабатывает только свойства сокета.
                           Аналогичный базовый для TCP сокетов не нужен, ибо будет просто пустой. */
                        template <int TYPE>
                        class SocketUDP: virtual public SocketBase<TYPE>                                                  {/*...*/};
                         
                        /*
                           Конкретные классы сокетов. Собираются из служебных классов по принципу
                           требуемой функциональности.
                        */
                         
                        /* Сокет UDP для серверов. Умеет только читать. */
                        template <int TYPE>
                        class SocketServerUDP: public SocketUDP<TYPE>, public SocketServer<TYPE>                          {/*...*/};
                         
                        /* Сокет UDP для клиентов. Умеет только писать. */
                        template <int TYPE>
                        class SocketClientUDP: public SocketUDP<TYPE>, public SocketClient<TYPE>                          {/*...*/};
                         
                        /* Групповой сокет для клиентов. Умеет только писать, но сразу всем. */
                        template <int TYPE>
                        class BroadcastClient: public SocketUDP<TYPE>, public BroadcastWrite<TYPE>                        {/*...*/};
                         
                        /* Групповой сокет для серверов. Умеет только читать, зато ото всех. */
                        template <int TYPE>
                        class BroadcastServer: virtual public ConnectBase<TYPE>,
                                                       public SocketUDP  <TYPE>,
                                                       public SocketRead <BroadcastReadRaw<TYPE>, TYPE>                   {/*...*/};
                         
                        /* Сокет коннекта для серверов. Умеет и читать и писать, ожидает от клиентов подключений. */
                        template <int TYPE>
                        class SocketServerTCP: public SocketServer<TYPE>, public SocketWrite<TYPE>{};
                         
                        /* Сокет коннекта для клиентов. Умеет и читать и писать, подключается к серверу как клиент. */
                        template <int TYPE>
                        class SocketClientTCP: public SocketClient<TYPE>, public SocketRead<SocketReadRaw<TYPE>, TYPE>    {/*...*/};
                         
                        /* Контейнер сокетов для групповых опросов с таймаутом.
                           Реализован через select() как единственный вариант реализации таймаутов. */
                        class FDS                                                                                         {/*...*/};
                      Каждый из служебных классов решает ровно свою задачу. Каждый из пользовательских собирается из служебных как кирпичиков, и по факту пусты. (Ну почти, там конструирование кое-где перегружено для удобства.) Виртуальное наследование присутствует из-за совмещения атрибутов. При этом сами классы неполиморфны, т.к. позднее связывание там не нужно.
                      Композиция в том виде, как она представляется в определении, ...смущает, скажем так. Слишком частное определение. Я могу себе представить композицию в виде атрибутов класса, но извне, т.е. на уровне интерфейса класса, это композицией не особо выглядит.
                      to be continued more
                      Сообщение отредактировано: Qraizer -
                        It is continue
                        • Агрегация мне как плюсовику за пределами C++ практически не встречалась. Обычно подразумевается, что на каких-нибудь условных Крестах или там Дельфях программер просто определяет некий класс полем в своём классе, что, учитывая ссылочность классов в этих языках, автоматически удовлетворяет приведённому определению. При этом, однако, уничтожение экземпляра класса всё равно попутно убъёт и агрегат, т.к. на него больше не будет ссылок. Если же так же поступить в Плюсах, то получится внезапно Композиция. Ибо Плюсы не используют смешанную семантику, и ИМХО это хорошо, т.к. меньше путаницы. (При желании можно любой тип сделать ссылочным, просто добавив & к нему. Строгость и одновременно гибкость Плюсов мне представляется более выгодным качеством, нежели медвежья услуга со стороны языка в лице смешанной семантики.) Тем не менее она обычно всё равно обычно называется агрегацией. Просто потому что, хотя и имеет другую семантику, синтаксически выглядит так же.
                        При желании реализовать настоящую агрегацию раз плюнуть. Типичный пример: архитектура потоков ввода-вывода, принимающих буферизирующий std::basic_streambuf<> в конструкторах std::basic_(i/o)stream<>. Типа создаём свой socket_buf на базе Sockets::SocketClientUDP<> производным от std::stream_buf<>, передаём в конструктор std::ostream и вуаля, пишем в сокет обычными operator<<. Вообще, в Плюсах агрегация часто соседствует с полиморфизмом, т.к. полагается на позднее связывание вкупе с фабриками объектов. Не знаю, мне практически никогда не требовалось. (Да, почти.)
                        Что же до агрегации в стиле Плюсов, то это более чем обычное дело. Композицией это не называют, потому как выше уже объяснена неоднозначность этого термина. Зато это прекрасная альтернатива наследованию, когда оно избыточно, см. пост о наследовании. Там мы лишаемся недостатка сильной связи, т.к. агрегат с агрегирующим классом связан слабо, только через свой интерфейс. Также от агрегирующего не требуется вести себя подобно агрегату, и при этом никто не мешает завести атрибут агрегата, чтобы передать куда надо. Есть только некая проблема с делегированием методов, но она категорически несложно решается :o препроцессором :D , а для особо грамотных и без оного, на шаблонах с несложным метакодом, при этом "бесплатным" бонусом получаем управляемость делегирования, а не тупо identity forward, т.к. при необходимости identity легко и точечно специализируется.
                        К слову, если кто из неплюсовиков или не очень опытных плюсовиков не понимают, нафика нам вся эта мета. Нет, не потому что можем, а потому, что мы, в отличие от них, понимаем его потенциал и видим, где он полезен. Внезапно – много где и чаще, чем им кажется.
                        Возможность отделить время жизни агрегата от времени жизни его контейнера, равно как и возможность подмены агрегатов в ран-тайм, мне кажется сомнительным преимуществом. Ну, второе ещё куда ни шло, но и это скорее всего приведёт к тому, что контейнер всё равно придётся перезапустить. Пусть и без пересоздания, но всяко с переинициализацией. Что же до первого, то позвольте вопрос: если ли смысл в существовании атрибута отдельно от его объекта? Ну вот создал я socket_buf. И что? Что я сним буду делать вне std::ostream? Не, ну архитектура потоков делает это возможным и даже относительно осмысленным, но часто ли std::stream_bufы используются сами по себе? Можно порыть статистику в гугле, и мне кажется, примерный результат я смогу предсказать. Агрегация в том своём определении ИМХО является архитектурным анахронизмом. С нею можно жить, как и с malloc() + init() вместо operator new. Но надо ли? Впрочем, совсем бесполезной она всё ж не является. Те же потоки и std::locale как пример, однако ж и там при std::basic_ios<>::imbue() следует некислая такая переинициализация, выливающаяся в приличный оверхед.
                        finalization will asap
                        Сообщение отредактировано: Qraizer -
                          the finalization
                          • Интерфейсы – это больная тема всегда. Г.о. потому что они упрощают планирование и при этом усложняют разработку. Нет, не надо понимать это превратно. Интерфейсы мастхэв, но требуют грамотного подхода. Очень грамотного. Студент всяко не осилит без кучи больных шишек. И джун не осилит. И не каждый сеньор, иначе зачем нужны были бы лиды. Ну кажется, вот счас всё распланируем, декомпозируем задачу на подзадачи, задокументируем интерфейсы взаимодействия между ними, раскидаем каждую подзадачу отдельному сотруднику и пойдём пить кофе и получать премию за грамотный менеджмент. Ага, счас.
                          Главное преимущество интерфейсов – абсолютное абстрагирование от реализации – одновременно и главный их недостаток: после публикации их категорически нельзя менять. Ну не то, чтобы совсем нельзя, но любая правка интерфейса влечёт уйму работы по регрешну и рефакторингу, т.е. правки интерфейсов очень дороги. Ситуация как с наследованием, сильная связь интерфейса с его реализациями налицо. (Не удивительно поэтому, что реализация интерфейса практически повсеместно является публичным производным классом от класса, этот интерфейс документирующего.) Не ну а как иначе-то, если они сильно связаны. Та и вообще, ровно для того, чтобы быть неизменными, интерфейсы и создаются, ибо ну а зачем они иначе нужны вообще. Так что это не недостаток, нет, я не так выразился.
                          Недостаток в том, что планировать надо в самом начале, и неудачно спланировать раз плюнуть, вот и приходится иногда перепланировать. А чтобы не было неудачно, нужно много шишек, причём через опыт, и желательно собственный, т.к. чужой усваивается плохо, а значит и книжки плохо помогают. Лично мне осознать помог лишь один случай, когда пришлось двухнедельную работу просто удалить и всё начать заново. Ох, я ходил курить раза три, панелька "Аре йоу суре ту делит зис перфект анд вондерфул директори?" в файловом менеджере висела минут сорок, всё не решался нажать ок.
                          Короче, это всё лирика. Хотя и не без пользы, за недостатки ж, как-никак. Самый прямой вариант документирования интерфейсов наличествует в практически любом языке. Interface ICoolProtocol { и вперёд вплоть до }; пишем всякую фигню, лишь бы скомпилилось, потом разберёмся в смысле свои лидовые договорённости с сеньорами. Ну а сеньоры такие "ок", раскидали эти Interface своим джуниорам и пошли за премией. Нуачё, они ж не лиды, не им за убытки на ковре в позе, им можно. тьфу-ты :angry: , только у нас Плюсы, у нас нет интерфейсов". И тут ты такой как лид "ок, тогда вы все прямиком в джуны с завтрашнего дня, если до сегодняшнего вечера не пересдадите собес у эйчара, придурки" последнее шёпотом, ибо корпоративная матьеё этика. А всё почему.
                          А потому, что:
                          1. берём и делаем class { public:, можно и просто сразу struct{, короче, с публичным наполнением; ибо зачем нам в документировании интерфейса что-то, чем всё равно не воспользоваться;
                          2. наполняем его методами с virtual в начале и =0 в конце;
                          3. никаких полей в нём не предусматриваем, как и конструкторов; а вот деструктор (хоть и не обязательно, но джуны есть джуны, пусть за ними следит компилятор, а не мы) можно, и тоже virtual, пусть и с пустым телом; всё, мы имеем интерфейс;
                          4. реализующие его производные классы должны его (внезапно) наследовать и наследовать виртуально (и публично? обычно да, и я не представляю, когда может быть выгодно не публично, но фикъего знает) и перекрывать все методы; это реализации интерфейса;
                          5. идём за премией.
                          Абстрактные классы не являются интерфейсами в прямом смысле, но они легко под них мимикрируют. Их экземпляры нельзя создавать сами по себе, даже в составе агрегатов, от них обязательно нужно унаследоваться и создавать уже производные экземпляры. Они документируют интерфейс взаимодействия и не содержат никаких атрибутов. Все вот такие вот =0 чистые методы обязательно перекрываются, а если не все, у нас всё равно получится абстрактный класс, чьи непосредственные экземпляры компилятор создать не позволит. Наследуясь от них виртуально, мы получаем форсирование совмещения экземпляров этого класса в производном вне зависимости от того, сколько раз прямо или опосредовано он встретится в списке базовых. Т.е. всё, что нужно, в них есть. Но в них есть и кое-что ещё, на что интерфейсы неспособны. Например, чистые методы можно реализовать, предоставив т.с. реализацию-по-умолчанию. (Это всё равно не позволит создавать экземпляры таких классов иначе чем в составе реализующих производных классах, ибо =0 никуда не делось.) Они могут иметь атрибуты сиречь поля и конструкторы. Они обязаны наследоваться, но не обязаны публично и/или виртуально. Ну т.е. они просто классы. Зачем оно всё надо? Вообще-то абстрактные классы ИМХО полезны чаще, чем интерфейсы. В конце концов они сами могут производными других классов, не обязательно абстрактных даже, что позволяет использовать их поверх отлаженного легаси. И да, можно реализовать только часть интерфейса, и дореализовать остальное в более верхнем производном. Зачем? А вот в Африке негров линчуют Плюсах зачем-то так можно, хотя, по правде говоря, частичная реализация интерфейса это как-то странно выглядит. И вот тогда непубличное наследование в этом промежуточном узле иерархии более чем к месту. Но по-любому виртуальное. И даже тем более виртуальное, т.к. совмещение тут выходит на передний план. Ну и невиртуальное наследование тоже ведь применяется. Не всегда ж атрибуты должны совмещаться, иногда они должны и разделяться, тут зависит от разрабатываемой архитектуры комплекса в целом. Правда, к интерфейсам это уже по-любому не относится, но относится к классам с множественными базами вообще, абстрактными в частности.
                          В заключение могу добавить, что работа с интерфейсами в Плюсах вполне на уровне. Легко можно проверить, поддерживает ли некий класс тот или иной интерфейс, просто сделав ему dynamic_cast<>. В тех же Дельфях, например, AS не даёт той же гибкости, так как не способен на перекрёстные приведения, так что там без API операционной системы, QueryInterface(), например, такого не сделать. Нужно лишь, чтобы класс был полиморфным, ну а куда он денется, коли методы виртуальные. Так, если у тебя коллекция QT-объектов с кучей всяко-разных кастомных пользовательских расширений, и тебе надо их сериализировать, то dynamic_cast<> к каждой кастомизации легко выявит те, которые требуют дополнительных усилий.
                          Так что я не знаю, когда может быть полезным частичная реализация интерфейса. Теоретически это должно быть плохой практикой, ибо видя перед собой ICoolProtocol в базах, я получу ложное впечатление, что он реализован. Но если вдруг такое нужно... Всё ж лучше разделить исходный интерфейс на части и тогда уж реализовывать их, но каждый полностью. Принцип неизменности интерфейса требует, что чтобы что-то в нём поменять, нужно заводить новый интерфейс. Если изменения касаются лишь добавления функционала, то пусть даже просто как производный от исходного. Типа ICoolProtocol2: virtual public ICoolProtocol. Если же изменения касаются не только добавления, но и подмены имеющегося, то имеет смысл переосмыслить старый функционал и перереализовать его в форме нового, и тогда в реализации-по-умолчанию сделать dynamic_cast<> к новому интерфейсу и заюзать его. В итоге реализации ICoolProtocol2 смогут просто не напрягаться депрекейтед ICoolProtocol. Такое невозможно в интерфейсах, но легко делается на абстрактных классах. Ну а что касается использования, то просто принимаем ICoolProtocol*, если нам достаточно ICoolProtocol, и тогда реализация и ICoolProtocol, и ICoolProtocol2 легко проходят derived to base convertion. А если недостаточно, то явно принимаем ICoolProtocol2*, и тогда вызовы с реализацией только ICoolProtocol не пройдут компиляцию.
                          Это всё касается динамического полиморфизма. Если есть желание поговорить за статический, то это выходит за рамки того определения. Ну не совсем, но выйдет, когда зайдёт разговор.
                          Fatality! бдыщь
                          Сообщение отредактировано: Qraizer -
                          0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                          0 пользователей:


                          Рейтинг@Mail.ru
                          [ Script execution time: 0,0443 ]   [ 16 queries used ]   [ Generated: 25.05.24, 05:15 GMT ]