В объектно-ориентированных языках, когда объекты должны выполнять операции над собой и когда должны выполняться операции над объектами?

11

Предположим, есть Pageкласс, который представляет собой набор инструкций для средства визуализации страниц. И предположим, есть Rendererкласс, который знает, как отобразить страницу на экране. Структурировать код можно двумя разными способами:

/*
 * 1) Page Uses Renderer internally,
 * or receives it explicitly
 */
$page->renderMe(); 
$page->renderMe($renderer); 

/*
 * 2) Page is passed to Renderer
 */
$renderer->renderPage($page);

Каковы плюсы и минусы каждого подхода? Когда будет лучше? Когда другой станет лучше?


ФОН

Чтобы добавить немного больше фона - я использую оба подхода в одном и том же коде. Я использую стороннюю библиотеку PDF под названием TCPDF. Где-то в моем коде я должен иметь следующее для рендеринга PDF для работы:

$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);

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

/*
 * A representation of the PDF page snippet:
 * a template directing how to render a specific PDF page snippet
 */
class PageSnippet
{    
    function runTemplate(TCPDF $pdf, array $data = null): void
    {
        $pdf->writeHTML($data['html']);
    }
}

/* To be used like so */
$pdf = new TCPDF();
$data['html'] = "some text";
$snippet = new PageSnippet();
$snippet->runTemplate($pdf, $data);

1) Обратите внимание, что здесь $snippet запускается сам , как в моем первом примере кода. Он также должен знать и быть знакомым $pdfс любым $dataдругим, чтобы он работал.

Но я могу создать PdfRendererкласс так:

class PdfRenderer
{
    /**@var TCPDF */
    protected $pdf;

    function __construct(TCPDF $pdf)
    {
        $this->pdf = $pdf;
    }

    function runTemplate(PageSnippet $template, array $data = null): void
    {
        $template->runTemplate($this->pdf, $data);
    }
}

и тогда мой код превращается в это:

$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));

2) Здесь $rendererполучает PageSnippetи все $dataнеобходимые для его работы. Это похоже на мой второй пример кода.

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

Деннис
источник
2
К сожалению, вы попали в мир программных «религиозных войн» здесь, по линии того, использовать ли пробелы или табуляции, какой стиль скобок использовать и т. Д. Здесь нет «лучшего», только сильные мнения с обеих сторон. Проведите в Интернете поиск преимуществ и недостатков как богатых, так и анемичных моделей предметной области и составьте собственное мнение.
Дэвид Арно
7
@DavidArno Используйте места, которые вы язычники! :)
candied_orange
1
Ха, я серьезно не понимаю этот сайт время от времени. Совершенно хорошие вопросы, которые получают хорошие ответы, быстро закрываются как основанные на мнении. И все же возникает очевидный, основанный на мнении вопрос, подобный этому, и этих обычных подозреваемых нигде не найти. О, хорошо, если ты не можешь победить их и все такое ... :)
Дэвид Арно
@ Эрик Эйдт, не могли бы вы отменить ответ, пожалуйста, потому что я считаю его очень хорошим ответом «четвертого варианта».
Дэвид Арно
1
Помимо принципов SOLID, вы можете взглянуть на GRASP , особенно на экспертную часть. Вопрос в том, какая информация у вас есть для выполнения ответственности?
OnesimusUnbound

Ответы:

13

Это полностью зависит от того, что вы думаете, что ОО .

Для OOP = SOLID операция должна быть частью класса, если она является частью единой ответственности класса.

Для OO = виртуальная диспетчеризация / полиморфизм операция должна быть частью объекта, если она должна отправляться динамически, то есть если она вызывается через интерфейс.

Для OO = инкапсуляция операция должна быть частью класса, если она использует внутреннее состояние, которое вы не хотите показывать.

Для ОО = «Мне нравятся беглые интерфейсы», вопрос в том, какой вариант читается более естественно.

Для ОО = моделирование объектов реального мира, какой объект реального мира выполняет эту операцию?


Все эти точки зрения обычно ошибочны в изоляции. Но иногда одна или несколько из этих перспектив полезны при принятии решения о дизайне.

Например, используя точку зрения полиморфизма: если у вас разные стратегии рендеринга (например, разные форматы вывода или разные движки рендеринга), то это $renderer->render($page)имеет большой смысл. Но если у вас разные типы страниц, которые должны отображаться по-разному, $page->render()может быть лучше. Если вывод зависит как от типа страницы, так и от стратегии рендеринга, вы можете выполнить двойную диспетчеризацию по шаблону посетителя.

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

