web-dev-qa-db-fra.com

Les soi-disant «préoccupations transversales» sont-elles une excuse valable pour casser SOLID / DI / IoC?

Mes collègues aiment dire que "la journalisation/la mise en cache/etc. est une préoccupation transversale", puis continuer à utiliser le singleton correspondant partout. Pourtant, ils adorent l'IoC et la DI.

Est-ce vraiment une excuse valable pour casser le principe SOLI D ?

42
Den

Non.

SOLID existe comme lignes directrices pour tenir compte des changements inévitables. Êtes-vous vraiment jamais changer votre bibliothèque de journalisation, ou cible, ou filtrage, ou formatage, ou ...? N'allez-vous pas vraiment ne pas changer votre bibliothèque de mise en cache, ou votre cible, ou votre stratégie, ou votre portée, ou ...?

Bien sûr, vous êtes. À tout le moins , vous allez vouloir vous moquer de ces choses d'une manière saine pour les isoler pour les tests. Et si vous voulez les isoler pour les tests, vous allez probablement rencontrer une raison commerciale où vous voulez les isoler pour des raisons réelles.

Et vous obtiendrez alors l'argument selon lequel l'enregistreur lui-même gérera le changement. "Oh, si la cible/le filtrage/le formatage/la stratégie change, alors nous changerons simplement la configuration!" Ce sont des ordures. Non seulement vous avez maintenant un objet divin qui gère toutes ces choses, vous écrivez votre code en XML (ou similaire) où vous n'obtenez pas d'analyse statique, vous n'obtenez pas d'erreurs de temps de compilation, et vous ne ' t vraiment obtenir des tests unitaires efficaces.

Existe-t-il des cas de violation des lignes directrices SOLID? Absolument. Parfois, les choses ne changent pas (sans nécessiter de toute façon une réécriture complète) Parfois, une légère violation de LSP est la solution la plus propre. Parfois, la création d'une interface isolée n'a aucune valeur.

