Supposons qu'il existe une Page
classe, qui représente un ensemble d'instructions pour un rendu de page. Et supposons qu'il existe une Renderer
classe qui sache rendre une page à l'écran. Il est possible de structurer le code de deux manières différentes:
/*
* 1) Page Uses Renderer internally,
* or receives it explicitly
*/
$page->renderMe();
$page->renderMe($renderer);
/*
* 2) Page is passed to Renderer
*/
$renderer->renderPage($page);
Quels sont les avantages et les inconvénients de chaque approche? Quand sera-t-on meilleur? Quand l'autre sera-t-il meilleur?
CONTEXTE
Pour ajouter un peu plus de contexte - je me retrouve à utiliser les deux approches dans le même code. J'utilise une bibliothèque PDF tierce appelée TCPDF
. Quelque part dans mon code, je dois disposer des éléments suivants pour que le rendu PDF fonctionne:
$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);
Disons que je souhaite créer une représentation de la page. Je pourrais créer un modèle contenant des instructions pour rendre un extrait de page PDF comme suit:
/*
* 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) Notez ici que cela $snippet
s'exécute , comme dans mon premier exemple de code. Il doit également connaître et être familier avec $pdf
, et avec tout $data
pour que cela fonctionne.
Mais, je peux créer une PdfRenderer
classe comme ceci:
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);
}
}
puis mon code se transforme en ceci:
$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));
2) Ici, le $renderer
reçoit le PageSnippet
et tout ce qui est $data
nécessaire pour qu'il fonctionne. Ceci est similaire à mon deuxième exemple de code.
Ainsi, même si le rendu reçoit l'extrait de page, à l'intérieur du rendu, l'extrait s'exécute toujours lui-même . C'est-à-dire que les deux approches sont en jeu. Je ne sais pas si vous pouvez limiter votre utilisation OO à un seul ou à l'autre. Les deux peuvent être requis, même si vous vous masquez l'un par l'autre.
Réponses:
Cela dépend entièrement de ce que vous pensez être OO .
Pour OOP = SOLID, l'opération doit faire partie de la classe si elle fait partie de la responsabilité unique de la classe.
Pour OO = envoi virtuel / polymorphisme, l'opération doit faire partie de l'objet si elle doit être envoyée dynamiquement, c'est-à-dire si elle est appelée via une interface.
Pour OO = encapsulation, l'opération doit faire partie de la classe si elle utilise un état interne que vous ne souhaitez pas exposer.
Pour OO = «J'aime les interfaces fluides», la question est de savoir quelle variante se lit le plus naturellement.
Pour OO = modélisation d'entités du monde réel, quelle entité du monde réel effectue cette opération?
Tous ces points de vue sont généralement erronés de manière isolée. Mais parfois, une ou plusieurs de ces perspectives sont utiles pour prendre une décision de conception.
Par exemple, en utilisant le point de vue du polymorphisme: si vous avez différentes stratégies de rendu (comme différents formats de sortie ou différents moteurs de rendu),
$renderer->render($page)
cela a beaucoup de sens. Mais si vous avez différents types de page qui devraient être rendus différemment, cela$page->render()
pourrait être mieux. Si la sortie dépend à la fois du type de page et de la stratégie de rendu, vous pouvez effectuer une double répartition via le modèle de visiteur.N'oubliez pas que dans de nombreux langages, les fonctions ne doivent pas nécessairement être des méthodes. Une fonction simple comme
render($page)
si souvent une solution parfaitement fine (et merveilleusement simple).la source
$renderer
allait décider comment rendre. Lorsque les$page
pourparlers à$renderer
tout ce qu'il dit est ce qu'il faut rendre. Pas comment. Le$page
n'a aucune idée de comment. Cela me met dans le pétrin SRP?Selon Alan Kay , les objets sont des organismes autonomes, "adultes" et responsables. Les adultes font des choses, ils ne sont pas opérés. C'est-à-dire que la transaction financière est responsable de sa sauvegarde , la page est responsable du rendu , etc., etc. Plus concis, l'encapsulation est la grande chose dans la POO. En particulier, cela se manifeste à travers le célèbre principe Tell don't ask (que @CandiedOrange aime à mentionner tout le temps :)) et la réprobation publique des getters et setters .
En pratique, il en résulte que les objets possèdent toutes les ressources nécessaires pour faire leur travail, comme les installations de base de données, les installations de rendu, etc.
Donc, compte tenu de votre exemple, ma version OOP ressemblerait à ceci:
Si vous êtes intéressé, David West parle des principes originaux de la POO dans son livre, Object Thinking .
la source
Ici, nous sommes
page
entièrement responsables du rendu. Il peut avoir été fourni avec un rendu via un constructeur, ou il peut avoir cette fonctionnalité intégrée.J'ignorerai le premier cas (fourni avec un rendu via un constructeur) ici, car il est assez similaire à le passer en paramètre. Au lieu de cela, j'examinerai les avantages et les inconvénients de la fonctionnalité intégrée.
Le pro est qu'il permet un très haut niveau d'encapsulation. La page n'a pas besoin de révéler directement son état intérieur. Il l'expose uniquement via un rendu de lui-même.
L'inconvénient est qu'il rompt le principe de responsabilité unique (PRS). Nous avons une classe qui est responsable de l'encapsulation de l'état d'une page et est également codée en dur avec des règles sur la façon de se rendre et donc probablement toute une gamme d'autres responsabilités car les objets devraient "se faire des choses, ne pas se faire faire par d'autres ".
Ici, nous avons toujours besoin d'une page pour pouvoir s'afficher elle-même, mais nous lui fournissons un objet d'aide qui peut faire le rendu réel. Deux scénarios peuvent se présenter ici:
Ici, nous avons pleinement respecté le PÉR. L'objet page est responsable de la conservation des informations sur une page et le moteur de rendu est responsable du rendu de cette page. Cependant, nous avons maintenant complètement affaibli l'encapsulation de l'objet page car il doit rendre son état entier public.
De plus, nous avons créé un nouveau problème: le moteur de rendu est désormais étroitement couplé à la classe de page. Que se passe-t-il lorsque nous voulons rendre quelque chose de différent sur une page?
Quel est le meilleur? Aucun d'entre eux. Ils ont tous leurs défauts.
la source
La réponse à cette question est sans équivoque. C'est
$renderer->renderPage($page);
ce qui est la bonne mise en œuvre. Pour comprendre comment nous sommes arrivés à cette conclusion, nous devons comprendre l'encapsulation.Qu'est-ce qu'une page? Il s'agit d'une représentation d'un affichage que quelqu'un consommera. Ce "quelqu'un" pourrait être humain ou bot. Notez que le
Page
est une représentation et non l'affichage lui-même. Une représentation existe-t-elle sans être représentée? Une page est-elle quelque chose sans rendu? La réponse est oui, une représentation peut exister sans être représentée. Représenter est une étape ultérieure.Qu'est-ce qu'un moteur de rendu sans page? Un moteur de rendu peut-il effectuer un rendu sans page? Non. Une interface de rendu a donc besoin de la
renderPage($page);
méthode.Qu'est-ce qui ne va pas
$page->renderMe($renderer);
?C'est le fait qu'il
renderMe($renderer)
faudra encore appeler en interne$renderer->renderPage($page);
. Cela viole la loi de Demeter qui stipuleLa
Page
classe ne se soucie pas de savoir s'il existe unRenderer
dans l'univers. Il se soucie seulement d'être une représentation d'une page. Ainsi, la classe ou l'interfaceRenderer
ne doit jamais être mentionnée dans unPage
.RÉPONSE MISE À JOUR
Si j'ai bien répondu à votre question, la
PageSnippet
classe ne devrait se préoccuper que d'être un extrait de page.PdfRenderer
est concerné par le rendu.Utilisation client
Quelques points à considérer:
$data
comme un tableau associatif. Ce doit être une instance d'une classe.html
propriété du$data
tableau est un détail spécifique à votre domaine etPageSnippet
est conscient de ce détail.la source
printOn:aStream
méthode, mais tout ce qu'il fait est de dire au flux d'imprimer l'objet. L'analogie avec votre réponse est qu'il n'y a aucune raison pour laquelle vous ne pouvez pas avoir à la fois une page qui peut être rendue dans un moteur de rendu et un moteur de rendu qui peut rendre une page, avec une implémentation et un choix d'interfaces pratiques.Page
est impossible de ne pas être au courant de $ renderer. J'ai ajouté du code dans ma question, voir laPageSnippet
classe. Il s'agit effectivement d'une page, mais elle ne peut exister sans faire référence à la$pdf
, qui est en fait un rendu PDF tiers dans ce cas. .. Cependant, je suppose que je pourrais créer une tellePageSnippet
classe qui ne contient qu'un tableau d'instructions de texte au format PDF, et demander à une autre classe d'interpréter ces instructions. De cette façon, je peux éviter d'injecter$pdf
dansPageSnippet
, au détriment de la complexité supplémentaireIdéalement, vous voulez aussi peu de dépendances que possible entre les classes, car cela réduit la complexité. Une classe ne devrait avoir une dépendance à une autre classe que si elle en a vraiment besoin.
Vous déclarez
Page
contenir "un ensemble d'instructions pour un rendu de page". J'imagine quelque chose comme ça:Il en serait ainsi
$page->renderMe($renderer)
, puisque la page a besoin d' une référence au moteur de rendu.Mais le rendu des instructions pourrait également être exprimé sous la forme d'une structure de données plutôt que d'appels directs, par exemple.
Dans ce cas, le rendu réel obtiendrait cette structure de données de la page et la traiterait en exécutant les instructions de rendu correspondantes. Avec une telle approche, les dépendances seraient inversées - la Page n'a pas besoin de connaître le Renderer, mais le Renderer devrait se voir fournir une Page qu'elle peut ensuite restituer. Donc, option deux:
$renderer->renderPage($page);
Alors, quel est le meilleur? La première approche est probablement la plus simple à mettre en œuvre, tandis que la seconde est beaucoup plus flexible et puissante, donc je suppose que cela dépend de vos besoins.
Si vous ne pouvez pas décider, ou si vous pensez que vous pourriez changer d'approche à l'avenir, vous pouvez masquer la décision derrière une couche d'indirection, une fonction:
La seule approche que je ne recommanderai pas est
$page->renderMe()
qu'elle suggère qu'une page ne peut avoir qu'un seul moteur de rendu. Mais que faire si vous en avez unScreenRenderer
et en ajoutez unPrintRenderer
? La même page peut être affichée par les deux.la source
page
est clairement une entrée pour le moteur de rendu, pas une sortie, cette notion ne correspond clairement pas.La partie D de SOLID dit
"Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions."
Ainsi, entre Page et Renderer, qui est plus susceptible d'être une abstraction stable, moins susceptible de changer, représentant éventuellement une interface? Au contraire, quel est le "détail"?
D'après mon expérience, l'abstraction est généralement le moteur de rendu. Par exemple, il peut s'agir d'un simple Stream ou XML, très abstrait et stable. Ou une disposition assez standard. Votre page est plus susceptible d'être un objet métier personnalisé, un "détail". Et vous avez d'autres objets métier à rendre, tels que "images", "rapports", "graphiques" etc ... (Probablement pas un "tryptich" comme dans mon commentaire)
Mais cela dépend évidemment de votre conception. La page peut être abstraite, par exemple l'équivalent d'une
<article>
balise HTML avec des sous-parties standard. Et vous disposez de nombreux "rendus" personnalisés de création de rapports commerciaux. Dans ce cas, le rendu doit dépendre de la page.la source
Je pense que la plupart des classes peuvent être divisées en l'une des deux catégories:
Ce sont des classes qui n'ont presque aucune dépendance à quoi que ce soit d'autre. Ils font généralement partie de votre domaine. Ils ne doivent contenir aucune logique ou uniquement une logique pouvant être dérivée directement de son état. Une classe Employee peut avoir une fonction
isAdult
qui peut être dérivée directement de sabirthDate
mais pas une fonctionhasBirthDay
qui nécessite des informations externes (la date du jour).Ces types de classes fonctionnent sur d'autres classes contenant des données. Ils sont généralement configurés une seule fois et immuables (ils remplissent donc toujours le même type de fonction). Ces types de classes peuvent toutefois fournir une instance d'aide dynamique de courte durée pour effectuer des opérations plus complexes qui nécessitent de maintenir un certain état pendant une courte période (comme les classes Builder).
Votre exemple
Dans votre exemple,
Page
serait une classe contenant des données. Il devrait avoir des fonctions pour obtenir ces données et peut-être les modifier si la classe est supposée être mutable. Gardez-le muet, afin qu'il puisse être utilisé sans beaucoup de dépendances.Les données, ou dans ce cas, les vôtres
Page
peuvent être représentées de multiples façons. Il pourrait être rendu sous forme de page Web, écrit sur disque, stocké dans une base de données, converti en JSON, peu importe. Vous ne voulez pas ajouter de méthodes à une telle classe pour chacun de ces cas (et créer des dépendances sur toutes sortes d'autres classes, même si votre classe est censée simplement contenir des données).Votre
Renderer
est une classe de type de service typique. Il peut fonctionner sur un certain ensemble de données et retourner un résultat. Il n'a pas beaucoup d'état propre, et son état est généralement immuable, peut être configuré une fois, puis réutilisé.Par exemple, vous pouvez avoir un
MobileRenderer
et unStandardRenderer
, les deux implémentations de laRenderer
classe mais avec des paramètres différents.Donc, comme
Page
contient des données et doit rester muet, la solution la plus propre dans ce cas serait de passerPage
à aRenderer
:la source