Амон
источник
Э-э, подожди минутку. Я все еще могу получить полиморфный рендеринг, если страница содержит ссылку на средство визуализации, но не имеет представления, какое средство визуализации оно содержит. Это просто означает, что полиморфизм немного дальше по кроличьей норе. Я также могу выбрать, что передать рендереру. Мне не нужно пропускать всю страницу.
candied_orange
@CandiedOrange Это хорошая точка зрения, но я бы заполучил ваш аргумент в рамках SRP: ответственность за его отрисовку будет лежать на ответственности Capital-R, возможно, с использованием какой-то полиморфной стратегии рендеринга.
Амон
Я подумал, что $rendererсобираюсь решить, как сделать. Когда $pageговорят $rendererвсе, что говорит, это то, что нужно сделать. Не как. Понятия $pageне имеет как. Это приводит меня к проблемам с SRP?
candied_orange
Я действительно не думаю, что мы не согласны. Я пытался отсортировать ваш первый комментарий в концептуальной структуре этого ответа, но, возможно, я использовал неуклюжие слова. Одна вещь, о которой вы мне напоминаете, о которой я не упомянул в ответе: поток данных типа «скажи, не спрашивай» также является хорошей эвристикой.
Амон
Хмм хорошо. Ты прав. То, о чем я говорил, следовало бы сказать «не спрашивай». Теперь поправьте меня, если я ошибаюсь. Другая стратегия, когда средство визуализации берет ссылку на страницу, означает, что средство визуализации должно было бы развернуться и спросить страницу для чего-либо, используя средства получения страниц.
candied_orange
2

По словам Алана Кея , объекты являются самодостаточными, «взрослыми» и ответственными организмами. Взрослые делают вещи, они не оперированы. То есть финансовая транзакция отвечает за сохранение себя , страница отвечает за ее отображение и т. Д. И т. Д. Вкратце, инкапсуляция - это главное в ООП. В частности, это проявляется через знаменитый принцип Tell not ask (который @CandiedOrange любит постоянно упоминать :)) и публичную опровержение геттеров и сеттеров .

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

Итак, учитывая ваш пример, моя OOP-версия будет выглядеть следующим образом:

class Page
{
    private $data;
    private $renderer;

    public function __construct(ICanRender $renderer, $data)
    {
        $this->renderer = $renderer;
        $this->data = $data;
    }

    public function render()
    {
        $this->renderer->render($this->data);
    }
}

Если вам интересно, Дэвид Уэст рассказывает об оригинальных принципах ООП в своей книге « Объектное мышление» .

Западло
источник
1
Проще говоря, кого волнует, что кто-то сказал о том, что связано с разработкой программного обеспечения 15 лет назад, кроме как из-за исторического интереса?
Дэвид Арно
1
« Мне все равно, что человек, который изобрел объектно-ориентированное понятие, сказал о том, что такое объект ». Почему? Помимо того, что вы склоняете вас к использованию в своих рассуждениях ошибок «апелляции к власти», какое возможное отношение могут иметь мысли изобретателя термина к его применению через 15 лет?
Дэвид Арно
2
@Zapadlo: Вы не представляете аргумент, почему сообщение отправлено с Page на Renderer, а не наоборот. Они оба объект, и, следовательно, оба взрослые, верно?
JacquesB
1
« Призыв к заблуждению авторитета не может быть применен здесь » ... « Таким образом, набор понятий, которые, по вашему мнению, представляют ООП, на самом деле неверен [потому что это искажение первоначального определения] ». Я так понимаю, вы не знаете, что такое призыв к власти? Подсказка: вы использовали один здесь. :)
Дэвид Арно
1
@ Дэвид Арно Итак, все апелляции к власти неправильны? Вы бы предпочли «Обращение к моему мнению?» Каждый раз, когда кто-то цитирует дядюшку Бобизма, вы будете жаловаться на обращение к власти? Сападио предоставил уважаемый источник. Вы можете не соглашаться или приводить противоречивые источники, но повторение, что жалоба на то, что кто-то предоставил ссылку,
неконструктивно
2

$page->renderMe();

Здесь мы pageнесем полную ответственность за визуализацию. Возможно, он был предоставлен с помощью рендера через конструктор, или он может иметь встроенную функциональность.

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

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

