Dans les langages orientés objet, quand les objets devraient-ils effectuer des opérations sur eux-mêmes et quand les opérations devraient-elles être effectuées sur les objets?

11

Supposons qu'il existe une Pageclasse, qui représente un ensemble d'instructions pour un rendu de page. Et supposons qu'il existe une Rendererclasse 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 $datapour que cela fonctionne.

Mais, je peux créer une PdfRendererclasse 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 $rendererreçoit le PageSnippetet tout ce qui est $datané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.

Dennis
la source
2
Malheureusement, vous vous êtes aventuré dans le monde des "guerres de religion" dans le domaine des logiciels, selon que vous devez utiliser des espaces ou des tabulations, quel style de renfort utiliser, etc. Il n'y a pas de "meilleur" ici, juste des opinions bien arrêtées des deux côtés. Faites une recherche sur Internet des avantages et des inconvénients des modèles de domaine riches et anémiques et faites-vous votre propre opinion.
David Arno
7
@DavidArno Utilisez des espaces païens! :)
candied_orange
1
Ha, je ne comprends vraiment pas ce site à certains moments. Les bonnes questions qui obtiennent de bonnes réponses sont fermées en un rien de temps car elles sont basées sur l'opinion. Pourtant, une question d'opinion évidente comme celle-ci se pose et ces suspects habituels sont introuvables. Eh bien, si vous ne pouvez pas les battre et tout ça ... :)
David Arno
@Erik Eidt, pourriez-vous annuler la suppression de votre réponse s'il vous plaît car je pense que c'est une très bonne réponse "quatrième option".
David Arno
1
Outre les principes SOLID, vous pouvez jeter un œil à GRASP , en particulier sur la partie Expert . La question est de savoir qui a les informations pour que vous puissiez assumer la responsabilité?
OnesimusUnbound

Réponses:

13

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).

amon
la source
Euh, attendez une minute. Je peux toujours obtenir un rendu polymorphe si la page contient une référence au rendu mais n'a aucune idée du rendu qu'elle contient. Cela signifie simplement que le polymorphisme est un peu plus bas dans le terrier du lapin. Je peux également choisir ce qui doit être transmis au moteur de rendu. Je n'ai pas besoin de passer toute la page.
candied_orange
@CandiedOrange C'est un bon point, mais je réserverais votre argument sous le SRP: ce serait la responsabilité capital-R de la page de décider comment il est rendu, peut-être en utilisant une sorte de stratégie de rendu polymorphe.
amon
Je me suis dit que ça $rendererallait décider comment rendre. Lorsque les $pagepourparlers à $renderertout ce qu'il dit est ce qu'il faut rendre. Pas comment. Le $pagen'a aucune idée de comment. Cela me met dans le pétrin SRP?
candied_orange
Je ne pense vraiment pas que nous soyons en désaccord. J'essayais de trier votre premier commentaire dans le cadre conceptuel de cette réponse, mais j'ai peut-être utilisé des mots maladroits. Vous me rappelez une chose que je n'ai pas mentionnée dans la réponse: le flux de données dites-ne-demandez pas est également une bonne heuristique.
amon
Hmm ok. Tu as raison. Ce dont j'ai parlé suivrait le principe de ne pas demander. Maintenant, corrigez-moi si je me trompe. L'autre stratégie, où le moteur de rendu prend une référence de page, signifie que le moteur de rendu devrait se retourner et demander des informations à la page, en utilisant les getters de pages.
candied_orange
2

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:

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);
    }
}

Si vous êtes intéressé, David West parle des principes originaux de la POO dans son livre, Object Thinking .

Zapadlo
la source
1
Pour parler franchement, qui se soucie de ce que quelqu'un a dit à propos de quelque chose à voir avec le développement de logiciels, il y a 15 ans, sauf en raison d'un intérêt historique?
David Arno
1
" Je me soucie de ce qu'un homme qui a inventé le concept orienté objet a dit à propos de ce qu'est un objet. " Pourquoi? Au-delà de vous endormir en utilisant des sophismes d '«appel à l'autorité» dans vos arguments, quelle incidence possible la pensée de l'inventeur d'un terme pourrait-elle avoir sur son application 15 ans plus tard?
David Arno
2
@Zapadlo: Vous ne présentez pas d'argument expliquant pourquoi le message provient de Page vers Renderer et non l'inverse. Ils sont tous les deux objets, et donc tous deux adultes, non?
JacquesB
1
"L' appel à l'erreur de l'autorité ne peut pas être appliqué ici " ... " Donc l'ensemble des concepts qui, à votre avis, représente la POO, est en fait faux [parce que c'est une distorsion de la définition originale] ". Je suppose que vous ne savez pas ce qu'est un appel à l'erreur de l'autorité? Indice: vous en avez utilisé un ici. :)
David Arno
1
@David Arno Alors, tous les appels à l'autorité sont-ils mauvais? Préférez-vous "Faire appel à mon avis?" Chaque fois que quelqu'un cite un oncle bobisme, vous plaindrez-vous d'un appel à l'autorité? Zapadio a fourni une source bien respectée. Vous pouvez être en désaccord ou citer des sources contradictoires, mais repeatefly se plaignant que quelqu'un a fourni une citation n'est pas constructif.
user949300
2