Mais la journalisation et la mise en cache (et d'autres préoccupations transversales omniprésentes) ne sont pas ces cas. Ce sont généralement d'excellents exemples des problèmes de couplage et de conception que vous obtenez lorsque vous ignorez les directives.

41
Telastyn

Oui

C'est tout l'intérêt du terme "préoccupation transversale" - cela signifie quelque chose qui ne rentre pas parfaitement dans le principe SOLID.

C'est là que l'idéalisme rencontre la réalité.

Les gens semi-nouveaux à SOLID et transversaux se heurtent souvent à ce défi mental. C'est OK, ne paniquez pas. Efforcez-vous de tout mettre en termes de SOLID, mais il y a quelques endroits comme la journalisation et la mise en cache où SOLID n'a tout simplement pas de sens. La transversalité est le frère de SOLID, ils vont de pair.

39
Klom Dark

Pour l'exploitation forestière, je pense que oui. La journalisation est omniprésente et généralement indépendante de la fonctionnalité du service. Il est courant et bien connu d'utiliser les modèles singleton du cadre de journalisation. Si vous ne le faites pas, vous créez et injectez des enregistreurs partout et vous ne le voulez pas.

Un des problèmes ci-dessus est que quelqu'un dira 'mais comment puis-je tester la journalisation?'. Je pense que je ne teste pas normalement la journalisation, au-delà de l'affirmation que je peux réellement lire les fichiers journaux et les comprendre. Lorsque j'ai vu la journalisation testée, c'est généralement parce que quelqu'un doit affirmer qu'une classe a réellement fait quelque chose et qu'ils utilisent les messages du journal pour obtenir ces commentaires. Je préfère de loin inscrire un auditeur/observateur sur cette classe et affirmer dans mes tests que cela s'appelle. Vous pouvez ensuite placer votre journal des événements dans cet observateur.

Je pense cependant que la mise en cache est un scénario complètement différent.

25
Brian Agnew

Mes 2 cents ...

Oui et non.

Vous ne devez jamais vraiment violer les principes que vous adoptez; mais, vos principes doivent toujours être nuancés et adoptés au service d'un objectif plus élevé. Ainsi, avec une compréhension correctement conditionnée, certaines violations apparentes peuvent ne pas être réelles violations de "l'esprit" ou de "l'ensemble des principes".

Les principes de SOLID en particulier, en plus d'exiger beaucoup de nuances, sont finalement subordonnés à l'objectif de "fournir un logiciel fonctionnel et maintenable". Ainsi, adhérer à tout SOLID est voué à l'échec et contradictoire lorsque cela entre en conflit avec les objectifs de SOLID. Et ici, je note souvent que livrer l'emporte sur la maintenabilité .

Alors, qu'en est-il du [~ # ~] d [~ # ~] dans [ ~ # ~] solid [~ # ~] ? Eh bien, il contribue à une maintenabilité accrue en rendant votre module réutilisable relativement agnostique à son contexte. Et nous pouvons définir le "module réutilisable" comme "une collection de code que vous prévoyez d'utiliser dans un autre contexte distinct". Et cela s'applique à une seule fonction, classe, ensemble de classes et programme.

Et oui, changer les implémentations de l'enregistreur place probablement votre module dans "un autre contexte distinct".

Alors, permettez-moi de vous proposer mes deux grandes mises en garde:

Premièrement: Tracer les lignes autour des blocs de code qui constituent "un module réutilisable" est une question de jugement professionnel. Et votre jugement se limite nécessairement à votre expérience.

Si vous ne prévoyez pas actuellement d'utiliser un module dans un autre contexte, c'est probablement OK pour qu'il en dépende impuissant. La mise en garde à la mise en garde: Vos plans sont probablement faux - mais c'est aussi OK. Plus vous écrivez module après module, plus votre sens de "si j'en aurai besoin à nouveau un jour" sera de plus en plus intuitif et précis. Mais, vous ne pourrez probablement jamais dire rétrospectivement: "J'ai tout modulaire et découplé dans toute la mesure du possible, mais sans excès . "

Si vous vous sentez coupable de vos erreurs de jugement, allez vous confesser et continuez ...

Deuxièmement: Inverser le contrôle n'est pas égal à - injection de dépendances.

Cela est particulièrement vrai lorsque vous commencez à injecter des dépendances ad nauseam . L'injection de dépendance est une tactique utile pour la stratégie globale d'IoC. Mais, je dirais que DI est moins efficace que certaines autres tactiques - comme l'utilisation d'interfaces et d'adaptateurs - points uniques d'exposition au contexte depuis le module.

Et concentrons-nous vraiment là-dessus une seconde. Parce que, même si vous injectez un Logger ad nauseam , vous devez écrire du code sur l'interface Logger. Vous ne pouviez pas commencer à utiliser un nouveau Logger d'un autre fournisseur qui prend les paramètres dans un ordre différent. Cette capacité provient du codage, dans le module, contre une interface qui existe dans le module et qui a un sous-module unique (adaptateur) pour gérer la dépendance .

Et si vous codez par rapport à un adaptateur, que le Logger soit injecté dans cet adaptateur ou découvert par l'adaptateur est généralement assez insignifiant pour l'objectif général de maintenabilité. Et plus important encore, si vous avez un adaptateur au niveau du module, il est probablement juste absurde de l'injecter dans quoi que ce soit. Il est écrit pour le module.

tl; dr - Arrêtez de discuter des principes sans tenir compte de la raison pour laquelle vous utilisez ces principes. Et, plus concrètement, il suffit de construire un Adapter pour chaque module. Utilisez votre jugement pour décider où vous dessinez les limites du "module". À partir de chaque module, allez-y et référez-vous directement au Adapter. Et bien sûr, injectez le vrai enregistreur dans le Adapter - mais pas dans chaque petite chose qui pourrait besoin de ça.

13
svidgen

L'idée que la journalisation devrait toujours être implémentée en tant que singleton est l'un de ces mensonges qui a été répété si souvent qu'il a gagné du terrain.

Depuis que les systèmes d'exploitation modernes existent, il a été reconnu que vous souhaiterez peut-être vous connecter à plusieurs endroits en fonction de la nature de la sortie .

Les concepteurs de systèmes devraient constamment remettre en question l'efficacité des solutions passées avant de les inclure aveuglément dans les nouvelles. S'ils n'effectuent pas une telle diligence, ils ne font pas leur travail.

8
Robbie Dee

La journalisation est véritablement un cas spécial.

@Telastyn écrit:

N'allez-vous vraiment jamais changer votre bibliothèque de journalisation, ou cible, ou filtrage, ou formatage, ou ...?

Si vous pensez que vous devrez peut-être modifier votre bibliothèque de journalisation, vous devriez utiliser une façade; c'est-à-dire SLF4J si vous êtes dans le monde Java.

Pour le reste, une bibliothèque de journalisation décente prend soin de changer où va la journalisation, quels événements sont filtrés, comment les événements de journal sont formatés à l'aide des fichiers de configuration de l'enregistreur et (si nécessaire) des classes de plug-in personnalisées. Il existe un certain nombre d'alternatives standard.

En bref, ce sont des problèmes résolus ... pour la journalisation ... et donc il n'est pas nécessaire d'utiliser l'injection de dépendance pour les résoudre.

Le seul cas où DI pourrait être bénéfique (par rapport aux approches de journalisation standard) est si vous souhaitez soumettre la journalisation de votre application à des tests unitaires. Cependant, je soupçonne que la plupart des développeurs diraient que la journalisation ne fait pas partie d'une fonctionnalité de classes, et non quelque chose qui doit être testé.

@Telastyn écrit:

Et vous obtiendrez alors l'argument selon lequel l'enregistreur lui-même gérera le changement. "Oh, si la cible/le filtrage/le formatage/la stratégie change, alors nous changerons simplement la configuration!" Ce sont des ordures. Non seulement vous avez maintenant un objet divin qui gère toutes ces choses, vous écrivez votre code en XML (ou similaire) où vous n'obtenez pas d'analyse statique, vous n'obtenez pas d'erreurs de temps de compilation, et vous ne ' t vraiment obtenir des tests unitaires efficaces.

Je crains que ce soit un ripost très théorique. Dans la pratique, la plupart des développeurs et intégrateurs de systèmes AIMENT le fait que vous pouvez configurer la journalisation via un fichier de configuration. Et ils AIMENT le fait qu'ils ne sont pas censés tester de façon unitaire la journalisation d'un module.

Bien sûr, si vous remplissez les configurations de journalisation, vous pouvez rencontrer des problèmes, mais ils se manifesteront soit par l'échec de l'application au démarrage, soit par une journalisation excessive/insuffisante. 1) Ces problèmes sont facilement résolus en corrigeant l'erreur dans le fichier de configuration. 2) L'alternative est un cycle complet de génération/analyse/test/déploiement à chaque fois que vous modifiez les niveaux de journalisation. Ce n'est pas acceptable.

