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

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

Обычно существует пример, аналогичный приведенному ниже примеру.

class Shape
{
public:
    Shape() {}
    virtual ~Shape  () {}
    virtual void Draw() = 0;
};

class Cube : public Shape
{
public:
   Cube(){}
   ~Cube(){}
   virtual void Draw();
};

Shape* newCube = new Cube();
newCube->Draw(); 

Мой вопрос в том, почему ответственность за Shape лежит на себе? Разве не должно быть, чтобы класс рендеринга знал, как рисовать фигуру и вместо этого предоставлять форму рендереру? Что делать, если мы хотим записать изменения в измерениях? И т.д? Будет ли у нас метод для каждой из этих различных задач внутри Shape?

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

Ответ 1

A Shape не должен знать, как он нарисован. Чем больше проектируемый проект, тем более важным является это решение.

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

Основной принцип Контроллер представления модели заключается в том, что то, что вы делаете (глаголы или "взгляд" ), явно отделены от вещей (существительных или "контролеров" ), которые манипулируют или анализируются: Разделение представления и логики. "Модель" - средний человек.

Он также принцип единой ответственности: "... у каждого класса должна быть отдельная ответственность, и эта ответственность должна быть полностью инкапсулирована класс"

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

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

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


Это не просто проблема программирования.

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

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

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


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

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

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

Ответ 2

ООП поддерживает отправку сообщений, напротив процедурного кода, который "запрашивает" некоторые внешние данные и затем обрабатывает.

Если вы разместите метод draw в рендерере, вы разделите инкапсуляцию класса Shape, так как ему, безусловно, потребуется получить доступ к его внутренним элементам (например, координатам (x, y ) и т.д.).

Отпустив Shape "сам", вы сохраняете инкапсуляцию, способствующую гибкости в отношении внутренних изменений.

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

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

Ответ 3

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

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

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

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

Ответ 4

Выбор метода Draw() метода базового класса зависит от контекста - конкретной проблемы, решаемой. Чтобы сделать проблему немного более ясной, это еще один пример, который я регулярно использовал во время собеседований для навыков OO.

Представьте себе класс Document и класс Printer. Где должна выполняться функция печати? Есть два очевидных варианта:

document.print(Printer &p);

или

printer.print(Document &d);

Какой правильный? Ответ: это зависит от того, где вы хотите полиморфного поведения - в документе или на принтере. Если мы предположим, что все принтеры имеют идентичные функциональные возможности (миф операционных систем пытается продвигать), очевидно, что полиморфное поведение должно быть в объекте Document. Однако, если мы предположим, что все документы примерно одинаковы (или, по крайней мере, те, о которых мы заботимся), и что принтеры сильно отличаются друг от друга (как это было раньше), рассмотрите: плоттеры, линейные принтеры, лазерные принтеры, и т.д.), тогда имеет смысл разрешить принтеру решать, как лучше всего сделать документ.

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

Ответ 5

Только объект действительно знает, как рисовать себя.

Представьте себе слесаря ​​... ГЭС может выбрать 1000 разных типов блокировок. Я могу пойти в магазин, купить любой замок и дать ему его, и он может выбрать его, потому что он знаком с lock-tech.

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

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

Ответ 6

Вышеуказанные ответы кажутся мне слишком сложными.

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

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

Рассмотрим случай наличия круга, треугольника и прямоугольника. Теперь, как Shape собирается рисовать себя? Он не знает, что это такое или как таковое, что делать.

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

Почему Кружок рисует себя, а не форму? Потому что он знает, как.

Ответ 7

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

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

Пусть семейство множеств A обладает свойством {x: P (x)}
Пусть A - элемент семейства A Пусть A 'также является элементом семейства A

A и A 'могут попадать под одну из следующих трех категорий.
(1) A и A 'эквивалентны Для всех элементов A, a является элементом из A ' И для всех b элементов из ~ A, b - элемент из ~ A '

(2) A и A 'пересекаются с Существует элемент A, где a - элемент из A ' Существует также b элементов A, где b - элемент из ~ A '-

(3) A и A 'не пересекаются Не существует элемента a из A, который также является элементом A '
WHERE ~ X относится ко всем x, которые не являются элементами множества X.

В случае (1) мы будем определять не абстрактное поведение, если U - элемент семейства A, влечет существование единственного значения u, для которого u = P (U) для всех U, являющихся элементами семейства А

В случае (2) мы будем определять виртуальное поведение, если U, являющийся элементом семейства A, влечет существование единственного значения u такого, что u = P (U '), где U' - подмножество U.

И в случае (3) мы будем определять чисто виртуальное поведение, потому что A и A 'только схожи, поскольку они оба являются членами семейства A, так что пересечение A и A' является пустым множеством, подразумевая что не существует общих элементов A и A '

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

(1) Должна ли функция быть абстрактной? (нет для случая 1, да для случая 2 и 3) (2) Должна ли функция быть чистой виртуальной? (нет для случая 1 и 2, да для случая 3)

В случае 2 это также зависит от того, где хранится информация о поведении - в базовом классе или в производном классе.

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