Tell non chiedere a vs constructor che fa lavoro

4

Quando cerchi la frase "i costruttori non devono lavorare", in vari post del blog troverai il consiglio di non lasciare che il costruttore lavori. Nonostante questo, ho qualche problema a capire perché questo è il caso. Inoltre, questo post popolare suggerisce di prendere tale consiglio con un tocco di sale.

Ho un esempio di due implementazioni della stessa situazione. Nella situazione, un AFactory ha un metodo createA , utilizzando un B . A ha bisogno di un risultato di una query, che B produce. Ci sono due modi per implementarlo:

Esempio 1:

class AFactory {
    public function createA(B $b): A {
        return new A($b->getQueryResult());
    }
}

class A {
    private $query_result;

    public function __construct(array $query_result) {
        $this->query_result = $query_result;
    }

    public function doFooWithQueryResult() {
        // Do something with query result
    }

    public function doBarWithQueryResult() {
        // Do something with query result
    }
}

Nel primo esempio, la factory recupera il risultato della query e lo passa al costruttore di A . A quindi assegna semplicemente il risultato della query alla proprietà di classe corrispondente. Tuttavia, c'è un problema qui: A non verifica se il risultato della query è una struttura dati valida, cioè un risultato di query effettivo adatto per A . Non sa da dove viene. La responsabilità di questa convalida è ora trapelata al AFactory e A è diventata strettamente accoppiata a AFactory . L'altra implementazione risolve questo problema, ma in seguito il costruttore esegue il lavoro. E a quanto pare è male.

Esempio 2:

class AFactory {
    public function createA(B $b): A {
        return new A($b);
    }
}

class A {
    private $query_result;

    public function __construct(B $b) {
        $this->query_result = $b->getQueryResult();
    }

    public function doFooWithQueryResult() {
        // Do something with query result
    }

    public function doBarWithQueryResult() {
        // Do something with query result
    }
}
    
posta user2180613 18.01.2017 - 13:44
fonte

3 risposte

4

Hai scritto

A does not verify if the query result is a valid data structure, i.e. an actual query result suited for A.

seguito direttamente da

It does not know where it came from

Ma queste sono due cose diverse! A non ha bisogno di "sapere da dove provengono i dati", ma può naturalmente convalidarne l'input:

class A {
    private $query_result;

    public function __construct(array $query_result) {
        // makes tests, throws an exception if $query_result is not valid
        validateInput($query_result);  

        $this->query_result = $query_result;
    }
    // ...
}

La convalida dell'input di IMHO nel costruttore non sta contando come "lavoro effettivo" che dovrebbe essere fatto da qualche altra parte. Tuttavia, il costruttore non ha bisogno di chiamare getQueryResult . Ciò manterrà A disaccoppiato da B , il che rende molto più facile il test e il riutilizzo.

    
risposta data 18.01.2017 - 14:05
fonte
0

Che cos'è A? Questa è la domanda fondamentale nella progettazione di una classe in OOP.

La maggior parte dei problemi insorge perché non si pone questa domanda all'inizio e si concentra invece sui dettagli di implementazione della classe. Il tuo esempio mi dà questa impressione.

Torna alla domanda:

  • Fai la domanda prima di fare qualsiasi dettaglio. In altre parole, è necessario definire quale sia la classe A. Non passare all'implementazione prima di finalizzare questa domanda. In genere, si è tentati di pensare che una volta che si inizia a programmare, la classe si unirà in qualche modo. Evita questa tentazione.
  • Quando fai questa domanda, prova a definire un'unica, unica responsabilità per la classe, non molte. E questa responsabilità deve essere chiara e non vaga.
  • Una volta definita la definizione, diventa molto più semplice vedere quali proprietà ha questa classe, poiché le proprietà derivano dalla sua definizione. Ad esempio, se decidi che la tua classe è un Veicolo, allora deve avere attributi come corpo, motore, pneumatici, timoni ecc. E azioni come muovere, fermarsi, girare, accelerare, rallentare, ecc.
  • È importante eseguire questi passaggi prima di immergerti nella codifica, indipendentemente da quanto possa essere allettante.

Ora, per la tua domanda riguardante i costruttori non dovresti fare il lavoro:

La mia comprensione di questa regola (sebbene non assoluta come hai menzionato) è questa: nella tua applicazione, nel momento in cui raggiungi il punto per costruire un oggetto di una classe, i dati per quella costruzione dovrebbero essere già disponibili e passati al costruttore. Ad esempio, ad esempio nella tua applicazione web, raccogli i dati immessi dall'utente attraverso un modulo per costruire un oggetto.

// Gather user information
email = email from form data
...

// Do data validation if necessary
if (name is valid)
if (email is valid) 
if (credit card is valid)
...

// Now ready to construct object
User user = new User(name, email, creditCard, ...)
...

La stessa cosa vale se hai bisogno di ottenere dati da un database da utilizzare per la costruzione di un oggetto, nel tuo esempio A.

L'idea è, nel contesto in cui si crea un oggetto, che il lavoro di pre-costruzione deve essere fatto lì. Ad esempio, l'esempio precedente potrebbe un controller in un'applicazione Web.

C'è una cosa che dovrebbe essere fatta meglio nel costruttore, e questo è il tipo di lavoro che logicamente fa parte della definizione di una classe (Spiegazione precedente sulla definizione di una classe). Ad esempio, nell'esempio utente web, se fa parte della logica aziendale che ad ogni utente viene assegnato un rank in base al limite di credito, questo tipo di lavoro non è adatto nel controller, non forse in una classe di servizio, ma in l'oggetto business stesso. Quindi, qualcosa di simile potrebbe essere fatto nel costruttore.

    
risposta data 18.01.2017 - 17:05
fonte
0

La classe A non dovrebbe sapere cosa fa B. La classe A non dovrebbe nemmeno sapere come si presenta un risultato della query.

Un miglioramento sarebbe avere parametri espliciti con nome - tu sai esattamente quali dati hai bisogno per costruire A - se questo deriva o meno da un risultato di una query.

Penserei a cosa potrebbe effettivamente essere sbagliato con il risultato della query - sembra che se la query è sbagliata, questa è più di una circostanza eccezionale che dovrebbe essere trattata come tale - portando a "A" istanze mai create .

Più in generale, come suggerisce Doc Brown, si tratta di ridurre "l'accoppiamento" - o quanto dipendente un pezzo di codice è su un pezzo diverso.

Vedi qui per una rapida guida su alcuni modi in cui il codice può essere accoppiato. Vedrai che è impossibile evitare in qualche modo. Vuoi che il tuo codice sia il più liberamente accoppiato possibile in modo da poterlo testare e modificare in modo indipendente.

Se il costruttore solo prende parametri con nome - non è necessario eseguire lavori complicati, ad eccezione dell'assegnazione di variabili semplici. Il complicato lavoro che fai per arrivare ai valori di quei parametri può quindi essere ignorato da A, o qualsiasi dipendenza da A, rendendo un test unitario davvero facile. Quel lavoro complicato è quindi anche un candidato a nascondersi dietro un'astrazione e un'unità testata.

    
risposta data 18.01.2017 - 17:32
fonte

Leggi altre domande sui tag