Pourquoi est-ce une bonne idée que les couches d'application «inférieures» ne se rendent pas compte des couches «supérieures»?


66

Dans une application Web MVC typique (bien conçue), la base de données ne connaît pas le code du modèle, le code du modèle ne connaît pas le code du contrôleur et le code du contrôleur ne connaît pas le code de vue. (J'imagine que vous pourriez même commencer aussi bas que le matériel, ou peut-être même plus loin, et le schéma pourrait être le même.)

En allant dans l'autre sens, vous pouvez descendre d'un niveau. La vue peut prendre en compte 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 comprendre intuitivement pourquoi c'est une bonne idée mais je ne peux pas l'exprimer. Pourquoi ce style de superposition unidirectionnel est-il une bonne idée?


10
Peut-être est-ce dû au fait que les données proviennent "de la base de données" dans la vue. Il "commence" à la base de données et "arrive" à la vue. La prise de conscience de la couche va dans le sens opposé lorsque les données "voyagent". J'aime utiliser des "citations".
Jason Swett

1
Vous l'avez marqué dans votre dernière phrase: Unidirectionnel. Pourquoi les listes chaînées sont-elles beaucoup plus typiques que les listes doublement chaînées? Le maintien des relations devient infiniment plus simple avec une liste à lien unique. Nous créons des graphiques de dépendance de cette manière, car les appels récursifs deviennent beaucoup moins probables et les caractéristiques générales du graphique deviennent plus faciles à raisonner dans leur ensemble. Les structures raisonnables sont intrinsèquement plus faciles à gérer, et les mêmes choses qui affectent les graphiques au niveau micro (implémentation) le sont également au niveau macro (architecture).
Jimmy Hoffa

2
En réalité, je ne pense pas que, dans la plupart des cas, le View s’informe du contrôleur. Puisque les contrôleurs sont presque toujours au courant de la vue, le fait de le voir crée une référence circulaire
Amy Blankenship

8
mauvaise analogie: pour la même raison, le responsable de l'accident, en général, est responsable de l'accident. Il peut voir ce que vous faites et devrait être en contrôle. S'il ne peut pas vous éviter, cela signifie qu'il ne respecte pas les règles de sécurité. Pas l'inverse. Et, en enchaînant, cela le libère de ce qui se passe derrière lui.
Hayem

1
Il est évident qu'une vue est consciente d'un modèle de vue fortement typé.
DazManCat

Réponses:


121

Les couches, les modules, voire l’architecture elle-même, permettent aux humains de mieux comprendre les programmes informatiques . La méthode optimale numériquement pour résoudre un problème est presque toujours un fouillis imbécile de code non modulaire, à référence automatique ou même à modification automatique - qu'il s'agisse d'un code assembleur fortement optimisé dans des systèmes embarqués avec des contraintes de mémoire invalidantes ou des séquences d'ADN après des millions d'années de pression de sélection. De tels systèmes n'ont pas de couches, pas de direction discernable du flux d'informations, en fait aucune structure que nous pouvons discerner du tout. Pour tout le monde sauf leur auteur, ils semblent travailler 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 partie de l'efficacité pour rendre le système compréhensible par les gens normaux. Comprendre une chose à la fois est plus facile que de comprendre deux choses qui n'ont de sens que si 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 pour que certaines pièces en nécessitent d'autres. Le compromis à privilégier est de les construire de manière à ce qu'une partie en exige une autre, sans que la première ne soit restituée. Et c’est exactement ce que nous donnent la superposition unidirectionnelle: il est possible de comprendre le schéma de la base de données sans connaître les règles commerciales et de comprendre les règles commerciales 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 rien savoirdu tout sur les règles de gestion - mais dans la pratique, cela n’est pratiquement jamais possible. Des règles empiriques telles que "Pas de dépendances cycliques" ou "Les dépendances ne doivent descendre que d'un niveau" capturent simplement la limite pratiquement réalisable de l'idée fondamentale selon laquelle une chose à la fois est plus facile à comprendre que deux.


1
Qu'entendez-vous par "rendre le système compréhensible pour les gens normaux "? Je pense que cette formulation encourage les nouveaux programmeurs à rejeter vos points positifs car, comme la plupart des gens, ils pensent qu’ils sont plus intelligents que la plupart des gens et que cela ne leur posera aucun problème. Je dirais "rendre le système compréhensible pour les humains"
Thomas Bonini

12
C'est une lecture indispensable pour ceux qui pensent que le découplage complet est l'idéal, mais ne comprennent pas pourquoi cela ne fonctionne pas.
Robert Harvey

6
Eh bien, @Andreas, il y a toujours Mel .
TRiG

6
Je pense que "plus facile à comprendre" ne suffit pas. Il s'agit également de faciliter la modification, l'extension et la maintenance du code.
Mike Weller

1
@Peri: une telle loi existe, voir en.wikipedia.org/wiki/Law_of_Demeter . Que vous soyez d'accord ou non, c'est une autre affaire.
Mike Chamberlain

61

La motivation fondamentale est la suivante: vous voulez pouvoir déchirer toute une couche et en substituer une complètement différente (réécrite), et personne ne devrait pouvoir (notifier) ​​la différence.

L'exemple le plus évident consiste à déchirer la couche inférieure et à en remplacer une autre. C'est ce que vous faites lorsque vous développez la (les) couche (s) supérieure (s) par rapport à une simulation du matériel, puis que vous le remplacez par le matériel réel.

L'exemple suivant concerne l'extraction d'un calque intermédiaire et son remplacement par un calque intermédiaire différent. Considérons 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 "quelque chose d'autre a changé". (Exemple: passer d'un codage ASCII droit à un codage Reed-Solomon de flux ASCII, car vous travailliez sur une liaison radio entre le centre-ville et Marina Del Rey, et vous travaillez maintenant sur une liaison radio entre le centre-ville et Los Angeles et une sonde en orbite autour de Europa , une des lunes de Jupiter, et ce lien nécessite une correction d'erreur beaucoup plus efficace.)

La seule façon de faire en sorte que cela fonctionne est que chaque couche exporte une interface définie connue vers la couche supérieure et attend une interface définie connue pour la couche inférieure.

Or, il n’est pas tout à fait vrai que les couches inférieures ne connaissent rien aux couches supérieures. Ce que la couche inférieure sait, c'est que la couche située immédiatement au-dessus fonctionnera exactement conformément à l'interface définie. Il ne peut rien savoir de plus car, par définition, tout ce qui ne se trouve pas dans l'interface définie est sujet à changement SANS PRÉAVIS.

La couche RS-232 ne sait pas si elle utilise ASCII, Reed-Solomon, Unicode (page de code arabe, page de code japonaise, page de code Rigellian Beta) ou quoi. Il sait simplement qu'il reçoit une séquence d'octets et qu'il écrit ces octets sur un port. La semaine prochaine, il obtiendra peut-être une séquence d'octets complètement différente. Il s'en fiche. Il ne fait que déplacer des octets.

La première (et la meilleure) explication de la conception en couches est le document classique de Dijkstra intitulé "Structure du système de multiprogrammation" . Il est nécessaire de lire dans cette affaire.


Ceci est utile et merci pour le lien. J'aimerais pouvoir choisir deux réponses comme étant la meilleure. En gros, j'ai retourné une pièce de monnaie dans ma tête et choisi l'autre, mais j'ai quand même voté pour la tienne.
Jason Swett

+1 pour d'excellents exemples. J'aime l'explication du JRS
ViSu,

@JasonSwett: Si j'avais retourné la pièce, je l'aurais retournée jusqu'à ce qu'elle désigne cette réponse! ^^ +1 à John.
Olivier Dulac

Je suis quelque peu en désaccord avec cela, car vous voulez rarement pouvoir extraire la couche de règles commerciales et la remplacer par une autre. Les règles métier changent beaucoup plus lentement que les technologies d’interface utilisateur ou d’accès aux données.
Andy

Ding Ding Ding !!! Je pense que le mot que vous recherchiez est «découplage». C'est à quoi servent les bonnes API. Définir les interfaces publiques d'un module pour qu'il puisse être utilisé universellement.
Evan Plaice

8

Parce que les niveaux les plus élevés peuvent changer.

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


4

Je pense que la raison principale est que cela rend les choses plus étroitement couplées. Plus le couplage est serré, plus les problèmes risquent de se poser plus tard. Voir cet article plus d'infos: Couplage

Voici un extrait:

Désavantages

Les systèmes à couplage étroit 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 modifications apportées aux 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 des modules dépendants doivent être inclus.

Cela étant dit, un système couplé plus haut est nécessaire pour des raisons de performances. L'article que j'ai mentionné contient également des informations à ce sujet.


4

OMI, c'est très simple. Vous ne pouvez pas réutiliser quelque chose qui continue à faire référence au contexte dans lequel il est utilisé.


4

Les couches ne doivent pas avoir de dépendances bidirectionnelles

Les avantages d’une architecture en couches sont que les couches doivent pouvoir être utilisées indépendamment:

  • vous devriez pouvoir créer une couche de présentation différente de la première sans modifier la couche inférieure (par exemple, créer une couche d'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 d’avoir une seule direction de dépendance, mais pas laquelle .

La direction de dépendance devrait suivre la direction de commandement

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 de A utilisent les objets de B, c'est ainsi que devraient dépendre les dépendances.

C'est en quelque sorte quelque peu arbitraire. Dans d'autres modèles, tels que MVVM, le contrôle dé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. Normalement, il est toujours 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 majeure partie du travail.

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

Il existe également d'autres moyens 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 async)
  • sous-classe (crée une sous-classe dans A d'une classe parent dans B qui est ensuite injectée dans la couche inférieure, un peu comme un plugin)

3

J'aimerais ajouter mes deux cents à ce que Matt Fenwick et Kilian Foth ont déjà expliqué.

L'un des principes de l'architecture logicielle est que les programmes complexes doivent être construits en composant des blocs plus petits et autonomes (boîtes noires): cela minimise les dépendances et réduit donc 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 en développement de logiciel.

Ainsi, dans une architecture en couches, les couches inférieures sont des boîtes noires qui implémentent des couches d'abstraction sur lesquelles 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 sa le contenu (sa mise en œuvre) n'est pas pertinent pour son utilisateur!


3

Juste pour le fun.

Pensez à une pyramide de pom-pom girls. La rangée inférieure 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 en équilibre afin que ceux qui se trouvent au-dessus d'elle ne tombent pas.

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

Pas vraiment technique, mais c'était une analogie que je pensais pouvoir aider.


3

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 aussi la raison pour laquelle les couches ont été inventées) repose sur la maintenance logicielle. L'essentiel est que les dépendances peuvent potentiellement casser des choses.

Par exemple, supposons que A dépende de B. Puisque rien ne dépend de A, les développeurs sont libres de modifier le contenu de A sans avoir à s'inquiéter du risque de casser autre chose que A. Toutefois, si le développeur souhaite modifier B, tout changement éventuel Cela pourrait potentiellement casser A. Ceci était un problème 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 non liées 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. Désormais, chaque fois qu’une modification est effectuée n’importe où, elle risque de briser l’autre module. Un changement de B pourrait toujours casser A, mais maintenant un changement de A pourrait aussi casser B.

Donc, dans votre question initiale, si vous faites partie d'une petite équipe pour un petit projet, tout cela est excessif, car vous pouvez modifier librement les modules à votre guise. Toutefois, si vous êtes sur un projet de taille importante, si tous les modules dépendent des autres, chaque fois qu’une modification est nécessaire, elle peut potentiellement endommager les autres modules. Sur un projet de grande envergure, connaître tous les impacts peut être difficile à déterminer, vous risquez donc de manquer certains impacts.

La situation empire dans le cas d’un projet de grande envergure comptant de nombreux développeurs (par exemple, certains ne travaillent que sur les couches A, B et C). Comme il est 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 brisent pas ou ne vous obligent pas à retravailler ce sur quoi elles travaillent. Si vos modifications imposent des modifications aux autres, vous devez les convaincre qu'ils doivent opérer ces modifications, car ils ne voudront plus travailler, simplement parce que vous avez cette nouvelle façon géniale de faire les choses dans votre module. IOW, un cauchemar bureaucratique.

Mais si vous dépendez uniquement de A pour les dépendances, alors que seules les personnes de la couche C ont besoin de coordonner leurs modifications pour les deux équipes. La couche B n'a besoin que de coordonner les modifications avec l'équipe de la couche A et l'équipe de la couche A sont libres de faire ce qu'elles veulent parce que leur code n'affecte pas les couches B ou C. Donc, idéalement, vous allez concevoir vos couches de manière à ce que la couche C change très petit, la couche B change quelque peu et la couche A fait la plupart des changements.


+1 Chez mon employeur, nous disposons d’un diagramme interne décrivant l’essence de votre dernier paragraphe, qui s’applique au produit sur lequel je travaille. En d’autres termes, plus vous avancez dans la pile, plus le taux de changement est bas (et devrait être).
RobV

1

La raison la plus fondamentale pour laquelle les couches inférieures ne doivent pas être conscientes des couches supérieures est qu'il existe beaucoup plus de 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 mallocfonction de bibliothèque C. Donc, la dépendance est de ces programmes à cette bibliothèque.

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

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

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érique du même type et ils partagent le même framework de pilote de périphérique. Parfois, les hackers du noyau doivent mettre en place des procédures spéciales dans le cadre pour protéger un matériel ou un périphérique particulier (exemple récent que j'ai rencontré: code spécifique à PL2303 dans le cadre usb-serial de Linux). Lorsque cela se produit, ils ajoutent généralement des commentaires sur la quantité de problèmes et doivent être supprimés. Même si les appels du système d'exploitation fonctionnent dans les pilotes, les appels passent par des points d'ancrage qui rendent les pilotes identiques, alors que lorsque les pilotes appellent le système d'exploitation, ils utilisent souvent des fonctions spécifiques directement par leur nom.

Ainsi, à 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 où les données sont commutées pour suivre les chemins appropriés. Il aide la conception du concentrateur de communication à exporter un service flexible qui peut être utilisé par n'importe quoi, et à ne déplacer aucun piratage spécifique à un périphérique ou à une application dans le concentrateur.


Je suis content tant que je n'ai pas à m'inquiéter de la configuration de tensions spécifiques sur des broches spécifiques du processeur :)
un CVn

1

La séparation des préoccupations et les approches diviser / conquérir 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 en termes de performances et de dimensionnement indépendants.

Dans ce contexte, si vous pensez à une architecture à 5 niveaux (client, présentation, entreprise, intégration et niveau de ressources), un niveau d'architecture inférieur ne devrait pas prendre conscience de la logique et de l'activité des niveaux supérieurs, et inversement. J'entends par niveau inférieur l'intégration et les ressources. Les interfaces d'intégration de base de données fournies dans Integration et les bases de données réelles et Webservices (fournisseurs de données tiers) appartiennent au niveau ressources. Donc, supposons 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 s'intéresse pas à la manière 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 à d'autres scénarios, mais la séparation des préoccupations pourrait en être la principale raison.


1

En développant la réponse de Kilian Foth, cette direction de superposition correspond à une direction dans laquelle un humain explore un système.

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

Les bugs sont généralement une inadéquation entre les besoins du client et ses avantages. Lorsque le client communique avec le système via l'interface utilisateur et obtient le résultat via l'interface utilisateur (l'interface utilisateur signifie littéralement 'interface utilisateur'), ​​les bogues sont également signalés en termes d'interface utilisateur. Ainsi, en tant que développeur, vous n'avez pas beaucoup d'autre choix que de commencer à regarder également l'interface utilisateur 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 relations qui vont dans les deux sens?

Eh bien, vous avez trois scénarios pour lesquels ce bogue pourrait se produire.

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

Cela pourrait se produire dans d'autres parties du système à la suite d'appels effectués à partir de l'interface utilisateur. Ce qui est modérément difficile, vous tracez un arbre d'appels, trouvez un endroit où l'erreur se produit et le corrigez.

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 où vous commencez est situé au plus profond d'une branche d'un arbre d'appels ET que vous devez d'abord trouver un arbre d'appels correct, il peut y avoir plusieurs appels dans le code de l'interface utilisateur. Votre débogage est alors conçu pour vous.

Pour éliminer autant que possible les cas les plus difficiles, les dépendances circulaires sont fortement déconseillées, les couches se connectant généralement de manière descendante. Même lorsqu'une connexion est nécessaire, elle est généralement limitée et clairement définie. Par exemple, même avec les callbacks, qui sont une sorte de connexion inversée, le code appelé dans callback fournit généralement ce rappel en premier lieu, mettant en œuvre une sorte de "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

Une autre raison pour laquelle je voudrais voir explicitement mentionné ici est la possibilité de réutilisation du code . Nous avons déjà eu l'exemple du support RS232 qui est remplacé, alors allons encore plus loin ...

Imaginez que vous développiez des pilotes. C'est ton travail et tu écris pas mal de choses. Les protocoles pourraient probablement commencer à se répéter à un moment donné, de même que les supports physiques.

Donc, ce que vous commencerez à faire - à moins que vous n'aimiez beaucoup faire la même chose encore et encore - est d'écrire des couches réutilisables pour ces choses.

Supposons que vous deviez écrire 5 pilotes pour les périphériques Modbus. L'un d'entre eux utilise Modbus TCP, deux utilisent Modbus sur RS485 et le reste utilise RS232. Vous n'allez pas réimplémenter Modbus 5 fois, car vous écrivez 5 pilotes. De plus, vous n'allez pas réimplémenter Modbus 3 fois, car vous avez 3 couches physiques différentes en dessous de vous.

Ce que vous faites est que vous écrivez un accès multimédia TCP, un accès multimédia RS485 et éventuellement un accès multimédia RS232. Est-il judicieux de savoir qu’il y aura une couche Modbus au-dessus, à ce stade? Probablement pas. Le prochain pilote que vous allez implémenter pourrait également utiliser Ethernet mais utiliser HTTP-REST. Il serait dommage que vous deviez réimplémenter Ethernet Media Access pour pouvoir communiquer via HTTP.

Une couche au-dessus, vous allez implémenter Modbus juste une fois. Cette couche Modbus, une fois de plus, ne saura pas que les pilotes, qui sont une couche en haut. Bien entendu, ces pilotes devront savoir qu’ils sont supposés parler Modbus et qu’ils doivent savoir qu’ils utilisent Ethernet. Quelle que soit la manière dont je viens de le décrire, vous ne pouvez pas simplement extraire un calque et le remplacer. vous pourriez bien sûr - et pour moi, c'est le plus grand avantage de tous, allez-y et réutilisez cette couche Ethernet existante pour quelque chose d'absolument étranger au projet qui a initialement provoqué sa création.

C’est quelque chose que nous voyons probablement tous les jours en tant que développeurs et qui nous fait gagner beaucoup de temps. Il existe d'innombrables bibliothèques pour toutes sortes de protocoles et autres. Celles-ci existent en raison de principes tels que le sens de dépendance qui suit le sens de commande, ce qui nous permet de construire des couches de logiciels réutilisables.


la réutilisation a déjà été explicitement mentionnée dans une réponse publiée il y a plus de six mois
Gnat le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.