Mon équipe a développé une nouvelle couche de service dans notre application. Ils ont créé un tas de services qui implémentent leurs interfaces (par exemple, ICustomerService
, IUserService
, etc.). C'est assez bien jusqu'à présent.
Voici où les choses deviennent un peu étranges: nous avons une classe appelée "CoreService", qui ressemble à ceci:
// ICoreService interface implements the interfaces of
// all services in the system 100+
public class CoreService : ICoreService
{
// I don't like these lazy instance variables. I think they are pointless
private readonly Lazy<CustomerService> _customerService;
private readonly Lazy<UserService> _userService;
public CoreService()
{
// These violate the Dependency inversion principle.
// It also news up its dependencies, which is bad.
_customerService = new CustomerService();
_userService = new UserService();
// And so forth
}
#region ICustomerService
public long? GetCustomerCount()
{
return _customerService.GetCustomerCount();
}
#endregion
#region IUserService
public User GetUser(int userId, int customerId)
{
return _userService.GetUser(userId, customerId);
}
#endregion
// ...
// And 100 other regions for all services
}
Le raisonnement de l'équipe est que les contrôleurs de l'application consommatrice peuvent facilement instancier CoreService
et utiliser ses services, et cela ne causera aucun problème de performances car tout est "paresseux".
J'ai essayé d'expliquer que c'est une mauvaise conception car:
CoreService
me semble être un anti-pattern "God Object".CustomerController
nécessite cinq services différents, injectez-les simplement via le constructeur!)Mon argument est-il valable? Y a-t-il d'autres violations des meilleures pratiques qui me manquent ici? Toute contribution serait très appréciée ici.
Modifier: j'ai mis à jour le titre car cette question est marquée comme doublon. Ce service n'est pas nécessairement un objet divin, c'est en fait un service "Passthrough" ou une façade. Mes excuses pour l'erreur.
Ce n'est pas un objet de Die .
Il semble que ce soit parce qu'il y a tellement de choses ici, mais d'une certaine manière, cela ne fait rien du tout. Il n'y a pas de code de comportement ici. Ce n'est pas un Dieu omnipotent qui fait tout. Il trouve juste tout. C'est moins du tout un véritable objet et plus une structure de données.
Ce modèle a un nom plus approprié: Service Locator . Il contraste fortement avec le Injection de dépendancevoleur modèle.
Votre plainte de testabilité est valide mais le problème est plus important que cela. Le principal principe violé ici est le principe Interface Segregation .
Voici pourquoi: Si j'arrive à cette chose avec l'intention de refactoriser le problème Dependency Inversion j'arrêterais simplement de coder en dur CoreService
. Je le remplacerais par un paramètre qui doit être transmis. Pouvez-vous voir pourquoi cela n'aide pas vraiment?
En plus d'être ennuyeux de voir un paramètre CoreService
passé dans tout, cela ne résout pas réellement le problème de dépendance car voir qu'un objet a besoin de CoreService
me dit zéro sur ses vrais besoins parce que CoreService
donne accès à tout et n'importe quoi. Nous externalisons les dépendances pour clarifier les besoins.
Si nous suivons le principe de la séparation d'interface, nous voyons qu'un objet qui, pour le moment, a besoin de CoreService
a en fait besoin de peut-être 5 des 100 choses qu'il fournit. Ce dont ces 5 choses ont besoin, c'est d'un bon nom descriptif.
Maintenant, quand je vois ce nom, je sais ce dont cette chose a besoin. Je peux aller trouver des moyens de fournir ces 5 choses. Il pourrait y avoir 2 ou 42 façons de fournir ces 5 choses. L'un de ces moyens pourrait être de passer par un test, mais cette idée va bien au-delà du test. Les tests rendent ce problème douloureusement évident.
Pour fournir ce nom et éviter un constructeur avec 5 paramètres, vous pouvez introduire un objet paramètrerefactoring.com , c2.com à condition que ces 5 choses représentent une idée cohérente avec, s'il vous plaît, un bon nom. (Il peut y avoir 2 idées qui ont besoin de 2 noms qui se cachent dans ces 5 choses. Si c'est bien, brisez-les et nommez-les).
Vous pourriez être tenté de réutiliser cet objet de paramètre si vous trouvez un autre objet qui en a besoin. Mais disons que cet objet a également besoin d'autre chose. Donc, vous ajoutez quelque chose d'autre à l'objet paramètre. Même si le premier objet n'a pas besoin de quelque chose d'autre. Eh bien, nous sommes maintenant sur le point de refaire un localisateur de service. Vous arrêtez cela en n'acceptant pas les choses dont vous n'avez pas besoin. C'est à cela que sert le principe de la séparation des interfaces.
Un autre motif qui se cache ici semble être Singletonc2.com. L'injection de dépendance a une merveilleuse alternative à cela. Construisez simplement ce dont vous avez besoin dans main et transmettez les références à ce que vous avez construit à tout ce qui en a besoin. Une fois que vous avez construit votre graphique d'objets à longue durée de vie, appelez une méthode sur l'un d'eux pour démarrer le tout. Juste essayez de ne pas devenir fo . Il est correct de rompre avec les usines et autres modèles de création tant que vous gardez code de création et de comportement séparé .
Pour convaincre les autres, vous devrez commencer à montrer comment résoudre ce problème. Créez des choses qui expriment leurs besoins et écrivez du code qui répond à ces besoins. Une simple usine avec un bon nom et son propre fichier pour y vivre obtient souvent cela. Si vous êtes obligé de trouver le reste de leurs affaires, laissez l'usine régler ce problème. Maintenant, votre objet, y compris son fichier de classe, est parfaitement inconscient de leur non-sens et ne se souciera pas de réparer le reste.
Il révèle également ses dépendances, ce qui est mauvais.
C'est un peu simpliste. Il est mauvais de "créer de nouvelles dépendances" à l'intérieur de l'objet client lorsque vous ne fournissez pas de moyen de les remplacer. Vous semblez utiliser C #. C # a nommé des arguments ! Ce qui signifie que vous pouvez satisfaire une dépendance avec un argument facultatif. Assurez-vous simplement que vous disposez d'une bonne valeur par défaut. Les bonnes valeurs par défaut doivent être choisies judicieusement. N'utilisez pas ceux qui surprennent les gens.
En outre, comme le souligne @JimmyJames, le chargement de tout sans vergogne paresseux est loin d'être idéal. Cela n'accélère pas vraiment les choses. Cela ne change que lorsque vous payez. Il peut être imprévisible lorsque vous payez pour cela et il peut être difficile de diagnostiquer les problèmes de configuration. C'est essentiellement le problème de mise en cache. Largement reconnu comme l'un des problèmes les plus difficiles en informatique (après avoir donné de bons noms aux choses). Alors s'il vous plaît ne l'utilisez pas inconsidérément.
Nous avons autre chose comme un véritable service divin.
L'ensemble du backend est accessible à partir du frontend par une seule interface de 15 méthodes environ, qui prennent toutes Stream comme argument (car il s'agit vraiment d'un accès distant à un autre processus). Maintenant, l'API stream est ridicule à coder, donc nous l'avons fait exactement une fois. L'objet de service est cet objet unique qui a les mêmes 15 méthodes et quelques autres qui se résument pour transmuter les arguments et appeler l'une des autres. L'objet de service est construit à volonté à partir de l'interface principale et d'une interface pour un objet décrivant l'utilisateur connecté.
Le service divin semble tout faire. Il effectue en fait la sérialisation des demandes et la désérialisation des réponses, pour environ 500 lignes. La philosophie de conception des méthodes est qu'un appel de méthode est une transaction, et le client peut créer une transaction de sauvegarde arbitraire de grande taille en passant un IEnumerable d'objets à sauvegarder.
L'API a en fait plusieurs implémentations, y compris deux trois simulations différentes pour les tests unitaires. Le God Service a exactement une implémentation et n'implémente aucune interface.
Je pense que l'idée d'avoir tous ces services séparés est juste idiote, et créer et injecter tous est encore plus idiot. Au moins, ce service divin les rend tous paresseux, c'est donc beaucoup plus facile à gérer.
Je n'ai jamais vu une bonne conception où l'échange de services un par un au niveau de la configuration était une bonne idée. Vous avez toujours voulu les échanger en gros morceaux (ou plus probablement à la fois) dans l'une des très nombreuses configurations valides, il n'y a donc pas d'inconvénient à cette conception de God Service. Il serait facilement adapté à la gestion d'une implémentation de tous les services de composants à charger sur la base d'une seule entrée de configuration. Étant donné que les configurations de chimères artificielles ne peuvent plus exister, il est moins difficile de supporter.
En termes simples, si j'ai bien compris votre problème ... vous avez beaucoup de services internes et ensuite vous avez un autre service externe qui est utilisé pour interagir\appeler tous ces services internes.
Dans World of Onion Architecture, nous appelons les services de domaine des services internes et les services externes en tant que services d'application. Comme le montre l'image ci-dessous,
C'est toujours une mauvaise pratique de faire des appels à tous les services internes à partir d'un seul service externe.
Le concept à retenir est que les services externes\d'application ne sont pas mauvais dans la mesure où ils ont un sens logique et pas seulement une station de vidage pour tout.
Dans le cas où vos services de domaine dépendent les uns des autres, vous pouvez les extraire dans un troisième service en les nommant correctement et y mettre des méthodes partagées.