Nei linguaggi orientati agli oggetti, quando gli oggetti devono eseguire operazioni su se stessi e quando devono essere eseguite operazioni sugli oggetti?


11

Supponiamo che ci sia una Pageclasse, che rappresenta un insieme di istruzioni per un renderer di pagine. E supponiamo che esista una Rendererclasse che sappia renderizzare una pagina sullo schermo. È possibile strutturare il codice in due modi diversi:

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

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

Quali sono i pro e i contro di ogni approccio? Quando uno sarà migliore? Quando sarà meglio l'altro?


SFONDO

Per aggiungere un po 'più di background, mi trovo a utilizzare entrambi gli approcci nello stesso codice. Sto usando una libreria PDF di terze parti chiamata TCPDF. Da qualche parte nel mio codice devo avere il seguente per far funzionare il rendering PDF:

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

Di 'che desidero creare una rappresentazione della pagina. Potrei creare un modello che contiene le istruzioni per il rendering di uno snippet di pagine PDF in questo modo:

/*
 * 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) Notare qui che si $snippet esegue da solo , come nel mio primo esempio di codice. Deve anche conoscere ed avere familiarità con $pdf, e con qualsiasi $dataper farlo funzionare.

Ma posso creare una PdfRendererclasse in questo modo:

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

e quindi il mio codice si trasforma in questo:

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

2) Qui $rendererriceve il PageSnippete qualsiasi $datarichiesto per farlo funzionare. Questo è simile al mio secondo esempio di codice.

Pertanto, anche se il renderer riceve lo snippet di pagina, all'interno del renderer lo snippet continua a funzionare da solo . Vale a dire che entrambi gli approcci sono in gioco. Non sono sicuro di poter limitare l'utilizzo di OO solo all'una o all'altra. Entrambi potrebbero essere richiesti, anche se si mascherano l'uno dall'altro.


2
Sfortunatamente, hai vagato nel mondo del software "guerre di religione" qui, sulla falsariga di usare spazi o schede, quale stile di rinforzo usare, ecc. Non c'è "migliore" qui, solo opinioni forti su entrambi i lati. Effettua una ricerca su Internet dei vantaggi e degli svantaggi dei modelli di dominio ricchi e anemici e forma la tua opinione.
David Arno,

7
@DavidArno Usa gli spazi che sei pagano! :)
candied_orange,

1
Ah, sul serio non capisco questo sito a volte. Le domande perfettamente valide che ottengono buone risposte vengono chiuse in pochissimo tempo come basate sull'opinione pubblica. Eppure arriva una domanda evidentemente basata sull'opinione come questa e quei soliti sospetti non si trovano da nessuna parte. Vabbè, se non puoi batterli e tutto il resto ... :)
David Arno,

@Erik Eidt, potresti annullare l'eliminazione della tua risposta per favore, dato che la trovo un'ottima risposta "quarta opzione".
David Arno,

1
Oltre ai principi SOLID, puoi dare un'occhiata a GRASP , in particolare sulla parte Esperto . La domanda è: chi ha le informazioni per farti adempiere alla responsabilità?
Onesimus Nessun impegno

Risposte:


13

Questo dipende interamente da cosa pensi che sia OO .

Per OOP = SOLID, l'operazione dovrebbe far parte della classe se fa parte della responsabilità singola della classe.

Per OO = invio virtuale / polimorfismo, l'operazione dovrebbe far parte dell'oggetto se deve essere inviato in modo dinamico, cioè se viene chiamato attraverso un'interfaccia.

Per OO = incapsulamento, l'operazione dovrebbe far parte della classe se utilizza uno stato interno che non si desidera esporre.

Per OO = "Mi piacciono le interfacce fluenti", la domanda è quale variante legge in modo più naturale.

Per OO = modellazione di entità del mondo reale, quale entità del mondo reale esegue questa operazione?


Tutti questi punti di vista sono generalmente errati in isolamento. Ma a volte una o più di queste prospettive sono utili per arrivare a una decisione di progettazione.

Ad esempio, usando il punto di vista del polimorfismo: se si hanno strategie di rendering diverse (come formati di output diversi o motori di rendering diversi), allora $renderer->render($page)ha molto senso. Ma se hai diversi tipi di pagina che dovrebbero essere resi in modo diverso, $page->render()potrebbe essere migliore. Se l'output dipende sia dal tipo di pagina sia dalla strategia di rendering, è possibile effettuare una doppia spedizione attraverso il modello visitatore.

Non dimenticare che in molte lingue, le funzioni non devono essere metodi. Una funzione semplice come render($page)se spesso una soluzione perfettamente perfetta (e meravigliosamente semplice).


Ehm aspetta un minuto. Posso ancora ottenere il rendering polimorfico se la pagina contiene un riferimento al renderer ma non ha idea del renderer che contiene. Significa solo che il polimorfismo è un po 'più in basso nella tana del coniglio. Posso anche scegliere cosa passare al renderer. Non devo passare l'intera pagina.
candied_orange,

@CandiedOrange Questo è un buon punto, ma vorrei prenotare la tua argomentazione nell'ambito dell'SRP: sarebbe la responsabilità capitale-R della Pagina a decidere come viene eseguito il rendering, magari usando un qualche tipo di strategia di rendering polimorfico.
am

Ho pensato che $rendereravrebbe deciso come renderizzare. Quando $pageparla a $renderertutto ciò che dice è cosa rendere. Non come. Non $pageha idea di come. Questo mi mette nei guai in SRP?
candied_orange,

Non credo proprio che non siamo d'accordo. Stavo cercando di ordinare il tuo primo commento nel quadro concettuale di questa risposta, ma potrei aver usato parole goffe. Una cosa che mi stai ricordando che non ho menzionato nella risposta: dire-non-chiedere il flusso di dati è anche una buona euristica.
amon,

Hmm va bene. Hai ragione. Quello di cui ho parlato sarebbe seguito da "non chiedere". Ora correggimi se sbaglio. L'altra strategia, in cui il renderer prende un riferimento alla pagina, significa che il renderer dovrebbe girarsi e chiedere cose alla pagina, usando i getter delle pagine.
candied_orange,

2

Secondo Alan Kay , gli oggetti sono organismi autosufficienti, "adulti" e responsabili. Gli adulti fanno cose, non vengono operati. Cioè, la transazione finanziaria è responsabile del salvataggio stesso , la pagina è responsabile del rendering stesso , ecc. Ecc. Più concisamente, l'incapsulamento è la cosa più importante in OOP. In particolare, si manifesta attraverso il famoso principio Tell not ask (che a @CandiedOrange piace sempre menzionare :)) e la riprovazione pubblica di getter e setter .

In pratica si traduce in oggetti che possiedono tutte le risorse necessarie per svolgere il proprio lavoro, come strutture di database, strutture di rendering, ecc.

Quindi, considerando il tuo esempio, la mia versione OOP sarebbe simile alla seguente:

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

Nel caso in cui tu sia interessato, David West parla dei principi OOP originali nel suo libro, Object Thinking .


1
Per dirla senza mezzi termini, a chi importa cosa qualcuno ha detto di qualcosa a che fare con lo sviluppo del software, 15 anni fa, se non per interesse storico?
David Arno,

1
" Mi interessa quello che un uomo che ha inventato il concetto orientato agli oggetti ha detto su quale oggetto sia. " Perché? Oltre a cullarti nell'usare fallacie "appello all'autorità" nelle tue argomentazioni, quale possibile impatto potrebbero avere i pensieri dell'inventore di un termine sull'applicazione del termine 15 anni dopo?
David Arno,

2
@Zapadlo: non si presenta un argomento per cui il messaggio va da Page a Renderer e non viceversa. Sono entrambi oggetti, e quindi entrambi adulti, giusto?
Jacques B

1
" Non è possibile applicare qui il ricorso all'errore di autorità " ... " Quindi l'insieme di concetti che secondo te rappresenta OOP, in realtà è sbagliato [perché è una distorsione della definizione originale] ". Presumo che tu non sappia quale appello all'autorità fallace sia? Indizio: ne hai usato uno qui. :)
David Arno,

1
@David Arno Quindi, tutti gli appelli all'autorità sono sbagliati? Preferiresti "Appello alla mia opinione?" Ogni volta che qualcuno cita un bobismo di zio, ti lamenti di un appello all'autorità? Zapadio ha fornito una fonte ben rispettata. Puoi essere in disaccordo o citare fonti contrastanti, ma lamentarti ripetutamente che qualcuno abbia fornito una citazione non è costruttivo.
user949300

2

$page->renderMe();

Qui siamo pagecompletamente responsabili del rendering stesso. Potrebbe essere stato fornito con un rendering tramite un costruttore o potrebbe avere quella funzionalità integrata.

Ignorerò il primo caso (fornito con un rendering tramite un costruttore) qui, poiché è abbastanza simile al passaggio come parametro. Invece, esaminerò i pro e i contro della funzionalità integrata.

Il pro è che consente un livello molto elevato di incapsulamento. La pagina non deve rivelare direttamente nulla sul suo stato interiore. Lo espone solo attraverso un rendering di se stesso.

La contro è che rompe il principio della responsabilità singola (SRP). Abbiamo una classe che è responsabile dell'incapsulamento dello stato di una pagina ed è anche codificata con regole su come renderizzarsi e quindi probabilmente tutta una serie di altre responsabilità in quanto gli oggetti dovrebbero "fare le cose da soli, non farle fare da altri ".

$page->renderMe($renderer);

Qui, stiamo ancora richiedendo che una pagina sia in grado di renderizzarsi, ma la stiamo fornendo con un oggetto helper in grado di eseguire il rendering effettivo. Qui possono presentarsi due scenari:

  1. La pagina deve semplicemente conoscere le regole di rendering (quali metodi chiamare in quale ordine) per creare quel rendering. L'incapsulamento viene conservato, ma l'SRP è ancora interrotto poiché la pagina deve ancora supervisionare il processo di rendering, oppure
  2. La pagina chiama solo un metodo sull'oggetto renderer, trasmettendone i dettagli. Ci stiamo avvicinando al rispetto dell'SRP, ma ora abbiamo indebolito l'incapsulamento.

$renderer->renderPage($page);

Qui, abbiamo pienamente rispettato l'SRP. L'oggetto pagina è responsabile del mantenimento delle informazioni su una pagina e il renderer è responsabile del rendering di quella pagina. Tuttavia, ora abbiamo completamente indebolito l'incapsulamento dell'oggetto pagina in quanto deve renderlo pubblico.

Inoltre, abbiamo creato un nuovo problema: il renderer è ora strettamente associato alla classe di pagine. Cosa succede quando vogliamo rendere qualcosa di diverso da una pagina?

Qual è il migliore? Nessuno di loro. Tutti hanno i loro difetti.


Non sono d'accordo sul fatto che V3 rispetti SRP. Il renderer ha almeno 2 motivi per cambiare: se la Pagina cambia o se cambia il modo in cui la rendi. E un terzo, di cui ti occupi, se Renderer deve renderizzare oggetti diversi da Pages. Altrimenti, bella analisi.
user949300

2

La risposta a questa domanda è inequivocabile. È $renderer->renderPage($page);qual è l'implementazione corretta. Per capire come siamo arrivati ​​a questa conclusione, dobbiamo capire l'incapsulamento.

Cos'è una pagina? È una rappresentazione di un display che qualcuno consumerà. Quel "qualcuno" potrebbe essere umano o robot. Si noti che Pageè una rappresentazione e non il display stesso. Esiste una rappresentazione senza essere rappresentata? Una pagina è qualcosa senza renderer? La risposta è Sì, una rappresentazione può esistere senza essere rappresentata. Rappresentare è una fase successiva.

Che cos'è un renderer senza una pagina? Un renderer può essere visualizzato senza una pagina? No. Quindi un'interfaccia Renderer necessita del renderPage($page);metodo.

Cosa c'è che non va $page->renderMe($renderer);?

È il fatto che renderMe($renderer)dovrà ancora chiamare internamente $renderer->renderPage($page);. Questo viola la Legge di Demetra che afferma

Ogni unità dovrebbe avere una conoscenza limitata delle altre unità

Alla Pageclasse non importa se esiste un Rendereruniverso. Si preoccupa solo di essere una rappresentazione di una pagina. Quindi la classe o l'interfaccia Renderernon dovrebbero mai essere menzionate in a Page.


RISPOSTA AGGIORNATA

Se la tua domanda è corretta, la PageSnippetclasse dovrebbe occuparsi solo di essere un frammento di pagina.

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

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

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

PdfRenderer si occupa del rendering.

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

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

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

Utilizzo del cliente

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

Un paio di punti da considerare:

  • È una cattiva pratica passare $datacome array associativo. Dovrebbe essere un'istanza di una classe.
  • Il fatto che il formato della pagina sia contenuto all'interno della htmlproprietà $datadell'array è un dettaglio specifico per il tuo dominio ed PageSnippetè a conoscenza di questi dettagli.

Ma cosa succede se, oltre alle pagine, hai immagini, articoli e trittici? Nel tuo schema, un Renderer dovrebbe conoscerli tutti. C'è molta perdita. Solo spunti di riflessione.
user949300,

@ user949300: Beh, se il Renderer deve essere in grado di renderizzare immagini ecc., allora ovviamente deve conoscerle.
Jacques B

1
Gli schemi Smalltalk Best Practice di Kent Beck introducono il modello del metodo di inversione , in cui sono supportati entrambi. L'articolo collegato mostra che un oggetto supporta un printOn:aStreammetodo, ma tutto ciò che fa è dire allo stream di stampare l'oggetto. L'analogia con la tua risposta è che non c'è motivo per cui non si possa avere sia una pagina che può essere renderizzata a un renderer sia un renderer che può renderizzare una pagina, con una implementazione e una scelta di interfacce convenienti.
Graham Lee,

2
Dovrai in ogni caso rompere / sfumare SRP, ma se Renderer ha bisogno di sapere come rendere molte cose diverse, questa è davvero "molte molte responsabilità" e, se possibile, da evitare.
user949300

1
Mi piace la tua risposta ma sono tentato di pensare che Pagenon essere a conoscenza di $ renderer sia impossibile. Ho aggiunto del codice alla mia domanda, vedi PageSnippetlezione. È effettivamente una pagina, ma non può esistere senza fare qualche tipo di riferimento a $pdf, che in realtà è un renderer PDF di terze parti in questo caso. .. Tuttavia, suppongo che potrei creare una PageSnippetclasse del genere che contiene solo una serie di istruzioni di testo nel PDF e che alcune altre classi interpretino tali istruzioni. In questo modo posso evitare l'iniezione $pdfin PageSnippet, a scapito di ulteriore complessità
Dennis

1

Idealmente, si desidera il minor numero possibile di dipendenze tra le classi, poiché riduce la complessità. Una classe dovrebbe avere una dipendenza da un'altra classe solo se ne ha davvero bisogno.

Lo stato Pagecontiene "un set di istruzioni per un renderer di pagine". Immagino qualcosa del genere:

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

Così sarebbe $page->renderMe($renderer), poiché la Pagina ha bisogno di un riferimento al renderer.

Ma in alternativa le istruzioni di rendering potrebbero anche essere espresse come una struttura di dati piuttosto che chiamate dirette, ad es.

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

In questo caso, il Renderer effettivo otterrebbe questa struttura di dati dalla Pagina ed elaborerà eseguendo le corrispondenti istruzioni di rendering. Con un tale approccio le dipendenze sarebbero invertite: la Pagina non ha bisogno di conoscere il Renderer, ma al Renderer dovrebbe essere fornita una Pagina che può quindi renderizzare. Quindi opzione due:$renderer->renderPage($page);

Quindi qual è il migliore? Il primo approccio è probabilmente il più semplice da implementare, mentre il secondo è molto più flessibile e potente, quindi immagino che dipenda dalle tue esigenze.

Se non riesci a decidere, o pensi di poter cambiare approccio in futuro, puoi nascondere la decisione dietro un livello di riferimento indiretto, una funzione:

renderPage($page, $renderer)

L'unico approccio che non consiglierò è $page->renderMe()dato che suggerisce che una pagina può avere un solo renderer. Ma cosa succede se hai un ScreenRenderere aggiungi un PrintRenderer? La stessa pagina potrebbe essere resa da entrambi.


Nel contesto di EPUB o HTML, il concetto di pagina non esiste senza un renderer.
mouviciel,

1
@mouviciel: non sono sicuro di aver capito cosa intendi. Sicuramente puoi avere una pagina HTML senza renderla? Ad esempio, le pagine di processo del crawler di Google senza renderle.
Jacques B

2
C'è una diversa idea della pagina di parole: il risultato di un processo di impaginazione quando una pagina HTML è stata formattata per essere stampata, forse questo è ciò che aveva in mente @mouviciel. Tuttavia, in questa domanda a pageè chiaramente un input per il renderer, non un output, a tale nozione chiaramente non si adatta.
Doc Brown,

1

La parte D di SOLID dice

"Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli dovrebbero dipendere dalle astrazioni."

Quindi, tra Page e Renderer, che è più probabile che sia un'astrazione stabile, meno probabilità di cambiare, forse rappresentando un'interfaccia? Al contrario, qual è il "dettaglio"?

Nella mia esperienza, l'astrazione è di solito il Renderer. Ad esempio, potrebbe essere un semplice Stream o XML, molto astratto e stabile. O un layout abbastanza standard. È più probabile che la tua Pagina sia un oggetto business personalizzato, un "dettaglio". E hai altri oggetti business da rendere, come "foto", "rapporti", "grafici" ecc ... (Probabilmente non un "trittico" come nel mio commento)

Ma ovviamente dipende dal tuo design. La pagina potrebbe essere astratta, ad esempio l'equivalente di un <article>tag HTML con sottoparti standard. E hai un sacco di diversi "renderer" personalizzati per i rapporti commerciali. In tal caso, il Renderer dovrebbe dipendere dalla Pagina.


0

Penso che la maggior parte delle Classi possa essere suddivisa in una di due categorie:

  • Classi contenenti dati (mutabili o immutabili non importa)

Queste sono classi che non hanno quasi alcuna dipendenza da nient'altro. Di solito fanno parte del tuo dominio. Non devono contenere alcuna logica o solo logica che può essere derivata direttamente dal suo stato. Una classe Employee può avere una funzione isAdultche può essere derivata direttamente dalla sua birthDatema non una funzione hasBirthDayche richiede informazioni esterne (la data corrente).

  • Classi che forniscono servizi

Questi tipi di classi operano su altre classi contenenti dati. In genere sono configurati una volta e immutabili (quindi svolgono sempre lo stesso tipo di funzione). Questi tipi di classi possono comunque fornire un'istanza di supporto di breve durata con stato per eseguire operazioni più complesse che richiedono il mantenimento di uno stato per un breve periodo (come le classi Builder).

Il tuo esempio

Nel tuo esempio, Pagesarebbe una classe contenente dati. Dovrebbe avere funzioni per ottenere questi dati e forse modificarli se si suppone che la classe sia mutabile. Tienilo stupido, quindi può essere usato senza molte dipendenze.

Dati, o in questo caso il tuo Pagepotrebbe essere rappresentato in molti modi. Potrebbe essere reso come una pagina web, scritta su disco, memorizzata in un database, convertita in JSON, qualunque cosa. Non si desidera aggiungere metodi a tale classe per ciascuno di questi casi (e creare dipendenze da tutti i tipi di altre classi, anche se la classe dovrebbe contenere solo dati).

La tua Rendererè una tipica classe di servizio. Può operare su un determinato set di dati e restituire un risultato. Non ha molti stati propri e quale stato è normalmente immutabile, può essere configurato una volta e poi riutilizzato.

Ad esempio, potresti avere a MobileRenderere a StandardRenderer, entrambe le implementazioni della Rendererclasse ma con impostazioni diverse.

Quindi, poiché Pagecontiene dati e deve essere mantenuto stupido, la soluzione più pulita in questo caso sarebbe quella di passare Pagea Renderer:

$renderer->renderPage($page)

2
Logica molto procedurale.
user949300
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.