4
Stephen C

Oui et non, mais surtout non

Je suppose que la plupart de la conversation est basée sur une instance statique vs injectée. Personne ne propose que la journalisation brise le SRP je suppose? Nous parlons principalement du "principe d'inversion de dépendance". Tbh je suis principalement d'accord avec la non réponse de Telastyn.

Quand est-il acceptable d'utiliser la statique? Parce que c'est clairement parfois acceptable. Le oui répond au point des avantages de l'abstraction et le "non" indique que c'est quelque chose que vous payez. L'une des raisons pour lesquelles votre travail est difficile est qu'il n'y a pas une seule réponse que vous pouvez écrire et appliquer à toutes les situations.

Prenez: Convert.ToInt32("1")

Je préfère ceci à:

private readonly IConverter _converter;

public MyClass(IConverter converter)
{
   Guard.NotNull(converter)
   _converter = conveter
}

.... 
var foo = _converter.ToInt32("1");

Pourquoi? J'accepte que je devrai refactoriser le code si j'ai besoin de la flexibilité nécessaire pour échanger le code de conversion. J'accepte que je ne puisse pas me moquer de ça. Je crois que la simplicité et la justesse valent ce métier.

En regardant l'autre extrémité du spectre, si IConverter était un SqlConnection, je serais assez horrifié de voir cela comme un appel statique. Les raisons de l'échec sont évidentes. Je voudrais souligner qu'un SQLConnection peut être assez "transversal" dans une applciation, donc je n'aurais pas utilisé ces mots exacts.

La journalisation ressemble-t-elle plus à un SQLConnection ou à Convert.ToInt32? Je dirais plus comme 'SQLConnection`.

Vous devriez vous moquer de la journalisation. Il parle au monde extérieur. Lorsque j'écris une méthode en utilisant Convert.ToIn32, Je l'utilise comme un outil pour calculer une autre sortie testable séparément de la classe. Je n'ai pas besoin de vérifier que Convert a été appelé correctement lors de la vérification que "1" + "2" == "3". La journalisation est différente, c'est une sortie entièrement indépendante de la classe. Je suppose que c'est une sortie qui a de la valeur pour vous, l'équipe d'assistance et l'entreprise. Votre classe ne fonctionne pas si la journalisation n'est pas correcte, donc les tests unitaires ne devraient pas réussir. Vous devriez tester ce que votre classe enregistre. Je pense que c'est l'argument tueur, je pourrais vraiment m'arrêter ici.

