web-dev-qa-db-fra.com

Pourquoi est-ce une bonne idée pour les couches d'application "inférieures" de ne pas être au courant des couches "supérieures"?

Dans une application Web MVC typique (bien conçue), la base de données n'a pas connaissance du code de modèle, le code de modèle n'a pas connaissance du code du contrôleur et le code du contrôleur n'a pas connaissance du code d'affichage. (J'imagine que vous pourriez même commencer aussi loin que le matériel, ou peut-être même plus loin, et le motif pourrait être le même.)

En allant dans l'autre sens, vous pouvez descendre d'une seule couche. La vue peut connaître le contrôleur mais pas le modèle; le contrôleur peut connaître le modèle mais pas la base de données; le modèle peut connaître la base de données mais pas le système d'exploitation. (Tout ce qui est plus profond n'est probablement pas pertinent.)

Je peux intuitivement comprendre pourquoi c'est une bonne idée mais je ne peux pas l'articuler. Pourquoi ce style unidirectionnel de superposition est-il une bonne idée?

67
Jason Swett

Les couches, les modules, voire l'architecture elle-même, sont des moyens de rendre les programmes informatiques plus faciles à comprendre par l'homme. La méthode numériquement optimale pour résoudre un problème est presque toujours un gâchis enchevêtré impie de code non modulaire, auto-référencé ou même auto-modifiable - que ce soit du code assembleur fortement optimisé dans des systèmes embarqués avec des contraintes de mémoire ou des séquences d'ADN paralysantes après des millions d'années de la pression de sélection. De tels systèmes n'ont pas de couches, pas de direction discernable du flux d'information, en fait aucune structure que nous pouvons discerner du tout. Pour tout le monde sauf leur auteur, ils semblent fonctionner par pure magie.

En génie logiciel, nous voulons éviter cela. Une bonne architecture est une décision délibérée de sacrifier une certaine efficacité pour rendre le système compréhensible par des gens normaux. Comprendre une chose à la fois est plus facile que de comprendre deux choses qui n'ont de sens que lorsqu'elles sont utilisées ensemble. C'est pourquoi les modules et les couches sont une bonne idée.

Mais inévitablement, les modules doivent appeler des fonctions les uns des autres et les couches doivent être créées les unes sur les autres. Donc, dans la pratique, il est toujours nécessaire de construire des systèmes de sorte que certaines pièces nécessitent d'autres pièces. Le compromis préféré est de les construire de telle manière qu'une partie en nécessite une autre, mais cette partie ne nécessite pas la première. Et c'est exactement ce que nous donne la superposition unidirectionnelle: il est possible de comprendre le schéma de la base de données sans connaître les règles métier, et de comprendre les règles métier sans connaître l'interface utilisateur. Ce serait bien d'avoir une indépendance dans les deux sens - permettre à quelqu'un de programmer une nouvelle interface utilisateur sans connaître quoi que ce soit des règles métier - mais en pratique, cela n'est pratiquement jamais possible. Des règles générales telles que "Pas de dépendances cycliques" ou "Les dépendances ne doivent atteindre qu'un seul niveau" capturent simplement la limite pratiquement réalisable de l'idée fondamentale qu'une chose à la fois est plus facile à comprendre que deux choses.

122
Kilian Foth

La motivation fondamentale est la suivante: vous voulez être capable d'extraire un calque entier et d'en remplacer un complètement différent (réécrit), et NOBODY DEVRAIT (ÊTRE CAPABLE DE) NOTER LA DIFFÉRENCE.

L'exemple le plus évident est d'extraire la couche inférieure et de la remplacer par une autre. C'est ce que vous faites lorsque vous développez la ou les couches supérieures par rapport à une simulation du matériel, puis remplacez-le par le vrai matériel.

L'exemple suivant est lorsque vous extrayez une couche intermédiaire et remplacez une couche intermédiaire différente. Prenons une application qui utilise un protocole qui s'exécute sur RS-232. Un jour, vous devez changer complètement l'encodage du protocole, car "autre chose a changé". (Exemple: passage du codage direct ASCII au codage Reed-Solomon de ASCII streams, car vous travailliez sur une liaison radio du centre-ville de LA à Marina Del Rey) , et vous travaillez maintenant sur une liaison radio du centre-ville de Los Angeles à une sonde en orbite autour d'Europa, l'une des lunes de Jupiter, et cette liaison nécessite une bien meilleure correction d'erreur directe.)

La seule façon de faire fonctionner cela est si chaque couche exporte une interface connue et définie vers la couche supérieure et attend une interface connue et définie vers la couche inférieure.

Maintenant, ce n'est pas exactement le cas que les couches inférieures ne connaissent RIEN sur les couches supérieures. Au contraire, ce que la couche inférieure sait, c'est que la couche immédiatement supérieure fonctionnera précisément conformément à son interface définie. Il ne peut rien savoir de plus, car par définition tout ce qui n'est pas dans l'interface définie est susceptible de changer SANS PRÉAVIS.

