Предположим, есть 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
необходимые для его работы. Это похоже на мой второй пример кода.
Таким образом, даже несмотря на то, что средство визуализации получает фрагмент страницы, внутри средства визуализации все еще выполняется сам фрагмент . То есть оба подхода находятся в игре. Я не уверен, что вы можете ограничить использование ОО только одним или только другим. Оба могут потребоваться, даже если вы маскируете одно за другим.
Ответы:
Это полностью зависит от того, что вы думаете, что ОО .
Для OOP = SOLID операция должна быть частью класса, если она является частью единой ответственности класса.
Для OO = виртуальная диспетчеризация / полиморфизм операция должна быть частью объекта, если она должна отправляться динамически, то есть если она вызывается через интерфейс.
Для OO = инкапсуляция операция должна быть частью класса, если она использует внутреннее состояние, которое вы не хотите показывать.
Для ОО = «Мне нравятся беглые интерфейсы», вопрос в том, какой вариант читается более естественно.
Для ОО = моделирование объектов реального мира, какой объект реального мира выполняет эту операцию?
Все эти точки зрения обычно ошибочны в изоляции. Но иногда одна или несколько из этих перспектив полезны при принятии решения о дизайне.
Например, используя точку зрения полиморфизма: если у вас разные стратегии рендеринга (например, разные форматы вывода или разные движки рендеринга), то это
$renderer->render($page)
имеет большой смысл. Но если у вас разные типы страниц, которые должны отображаться по-разному,$page->render()
может быть лучше. Если вывод зависит как от типа страницы, так и от стратегии рендеринга, вы можете выполнить двойную диспетчеризацию по шаблону посетителя.Не забывайте, что во многих языках функции не обязательно должны быть методами. Простая функция, как
render($page)
если бы это было очень хорошее (и удивительно простое) решение.источник
$renderer
собираюсь решить, как сделать. Когда$page
говорят$renderer
все, что говорит, это то, что нужно сделать. Не как. Понятия$page
не имеет как. Это приводит меня к проблемам с SRP?По словам Алана Кея , объекты являются самодостаточными, «взрослыми» и ответственными организмами. Взрослые делают вещи, они не оперированы. То есть финансовая транзакция отвечает за сохранение себя , страница отвечает за ее отображение и т. Д. И т. Д. Вкратце, инкапсуляция - это главное в ООП. В частности, это проявляется через знаменитый принцип Tell not ask (который @CandiedOrange любит постоянно упоминать :)) и публичную опровержение геттеров и сеттеров .
На практике это приводит к тому, что объекты обладают всеми необходимыми ресурсами для выполнения своей работы, такими как базы данных, средства визуализации и т. Д.
Итак, учитывая ваш пример, моя OOP-версия будет выглядеть следующим образом:
Если вам интересно, Дэвид Уэст рассказывает об оригинальных принципах ООП в своей книге « Объектное мышление» .
источник
Здесь мы
page
несем полную ответственность за визуализацию. Возможно, он был предоставлен с помощью рендера через конструктор, или он может иметь встроенную функциональность.Я проигнорирую первый случай (поставляется с рендером через конструктор), так как он очень похож на передачу его в качестве параметра. Вместо этого я посмотрю на плюсы и минусы встроенной функциональности.
Преимущество заключается в том, что он обеспечивает очень высокий уровень инкапсуляции. Страница не должна ничего раскрывать о своем внутреннем состоянии напрямую. Это только выставляет это через рендеринг себя.
Дело в том, что это нарушает принцип единой ответственности (SRP). У нас есть класс, который отвечает за инкапсуляцию состояния страницы, а также жестко запрограммирован с правилами о том, как визуализировать себя, и, таким образом, вероятно, целый ряд других обязанностей, поскольку объекты должны «делать что-то для себя, а не делать что-то для них другими». ».
Здесь нам все еще требуется, чтобы страница могла отображаться сама, но мы предоставляем ей вспомогательный объект, который может выполнять фактический рендеринг. Здесь могут возникнуть два сценария:
Здесь мы полностью соблюдали ПСП. Объект страницы отвечает за хранение информации на странице, а средство визуализации отвечает за отображение этой страницы. Однако теперь мы полностью ослабили инкапсуляцию объекта страницы, так как он должен сделать все свое состояние общедоступным.
Кроме того, мы создали новую проблему: рендерер теперь тесно связан с классом страницы. Что происходит, когда мы хотим отобразить что-то другое на странице?
Какой лучше? Ни один из них. У всех есть свои недостатки.
источник
Ответ на этот вопрос однозначен. Это
$renderer->renderPage($page);
правильная реализация. Чтобы понять, как мы пришли к такому выводу, нам нужно понять инкапсуляцию.Что такое страница? Это представление дисплея, которое кто-то будет потреблять. Этот "кто-то" мог быть человеком или ботом. Обратите внимание, что
Page
это представление, а не само отображение. Существует ли представление, не будучи представленным? Это страница без рендера Ответ - да, представление может существовать, не будучи представленным. Представлять это более поздняя стадия.Что такое рендерер без страницы? Может ли рендер отображаться без страницы? Нет. Таким образом, интерфейсу Renderer нужен
renderPage($page);
метод.Что не так с
$page->renderMe($renderer);
?Это факт, что
renderMe($renderer)
все равно придется звонить изнутри$renderer->renderPage($page);
. Это нарушает закон Деметры, который гласитPage
Класс не волнует , существует лиRenderer
во Вселенной. Это заботится только о том, чтобы быть представлением страницы. Таким образом, класс или интерфейсRenderer
никогда не должны быть упомянуты внутриPage
.ОБНОВЛЕННЫЙ ОТВЕТ
Если я правильно понял ваш вопрос,
PageSnippet
класс должен относиться только к тому, чтобы быть фрагментом страницы.PdfRenderer
занимается рендерингом.Использование клиента
Пара моментов для рассмотрения:
$data
как ассоциативный массив. Это должен быть экземпляр класса.html
свойства$data
массива, является подробностями, специфичными для вашего домена, иPageSnippet
знает об этих деталях.источник
printOn:aStream
метод, но все, что он делает, это указывает потоку напечатать объект. Аналогия с вашим ответом состоит в том, что нет причины, по которой вы не могли бы иметь страницу, которая может быть отображена для средства визуализации, и средство визуализации, которое может отображать страницу, с одной реализацией и выбором удобных интерфейсов.Page
не знать о $ renderer невозможно. Я добавил код в свой вопрос, см.PageSnippet
Класс. По сути, это страница, но она не может существовать без каких-либо ссылок на$pdf
каталог, который в данном случае фактически является сторонним рендером PDF. Однако, я полагаю, что я мог бы создать такойPageSnippet
класс, который будет содержать только массив текстовых инструкций для PDF, и иметь некоторый другой класс для интерпретации этих инструкций. Таким образом , я могу избежать инъекционного$pdf
вPageSnippet
, за счет дополнительной сложностиВ идеале вам нужно как можно меньше зависимостей между классами, поскольку это снижает сложность. Класс должен иметь зависимость от другого класса, только если он действительно в этом нуждается.
Ваше состояние
Page
содержит «набор инструкций для средства визуализации страниц». Я представляю что-то вроде этого:Так и было бы
$page->renderMe($renderer)
, так как странице требуется ссылка на средство визуализации.Но, альтернативно, команды рендеринга также могут быть выражены как структура данных, а не как прямые вызовы, например.
В этом случае фактический рендерер получит эту структуру данных со страницы и обработает ее, выполнив соответствующие инструкции рендеринга. При таком подходе зависимости будут перевернуты - странице не нужно знать о рендере, но рендереру должна быть предоставлена страница, которую он затем может отобразить. Итак, вариант второй:
$renderer->renderPage($page);
Так что лучше? Первый подход, вероятно, проще всего реализовать, в то время как второй гораздо более гибкий и мощный, поэтому, я думаю, это зависит от ваших требований.
Если вы не можете решить, или вы думаете, что можете изменить подход в будущем, вы можете скрыть решение за слоем косвенности, функцию:
Единственный подход, который я не буду рекомендовать, заключается в том, что
$page->renderMe()
он предполагает, что на странице может быть только один рендер. Но что, если у вас естьScreenRenderer
и добавитьPrintRenderer
? Одна и та же страница может быть предоставлена обоими.источник
page
явно является входом для средства визуализации, а не выходом, к которому это понятие явно не подходит.D часть SOLID говорит
«Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций».
Таким образом, между Page и Renderer, который, скорее всего, будет стабильной абстракцией, будет меняться меньше, возможно, представляя интерфейс? Наоборот, что такое «деталь»?
По моему опыту, абстракция - это обычно Renderer. Например, это может быть простой поток или XML, очень абстрактный и стабильный. Или какой-то довольно стандартный макет. Ваша страница, скорее всего, будет пользовательским бизнес-объектом, «деталью». И у вас есть другие бизнес-объекты для рендеринга, такие как «картинки», «отчеты», «диаграммы» и т. Д. (Вероятно, не «триптих», как в моем комментарии)
Но это, очевидно, зависит от вашего дизайна. Страница может быть абстрактной, например эквивалент HTML-
<article>
тега со стандартными частями. И у вас есть много различных пользовательских отчетов о «рендерерах». В этом случае рендер должен зависеть от страницы.источник
Я думаю, что большинство классов можно разделить на одну из двух категорий:
Это классы, которые практически не зависят ни от чего другого. Они обычно являются частью вашего домена. Они не должны содержать логику или только логику, которая может быть получена непосредственно из его состояния. Класс Employee может иметь функцию,
isAdult
которая может быть получена непосредственно из его,birthDate
но не функции,hasBirthDay
поскольку для этого требуется внешняя информация (текущая дата).Эти типы классов работают с другими классами, содержащими данные. Обычно они настраиваются один раз и являются неизменяемыми (поэтому они всегда выполняют одну и ту же функцию). Эти классы могут, однако, по-прежнему предоставлять кратковременный вспомогательный экземпляр с сохранением состояния для выполнения более сложных операций, требующих поддержания некоторого состояния в течение короткого периода (например, классов Builder).
Ваш пример
В вашем примере
Page
будет класс, содержащий данные. Он должен иметь функции для получения этих данных и, возможно, для их изменения, если предполагается, что класс изменчив. Держите его тупым, чтобы его можно было использовать без большого количества зависимостей.Данные, или в этом случае ваши
Page
могут быть представлены множеством способов. Его можно отобразить как веб-страницу, записать на диск, сохранить в базе данных, преобразовать в JSON, что угодно. Вы не хотите добавлять методы в такой класс для каждого из этих случаев (и создавать зависимости от всех других классов, даже если ваш класс должен содержать данные).Ваш
Renderer
типичный класс обслуживания. Он может работать с определенным набором данных и возвращать результат. У него не так много собственного состояния, и то, какое состояние оно имеет, обычно является неизменным, его можно настроить один раз, а затем использовать повторно.Например, вы можете иметь a
MobileRenderer
и aStandardRenderer
, обе реализацииRenderer
класса, но с разными настройками.Так , как
Page
содержит данные и должны быть немым, чистейшая решение в этом случае должен был бы пройтиPage
кRenderer
:источник