Devo passare un oggetto a un costruttore o istanziare in classe?

8

Considera questi due esempi:

Passaggio di un oggetto a un costruttore

class ExampleA
{
  private $config;
  public function __construct($config)
  {
     $this->config = $config;
  }
}
$config = new Config;
$exampleA = new ExampleA($config);

Creazione di un'istanza di una classe

class ExampleB
{
  private $config;
  public function __construct()
  {
     $this->config = new Config;
  }
}
$exampleA = new ExampleA();

Qual è il modo corretto di gestire l'aggiunta di un oggetto come proprietà? Quando dovrei usarne uno sull'altro? Il test delle unità influenza ciò che dovrei usare?

    
posta Prisoner 18.01.2012 - 16:37
fonte

9 risposte

14

Penso che il primo ti darà la possibilità di creare un oggetto config altrove e di passarlo a ExampleA . Se hai bisogno di Iniezione di dipendenza, questa può essere una buona cosa perché puoi assicurarti che tutte le intenzioni condividano lo stesso oggetto.

D'altra parte, forse il tuo ExampleA richiede un nuovo oggetto config pulito, quindi potrebbero esserci casi in cui il secondo esempio è più appropriato, come i casi in cui ogni istanza potrebbe potenzialmente avere una configurazione diversa.

    
risposta data 18.01.2012 - 16:42
fonte
8

Non dimenticare la testabilità!

Solitamente se il comportamento della classe Example dipende dalla configurazione, si vuole essere in grado di testarlo senza salvare / modificare lo stato interno dell'istanza della classe (es. si vorranno test semplici per percorso felice e errato config senza modificare la proprietà / memeber di Example class).

Quindi andrei con la prima opzione di ExampleA

    
risposta data 18.01.2012 - 17:26
fonte
4

Se l'oggetto ha la responsabilità di gestire la durata della dipendenza, allora è OK creare l'oggetto nel costruttore (e smaltirlo nel distruttore). *

Se l'oggetto non è responsabile della gestione della durata della dipendenza, deve essere passato al costruttore e gestito dall'esterno (ad esempio da un contenitore IoC).

In questo caso non penso che la tua ClasseA debba assumersi la responsabilità della creazione di $ config, a meno che non sia anche responsabile della sua eliminazione, o se la configurazione sia unica per ogni istanza di ClassA.

* Per facilitare la testabilità, il costruttore potrebbe fare riferimento a una classe / metodo factory per costruire la dipendenza nel suo costruttore, aumentando la coesione e la testabilità.

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}
    
risposta data 19.01.2012 - 11:00
fonte
2

Ho avuto la stessa identica discussione con il nostro team di architettura di recente e ci sono alcuni motivi per farlo in un modo o nell'altro. Dipende principalmente da Dependency Injection (come altri hanno notato) e indipendentemente dal fatto che tu abbia o meno il controllo sulla creazione dell'oggetto che sei nuovo nel costruttore.

Nel tuo esempio, cosa succede se la tua classe di configurazione:

a) ha un'assegnazione non banale, ad esempio proveniente da un metodo pool o factory.

b) potrebbe non essere allocato. Passarlo nel costruttore evita accuratamente questo problema.

c) è in realtà una sottoclasse di Config.

Passare l'oggetto nel costruttore offre la massima flessibilità.

    
risposta data 19.01.2012 - 08:32
fonte
1

La risposta qui sotto è sbagliata, ma terrò che gli altri imparino da esso (vedi sotto)

In ExampleA , puoi utilizzare la stessa istanza Config su più classi. Tuttavia, se dovesse esserci solo un'istanza Config nell'intera applicazione, considera l'applicazione del modello Singleton su Config per evitare di avere più istanze di Config . E se Config è un Singleton, puoi fare invece quanto segue:

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

In ExampleB , d'altra parte, otterrai sempre un'istanza separata di Config per ogni istanza di ExampleB .

Quale versione devi applicare dipende da come l'applicazione gestirà le istanze di Config :

  • se ogni istanza di ExampleX deve avere un'istanza separata di Config , vai con ExampleB ;
  • se ogni istanza di ExampleX condividerà una (e solo una) istanza di Config , usa ExampleA with Config Singleton ;
  • se le istanze di ExampleX possono utilizzare diverse istanze di Config , bastone con ExampleA .

Perché la conversione di Config in Singleton è errata:

