Dois-je passer un objet dans un constructeur, ou instancier en classe?

10

Considérez ces deux exemples:

Passer un objet à un constructeur

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

Instanciation d'une classe

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

Quelle est la bonne façon de gérer l'ajout d'un objet en tant que propriété? Quand devrais-je utiliser l'un sur l'autre? Les tests unitaires affectent-ils ce que je dois utiliser?

Prisonnier
la source
Cela pourrait-il être mieux sur codereview.stackexchange.com/questions ?
FrustratedWithFormsDesigner
9
@FrustratedWithFormsDesigner - cela ne convient pas pour la révision de code.
ChrisF

Réponses:

14

Je pense que le premier vous donnera la possibilité de créer un configobjet ailleurs et de le transmettre ExampleA. Si vous avez besoin de l'injection de dépendances, cela peut être une bonne chose car vous pouvez vous assurer que toutes les instances partagent le même objet.

D'un autre côté, peut-être que votre ExampleA nécessite un configobjet nouveau et propre , il pourrait donc y avoir des cas où le deuxième exemple est plus approprié, tels que des cas où chaque instance pourrait potentiellement avoir une configuration différente.

FrustratedWithFormsDesigner
la source
1
Donc, A est bon pour les tests unitaires, B est bon pour d'autres circonstances ... pourquoi ne pas avoir les deux? J'ai souvent deux constructeurs, un pour DI manuel, un pour une utilisation normale (j'utilise Unity pour DI, qui générera des constructeurs pour répondre à ses exigences DI).
Ed James
3
@EdWoodcock, il ne s'agit pas vraiment de tests unitaires par rapport à d'autres circonstances. L'objet nécessite soit la dépendance de l'extérieur, soit il la gère de l'intérieur. Jamais les deux / non plus.
MattDavey
9

N'oubliez pas la testabilité!

Habituellement, si le comportement de la Exampleclasse dépend de la configuration, vous voulez pouvoir la tester sans enregistrer / modifier l'état interne de l'instance de classe (c'est-à-dire que vous voudriez avoir des tests simples pour un chemin heureux et une mauvaise configuration sans modifier la propriété / membre de Exampleclasse).

Par conséquent, j'irais avec la première option de ExampleA

Paul
la source
C'est pourquoi j'ai mentionné les tests - merci d'avoir ajouté ça :)
Prisonnier
5

Si l'objet a la responsabilité de gérer la durée de vie de la dépendance, alors il est OK de créer l'objet dans le constructeur (et de le disposer dans le destructeur). *

Si l'objet n'est pas responsable de la gestion de la durée de vie de la dépendance, il doit être transmis au constructeur et géré de l'extérieur (par exemple par un conteneur IoC).

Dans ce cas, je ne pense pas que votre ClassA devrait prendre la responsabilité de créer $ config, à moins qu'il ne soit également responsable de son élimination, ou si la configuration est unique pour chaque instance de ClassA.

* Pour faciliter la testabilité, le constructeur pourrait prendre une référence à une classe / méthode d'usine pour construire la dépendance dans son constructeur, augmentant la cohésion et 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();
    }
}
MattDavey
la source
2

J'ai eu exactement la même discussion avec notre équipe d'architecture récemment et il y a quelques raisons subtiles de le faire d'une manière ou d'une autre. Cela se résume principalement à l'injection de dépendance (comme d'autres l'ont noté) et à la question de savoir si vous avez vraiment le contrôle sur la création de l'objet que vous créez dans le constructeur.

Dans votre exemple, que se passe-t-il si votre classe Config:

a) a une allocation non triviale, telle que provenant d'une méthode de pool ou d'usine.

b) pourrait ne pas être attribué. Le transmettre au constructeur évite parfaitement ce problème.

c) est en fait une sous-classe de Config.

Passer l'objet dans le constructeur donne le plus de flexibilité.

JBRWilkinson
la source
1

