Devo passare un oggetto in un costruttore o creare un'istanza in classe?


10

Considera questi due esempi:

Passare un oggetto a un costruttore

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

Istanziare 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 sopra l'altro? Il test unitario influenza ciò che dovrei usare?



9
@FrustratedWithFormsDesigner - questo non è adatto per la revisione del codice.
ChrisF

Risposte:


14

Penso che il primo ti darà la possibilità di creare un configoggetto altrove e passarlo a ExampleA. Se hai bisogno di Dependency Injection, 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 configoggetto nuovo e pulito , quindi potrebbero esserci casi in cui il secondo esempio è più appropriato, come casi in cui ogni istanza potrebbe potenzialmente avere una configurazione diversa.


1
Quindi, A è buono per i test unitari, B è buono per altre circostanze ... perché non avere entrambi? Ho spesso due costruttori, uno per DI manuale, uno per uso normale (utilizzo Unity per DI, che genererà costruttori per soddisfare i suoi requisiti DI).
Ed James,

3
@EdWoodcock non si tratta davvero di unit test contro altre circostanze. L'oggetto richiede la dipendenza dall'esterno o lo gestisce all'interno. Mai entrambi / neanche.
MattDavey,

9

Non dimenticare la testabilità!

Di solito se il comportamento della Exampleclasse dipende dalla configurazione che si desidera essere in grado di testare senza salvare / modificare lo stato interno dell'istanza della classe (vale a dire si desidera avere test semplici per il percorso felice e per la configurazione errata senza modificare la proprietà / membro di Exampleclasse).

Quindi vorrei andare con la prima opzione di ExampleA


Ecco perché ho menzionato i test - grazie per averlo aggiunto :)
Prigioniero

5

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, dovrebbe essere passato al costruttore e gestito dall'esterno (ad esempio da un contenitore IoC).

In questo caso, non credo che la tua Classe A dovrebbe assumersi la responsabilità di creare $ config, a meno che non sia anche responsabile della sua disposizione o se la configurazione è unica per ogni istanza di ClassA.

* Per facilitare la testabilità, il costruttore potrebbe prendere un riferimento a una classe / metodo di fabbrica 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();
    }
}

2

Recentemente ho avuto la stessa identica discussione con il nostro team di architettura e ci sono alcune ragioni sottili per farlo in un modo o nell'altro. Dipende principalmente dalla Dependency Injection (come altri hanno notato) e 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 Config:

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

b) potrebbe non essere assegnato. Passarlo nel costruttore evita chiaramente questo problema.

c) è in realtà una sottoclasse di Config.

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


1

La risposta qui sotto è sbagliata, ma terrò per gli altri imparare da essa (vedi sotto)

In ExampleA, è possibile utilizzare la stessa Configistanza in più classi. Tuttavia, se ci dovrebbe essere solo Configun'istanza all'interno dell'intera applicazione, prendere in considerazione l'applicazione del modello Singleton Configper evitare di avere più istanze di Config. E se Configè un Singleton, puoi invece fare quanto segue:

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

D' ExampleBaltra parte, otterrai sempre un'istanza separata di Configper ogni istanza di ExampleB.

La versione che dovresti applicare dipende davvero da come l'applicazione gestirà le istanze di Config:

  • se ogni istanza di ExampleXdovrebbe avere un'istanza separata di Config, andare con ExampleB;
  • se ogni istanza di ExampleXcondividerà una (e una sola) istanza di Config, utilizzare ExampleA with Config Singleton;
  • se le istanze di ExampleXpossono usare istanze diverse di Config, attenersi a ExampleA.

Perché la conversione Configin Singleton è errata:

Devo ammettere che ieri ho imparato a conoscere il modello Singleton (leggendo il primo libro dei modelli di design di Head ). Ingenuamente sono andato in giro e l'ho applicato per questo esempio, ma come molti hanno sottolineato, in un modo sono un altro (alcuni sono stati più criptici e hanno detto solo "Stai sbagliando!"), Questa non è una buona idea. Quindi, per impedire ad altri di fare lo stesso errore che ho appena fatto, ecco un riassunto del motivo per cui il modello Singleton può essere dannoso (basato sui commenti e su ciò che ho scoperto cercandolo su Google):

  1. Se ExampleArecupera il proprio riferimento Configall'istanza, le classi saranno strettamente accoppiate. Non ci sarà modo di avere un'istanza ExampleAper usare una versione diversa di Config(diciamo una sottoclasse). Questo è orribile se vuoi testare ExampleAusando un'istanza di mock-up in Configquanto non c'è modo di fornirlo ExampleA.

  2. La premessa di ci sarà una, e una sola, istanza di Configforse vale ora , ma non si può sempre essere sicuri che lo stesso accadrà in futuro . Se in un momento successivo risulta che Configsaranno desiderabili più istanze di , non è possibile raggiungere questo obiettivo senza riscrivere il codice.

  3. Anche se la sola e unica istanza di Configè forse vera per tutta l'eternità, potrebbe accadere che tu voglia poter usare una sottoclasse di Config(pur avendo ancora una sola istanza). Tuttavia, poiché il codice ottiene direttamente l'istanza tramite getInstance()of Config, che è un staticmetodo, non è possibile ottenere la sottoclasse. Ancora una volta, il codice deve essere riscritto.

  4. Il fatto che ExampleAusi Configsarà nascosto, almeno quando si visualizza semplicemente l'API di ExampleA. Questa può essere o meno una brutta cosa, ma personalmente ritengo che ciò sembri uno svantaggio; ad esempio, quando lo si mantiene, non esiste un modo semplice per scoprire a quali classi saranno interessate le modifiche Configsenza esaminare l'implementazione di ogni altra classe.

  5. Anche se il fatto che ExampleAutilizza un Singleton Config non è un problema in sé, può comunque diventare un problema dal punto di vista del test. Gli oggetti Singleton avranno uno stato che persisterà fino al termine dell'applicazione. Questo può essere un problema quando si eseguono unit test in quanto si desidera che un test sia isolato da un altro (vale a dire che aver eseguito un test non dovrebbe influire sul risultato di un altro). Per risolvere questo problema, l' oggetto Singleton deve essere distrutto tra ogni esecuzione di test (potrebbe essere necessario riavviare l'intera applicazione), il che può richiedere molto tempo (per non dire noioso e fastidioso).

Detto questo, sono contento di aver fatto questo errore qui e non nell'implementazione di una vera applicazione. In effetti, stavo davvero considerando di riscrivere il mio ultimo codice per utilizzare il modello Singleton per alcune delle classi. Anche se avrei potuto facilmente ripristinare le modifiche (tutto è archiviato in un SVN, ovviamente), avrei comunque perso tempo a farlo.


4
Non consiglierei di farlo .. in questo modo si accoppiano strettamente le lezioni ExampleAe Config- il che non è una buona cosa.
Paul,

@Paul: è vero. Buona cattura, non ci ho pensato.
gablin

3
Consiglierei sempre di non usare Singletons per motivi di testabilità. Sono essenzialmente variabili globali ed è impossibile deridere la dipendenza.
MattDavey,

4
Consiglierei sempre di nuovo l'uso di Singleton per motivi di errore.
Raynos,

1

La cosa più semplice da fare è di accoppiare ExampleAa Config. Dovresti fare la cosa più semplice, a meno che non ci sia una ragione convincente per fare qualcosa di più complesso.

Un motivo per disaccoppiare ExampleAe Configsarebbe migliorare la testabilità di ExampleA. Accoppiamento diretto degrada la controllabilità di ExampleAse Configha metodi che sono lenti, complessi, o rapida evoluzione. Per i test, un metodo è lento se viene eseguito per più di alcuni microsecondi. Se tutti i metodi di Configsono semplici e veloci, quindi vorrei prendere l'approccio semplice e direttamente paio ExampleAdi Config.


1

Il tuo primo esempio è un esempio del modello di iniezione di dipendenza. A una classe con una dipendenza esterna viene assegnata la dipendenza da costruttore, setter, ecc.

Questo approccio si traduce in codice debolmente accoppiato. La maggior parte delle persone pensa che l'accoppiamento lento sia una buona cosa, perché puoi facilmente sostituire la configurazione nei casi in cui un'istanza particolare di un oggetto deve essere configurata diversamente dalle altre, puoi passare un oggetto di configurazione falso per il test, e così su.

Il secondo approccio è più vicino al modello creatore GRASP. In questo caso, l'oggetto crea le proprie dipendenze. Ciò si traduce in codice strettamente accoppiato, questo può limitare la flessibilità della classe e rendere più difficile il test. Se è necessaria un'istanza di una classe per avere una dipendenza diversa dalle altre, l'unica opzione è sottoclassarla.

Può essere il modello appropriato, tuttavia, nei casi in cui la durata dell'oggetto dipendente è dettata dalla durata dell'oggetto dipendente e in cui l'oggetto dipendente non viene utilizzato in alcun luogo al di fuori dell'oggetto che dipende da esso. Normalmente consiglierei a DI di essere la posizione predefinita, ma non è necessario escludere completamente l'altro approccio fintanto che si è consapevoli delle sue conseguenze.


0

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

Se i $configrequisiti richiedono che il proprio stato interno sia impostato correttamente (ad es. Necessiti di una connessione al database o di alcuni campi interni inizializzati) prima dell'uso, ha senso rinviare l'inizializzazione a un codice esterno (possibilmente una classe di fabbrica) e iniettarlo in il costruttore. Oppure, come altri hanno sottolineato, se deve essere condiviso tra altri oggetti.


0

L'esempio A è disaccoppiato dalla classe concreta Config che è buona , a condizione che l'oggetto ricevuto non sia di tipo Config ma il tipo di una superclasse astratta di Config.

L'esempio B è fortemente accoppiato alla classe concreta Config, il che è negativo .

L'istanza di un oggetto crea un forte accoppiamento tra le classi. Dovrebbe essere fatto in una classe Factory.

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.