La couche RS-232 ne sait pas si elle exécute ASCII, Reed-Solomon, Unicode (page de code arabe, page de code japonaise, page de code Rigellian Beta) ou quoi. Il sait juste qu'il obtient une séquence d'octets et qu'il écrit ces octets sur un port. La semaine prochaine, il pourrait obtenir une séquence d'octets complètement différente de quelque chose de complètement différent. Il s'en fiche. Il déplace juste des octets.

La première (et la meilleure) explication de la conception en couches est le papier classique de Dijkstra "Structure of the T.H.E. Multiprogramming System" . Il est nécessaire de lire dans cette entreprise.

63
John R. Strohm

Parce que les niveaux supérieurs maichangera.

Lorsque cela se produit, que ce soit en raison des changements d'exigences, de nouveaux utilisateurs, de technologies différentes, une application modulaire (c'est-à-dire en couches unidirectionnelles) devrait nécessiter moins de maintenance et être plus facilement adaptée pour répondre aux nouveaux besoins.

8
user39685

Je pense que la raison principale est que cela rend les choses plus étroitement couplées. Plus le couplage est serré, plus il est probable que des problèmes surviennent plus tard. Voir cet article plus d'informations: Couplage

En voici un extrait:

Désavantages

Les systèmes étroitement couplés ont tendance à présenter les caractéristiques de développement suivantes, qui sont souvent considérées comme des inconvénients: Un changement dans un module force généralement un effet d'entraînement des changements dans d'autres modules. L'assemblage des modules peut nécessiter plus d'efforts et/ou de temps en raison de la dépendance accrue entre les modules. Un module particulier peut être plus difficile à réutiliser et/ou à tester car les modules dépendants doivent être inclus.

Cela étant dit, la raison d'avoir un système couplé supérieur est pour des raisons de performances. L'article que j'ai mentionné contient également des informations à ce sujet.

4
barrem23

OMI, c'est très simple. Vous ne pouvez pas réutiliser quelque chose qui continue de référencer le contexte dans lequel il est utilisé.

4
Erik Reppen

Les couches ne doivent pas avoir de dépendances bidirectionnelles

