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

12

Supponiamo che esista una classe Page , che rappresenta un set di istruzioni per un renderer di pagine. Supponiamo che esista una classe Renderer che sappia come visualizzare 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 ciascun approccio? Quando si sarà migliori? Quando sarà migliore l'altro?

SFONDO

Per aggiungere un po 'più di background - Mi sto ritrovando ad utilizzare entrambi gli approcci nello stesso codice. Sto usando una libreria PDF di terze parti chiamata TCPDF . Da qualche parte nel mio codice ho avere il seguente aspetto per il rendering PDF:

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

Dire 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) Si noti qui che $snippet si esegue da sé , come nel mio primo esempio di codice. Deve anche conoscere e avere familiarità con $pdf e con qualsiasi $data affinché funzioni.

Tuttavia, posso creare una classe PdfRenderer 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 rivolge a questo:

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

2) Qui $renderer riceve PageSnippet e qualsiasi $data richiesto perché funzioni. Questo è simile al mio secondo esempio di codice.

Quindi, anche se il renderer riceve lo snippet di pagina, all'interno del renderer, lo snippet ancora si esegue da solo . Ciò significa che entrambi gli approcci sono in gioco. Non sono sicuro che tu possa limitare l'utilizzo di OO a uno solo o solo all'altro. Entrambi potrebbero essere richiesti, anche se si maschera uno per l'altro.

    
posta Dennis 05.02.2018 - 22:33
fonte

7 risposte

14

Dipende interamente da cosa pensi che sia OO .

Per OOP = SOLID, l'operazione dovrebbe far parte della classe se fa parte della Single Responsibility della classe.

Per OO = dispatch / polimorfismo virtuale, l'operazione dovrebbe essere parte dell'oggetto se deve essere inviata dinamicamente, cioè se viene chiamata attraverso un'interfaccia.

Per OO = incapsulamento, l'operazione dovrebbe far parte della classe se usa lo stato interno che non vuoi esporre.

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

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

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

es. utilizzando il punto di vista del polimorfismo: se si dispone di strategie di rendering diverse (come formati di output diversi o diversi motori di rendering), quindi $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 dal tipo di pagina e dalla strategia di rendering, puoi eseguire il doppio invio tramite il pattern visitor.

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

    
risposta data 05.02.2018 - 22:50
fonte
2

Secondo Alan Kay , gli oggetti sono autosufficienti, "adulti" e responsabili organismi. Gli adulti fanno cose, non sono operati. Cioè, la transazione finanziaria è responsabile per che si salva da solo , la pagina è responsabile per rendering stesso , ecc. ecc. Più concisamente, l'incapsulamento è il grande cosa in OOP. In particolare, si manifesta attraverso il famoso principio Tell do not ask (che @CandiedOrange ama citare tutto il tempo :)) e la riprovazione pubblica di getter e setter .

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

Considerando il tuo esempio, la mia versione di 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 tu sia interessato, David West parla dei principi originali dell'OOP nel suo libro, Pensiero degli oggetti .

    
risposta data 06.02.2018 - 11:39
fonte
2

$page->renderMe();

Qui abbiamo page completamente responsabile per il rendering stesso. Potrebbe essere stato fornito con un rendering tramite un costruttore, o potrebbe avere quella funzionalità integrata.

Qui ignorerò il primo caso (fornito con un rendering tramite un costruttore), poiché è piuttosto simile al suo passaggio come parametro. Vedrò invece i pro e i contro della funzionalità in fase di creazione.

Il pro è che consente un livello molto alto di incapsulamento. La pagina non ha bisogno di rivelare nulla sul suo stato interiore direttamente. Lo espone solo tramite un rendering di se stesso.

L'idea è che infrange il principio di responsabilità singola (SRP). Abbiamo una classe che è responsabile dell'incapsulamento dello stato di una pagina ed è anche hard-coded con regole su come eseguire il rendering stesso e quindi probabilmente tutta una serie di altre responsabilità in quanto gli oggetti dovrebbero "fare cose a se stessi, non avere cose fatte loro da altri ".

$page->renderMe($renderer);

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

  1. La pagina deve semplicemente conoscere le regole di rendering (quali metodi chiamare in quale ordine) per creare quel rendering. L'incapsulamento è preservato, ma l'SRP è ancora rotto perché la pagina deve ancora sorvegliare il processo di rendering, o
  2. La pagina chiama un solo metodo sull'oggetto di rendering, 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 della conservazione delle informazioni su una pagina e il renderer è responsabile del rendering di quella pagina. Tuttavia, ora abbiamo completamente indebolito l'incapsulamento dell'oggetto di pagina in quanto ha bisogno di rendere tutto il suo stato, pubblico.

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

Quale è il migliore? Nessuno di loro. Hanno tutti i loro difetti.

    
risposta data 06.02.2018 - 12:55
fonte
2

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