Дело в том, что это нарушает принцип единой ответственности (SRP). У нас есть класс, который отвечает за инкапсуляцию состояния страницы, а также жестко запрограммирован с правилами о том, как визуализировать себя, и, таким образом, вероятно, целый ряд других обязанностей, поскольку объекты должны «делать что-то для себя, а не делать что-то для них другими». ».

$page->renderMe($renderer);

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

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

$renderer->renderPage($page);

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

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

Какой лучше? Ни один из них. У всех есть свои недостатки.

Дэвид Арно
источник
Не согласен с тем, что V3 уважает SRP. У Renderer есть по крайней мере две причины для изменения: если страница изменяется или изменяется способ ее отображения. И третий, который вы затрагиваете, если Renderer нужно визуализировать объекты, отличные от Pages. В противном случае, хороший анализ.
user949300
2

Ответ на этот вопрос однозначен. Это $renderer->renderPage($page);правильная реализация. Чтобы понять, как мы пришли к такому выводу, нам нужно понять инкапсуляцию.

Что такое страница? Это представление дисплея, которое кто-то будет потреблять. Этот "кто-то" мог быть человеком или ботом. Обратите внимание, что Pageэто представление, а не само отображение. Существует ли представление, не будучи представленным? Это страница без рендера Ответ - да, представление может существовать, не будучи представленным. Представлять это более поздняя стадия.

Что такое рендерер без страницы? Может ли рендер отображаться без страницы? Нет. Таким образом, интерфейсу Renderer нужен renderPage($page);метод.

Что не так с $page->renderMe($renderer);?

Это факт, что renderMe($renderer)все равно придется звонить изнутри $renderer->renderPage($page);. Это нарушает закон Деметры, который гласит

Каждый юнит должен иметь только ограниченные знания о других юнитах

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


ОБНОВЛЕННЫЙ ОТВЕТ

Если я правильно понял ваш вопрос, PageSnippetкласс должен относиться только к тому, чтобы быть фрагментом страницы.

class PageSnippet
{    
    /** string */
    private $html;

    function __construct($data = ['html' => '']): void
    {
        $this->html = $data['html'];
    }

   public function getHtml()
   {
       return $this->html;
   }
}

PdfRenderer занимается рендерингом.

class PdfRenderer
{
    /**@var TCPDF */
    protected $pdf;

    function __construct(TCPDF $pdf = new TCPDF())
    {
        $this->pdf = $pdf;
    }

    function runTemplate(string $html): void
    {
        $this->pdf->writeHTML($html);
    }
}

Использование клиента

$renderer = new PdfRenderer();
$snippet = new PageSnippet(['html' => '<html />']);
$renderer->runTemplate($snippet->getHtml());

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

  • Это плохая практика, чтобы передавать $dataкак ассоциативный массив. Это должен быть экземпляр класса.
  • Тот факт, что формат страницы содержится внутри htmlсвойства $dataмассива, является подробностями, специфичными для вашего домена, и PageSnippetзнает об этих деталях.
Джузер Али
источник
Но что, если, кроме страниц, у вас есть картинки, статьи и триптихи? В вашей схеме рендерер должен знать обо всех них. Это много утечки. Просто пища для размышлений.
user949300
@ user949300: Ну, если рендереру нужно уметь рисовать картинки и т. д., то, очевидно, ему нужно знать о них.
JacquesB
1
Шаблоны лучших практик Smalltalk от Кента Бека представляют модель метода реверсирования , в которой поддерживаются оба метода. В связанной статье показано, что объект поддерживает printOn:aStreamметод, но все, что он делает, это указывает потоку напечатать объект. Аналогия с вашим ответом состоит в том, что нет причины, по которой вы не могли бы иметь страницу, которая может быть отображена для средства визуализации, и средство визуализации, которое может отображать страницу, с одной реализацией и выбором удобных интерфейсов.
Грэм Ли
2
В любом случае вам придется ломать / выдумывать SRP, но если Renderer нужно знать, как отображать много-много разных вещей, это действительно «много-много ответственности», и, если возможно, этого следует избегать.
user949300
1
Мне нравится ваш ответ, но я склонен думать, что Pageне знать о $ renderer невозможно. Я добавил код в свой вопрос, см. PageSnippetКласс. По сути, это страница, но она не может существовать без каких-либо ссылок на $pdfкаталог, который в данном случае фактически является сторонним рендером PDF. Однако, я полагаю, что я мог бы создать такой PageSnippetкласс, который будет содержать только массив текстовых инструкций для PDF, и иметь некоторый другой класс для интерпретации этих инструкций. Таким образом , я могу избежать инъекционного $pdfв PageSnippet, за счет дополнительной сложности
Деннис
1