Devo ammettere che ho appreso solo ieri del pattern Singleton (leggendo il libro Head First dei pattern design). Ingenuamente sono andato in giro e l'ho applicato per questo esempio, ma come molti hanno sottolineato, in un modo ce ne sono altri (alcuni sono stati più criptici e hanno detto solo "Stai sbagliando!"), Questa non è una buona idea. Quindi, per evitare che altri facciano lo stesso errore che ho appena fatto, ecco un riassunto del motivo per cui il pattern Singleton può essere dannoso (basato sui commenti e su quello che ho trovato su Google):

  1. Se ExampleA recupera il proprio riferimento all'istanza Config , le classi saranno strettamente accoppiate. Non ci sarà modo di avere un'istanza di ExampleA per utilizzare una versione diversa di Config (ad esempio alcune sottoclassi). È orribile se vuoi testare ExampleA usando un'istanza di simulazione di Config poiché non c'è modo di fornirlo a ExampleA .

  2. La premessa di ci sarà una, e solo una, istanza di Config forse contiene ora , ma non puoi sempre essere sicuro che lo stesso rimarrà nel futuro . Se in un secondo momento risultasse che sarebbero desiderabili più istanze di Config , non c'è modo di ottenere ciò senza riscrivere il codice.

  3. Anche se l'unica ed unica istanza di Config è forse vera per l'eternità, potrebbe succedere che tu voglia utilizzare alcune sottoclassi di Config (pur avendo ancora solo un esempio). Ma poiché il codice ottiene direttamente l'istanza tramite getInstance() di Config , che è un metodo static , non c'è modo di ottenere la sottoclasse. Ancora una volta, il codice deve essere riscritto.

  4. Il fatto che ExampleA usi Config sarà nascosto, almeno quando visualizzi solo l'API di ExampleA . Questo può o non può essere una cosa negativa, ma personalmente ritengo che ciò sembri uno svantaggio; ad esempio, quando si mantiene, non esiste un modo semplice per scoprire quali classi saranno influenzate dalle modifiche a Config senza esaminare l'implementazione di ogni altra classe.

  5. Anche se il fatto che ExampleA utilizzi un Singleton Config non è un problema in sé, può ancora diventare un problema dal punto di vista del test. Gli oggetti Singleton porteranno in uno stato che durerà fino alla fine dell'applicazione. Questo può essere un problema quando si eseguono i test di unità in quanto si desidera isolare un test da un altro (ad esempio, l'esecuzione di un test non dovrebbe influire sul risultato di un altro). Per risolvere questo problema, l'oggetto Singleton deve essere distrutto tra ogni esecuzione di prova (potenzialmente dover riavviare l'intera applicazione), il che potrebbe richiedere molto tempo (per non dire noioso e noioso).

Detto questo, sono contento di aver fatto questo errore qui e non nell'implementazione di un'applicazione reale. In effetti, stavo davvero pensando di riscrivere il mio ultimo codice per usare il pattern Singleton per alcune delle classi. Anche se potrei facilmente ripristinare le modifiche (ovviamente tutto è memorizzato in un SVN), avrei comunque perso tempo a farlo.

    
risposta data 18.01.2012 - 17:07
fonte
1

La cosa più semplice da fare è abbinare ExampleA a Config . Dovresti fare la cosa più semplice, a meno che non ci sia un motivo convincente per fare qualcosa di più complesso.

Un motivo per il disaccoppiamento di ExampleA e Config sarebbe il miglioramento della verificabilità di ExampleA . L'accoppiamento diretto degraderà la testabilità di ExampleA se Config ha metodi lenti, complessi o in rapida evoluzione. Per il test, un metodo è lento se viene eseguito più di pochi microsecondi. Se tutti i metodi di Config sono semplici e veloci, allora prenderei l'approccio semplice e accoppiare direttamente ExampleA a Config .

    
risposta data 19.01.2012 - 19:15
fonte
1

Il tuo primo esempio è un esempio del modello Iniezione di dipendenza. Una classe con una dipendenza esterna riceve la dipendenza da costruttore, setter, ecc.

Questo approccio si traduce in un codice liberamente accoppiato. La maggior parte delle persone pensa che l'accoppiamento libero sia una buona cosa, perché si può facilmente sostituire la configurazione nei casi in cui una particolare istanza di un oggetto deve essere configurata in modo diverso dalle altre, si può passare in un oggetto di configurazione fittizio per il test, e così sopra.

Il secondo approccio è più vicino al modello di creatore GRASP. In questo caso, l'oggetto crea le sue dipendenze. Ciò si traduce in codice strettamente accoppiato, questo può limitare la flessibilità della classe e renderla più difficile da testare. Se hai bisogno di un'istanza di una classe per avere una dipendenza diversa dalle altre, l'unica opzione è quella di sottoclasse.

Può essere lo schema appropriato, tuttavia, nei casi in cui la durata di vita dell'oggetto dipendente dipende dalla durata dell'oggetto dipendente e in cui l'oggetto depended-on non viene utilizzato ovunque al di fuori dell'oggetto che dipende su di esso. Normalmente suggerirei a DI di essere la posizione predefinita, ma non devi escludere completamente l'altro approccio finché sei consapevole delle sue conseguenze.

    
risposta data 29.08.2012 - 10:04
fonte
0

Se la tua classe non espone $config a classi esterne, la creerei all'interno del costruttore. In questo modo, stai mantenendo il tuo stato interno privato.

Se il $config richiede che il proprio stato interno sia impostato correttamente (ad esempio, richiede una connessione al database o alcuni campi interni inizializzati) prima dell'uso, allora è opportuno posticipare l'inizializzazione ad un codice esterno (possibilmente una fabbrica classe) e iniettarlo nel costruttore. O, come altri hanno sottolineato, se deve essere condiviso tra altri oggetti.

    
risposta data 19.01.2012 - 18:09
fonte
0

ExampleA è disaccoppiato dalla classe concreta Config che è buona , oggetto fornito ricevuto se non di tipo Config ma il tipo di una superclasse astratta di Config.

ExampleB è strongmente accoppiato alla classe di calcestruzzo Config che è male .

Istanziare un oggetto crea un strong accoppiamento tra le classi. Dovrebbe essere fatto in una classe di fabbrica.

    
risposta data 03.10.2012 - 17:12
fonte

Leggi altre domande sui tag