Je pense aussi que c'est quelque chose qui est susceptible de changer. Une bonne journalisation n'imprime pas seulement des chaînes, c'est une vue de ce que fait votre application (je suis un grand fan de la journalisation basée sur les événements). J'ai vu la journalisation de base se transformer en interfaces utilisateur de rapport assez élaborées. Il est évidemment beaucoup plus facile de se diriger dans cette direction si votre journalisation ressemble à _logger.Log(new ApplicationStartingEvent()) et moins à Logger.Log("Application has started"). On pourrait dire que cela crée un inventaire pour un avenir qui ne se produira peut-être jamais, c'est un appel au jugement et je pense que cela en vaut la peine.

En fait, dans un de mes projets personnels, j'ai créé une interface utilisateur sans journalisation en utilisant uniquement le _logger Pour comprendre ce que faisait l'application. Cela signifiait que je n'avais pas à écrire de code pour comprendre ce que faisait l'application, et je me suis retrouvé avec une journalisation solide. J'ai l'impression que si mon attitude vis-à-vis de l'exploitation forestière était simple et immuable, cette idée ne me serait pas venue à l'esprit.

Je suis donc d'accord avec Telastyn pour le cas de la journalisation.

3
Nathan Cooper

Les premières préoccupations transversales ne sont pas des éléments constitutifs majeurs et ne doivent pas être traitées comme des dépendances dans un système. Un système devrait fonctionner si par ex. L'enregistreur n'est pas initialisé ou le cache ne fonctionne pas. Comment allez-vous rendre le système moins couplé et cohésif? C'est là que SOLID entre en scène dans la conception du système OO.

Garder l'objet en tant que singleton n'a rien à voir avec SOLID. C'est le cycle de vie de votre objet pendant combien de temps vous voulez que l'objet vive en mémoire.

Une classe qui a besoin d'une dépendance pour s'initialiser ne devrait pas savoir si l'instance de classe fournie est singleton ou transitoire. Mais tldr; si vous écrivez Logger.Instance.Log () dans chaque méthode ou classe, alors c'est un code problématique (odeur de code/couplage dur) vraiment désordonné. C'est le moment où les gens commencent à abuser de SOLID. Et mes collègues développeurs comme OP commencent à poser une véritable question comme celle-ci.

3
vendettamit

Oui et Non !

Oui: je pense qu'il est raisonnable que différents sous-systèmes (ou couches sémantiques ou bibliothèques ou autres notions de regroupement modulaire) acceptent chacun (le même ou) enregistreur potentiellement différent lors de leur initialisation plutôt que tous les sous-systèmes reposant sur le même singleton partagé commun .

Cependant,

Non: il est en même temps déraisonnable de paramétrer la journalisation dans chaque petit objet (par constructeur ou méthode d'instance). Pour éviter les ballonnements inutiles et inutiles, les petites entités doivent utiliser l'enregistreur singleton de leur contexte englobant.


C'est une raison parmi tant d'autres de penser à la modularité des niveaux: les méthodes sont regroupées en classes, tandis que les classes sont regroupées en sous-systèmes et/ou couches sémantiques. Ces faisceaux plus gros sont de précieux outils d'abstraction; nous devons donner des considérations différentes dans les limites modulaires que lors de leur franchissement.

3
Erik Eidt

Tout d'abord, il commence par un cache singleton solide, les prochaines choses que vous voyez sont des singletons forts pour la couche de base de données présentant l'état global, des API non descriptives de classes et du code non testable.

Si vous décidez de ne pas avoir de singleton pour une base de données, ce n'est probablement pas une bonne idée d'avoir un singleton pour un cache, après tout, ils représentent un concept très similaire, le stockage de données, en utilisant uniquement des mécanismes différents.

L'utilisation d'un singleton dans une classe transforme une classe ayant une quantité spécifique de dépendances en une classe ayant, théoriquement, un nombre infini d'entre elles, car vous ne savez jamais ce qui est vraiment caché derrière la méthode statique.

Au cours de la dernière décennie, j'ai passé de la programmation, il n'y a eu qu'un seul cas où j'ai été témoin d'un effort pour changer la logique de journalisation (qui a ensuite été écrite en singleton). Donc, même si j'aime les injections de dépendance, la journalisation n'est vraiment pas une préoccupation énorme. Cache, d'autre part, je ferais certainement toujours une dépendance.

3
Andy

J'ai résolu ce problème en utilisant une combinaison d'héritage et de traits (également appelés mixins dans certaines langues). Les traits sont très pratiques pour résoudre ce genre de problème transversal. C'est généralement une fonctionnalité linguistique, donc je pense que la vraie réponse est que cela dépend des fonctionnalités linguistiques.

2
RibaldEddie