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?
object-oriented
unit-testing
configuration
Prisonnier
la source
la source
Réponses:
Je pense que le premier vous donnera la possibilité de créer un
config
objet ailleurs et de le transmettreExampleA
. 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 unconfig
objet 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.la source
N'oubliez pas la testabilité!
Habituellement, si le comportement de la
Example
classe 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 deExample
classe).Par conséquent, j'irais avec la première option de
ExampleA
la source
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é.
la source
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é.
la source
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êmeConfig
instance sur plusieurs classes. Cependant, s'il ne doit y avoir qu'une seuleConfig
instance dans toute l'application, envisagez d'appliquer le modèle SingletonConfig
pour éviter d'avoir plusieurs instances deConfig
. Et siConfig
c'est un Singleton, vous pouvez faire ce qui suit à la place:Dans
ExampleB
, d'autre part, vous obtiendrez toujours une instance distincte deConfig
pour chaque instance deExampleB
.La version à appliquer dépend vraiment de la manière dont l'application traitera les instances de
Config
:ExampleX
doit avoir une instance distincte deConfig
, allez avecExampleB
;ExampleX
partage une (et une seule) instance deConfig
, utilisezExampleA with Config Singleton
;ExampleX
peuvent utiliser différentes instances deConfig
, respectezExampleA
.Pourquoi se convertir
Config
en 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):
Si
ExampleA
récupère sa propre référence à l'Config
instance, les classes seront étroitement couplées. Il n'y aura aucun moyen d'avoir une instance deExampleA
pour utiliser une version différente deConfig
(par exemple une sous-classe). C'est horrible si vous voulez tester enExampleA
utilisant une instance de maquetteConfig
car il n'y a aucun moyen de le fournirExampleA
.La prémisse de qu'il y en aura une, et une seule,
Config
peut - ê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 deConfig
seront souhaitables, il n'y a aucun moyen d'y parvenir sans réécrire le code.Même si l'instance une et une seule de
Config
est peut-être vraie pour toute l'éternité, il peut arriver que vous souhaitiez pouvoir utiliser une sous-classe deConfig
(tout en n'ayant qu'une seule instance). Mais, puisque le code obtient directement l'instance viagetInstance()
ofConfig
, qui est unestatic
méthode, il n'y a aucun moyen d'obtenir la sous-classe. Encore une fois, le code doit être réécrit.Le fait que les
ExampleA
utilisationsConfig
soient masquées, du moins lors de la simple visualisation de l'API deExampleA
. 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 modificationsConfig
sans examiner la mise en œuvre de toutes les autres classes.Même si le fait d'
ExampleA
utiliser un SingletonConfig
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.
la source
ExampleA
etConfig
- ce qui n'est pas une bonne chose.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
ExampleA
etConfig
serait d'améliorer la testabilitéExampleA
. Le couplage direct dégradera la testabilité deExampleA
ifConfig
a 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 deConfig
sont simples et rapides, alors je prendrais l'approche simple et directe deuxExampleA
àConfig
.la source
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.
la source
Si votre classe n'expose pas le
$config
aux classes externes, je le créerais à l'intérieur du constructeur. De cette façon, vous gardez votre état interne privé.Si le
$config
requiert 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.la source
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.
la source