Angenommen, es gibt eine Page
Klasse, die eine Reihe von Anweisungen für einen Seitenrenderer darstellt. Angenommen, es gibt eine Renderer
Klasse, die weiß, wie eine Seite auf dem Bildschirm gerendert wird. Code kann auf zwei verschiedene Arten strukturiert werden:
/*
* 1) Page Uses Renderer internally,
* or receives it explicitly
*/
$page->renderMe();
$page->renderMe($renderer);
/*
* 2) Page is passed to Renderer
*/
$renderer->renderPage($page);
Welche Vor- und Nachteile hat jeder Ansatz? Wann wird man besser sein? Wann wird der andere besser sein?
HINTERGRUND
Um ein bisschen mehr Hintergrund hinzuzufügen - ich finde mich dabei, beide Ansätze im selben Code zu verwenden. Ich verwende eine PDF-Bibliothek eines Drittanbieters namens TCPDF
. Irgendwo in meinem Code muss ich Folgendes haben , damit das PDF-Rendering funktioniert:
$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);
Angenommen, ich möchte eine Darstellung der Seite erstellen. Ich könnte eine Vorlage erstellen, die Anweisungen zum Rendern eines PDF-Seitenausschnitts enthält:
/*
* 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) Beachten Sie hier, dass es $snippet
sich wie in meinem ersten Codebeispiel selbst ausführt. Es muss auch wissen und vertraut sein mit $pdf
und mit jedem, $data
damit es funktioniert.
Aber ich kann eine PdfRenderer
Klasse wie folgt erstellen :
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);
}
}
und dann dreht sich mein Code dazu:
$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));
2) Hier $renderer
erhält der PageSnippet
und alle dafür $data
notwendigen Arbeiten. Dies ähnelt meinem zweiten Codebeispiel.
Obwohl der Renderer das Seiten-Snippet empfängt, wird das Snippet im Renderer dennoch selbst ausgeführt . Das heißt, beide Ansätze spielen eine Rolle. Ich bin mir nicht sicher, ob Sie Ihre OO-Nutzung auf den einen oder den anderen beschränken können. Beides kann erforderlich sein, auch wenn Sie eines nach dem anderen maskieren.
Antworten:
Dies hängt ganz davon ab, was Sie für OO halten .
Bei OOP = SOLID sollte die Operation Teil der Klasse sein, wenn sie Teil der Einzelverantwortung der Klasse ist.
Bei OO = virtueller Versand / Polymorphismus sollte die Operation Teil des Objekts sein, wenn sie dynamisch versendet werden soll, dh wenn sie über eine Schnittstelle aufgerufen wird.
Für die OO = -Kapselung sollte die Operation Teil der Klasse sein, wenn sie einen internen Status verwendet, den Sie nicht verfügbar machen möchten.
Für OO = „Ich mag fließende Schnittstellen“ lautet die Frage, welche Variante natürlicher liest.
Für OO = Modellieren von Entitäten in der realen Welt, welche Entität in der realen Welt führt diese Operation aus?
Alle diese Standpunkte sind normalerweise für sich genommen falsch. Manchmal sind jedoch eine oder mehrere dieser Perspektiven hilfreich, um eine Entwurfsentscheidung zu treffen.
ZB unter dem Gesichtspunkt des Polymorphismus: Wenn Sie unterschiedliche Rendering-Strategien (wie unterschiedliche Ausgabeformate oder unterschiedliche Rendering-Engines) haben,
$renderer->render($page)
ist dies sehr sinnvoll. Wenn Sie jedoch unterschiedliche Seitentypen haben, die unterschiedlich gerendert werden sollen,$page->render()
möglicherweise besser. Wenn die Ausgabe sowohl vom Seitentyp als auch von der Rendering-Strategie abhängt, können Sie über das Besuchermuster einen doppelten Versand durchführen.Vergessen Sie nicht, dass Funktionen in vielen Sprachen keine Methoden sein müssen. Eine einfache Funktion, wie
render($page)
wenn auch oft eine vollkommen feine (und wunderbar einfache) Lösung.quelle
$renderer
würde entscheiden, wie gerendert werden soll. Bei den$page
Gesprächen mit dem$renderer
Ganzen heißt es, was zu rendern ist. Nicht wie. Der$page
hat keine Ahnung wie. Das bringt mich in SRP-Probleme?Laut Alan Kay sind Objekte autark, "erwachsen" und verantwortliche Organismen. Erwachsene machen Dinge, sie werden nicht operiert. Das heißt, die Finanztransaktion ist für das Speichern selbst verantwortlich , die Seite ist für das Rendern selbst usw. usw. verantwortlich. Genauer gesagt, die Kapselung ist die große Sache in OOP. Insbesondere manifestiert es sich durch das berühmte Tell-Don't-Ask-Prinzip (das @CandiedOrange immer wieder erwähnt :)) und die öffentliche Ablehnung von Gettern und Setzern .
In der Praxis führt dies dazu, dass Objekte über alle für ihre Arbeit erforderlichen Ressourcen verfügen, z. B. Datenbankfunktionen, Renderfunktionen usw.
In Anbetracht Ihres Beispiels würde meine OOP-Version folgendermaßen aussehen:
Falls Sie interessiert sind, spricht David West in seinem Buch Object Thinking über die ursprünglichen OOP-Prinzipien .
quelle
Hier haben wir
page
vollständig für das Rendern selbst verantwortlich. Sie wurde möglicherweise über einen Konstruktor mit einem Rendering ausgeliefert oder verfügt möglicherweise über diese integrierte Funktionalität.Ich werde den ersten Fall (der mit einem Render über einen Konstruktor geliefert wird) hier ignorieren, da er der Übergabe als Parameter ziemlich ähnlich ist. Stattdessen werde ich die Vor- und Nachteile der eingebauten Funktionalität betrachten.
Der Vorteil ist, dass es ein sehr hohes Maß an Einkapselung ermöglicht. Die Seite muss direkt nichts über ihren inneren Zustand preisgeben. Es macht es nur über ein Rendering von sich selbst verfügbar.
Der Nachteil ist, dass das Prinzip der einmaligen Verantwortung (Single Responsibility Principle, SRP) verletzt wird. Wir haben eine Klasse, die für die Verkapselung des Zustands einer Seite verantwortlich ist und die auch Regeln für die Darstellung selbst und damit wahrscheinlich eine ganze Reihe anderer Verantwortlichkeiten enthält, da Objekte "sich selbst etwas antun sollten, ohne dass andere Dinge damit anstellen ".
Hier benötigen wir noch eine Seite, um sich selbst rendern zu können, aber wir liefern ihr ein Hilfsobjekt, das das eigentliche Rendern übernehmen kann. Hierbei können zwei Szenarien auftreten:
Hier haben wir die SRP voll respektiert. Das Seitenobjekt ist für das Speichern von Informationen auf einer Seite verantwortlich, und der Renderer ist für das Rendern dieser Seite verantwortlich. Wir haben jedoch die Kapselung des Seitenobjekts jetzt vollständig abgeschwächt, da es seinen gesamten Zustand öffentlich machen muss.
Außerdem haben wir ein neues Problem erstellt: Der Renderer ist jetzt eng an die Seitenklasse gekoppelt. Was passiert, wenn wir etwas anderes als eine Seite rendern möchten?
Welches das Beste ist? Keines von denen. Sie haben alle ihre Fehler.
quelle
Die Antwort auf diese Frage ist eindeutig. Es ist
$renderer->renderPage($page);
das, was die richtige Implementierung ist. Um zu verstehen, wie wir zu dieser Schlussfolgerung gekommen sind, müssen wir die Verkapselung verstehen.Was ist eine Seite? Es ist eine Darstellung eines Displays, das jemand konsumieren wird. Dieser "Jemand" könnte ein Mensch oder ein Roboter sein. Beachten Sie, dass dies
Page
eine Darstellung ist und nicht die Anzeige selbst. Existiert eine Repräsentation ohne repräsentiert zu sein? Ist eine Seite etwas ohne Renderer? Die Antwort lautet Ja, eine Repräsentation kann existieren, ohne repräsentiert zu sein. Darstellen ist eine spätere Phase.Was ist ein Renderer ohne Seite? Kann ein Renderer ohne Seite rendern? Nein. Eine Renderer-Oberfläche benötigt diese
renderPage($page);
Methode.Was ist los mit
$page->renderMe($renderer);
?Es ist die Tatsache, dass
renderMe($renderer)
noch intern anrufen müssen$renderer->renderPage($page);
. Dies verstößt gegen das Gesetz von Demeter, das besagtDer
Page
Klasse ist es egal, ob esRenderer
im Universum eine gibt. Es geht nur darum, eine Seite darzustellen. Daher sollte die Klasse oder SchnittstelleRenderer
niemals in einem angegeben werdenPage
.AKTUALISIERTE ANTWORT
Wenn ich Ihre Frage richtig gestellt habe, sollte sich die
PageSnippet
Klasse nur darum kümmern, ein Seitenausschnitt zu sein.PdfRenderer
befasst sich mit Rendering.Client-Nutzung
Einige Punkte zu beachten:
$data
als assoziatives Array auszugeben. Es sollte eine Instanz einer Klasse sein.html
Eigenschaft des$data
Arrays enthalten ist, ist spezifisch für Ihre Domain undPageSnippet
kennt diese Details.quelle
printOn:aStream
Methode unterstützt , der Stream jedoch nur angewiesen wird, das Objekt zu drucken. Die Analogie zu Ihrer Antwort ist, dass es keinen Grund gibt, nicht sowohl eine Seite, die in einem Renderer gerendert werden kann, als auch einen Renderer, der eine Seite rendern kann, mit einer Implementierung und einer Auswahl praktischer Schnittstellen.Page
es unmöglich ist, $ renderer nicht zu kennen. Ich habe meiner Frage Code hinzugefügt, siehePageSnippet
Klasse.$pdf
Eigentlich ist es eine Seite, aber es kann nicht existieren, ohne auf die zu verweisen, die in diesem Fall ein PDF-Renderer von Drittanbietern ist. Ich nehme jedoch an, dass ich eine solchePageSnippet
Klasse erstellen könnte, die nur eine Reihe von Textanweisungen für das PDF enthält, und dass eine andere Klasse diese Anweisungen interpretiert. So kann ich vermeiden , kann die Injektion$pdf
inPageSnippet
, auf Kosten der zusätzlichen KomplexitätIdealerweise möchten Sie so wenig Abhängigkeiten wie möglich zwischen Klassen, da dies die Komplexität verringert. Eine Klasse sollte nur dann von einer anderen Klasse abhängig sein, wenn sie diese wirklich benötigt.
Sie geben an,
Page
"eine Reihe von Anweisungen für einen Seitenrenderer" zu enthalten. Ich stelle mir so etwas vor:So wäre es
$page->renderMe($renderer)
, da die Seite benötigt einen Verweis auf Renderer.Alternativ könnte das Rendern von Anweisungen auch als Datenstruktur anstatt als direkte Aufrufe ausgedrückt werden, z.
In diesem Fall würde der eigentliche Renderer diese Datenstruktur von der Seite abrufen und sie verarbeiten, indem er die entsprechenden Renderanweisungen ausführt. Bei einem solchen Ansatz würden die Abhängigkeiten umgekehrt - die Seite muss nichts über den Renderer wissen, aber dem Renderer sollte eine Seite bereitgestellt werden, die er dann rendern kann. Also Option zwei:
$renderer->renderPage($page);
Welches ist das Beste? Der erste Ansatz ist wahrscheinlich am einfachsten zu implementieren, während der zweite Ansatz viel flexibler und leistungsfähiger ist. Ich denke, das hängt von Ihren Anforderungen ab.
Wenn Sie sich nicht entscheiden können oder glauben, Sie könnten in Zukunft den Ansatz ändern, können Sie die Entscheidung hinter einer Indirektionsebene, einer Funktion, verbergen:
Der einzige Ansatz, den ich nicht empfehlen werde, ist der
$page->renderMe()
, dass eine Seite nur einen einzigen Renderer haben kann. Aber was ist, wenn Sie eine habenScreenRenderer
und eine hinzufügenPrintRenderer
? Dieselbe Seite wird möglicherweise von beiden gerendert.quelle
page
jedoch eindeutig eine Eingabe für den Renderer, keine Ausgabe, zu der dieser Begriff eindeutig nicht passt.Der D-Teil von SOLID sagt
"Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen."
Also, zwischen Seite und Renderer, was ist eher eine stabile Abstraktion, weniger wahrscheinlich zu ändern, möglicherweise eine Schnittstelle darstellt? Welches ist im Gegensatz dazu das "Detail"?
Nach meiner Erfahrung ist die Abstraktion normalerweise der Renderer. Zum Beispiel könnte es ein einfacher Stream oder XML sein, sehr abstrakt und stabil. Oder ein ziemlich normales Layout. Ihre Seite ist eher ein benutzerdefiniertes Geschäftsobjekt, ein "Detail". Und Sie haben andere Geschäftsobjekte zu rendern, wie "Bilder", "Berichte", "Diagramme" usw. (Wahrscheinlich kein "Tryptich" wie in meinem Kommentar)
Aber es hängt natürlich von Ihrem Design ab. Die Seite kann abstrakt sein, z. B. das Äquivalent eines HTML-
<article>
Tags mit Standardunterteilen. Und Sie haben viele verschiedene benutzerdefinierte "Renderer" für Geschäftsberichte. In diesem Fall sollte der Renderer von der Seite abhängen.quelle
Ich denke, die meisten Klassen können in zwei Kategorien unterteilt werden:
Dies sind Klassen, die fast keine Abhängigkeiten von irgendetwas anderem haben. Sie sind normalerweise Teil Ihrer Domain. Sie sollten keine oder nur eine Logik enthalten, die direkt aus ihrem Zustand abgeleitet werden kann. Eine Employee-Klasse kann eine Funktion haben
isAdult
, die direkt von ihrer abgeleitet werden kann,birthDate
aber keine FunktionhasBirthDay
, die externe Informationen erfordert (das aktuelle Datum).Diese Arten von Klassen können für andere Klassen verwendet werden, die Daten enthalten. Sie sind in der Regel einmal konfiguriert und unveränderlich (sodass sie immer die gleiche Art von Funktion ausführen). Diese Arten von Klassen können jedoch immer noch eine zustandsbehaftete kurzlebige Hilfsinstanz bereitstellen, um komplexere Vorgänge auszuführen, bei denen ein bestimmter Zustand für einen kurzen Zeitraum beibehalten werden muss (z. B. Builder-Klassen).
Dein Beispiel
In Ihrem Beispiel
Page
wäre dies eine Klasse, die Daten enthält. Es sollte Funktionen haben, um diese Daten abzurufen und möglicherweise zu ändern, wenn die Klasse veränderbar sein soll. Halten Sie es dumm, damit es ohne viele Abhängigkeiten verwendet werden kann.Daten, oder in diesem Fall Ihre,
Page
könnten auf vielfältige Weise dargestellt werden. Es könnte als Webseite gerendert, auf eine Festplatte geschrieben, in einer Datenbank gespeichert und in JSON konvertiert werden. Sie möchten einer solchen Klasse für jeden dieser Fälle keine Methoden hinzufügen (und Abhängigkeiten von allen Arten anderer Klassen erstellen, obwohl Ihre Klasse nur Daten enthalten soll).Ihr
Renderer
ist eine typische Service-Typ-Klasse. Es kann einen bestimmten Datensatz verarbeiten und ein Ergebnis zurückgeben. Es hat nicht viel eigenen Status und welcher Status normalerweise unveränderlich ist, kann einmal konfiguriert und dann wiederverwendet werden.Beispielsweise könnten Sie eine
MobileRenderer
und eine habenStandardRenderer
, beide Implementierungen derRenderer
Klasse, aber mit unterschiedlichen Einstellungen.Da also
Page
Daten enthalten und stumm gehalten werden sollten, wäre die sauberste Lösung in diesem Fall, dasPage
an a zu übergebenRenderer
:quelle