La réponse ci-dessous est fausse, mais je la garderai pour que les autres puissent en tirer des leçons (voir ci-dessous)

Dans ExampleA, vous pouvez utiliser la même Configinstance sur plusieurs classes. Cependant, s'il ne doit y avoir qu'une seule Configinstance dans toute l'application, envisagez d'appliquer le modèle Singleton Configpour éviter d'avoir plusieurs instances de Config. Et si Configc'est un Singleton, vous pouvez faire ce qui suit à la place:

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

Dans ExampleB, d'autre part, vous obtiendrez toujours une instance distincte de Configpour chaque instance de ExampleB.

La version à appliquer dépend vraiment de la manière dont l'application traitera les instances de Config:

  • si chaque instance de ExampleXdoit avoir une instance distincte de Config, allez avec ExampleB;
  • si chaque instance de ExampleXpartage une (et une seule) instance de Config, utilisez ExampleA with Config Singleton;
  • si les instances de ExampleXpeuvent utiliser différentes instances de Config, respectez ExampleA.

Pourquoi se convertir Configen Singleton est faux:

Je dois admettre que je n'ai appris le modèle Singleton qu'hier (en lisant le livre Head First sur les modèles de conception). Naïvement, je me suis déplacé et l'ai appliqué à cet exemple, mais comme beaucoup l'ont souligné, une manière est une autre (certains ont été plus cryptiques et ont seulement dit "vous vous trompez!"), Ce n'est pas une bonne idée. Donc, pour empêcher les autres de faire la même erreur que je viens de faire, voici un résumé des raisons pour lesquelles le modèle Singleton peut être nocif (sur la base des commentaires et de ce que j'ai découvert sur Google):

  1. Si ExampleArécupère sa propre référence à l' Configinstance, les classes seront étroitement couplées. Il n'y aura aucun moyen d'avoir une instance de ExampleApour utiliser une version différente de Config(par exemple une sous-classe). C'est horrible si vous voulez tester en ExampleAutilisant une instance de maquette Configcar il n'y a aucun moyen de le fournir ExampleA.

  2. La prémisse de qu'il y en aura une, et une seule, Configpeut - être maintenant , mais vous ne pouvez pas toujours être sûr que la même chose se reproduira à l'avenir . Si à un moment ultérieur, il s'avère que plusieurs instances de Configseront souhaitables, il n'y a aucun moyen d'y parvenir sans réécrire le code.

  3. Même si l'instance une et une seule de Configest peut-être vraie pour toute l'éternité, il peut arriver que vous souhaitiez pouvoir utiliser une sous-classe de Config(tout en n'ayant qu'une seule instance). Mais, puisque le code obtient directement l'instance via getInstance()of Config, qui est une staticméthode, il n'y a aucun moyen d'obtenir la sous-classe. Encore une fois, le code doit être réécrit.

  4. Le fait que les ExampleAutilisations Configsoient masquées, du moins lors de la simple visualisation de l'API de ExampleA. Cela peut ou non être une mauvaise chose, mais personnellement, je pense que cela ressemble à un inconvénient; par exemple, lors de la maintenance, il n'y a pas de moyen simple de savoir à quelles classes seront affectées les modifications Configsans examiner la mise en œuvre de toutes les autres classes.

  5. Même si le fait d' ExampleAutiliser un Singleton Config n'est pas un problème en soi, il peut quand même devenir un problème du point de vue des tests. Les objets singleton porteront un état qui persistera jusqu'à la fin de l'application. Cela peut être un problème lors de l'exécution de tests unitaires car vous voulez qu'un test soit isolé d'un autre (c'est-à-dire que l'exécution d'un test ne devrait pas affecter le résultat d'un autre). Pour résoudre ce problème, l' objet Singleton doit être détruit entre chaque exécution de test (devant potentiellement redémarrer l'application entière), ce qui peut prendre du temps (sans parler de fastidieux et ennuyeux).

Cela dit, je suis heureux d'avoir fait cette erreur ici et non dans la mise en œuvre d'une véritable application. En fait, j'envisageais de réécrire mon dernier code pour utiliser le modèle Singleton pour certaines classes. Bien que j'aurais pu facilement annuler les modifications (tout est stocké dans un SVN, bien sûr), j'aurais quand même perdu du temps à le faire.

gablin
la source
4
Je ne recommanderais pas de le faire .. de cette façon, vous couple étroitement classe ExampleAet Config- ce qui n'est pas une bonne chose.
Paul
@ Paul: C'est vrai. Bonne prise, je n'y ai pas pensé.
gablin
3
Je recommanderais toujours de ne pas utiliser Singletons pour des raisons de testabilité. Ce sont essentiellement des variables globales et il est impossible de se moquer de la dépendance.
MattDavey
4
Je recommanderais toujours à nouveau d'utiliser des singletons pour des raisons de faute.
Raynos
1

La chose la plus simple à faire est de se coupler ExampleAà Config. Vous devriez faire la chose la plus simple, à moins qu'il n'y ait une raison impérieuse de faire quelque chose de plus complexe.

L' une des raisons pour le découplage ExampleAet Configserait d'améliorer la testabilité ExampleA. Le couplage direct dégradera la testabilité de ExampleAif Configa des méthodes qui sont lentes, complexes ou évoluent rapidement. Pour les tests, une méthode est lente si elle s'exécute plus de quelques microsecondes. Si toutes les méthodes de Configsont simples et rapides, alors je prendrais l'approche simple et directe deux ExampleAà Config.

Kevin Cline
la source
1

Votre premier exemple est un exemple de schéma d'injection de dépendance. Une classe avec une dépendance externe reçoit la dépendance par constructeur, setter, etc.

Cette approche aboutit à un code faiblement couplé. La plupart des gens pensent que le couplage lâche est une bonne chose, car vous pouvez facilement remplacer la configuration dans les cas où une instance particulière d'un objet doit être configurée différemment des autres, vous pouvez passer un objet de configuration factice pour le test, etc. sur.

La deuxième approche est plus proche du modèle créateur GRASP. Dans ce cas, l'objet crée ses propres dépendances. Il en résulte un code étroitement couplé, ce qui peut limiter la flexibilité de la classe et le rendre plus difficile à tester. Si vous avez besoin d'une instance d'une classe pour avoir une dépendance différente des autres, votre seule option est de la sous-classer.

Il peut cependant s'agir du modèle approprié dans les cas où la durée de vie de l'objet dépendant est dictée par la durée de vie de l'objet dépendant et où l'objet dépendant n'est utilisé nulle part en dehors de l'objet qui en dépend. Normalement, je conseillerais à DI d'être la position par défaut, mais vous n'avez pas à exclure complètement l'autre approche tant que vous êtes conscient de ses conséquences.

GordonM
la source
0

Si votre classe n'expose pas le $configaux classes externes, je le créerais à l'intérieur du constructeur. De cette façon, vous gardez votre état interne privé.

Si le $configrequiert nécessite que son propre état interne soit correctement défini (par exemple, nécessite une connexion à la base de données ou certains champs internes initialisés) avant utilisation, il est alors judicieux de reporter l'initialisation à un code externe (éventuellement une classe d'usine) et de l'injecter dans le constructeur. Ou, comme d'autres l'ont souligné, s'il doit être partagé entre d'autres objets.

TMN
la source
0

L'exemple A est découplé de la classe concrète Config qui est bonne , à condition que l'objet soit reçu s'il ne s'agit pas du type Config mais du type d'une superclasse abstraite de Config.

ExampleB est fortement couplé à la classe concrète Config qui est mauvaise .

L'instanciation d'un objet crée un couplage fort entre les classes. Cela devrait être fait dans une classe Factory.

Tulains Córdova
la source