Lors de la conception d'un système, je suis souvent confronté au problème d'avoir un tas de modules (journalisation, accès à la base de données, etc.) utilisés par les autres modules. La question est de savoir comment procéder pour fournir ces composants à d'autres composants. Deux réponses semblent être une injection de dépendance possible ou l'utilisation du modèle d'usine. Cependant, les deux semblent faux:
(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)
Voici une situation typique avec laquelle j'ai un problème: j'ai des classes d'exception, qui utilisent des descriptions d'erreurs chargées à partir de la base de données, en utilisant une requête qui a un paramètre de paramètre de langue utilisateur, qui est dans l'objet de session utilisateur. Donc, pour créer une nouvelle exception, j'ai besoin d'une description, qui nécessite une session de base de données et la session utilisateur. Je suis donc voué à faire glisser tous ces objets sur toutes mes méthodes au cas où je pourrais avoir besoin de lever une exception.
Comment puis-je résoudre un tel problème ??
Utilisez l'injection de dépendances, mais chaque fois que vos listes d'arguments constructeurs deviennent trop grandes, refactorisez-la à l'aide d'un Facade Service . L'idée est de regrouper certains des arguments du constructeur, en introduisant une nouvelle abstraction.
Par exemple, vous pouvez introduire un nouveau type SessionEnvironment
encapsulant un DBSessionProvider
, le UserSession
et le Descriptions
chargé. Cependant, pour savoir quelles abstractions ont le plus de sens, il faut connaître les détails de votre programme.
Une question similaire a déjà été posée ici sur SO .
L'injection de dépendances gonfle massivement les listes d'arguments des constructeurs et elle étale certains aspects partout dans votre code.
De cela, il ne semble pas que vous compreniez DI correctement - l'idée est d'inverser le modèle d'instanciation d'objet à l'intérieur d'une usine.
Votre problème spécifique semble être un problème plus général OOP. Pourquoi les objets ne peuvent-ils pas simplement lancer des exceptions normales non lisibles par l'homme pendant leur exécution, puis avoir quelque chose avant le dernier essai/catch qui intercepte cette exception, et à ce stade utilise les informations de session pour lever une nouvelle exception, plus jolie?
Une autre approche serait d'avoir une fabrique d'exceptions, qui est transmise aux objets via leurs constructeurs. Au lieu de lever une nouvelle exception, la classe peut lancer une méthode de la fabrique (par exemple throw PrettyExceptionFactory.createException(data)
.
Gardez à l'esprit que vos objets, en dehors de vos objets d'usine, ne doivent jamais utiliser l'opérateur new
. Les exceptions sont généralement le seul cas spécial, mais dans votre cas, elles pourraient être une exception!
Vous avez déjà très bien répertorié les inconvénients du modèle d'usine statique, mais je ne suis pas tout à fait d'accord avec les inconvénients du modèle d'injection de dépendance:
Cette injection de dépendance vous oblige à écrire du code pour chaque dépendance n'est pas un bogue, mais une fonctionnalité: elle vous oblige à penser si vous avez vraiment besoin de ces dépendances, favorisant ainsi un couplage lâche. Dans votre exemple:
Voici une situation typique avec laquelle j'ai un problème: j'ai des classes d'exception, qui utilisent des descriptions d'erreurs chargées à partir de la base de données, en utilisant une requête qui a un paramètre de paramètre de langue utilisateur, qui est dans l'objet de session utilisateur. Donc, pour créer une nouvelle exception, j'ai besoin d'une description, qui nécessite une session de base de données et la session utilisateur. Je suis donc voué à faire glisser tous ces objets sur toutes mes méthodes au cas où je pourrais avoir besoin de lever une exception.
Non, tu n'es pas condamné. Pourquoi est-il de la responsabilité de la logique métier de localiser vos messages d'erreur pour une session utilisateur particulière? Et si, dans le futur, vous souhaitiez utiliser ce service métier à partir d'un programme batch (qui n'a pas de session utilisateur ...)? Ou que faire si le message d'erreur ne doit pas être affiché à l'utilisateur actuellement connecté, mais à son superviseur (qui peut préférer une langue différente)? Ou si vous vouliez réutiliser la logique métier sur le client (qui n'a pas accès à une base de données ...)?
De toute évidence, la localisation des messages dépend de qui regarde ces messages, c'est-à-dire que c'est la responsabilité de la couche de présentation. Par conséquent, je lèverais des exceptions ordinaires du service métier, qui transportent un identifiant de message qui peut ensuite être recherché dans le gestionnaire d'exceptions de la couche de présentation dans la source de message qu'il utilise.
De cette façon, vous pouvez supprimer 3 dépendances inutiles (UserSession, ExceptionFactory et probablement des descriptions), rendant ainsi votre code à la fois plus simple et plus polyvalent.
De manière générale, je n'utiliserais des usines statiques que pour les choses dont vous avez besoin d'un accès omniprésent et qui sont garanties d'être disponibles dans tous les environnements auxquels nous pourrions souhaiter exécuter le code (comme la journalisation). Pour tout le reste, j'utiliserais une injection de dépendance ancienne.
Les usines rendent les tests difficiles et ne permettent pas de permuter facilement les implémentations. Ils ne rendent pas non plus les dépendances apparentes (par exemple, vous examinez une méthode, inconscient du fait qu'elle appelle une méthode qui appelle une méthode qui appelle une méthode qui utilise une base de données).
Je ne suis pas tout à fait d'accord. Du moins pas en général.
Usine simple:
public IFoo GetIFoo()
{
return new Foo();
}
Injection simple:
myDependencyInjector.Bind<IFoo>().To<Foo>();
Les deux extraits ont le même objectif, ils établissent un lien entre IFoo
et Foo
. Tout le reste n'est que de la syntaxe.
Changer Foo
en ADifferentFoo
demande exactement autant d'efforts dans l'un ou l'autre exemple de code.
J'ai entendu des gens dire que l'injection de dépendances permet d'utiliser différentes liaisons, mais le même argument peut être avancé pour la fabrication de différentes usines. Choisir la bonne reliure est exactement aussi complexe que choisir la bonne usine.
Les méthodes d'usine vous permettent par ex. utilisez Foo
à certains endroits et ADifferentFoo
à d'autres endroits. Certains peuvent appeler cela bon (utile si vous en avez besoin), certains peuvent appeler cela mauvais (vous pouvez faire un travail à moitié assumé pour tout remplacer).
Cependant, il n'est pas si difficile d'éviter cette ambiguïté si vous vous en tenez à une seule méthode qui retourne IFoo
afin que vous ayez toujours une seule source. Si vous ne voulez pas vous tirer une balle dans le pied, ne tenez pas un pistolet chargé ou assurez-vous de ne pas le viser.
L'injection de dépendances gonfle massivement les listes d'arguments des constructeurs et elle étale certains aspects partout dans votre code.
C'est pourquoi certaines personnes préfèrent récupérer explicitement les dépendances dans le constructeur, comme ceci:
public MyService()
{
_myFoo = DependencyFramework.Get<IFoo>();
}
J'ai entendu des arguments pro (pas de gonflement du constructeur), j'ai entendu des arguments con (l'utilisation du constructeur permet plus d'automatisation DI).
Personnellement, alors que j'ai cédé à notre senior qui veut utiliser des arguments de constructeur, j'ai remarqué un problème avec la liste déroulante dans VS (en haut à droite, pour parcourir les méthodes de la classe actuelle) où les noms de méthode disparaissent quand on des signatures de méthode est plus longue que mon écran (=> le constructeur gonflé).
Sur le plan technique, je m'en fiche de toute façon. L'une ou l'autre option demande environ autant d'efforts pour taper. Et puisque vous utilisez DI, vous n'appelez généralement pas un constructeur manuellement de toute façon. Mais le bogue de Visual Studio UI me fait préférer de ne pas gonfler l'argument constructeur.
En remarque, l'injection de dépendances et les usines ne s'excluent pas mutuellement. J'ai eu des cas où au lieu d'insérer une dépendance, j'ai inséré une fabrique qui génère des dépendances (NInject vous permet heureusement d'utiliser Func<IFoo>
pour que vous n'ayez pas besoin de créer une classe d'usine réelle).
Les cas d'utilisation sont rares, mais ils existent.
Utilisez l'injection de dépendance. L'utilisation d'usines statiques est un emploi du Service Locator
antipattern. Voir le travail fondateur de Martin Fowler ici - http://martinfowler.com/articles/injection.html
Si vos arguments de constructeur deviennent trop volumineux et que vous n'utilisez pas un conteneur DI, écrivez vos propres usines pour l'instanciation, ce qui permet de le configurer, soit par XML, soit en liant une implémentation à une interface.
J'irais aussi bien avec l'injection de dépendance. N'oubliez pas que l'ID ne se fait pas uniquement par le biais de constructeurs, mais également par le biais de setters de propriétés. Par exemple, l'enregistreur peut être injecté en tant que propriété.
En outre, vous souhaiterez peut-être utiliser un conteneur IoC qui peut alléger une partie de la charge pour vous, par exemple en conservant les paramètres du constructeur aux éléments nécessaires à l'exécution par votre logique de domaine (en conservant le constructeur d'une manière qui révèle l'intention de la classe et les dépendances de domaine réel) et peut-être injecter d'autres classes d'assistance via les propriétés.
Une étape supplémentaire que vous voudrez peut-être aller est la programmation orientée aspect, qui est implémentée dans de nombreux cadres principaux. Cela peut vous permettre d'intercepter (ou de "conseiller" d'utiliser la terminologie AspectJ) le constructeur de la classe et d'injecter les propriétés pertinentes, en lui attribuant peut-être un attribut spécial.
Dans cet exemple factice, une classe de fabrique est utilisée au moment de l'exécution pour déterminer le type d'objet de requête HTTP entrant à instancier, en fonction de la méthode de requête HTTP. L'usine elle-même est injectée avec une instance d'un conteneur d'injection de dépendance. Cela permet à l'usine de déterminer sa durée d'exécution et de laisser le conteneur d'injection de dépendances gérer les dépendances. Chaque objet de requête HTTP entrant a au moins quatre dépendances (superglobaux et autres objets).
<?php
namespace TFWD\Factories;
/**
* A class responsible for instantiating
* InboundHttpRequest objects (PHP 7.x)
*
* @author Anthony E. Rutledge
* @version 2.0
*/
class InboundHttpRequestFactory
{
private const GET = 'GET';
private const POST = 'POST';
private const PUT = 'PUT';
private const PATCH = 'PATCH';
private const DELETE = 'DELETE';
private static $di;
private static $method;
// public function __construct(Injector $di, Validator $httpRequestValidator)
// {
// $this->di = $di;
// $this->method = $httpRequestValidator->getMethod();
// }
public static function setInjector(Injector $di)
{
self::$di = $di;
}
public static setMethod(string $method)
{
self::$method = $method;
}
public static function getRequest()
{
if (self::$method == self::GET) {
return self::$di->get('InboundGetHttpRequest');
} elseif ((self::$method == self::POST) && empty($_FILES)) {
return self::$di->get('InboundPostHttpRequest');
} elseif (self::$method == self::POST) {
return self::$di->get('InboundFilePostHttpRequest');
} elseif (self::$method == self::PUT) {
return self::$di->get('InboundPutHttpRequest');
} elseif (self::$method == self::PATCH) {
return self::$di->get('InboundPatchHttpRequest');
} elseif (self::$method == self::DELETE) {
return self::$di->get('InboundDeleteHttpRequest');
} else {
throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
}
}
}
Le code client pour une configuration de type MVC, dans un index.php
Centralisé, pourrait ressembler à ceci (validations omises).
InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router'); // The Router class depends on InboundHttpRequest objects.
$router->dispatch();
Alternativement, vous pouvez supprimer la nature statique (et le mot-clé) de la fabrique et autoriser un injecteur de dépendances à gérer le tout (d'où le constructeur commenté). Cependant, vous devrez modifier certaines (pas les constantes) des références de membre de classe (self
) en membres d'instance ($this
).