Che cos'è una pagina? È una rappresentazione di un display che qualcuno consumerà. Quel "qualcuno" potrebbe essere umano o robot. Nota 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 è uno stadio successivo.

Che cos'è un renderer senza una pagina? Un renderer può eseguire il rendering senza una pagina? No. Quindi un'interfaccia di Renderer ha bisogno del metodo renderPage($page); .

Che cosa c'è di sbagliato in $page->renderMe($renderer); ?

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

Each unit should have only limited knowledge about other units

Alla classe Page non interessa se esiste un Renderer nell'universo. Si preoccupa solo di essere una rappresentazione di una pagina. Quindi la classe o l'interfaccia Renderer non dovrebbe mai essere menzionata in un Page .

RISPOSTA AGGIORNATA

Se ho risposto correttamente alla tua domanda, la classe PageSnippet dovrebbe riguardare solo lo snippet di una pagina.

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

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

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

PdfRenderer riguarda il 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 client

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

Coppia di punti da considerare:

  • La sua cattiva pratica è di passare attorno a $data come array associativo. Dovrebbe essere un'istanza di una classe.
  • Il fatto che il formato della pagina sia contenuto all'interno della proprietà html dell'array $data è un dettaglio specifico per il tuo dominio e PageSnippet è a conoscenza di questi dettagli.
risposta data 06.02.2018 - 09:04
fonte
1

Idealmente, vuoi meno dipendenze tra le classi, dal momento che riduce la complessità. Una classe dovrebbe avere una dipendenza solo da un'altra classe se ne ha davvero bisogno.

Lo stato Page contiene "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...

Quindi sarebbe $page->renderMe($renderer) , poiché la <<>> ha bisogno di un riferimento al programma di rendering.

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

[
  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 dati dalla Pagina e la processerebbe eseguendo le corrispondenti istruzioni di rendering. Con un tale approccio le dipendenze sarebbero invertite - la Pagina non ha bisogno di sapere del Renderer, ma al Renderer dovrebbe essere fornita una Pagina che possa poi 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 suppongo che dipenda dalle tue esigenze.

Se non puoi decidere, o pensi di poter cambiare approccio in futuro, puoi nascondere la decisione dietro uno strato di riferimento indiretto, una funzione:

renderPage($page, $renderer)

L'unico approccio che non consiglierei è $page->renderMe() poiché suggerisce che una pagina può avere un solo riproduttore. Ma cosa succede se hai un ScreenRenderer e aggiungi un PrintRenderer ? La stessa pagina potrebbe essere visualizzata da entrambi.

    
risposta data 06.02.2018 - 10:59
fonte
1

D parte 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 probabile che cambi, possibilmente rappresentando un'interfaccia? Al contrario, che è il "dettaglio"?

Nella mia esperienza, l'astrazione è solitamente il Renderer. Ad esempio, potrebbe essere un semplice flusso o XML, molto astratto e stabile. O qualche layout abbastanza standard. La tua pagina è più probabile che sia un oggetto aziendale personalizzato, un "dettaglio". E hai altri oggetti business da rendere, come "immagini", "report", "grafici" ecc ... (Probabilmente non è un "tryptich" come nel mio commento)

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

    
risposta data 07.02.2018 - 03:23
fonte
0

Penso che la maggior parte delle Classi possa essere divisa in una delle due seguenti categorie:

  • Le classi contenenti dati (mutevoli o immutabili non contano)

Queste sono classi che non hanno quasi nessuna dipendenza da altro. Di solito fanno parte del tuo dominio. Non dovrebbero contenere alcuna logica o solo logica che possa essere derivata direttamente dal suo stato. Una classe Employee può avere una funzione isAdult in quanto può essere derivata direttamente dal suo birthDate ma non da una funzione hasBirthDay in quanto richiede informazioni esterne (la data corrente).

  • Classi che forniscono servizi

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

Il tuo esempio

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

Dati, o in questo caso il tuo Page potrebbe essere rappresentato in una moltitudine di modi. Potrebbe essere reso come una pagina web, scritta su disco, archiviata in un database, convertita in JSON, qualunque cosa. Non è necessario aggiungere metodi a tale classe per ciascuno di questi casi (e creare dipendenze su tutti i tipi di altre classi, anche se si suppone che la classe contenga solo dati).

La tua Renderer è una tipica classe di tipi di servizio. Può operare su un determinato insieme di dati e restituire un risultato. Non ha molto stato a sé stante, e quale stato è solitamente immutabile, può essere configurato una volta e quindi riutilizzato.

Ad esempio, potresti avere MobileRenderer e StandardRenderer , entrambe le implementazioni della classe Renderer ma con impostazioni diverse.

Così come Page contiene dati e dovrebbe essere tenuto muto, la soluzione più pulita in questo caso sarebbe passare il Page a Renderer :

$renderer->renderPage($page)
    
risposta data 06.02.2018 - 15:05
fonte

Leggi altre domande sui tag