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

Почему вывод из конкретного класса - плохой дизайн

Я читал о шаблоне NonVirtual Interface: Herb Sutter говорит о том, почему виртуальная функция должна быть частной в большинстве случаев, защищенной в некотором случае и никогда публичной.

Но в конце статьи он пишет:

Не вытекает из конкретных классов. Или, как Скотт Мейерс помещает его в пункт 33 "Более эффективный С++", [8] "Сделать абстрактные листовые классы". (По общему признанию, это может произойти на практике - в коде, написанном кем-то другим, конечно, не вами! - и в этом случае вам может потребоваться публичный виртуальный деструктор, чтобы учесть то, что уже плохой дизайн. Лучше рефакторинг и исправьте дизайн, хотя, если сможете.)

Но я не понимаю, почему это плохой дизайн.

4b9b3361

Ответ 1

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

Ответ 2

Вы можете купить копию More Effective С++ или проверить свою локальную библиотеку для копирования и прочитать статью 33 для полного объяснения. Приведенное здесь объяснение состоит в том, что это делает ваш класс подверженным частичному назначению, также известному как нарезка:

Здесь есть две проблемы. Во-первых, оператор назначения, который вызывается в последней строке, относится к классу Animal, хотя задействованные объекты имеют тип Lizard. В результате будет изменена только часть Animal liz1. Это частичное назначение. После назначения члены liz1 Animal имеют значения, полученные из liz2, но члены liz1 Lizard остаются неизменными.

 Вторая проблема заключается в том, что реальные программисты пишут такой код. Нередко делать назначения объектам через указатели, особенно для опытных программистов на C, которые перешли на С++. В этом случае мы хотели бы, чтобы назначение выполнялось более разумным образом. Как указывает пункт 32, наши классы должны быть удобны в использовании правильно и трудно использовать неправильно, а классы в иерархии выше легко использовать неправильно.

См. этот вопрос и другие для описания проблемы среза объекта в С++.

Пункт 33 также говорит об этом, позже:

Замена конкретного базового класса, такого как Animal с абстрактным базовым классом типа AbstractAnimal, дает преимущества далеко за пределами простого упрощения понимания поведения operator=. Это также уменьшает вероятность того, что вы попытаетесь обработать массивы полиморфно, неприятные последствия которых будут рассмотрены в пункте 3. Однако наиболее существенное преимущество этой технологии происходит на уровне проектирования, поскольку замена конкретных базовых классов на абстрактную базу классы заставляют вас явно признавать существование полезных абстракций. То есть, это заставляет вас создавать новые абстрактные классы для полезных понятий, даже если вы не знаете о том, что существуют полезные понятия.

Ответ 3

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

Отказ от ответственности: я не опытный разработчик; это всего лишь предложение.

Ответ 4

В целом трудно строго поддерживать контракт между двумя конкретными конкретными объектами.

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

Представьте, что мы хотим создать класс с именем Ferrari и подкласс с именем SuperFerrari. Контракт: turnTheKey(), goFast(), skid()

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

Однако теперь мы хотим добавить функцию к SuperFerrari: turnOnBluRayDisc().

Проблема: Наследование ожидает отношения IS-A между компонентами. Благодаря этой новой функции SuperFerrari уже не является простым Ferrari с его собственным поведением; это теперь поведение ADDS. Это привело бы к некоторому уродливому коду с некоторым броском, чтобы выбрать новую функцию при работе с ссылкой Ferrari изначально (полиморфизм).

С абстрактным/интерфейсным классом, общим для обоих классов, эта проблема исчезнет, ​​так как Ferrari и SuperFerrari будут двумя листами, и это более логично, так как теперь Ferrari и SuperFerrari не следует рассматривать как аналогичные (а не отношения IS-A) больше.)

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

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

Ответ 5

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