Les avantages d'une architecture en couches sont que les couches doivent être utilisables indépendamment:

  • vous devriez pouvoir créer une couche de présentation différente en plus de la première sans changer la couche inférieure (par exemple, créer une couche API en plus d'une interface Web existante)
  • vous devriez pouvoir refactoriser ou éventuellement remplacer la couche inférieure sans changer la couche supérieure

Ces conditions sont fondamentalement symétriques. Ils expliquent pourquoi il est généralement préférable de n'avoir qu'une seule direction de dépendance, mais pas qui.

La direction de la dépendance doit suivre la direction de la commande

La raison pour laquelle nous préférons une structure de dépendance descendante est que les objets supérieurs créent et utilisent les objets inférieurs. Une dépendance est fondamentalement une relation qui signifie "A dépend de B si A ne peut pas fonctionner sans B". Donc, si les objets dans A utilisent les objets dans B, c'est comme ça que les dépendances devraient se passer.

C'est en quelque sorte quelque peu arbitraire. Dans d'autres modèles, comme MVVM, le contrôle s'écoule facilement des couches inférieures. Par exemple, vous pouvez configurer une étiquette dont la légende visible est liée à une variable et change avec elle. Cependant, il est normalement préférable d'avoir des dépendances descendantes, car les objets principaux sont toujours ceux avec lesquels l'utilisateur interagit, et ces objets effectuent la majorité du travail.

Alors que de haut en bas, nous utilisons l'invocation de méthodes, de bas en haut (généralement), nous utilisons des événements. Les événements permettent aux dépendances de descendre du haut même lorsque le contrôle est inversé. Les objets du calque supérieur s'abonnent aux événements du calque inférieur. La couche inférieure ne sait rien de la couche supérieure, qui agit comme un plug-in.

Il existe également d'autres façons de maintenir une seule direction, par exemple:

  • continuations (passage d'un lambda ou d'une méthode à appeler et d'un événement à une méthode asynchrone)
  • sous-classe (créer une sous-classe en A d'une classe parente en B qui est ensuite injectée dans la couche inférieure, un peu comme un plugin)
4
Sklivvz

Juste pour le fun.

Pensez à une pyramide de pom-pom girls. La rangée du bas soutient les rangées au-dessus d'eux.

Si la pom-pom girl de cette rangée regarde vers le bas, elle est stable et restera équilibrée afin que ceux au-dessus d'elle ne tombent pas.

Si elle lève les yeux pour voir comment va tout le monde au-dessus d'elle, elle perdra son équilibre, faisant tomber toute la pile.

Pas vraiment technique, mais c'était une analogie qui pourrait aider.

3
Bill Leeper

Je voudrais ajouter mes deux cents à ce que Matt Fenwick et Kilian Foth ont déjà expliqué.

Un principe de l'architecture logicielle est que les programmes complexes doivent être construits en composant des blocs autonomes plus petits (boîtes noires): cela minimise les dépendances et réduit ainsi la complexité. Cette dépendance unidirectionnelle est donc une bonne idée car elle facilite la compréhension du logiciel et la gestion de la complexité est l'un des problèmes les plus importants du développement logiciel.

Ainsi, dans une architecture en couches, les couches inférieures sont des boîtes noires qui implémentent des couches d'abstraction au-dessus desquelles les couches supérieures sont construites. Si une couche inférieure (par exemple, la couche B) peut voir les détails d'une couche supérieure A, alors B n'est plus une boîte noire: ses détails d'implémentation dépendent de certains détails de son propre utilisateur, mais l'idée d'une boîte noire est que son le contenu (sa mise en œuvre) n'est pas pertinent pour son utilisateur!

3
Giorgio

Bien que la facilité de compréhension et, dans une certaine mesure, les composants remplaçables soient certainement de bonnes raisons, une raison tout aussi importante (et probablement la raison pour laquelle les couches ont été inventées en premier lieu) est du point de vue de la maintenance logicielle. L'essentiel est que les dépendances provoquent le potentiel de briser les choses.

Par exemple, supposons que A dépend de B. Étant donné que rien ne dépend de A, les développeurs sont libres de changer A au contenu de leur cœur sans avoir à craindre qu'ils puissent casser autre chose que A. Cependant, si le développeur veut changer B, tout changement en B qui est fait pourrait potentiellement casser A. Ce problème était fréquent au début de l'informatique (pensez au développement structuré) où les développeurs corrigeaient un bogue dans une partie du programme et soulevaient des bogues dans des parties apparemment totalement indépendantes du programme ailleurs. Tout cela à cause des dépendances.

Pour continuer avec l'exemple, supposons maintenant que A dépend de B ET B dépend de A. IOW, une dépendance circulaire. Maintenant, chaque fois qu'un changement est effectué n'importe où, il pourrait potentiellement casser l'autre module. Un changement dans B pourrait encore casser A, mais maintenant un changement dans A pourrait aussi casser B.

Donc, dans votre question d'origine, si vous faites partie d'une petite équipe pour un petit projet, tout cela est à peu près exagéré car vous pouvez changer librement de modules à votre gré. Cependant, si vous êtes sur un projet important, si tous les modules dépendaient des autres, chaque fois qu'un changement était nécessaire, cela pourrait potentiellement casser les autres modules. Sur un grand projet, il peut être difficile de déterminer tous les impacts, il est donc probable que vous en manquiez certains.

Cela empire sur un grand projet où il y a beaucoup de développeurs, (par exemple certains qui ne travaillent que la couche A, une couche B et une couche C). Comme il devient probable que chaque modification doit être examinée/discutée avec les membres des autres couches afin de s'assurer que vos modifications ne se cassent pas ou ne forcent pas à retravailler sur ce sur quoi elles travaillent. Si vos modifications imposent des modifications aux autres, vous devez les convaincre qu'ils doivent apporter la modification, car ils ne voudront pas prendre plus de travail simplement parce que vous avez cette excellente nouvelle façon de faire les choses dans votre module. IOW, un cauchemar bureaucratique.

Mais si vous avez limité les dépendances à A, cela dépend de B, B dépend de C, alors seules les personnes de la couche C doivent coordonner leurs modifications aux deux équipes. La couche B a seulement besoin de coordonner les changements avec l'équipe de la couche A et l'équipe de la couche A est libre de faire ce qu'elle veut parce que son code n'affecte pas la couche B ou C. Donc, idéalement, vous allez concevoir vos couches de sorte que la couche C change très peu, le calque B change quelque peu et le calque A fait la plupart des changements.

3
Dunk

La séparation des préoccupations et les approches de division/conquête peuvent être une autre explication de ces questions. La séparation des préoccupations donne la capacité de portabilité et dans certaines architectures plus complexes, elle offre à la plate-forme des avantages de mise à l'échelle et de performance indépendants.

Dans ce contexte, si vous pensez à une architecture à 5 niveaux (client, présentation, bussiness, intégration et niveau de ressource), le niveau inférieur de l'architecture ne devrait pas être conscient de la logique et du bussiness des niveaux supérieurs et vice versa. Je veux dire par niveau inférieur en tant que niveaux d'intégration et de ressources. Les interfaces d'intégration de base de données fournies dans l'intégration et la base de données réelle et les services Web (fournisseurs de données tiers) appartiennent au niveau des ressources. Cela suppose donc que vous changiez votre base de données MySQL en une base de données NoSQL comme MangoDB en termes d'évolutivité ou autre.

Dans cette approche, le niveau bussiness ne se soucie pas de la façon dont le niveau d'intégration fournit la connexion/transmission par la ressource. Il recherche uniquement les objets d'accès aux données fournis par le niveau d'intégration. Cela pourrait être étendu à plus de scénarios mais, fondamentalement, la séparation des préoccupations pourrait être la principale raison de cela.

1
Mahmut Canga

La raison la plus fondamentale pour laquelle les couches inférieures ne devraient pas être conscientes des couches supérieures est qu'il existe de nombreux types de couches supérieures. Par exemple, il existe des milliers et des milliers de programmes différents sur votre système Linux, mais ils appellent la même fonction de bibliothèque C malloc. La dépendance est donc de ces programmes à cette bibliothèque.

Notez que les "couches inférieures" sont en fait les couches intermédiaires.

Pensez à une application qui communique à travers le monde extérieur via certains pilotes de périphérique. Le système d'exploitation est au milie.

Le système d'exploitation ne dépend pas des détails des applications ni des pilotes de périphérique. Il existe de nombreux types de pilotes de périphériques du même type et ils partagent la même infrastructure de pilotes de périphériques. Parfois, les pirates du noyau doivent mettre en place une gestion de cas particulière dans le cadre pour un matériel ou un périphérique particulier (exemple récent que j'ai rencontré: code spécifique à PL2303 dans le cadre série USB de Linux). Lorsque cela se produit, ils font généralement des commentaires sur la quantité qui aspire et devrait être supprimée. Même si le système d'exploitation appelle des fonctions dans les pilotes, les appels passent par des crochets qui donnent aux pilotes la même apparence, tandis que lorsque les pilotes appellent le système d'exploitation, ils utilisent souvent des fonctions spécifiques directement par leur nom.

Donc, à certains égards, le système d'exploitation est vraiment une couche inférieure du point de vue de l'application et du point de vue de l'application: une sorte de centre de communication où les choses se connectent et les données sont commutées pour passer voies appropriées. Il aide la conception du hub de communication à exporter un service flexible qui peut être utilisé par n'importe quoi, et à ne pas déplacer de hacks spécifiques à un appareil ou une application dans le hub.

1
Kaz

S'étendant sur la réponse de Kilian Foth, cette direction de la superposition correspond à une direction dans laquelle un humain explore un système.

Imaginez que vous êtes un nouveau développeur chargé de corriger un bogue dans le système en couches.

Les bogues sont généralement un décalage entre les besoins du client et ce qu'il obtient. Lorsque le client communique avec le système via l'interface utilisateur et obtient les résultats via l'interface utilisateur (UI signifie littéralement "interface utilisateur"), les bogues sont également signalés en termes d'interface utilisateur. Donc, en tant que développeur, vous n'avez pas d'autre choix que de commencer à regarder l'interface utilisateur aussi, pour comprendre ce qui s'est passé.

C'est pourquoi il est nécessaire d'avoir des connexions de couche descendante. Maintenant, pourquoi n'avons-nous pas de connexions dans les deux sens?

Eh bien, vous avez trois scénarios sur la façon dont ce bogue pourrait se produire.

Cela pourrait se produire dans le code de l'interface utilisateur lui-même, et donc y être localisé. C'est facile, il vous suffit de trouver un endroit et de le réparer.

Cela peut se produire dans d'autres parties du système à la suite d'appels depuis l'interface utilisateur. Ce qui est modérément difficile, vous tracez une arborescence d'appels, trouvez un endroit où l'erreur se produit et corrigez-la.

Et cela pourrait se produire à la suite d'un appel dans votre code d'interface utilisateur. Ce qui est difficile, vous devez intercepter l'appel, trouver sa source, puis déterminer où l'erreur se produit. Étant donné qu'un point de départ est situé au fond d'une seule branche d'une arborescence d'appels ET que vous devez d'abord trouver une arborescence d'appels correcte, il peut y avoir plusieurs appels dans le code de l'interface utilisateur, vous avez votre débogage coupé pour vous.

Pour éliminer autant que possible le cas le plus difficile, les dépendances circulaires sont fortement déconseillées, les couches se connectent principalement de manière descendante. Même lorsqu'une connexion dans l'autre sens est nécessaire, elle est généralement limitée et clairement définie. Par exemple, même avec des rappels, qui sont une sorte de connexion inverse, le code appelé dans le rappel fournit généralement ce rappel en premier lieu, implémentant une sorte d '"opt-in" pour les connexions inversées et limitant leur impact sur la compréhension d'un système.

La superposition est un outil, principalement destiné aux développeurs prenant en charge un système existant. Eh bien, les connexions entre les couches reflètent cela aussi.

1
Kaerber