En considérant un logiciel de taille moyenne à grande avec une architecture à n couches et une injection de dépendances, je suis à l'aise de dire qu'un objet appartenant à une couche peut dépendre des objets des couches inférieures mais jamais des objets des couches supérieures.
Mais je ne sais pas quoi penser des objets qui dépendent d'autres objets du même calque.
À titre d'exemple, supposons une application avec trois couches et plusieurs objets comme celui de l'image. Évidemment, les dépendances de haut en bas (flèches vertes) sont correctes, de bas en haut (flèche rouge) ne le sont pas, mais qu'en est-il d'une dépendance à l'intérieur du même calque (flèche jaune)?
À l'exclusion de la dépendance circulaire, je suis curieux de savoir tout autre problème qui pourrait survenir et à quel point l'architecture en couches est violée dans ce cas.
Oui, les objets d'une couche peut ont des dépendances directes entre eux, parfois même cycliques - c'est en fait ce qui fait la différence de base avec les dépendances autorisées entre les objets de différentes couches, où aucune dépendance directe n'est autorisé, ou tout simplement une direction de dépendance stricte.
Cependant, cela ne signifie pas qu'ils devraient avoir de telles dépendances de manière arbitraire. Cela dépend en fait de ce que représentent vos couches, de la taille du système et de la responsabilité des pièces. Notez que "architecture en couches" est un terme vague, il y a une énorme variation de ce que cela signifie réellement dans différents types de systèmes.
Par exemple, supposons que vous ayez un "système à couches horizontales", avec une couche de base de données, une couche métier et une couche d'interface utilisateur (UI). Disons que la couche d'interface utilisateur contient au moins plusieurs dizaines de classes de dialogue différentes.
On peut choisir une conception où aucune des classes de dialogue ne dépend directement d'une autre classe de dialogue. On peut choisir une conception où les "dialogues principaux" et les "dialogues secondaires" existent et il n'y a que des dépendances directes des dialogues "principaux" à "secondaires". Ou on peut préférer une conception où n'importe quelle classe d'interface utilisateur existante peut utiliser/réutiliser toute autre classe d'interface utilisateur de la même couche.
Ce sont tous des choix de conception possibles, peut-être plus ou moins judicieux selon le type de système que vous construisez, mais aucun d'entre eux ne rend la "superposition" de votre système invalide.
Je suis à l'aise de dire qu'un objet appartenant à un calque peut dépendre des objets des calques inférieurs
Pour être honnête, je ne pense pas que vous devriez être à l'aise avec cela. Lorsque je traite avec autre chose qu'un système trivial, je viserais à ce que toutes les couches ne dépendent que des abstractions d'autres couches; à la fois inférieur et supérieur.
Ainsi, par exemple, Obj 1
ne doit pas dépendre de Obj 3
. Il devrait avoir une dépendance par exemple sur IObj 3
et doit être informé de l'implémentation de cette abstraction avec laquelle elle doit fonctionner lors de l'exécution. La chose qui fait le récit ne devrait être liée à aucun des niveaux car il est de mapper ces dépendances. Cela pourrait être un conteneur IoC, un code personnalisé appelé par exemple par main
qui utilise DI pur. Ou lors d'un Push, il pourrait même s'agir d'un localisateur de services. Quoi qu'il en soit, les dépendances n'existent pas entre les couches jusqu'à ce que cette chose fournisse le mappage.
Mais je ne sais pas quoi penser des objets qui dépendent d'autres objets du même calque.
Je dirais que c'est la seule fois où vous devriez avoir des dépendances directes. Cela fait partie du fonctionnement interne de cette couche et peut être modifié sans affecter les autres couches. Ce n'est donc pas un couplage nuisible.
Regardons cela pratiquement
Obj 3
sait maintenant Obj 4
existe. Et alors? Pourquoi nous en soucions-nous?
DIP dit
"Les modules de haut niveau ne devraient pas dépendre de modules de bas niveau. Les deux devraient dépendre d'abstractions."
OK, mais tous les objets ne sont-ils pas des abstractions?
DIP dit aussi
"Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions."
OK, mais si mon objet est correctement encapsulé, cela ne cache-t-il aucun détail?
Certaines personnes aiment insister aveuglément sur le fait que chaque objet a besoin d'une interface de mot-clé. Je n'en fais pas partie. J'aime insister aveuglément sur le fait que si vous ne les utilisez pas maintenant, vous avez besoin d'un plan pour faire face à quelque chose comme eux plus tard.
Si votre code est entièrement refactorable sur chaque version, vous pouvez simplement extraire les interfaces plus tard si vous en avez besoin. Si vous avez publié du code que vous ne voulez pas recompiler et que vous souhaitez que vous parliez via une interface, vous aurez besoin d'un plan.
Obj 3
sait Obj 4
existe. Mais Obj 3
savoir si Obj 4
est concret?
C'est ici pourquoi il est si agréable de NE PAS répandre new
partout. Si Obj 3
ne sait pas si Obj 4
est concret, probablement parce qu'il ne l'a pas créé, alors si vous vous êtes glissé plus tard et avez tourné Obj 4
dans une classe abstraite Obj 3
je m'en fous.
Si vous pouvez le faire, alors Obj 4
a toujours été complètement abstrait. La seule chose qui crée une interface entre eux dès le départ vous donne l'assurance que quelqu'un n'ajoutera pas accidentellement du code qui révèle que Obj 4
est concret en ce moment. Les constructeurs protégés peuvent atténuer ce risque mais cela conduit à une autre question:
Obj 3 et obj 4 sont-ils dans le même package?
Les objets sont souvent regroupés d'une manière ou d'une autre (package, espace de noms, etc.). Lorsqu'ils sont regroupés de façon judicieuse, les impacts plus probables au sein d'un groupe plutôt qu'entre les groupes changent.
J'aime grouper par fonctionnalité. Si Obj 3
et Obj 4
sont dans le même groupe et calque, il est très peu probable que vous en ayez publié un et que vous ne souhaitiez pas le refactoriser tout en n'ayant besoin que de changer l'autre. Cela signifie que ces objets sont moins susceptibles de bénéficier d'une abstraction entre eux avant d'avoir un besoin clair.
Si vous traversez une limite de groupe, c'est vraiment une bonne idée de laisser les objets de chaque côté varier indépendamment.
Cela devrait être aussi simple mais malheureusement, les deux Java et C # ont fait des choix malheureux qui compliquent cela.
En C #, il est de tradition de nommer chaque interface de mot-clé avec un préfixe I
. Cela oblige les clients à SAVOIR qu'ils parlent à une interface de mots clés. Cela perturbe le plan de refactoring.
Dans Java c'est la tradition d'utiliser un meilleur modèle de nommage: FooImple implements Foo
Cependant, cela n'aide qu'au niveau du code source puisque Java compile les interfaces de mots-clés vers un autre binaire. Cela signifie que lorsque vous refactorisez Foo
du client concret au client abstrait qui ne ' t besoin d'un seul caractère de code modifié doit encore être recompilé.
Ce sont ces BOGUES dans ces langages particuliers qui empêchent les gens de repousser l'abstrait formel jusqu'à ce qu'ils en aient vraiment besoin. Vous n'avez pas dit quelle langue vous utilisez mais comprenez qu'il y a des langues qui n'ont tout simplement pas ces problèmes.
Vous n'avez pas dit la langue que vous utilisez, je vous invite donc à analyser attentivement votre langue et votre situation avant de décider que ce seront des interfaces de mots clés partout.
Le principe YAGNI joue ici un rôle clé. Mais il en va de même "S'il vous plaît, rendez-vous difficile de me tirer une balle dans le pied".
En plus des réponses ci-dessus, je pense que cela pourrait vous aider à le regarder sous différents points de vue.
Par exemple, du point de vue règle de dépendance . DR est une règle suggérée par Robert C. Martin pour sa célèbre Architecture propre .
Ça dit
Les dépendances du code source doivent pointer uniquement vers l'intérieur, vers des stratégies de niveau supérieur.
Par politiques de niveau supérieur , il entend niveau supérieur d'abstractions. Composants qui fuient sur les détails d'implémentation, comme par exemple les interfaces ou les classes abstraites contrairement aux classes concrètes ou aux structures de données.
Le fait est que la règle n'est pas limitée à la dépendance inter-couches. Il souligne uniquement la dépendance entre les morceaux de code, quel que soit l'emplacement ou la couche à laquelle ils appartiennent.
Donc, non, il n'y a rien de mal à avoir des dépendances entre les éléments d'une même couche. Cependant, la dépendance peut toujours être implémentée pour transmettre avec le principe de dépendance stable .
Un autre point de vue est SRP.
Le découplage est notre façon de briser les dépendances nuisibles et de transmettre avec certaines des meilleures pratiques comme l'inversion de dépendance (IoC). Cependant, les éléments qui partagent les raisons de changer ne donnent pas de raisons de découplage car les éléments ayant la même raison de changer changeront en même temps (très probablement) et ils seront déployés en même temps aussi. Si tel est le cas entre Obj3
et Obj4
alors, encore une fois, il n'y a rien de fondamentalement mauvais.