$page->renderMe();

Ici, nous sommes pageentiè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 ".

$page->renderMe($renderer);

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:

  1. La page doit simplement connaître les règles de rendu (quelles méthodes appeler dans quel ordre) pour créer ce rendu. L'encapsulation est préservée, mais le SRP est toujours rompu car la page doit encore superviser le processus de rendu, ou
  2. La page appelle juste une méthode sur l'objet de rendu, en passant ses détails. Nous nous rapprochons du respect du SRP, mais nous avons maintenant affaibli l'encapsulation.

$renderer->renderPage($page);

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.

David Arno
la source
Pas d'accord que V3 respecte SRP. Le moteur de rendu a au moins 2 raisons de changer: si la page change ou si la façon dont vous la rendez change. Et un troisième, que vous couvrez, si Renderer doit rendre des objets autres que Pages. Sinon, belle analyse.
user949300
2

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 Pageest 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 stipule

Chaque unité ne devrait avoir qu'une connaissance limitée des autres unités

La Pageclasse ne se soucie pas de savoir s'il existe un Rendererdans l'univers. Il se soucie seulement d'être une représentation d'une page. Ainsi, la classe ou l'interface Rendererne doit jamais être mentionnée dans un Page.


RÉPONSE MISE À JOUR

Si j'ai bien répondu à votre question, la PageSnippetclasse ne devrait se préoccuper que d'être un extrait de page.

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

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

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

PdfRenderer est concerné par le rendu.

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

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

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

Utilisation client

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

Quelques points à considérer:

  • C'est une mauvaise pratique de passer $datacomme un tableau associatif. Ce doit être une instance d'une classe.
  • Le fait que le format de page soit contenu dans la htmlpropriété du $datatableau est un détail spécifique à votre domaine et PageSnippetest conscient de ce détail.
Juzer Ali
la source
Mais que se passe-t-il si, en plus des pages, vous avez des images, des articles et des triptyques? Dans votre schéma, un Renderer devrait les connaître tous. C'est beaucoup de fuites. Juste matière à réflexion.
user949300
@ user949300: Eh bien, si le moteur de rendu doit pouvoir rendre des images, etc., il doit évidemment les connaître.
JacquesB
1
Smalltalk Best Practice Patterns de Kent Beck présente le modèle de méthode d'inversion , dans lequel les deux sont pris en charge. L'article lié montre qu'un objet prend en charge une printOn:aStreammé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.
Graham Lee
2
Vous allez devoir casser / truquer SRP dans tous les cas, mais si Renderer a besoin de savoir comment rendre beaucoup de choses différentes, c'est vraiment "beaucoup de responsabilités" et, si possible, à éviter.
user949300
1
J'aime votre réponse mais je suis tenté de penser qu'il Pageest impossible de ne pas être au courant de $ renderer. J'ai ajouté du code dans ma question, voir la PageSnippetclasse. 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 telle PageSnippetclasse 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 $pdfdans PageSnippet, au détriment de la complexité supplémentaire
Dennis
1

Idé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 Pagecontenir "un ensemble d'instructions pour un rendu de page". J'imagine quelque chose comme ça:

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

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.

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

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:

renderPage($page, $renderer)

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 un ScreenRendereret en ajoutez un PrintRenderer? La même page peut être affichée par les deux.

JacquesB
la source
Dans le contexte d'EPUB ou HTML, le concept de page n'existe pas sans rendu.
mouviciel
1
@mouviciel: Je ne suis pas sûr de comprendre ce que vous voulez dire. Vous pouvez sûrement avoir une page HTML sans la rendre? Par exemple, le robot d'exploration Google traite les pages sans les afficher.
JacquesB
2
Il y a une notion différente du mot page: le résultat d'un processus de pagination lorsqu'une page HTML formatée pour être imprimée, c'est peut-être ce que @mouviciel avait en tête. Cependant, dans cette question, a pageest clairement une entrée pour le moteur de rendu, pas une sortie, cette notion ne correspond clairement pas.
Doc Brown
1

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.

user949300
la source
0

Je pense que la plupart des classes peuvent être divisées en l'une des deux catégories:

  • Classes contenant des données (mutable ou immuable n'a pas d'importance)

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 isAdultqui peut être dérivée directement de sa birthDatemais pas une fonction hasBirthDayqui nécessite des informations externes (la date du jour).

  • Classes qui fournissent des services

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, Pageserait 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 Pagepeuvent ê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 Rendererest 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 MobileRendereret un StandardRenderer, les deux implémentations de la Rendererclasse mais avec des paramètres différents.

Donc, comme Pagecontient des données et doit rester muet, la solution la plus propre dans ce cas serait de passer Pageà a Renderer:

$renderer->renderPage($page)
john16384
la source
2
Logique très procédurale.
user949300