В идеале вам нужно как можно меньше зависимостей между классами, поскольку это снижает сложность. Класс должен иметь зависимость от другого класса, только если он действительно в этом нуждается.

Ваше состояние Pageсодержит «набор инструкций для средства визуализации страниц». Я представляю что-то вроде этого:

renderer.renderLine(x, y, w, h, Color.Black)
renderer.renderText(a, b, Font.Helvetica, Color.Black, "bla bla...")
etc...

Так и было бы $page->renderMe($renderer), так как странице требуется ссылка на средство визуализации.

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

[
  Line(x, y, w, h, Color.Black), 
  Text(a, b, Font.Helvetica, Color.Black, "bla bla...")
]

В этом случае фактический рендерер получит эту структуру данных со страницы и обработает ее, выполнив соответствующие инструкции рендеринга. При таком подходе зависимости будут перевернуты - странице не нужно знать о рендере, но рендереру должна быть предоставлена ​​страница, которую он затем может отобразить. Итак, вариант второй:$renderer->renderPage($page);

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

Если вы не можете решить, или вы думаете, что можете изменить подход в будущем, вы можете скрыть решение за слоем косвенности, функцию:

renderPage($page, $renderer)

Единственный подход, который я не буду рекомендовать, заключается в том, что $page->renderMe()он предполагает, что на странице может быть только один рендер. Но что, если у вас есть ScreenRendererи добавить PrintRenderer? Одна и та же страница может быть предоставлена ​​обоими.

JacquesB
источник
В контексте EPUB или HTML концепция страницы не существует без средства визуализации.
mouviciel
1
@mouviciel: Я не уверен, что понимаю, что вы имеете в виду. Конечно, вы можете иметь HTML-страницу без ее рендеринга? Например, сканер Google обрабатывает страницы без их рендеринга.
JacquesB
2
Существует другое понятие слова page: результат процесса разбивки на страницы, когда HTML-страница отформатирована для печати, возможно, именно это и имел в виду @mouviciel. Однако в этом вопросе a pageявно является входом для средства визуализации, а не выходом, к которому это понятие явно не подходит.
Док Браун
1

D часть SOLID говорит

«Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций».

Таким образом, между Page и Renderer, который, скорее всего, будет стабильной абстракцией, будет меняться меньше, возможно, представляя интерфейс? Наоборот, что такое «деталь»?

По моему опыту, абстракция - это обычно Renderer. Например, это может быть простой поток или XML, очень абстрактный и стабильный. Или какой-то довольно стандартный макет. Ваша страница, скорее всего, будет пользовательским бизнес-объектом, «деталью». И у вас есть другие бизнес-объекты для рендеринга, такие как «картинки», «отчеты», «диаграммы» и т. Д. (Вероятно, не «триптих», как в моем комментарии)

Но это, очевидно, зависит от вашего дизайна. Страница может быть абстрактной, например эквивалент HTML- <article>тега со стандартными частями. И у вас есть много различных пользовательских отчетов о «рендерерах». В этом случае рендер должен зависеть от страницы.

user949300
источник
0

Я думаю, что большинство классов можно разделить на одну из двух категорий:

  • Классы, содержащие данные (изменяемые или неизменяемые не имеют значения)

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

  • Классы, которые предоставляют услуги

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

Ваш пример

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

Данные, или в этом случае ваши Pageмогут быть представлены множеством способов. Его можно отобразить как веб-страницу, записать на диск, сохранить в базе данных, преобразовать в JSON, что угодно. Вы не хотите добавлять методы в такой класс для каждого из этих случаев (и создавать зависимости от всех других классов, даже если ваш класс должен содержать данные).

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

Например, вы можете иметь a MobileRendererи a StandardRenderer, обе реализации Rendererкласса, но с разными настройками.

Так , как Pageсодержит данные и должны быть немым, чистейшая решение в этом случае должен был бы пройти Pageк Renderer:

$renderer->renderPage($page)
john16384
источник
2
Очень процедурная логика.
user949300