Architecture logicielle & Développement

Le principe d'inversion de dépendance | 0 vote(s)

Tags: ,

Après avoir discuté des LSP, ISP, SRP et autres OCP, il ne nous reste qu’un seul principe de programmation orienté objet important à considérer – le principe d’inversion de dépendance (Dependency Inversion Principle selon R. C. Martin[1], ou DIP pour faire court). Si ils existent d’autre principes, ils touchent surtout à la notion de package. Bien qu’ils aient leur intérêt et qu’ils feront peut être plus tard l’objet d’études similaires, les cinq grand principes présentés résument à eux seuls un grand nombre de bonnes pratiques à utiliser lors de la conception d’un programme.

Notes

[1] Object Oriented Design Quality Metrics: An Analysis of dependencies , Robert C. Martin, ROAD, Vol. 2, No. 3, Sep-Oct, 1995

Pourquoi

On a coutume de représenter un système informatique comme étant un programme dialoguant avec ses périphériques, qu’ils soient des disques dur, la console, un écran, une souris ou une imprimante. On sait que ce dialogue se fait via l’intermédiaire du système d’exploitation, qui permet d’offrir une interface plus aisée à manipuler que le périphérique lui-même. Nous allons voir à quel point cette vision peut être intégrée et généralisée dans le domaine de l’architecture logicielle.

Considérons une application serveur, répondant à des requêtes envoyées par des programmes clients via un réseau. Un protocole de communication est défini et le code suivant, situé sur le serveur, permet de traiter les requètes :

void serversocket ::receive(char* buf)
{
  switch (buf0) {
  case SVR_COMMAND_1: 
    {
      exec_command_1(std::string(buf+1));
    }
    break;
  case SVR_COMMAND_2: 
    {
      int v1 = *(static_cast<int*>(buf+1));
      int v2 = *(static_cast<int*>(buf+1+sizeof(v1)));
      exec_command_2(v1, v2);
    }
    break;
    // and so on…
  default:
    throw std::exception(“unknown command”);
  }
}

Les problèmes de ce code sont nombreux. Ainsi, il peut devenir difficile à étendre, chaque extension nécessitant une modification de cette fonction – le code n’obéit donc pas au principe "ouvert/fermé". Plus grave encore, et c’est ce qui nous intéresse aujourd’hui, ce code est particulièrement sensible aux changements mêmes mineurs du protocole. En effet, il se repose trop sur les détails de celui-ci pour réaliser les tâches qu’on attend de lui.

Bien évidemment, j’ai choisi un exemple extrême - mais tout de même probable, et certainement déjà rencontré. Ceci dit, ce qu’il nous apprend reste valide dans bien des situations : si une implémentation est trop liée à des détails, alors le moindre changement de ces détails peut avoir des effets dramatiques et globaux dans l'application, voire induire des bugs difficile à détecter. Quid d’une amélioration du protocole qui permet de traiter les entiers stockés dans l’ordre dit network order ? Supposons que le serveur se doit de stocker un certain nombre de commandes afin de les utiliser pour moduler le comportement d’autres fonctions - comment dans ce cas gérer la plus infime modification du protocole ? Le fonctionnement global de l’application devient alors entièrement tributaire de ce protocole de communication, fragilisant du même coup l’ensemble du système.

Clairement, il manque un niveau d’interface entre les détails et l’implémentation. Plus exactement, il manque un niveau d’abstraction entre ces deux parties.

De l’utilisation d’une abstraction

Il ne faut bien évidemment pas confondre abstraction et classe abstraite. La classe abstraite est un outil, un moyen qui peut être utilisé dans la construction d’une abstraction, mais il est tout a fait possible de construire une abstraction sur la base d’une classe concrète, et rien n’empêche une classe abstraite d’être très spécialisée. Mais l’abstraction est un concept qui représente de manière simplifiée un ensemble d’entités concrètes, ce qui exclu cette spécialisation. Il n’y a donc pas d’équivalence entre les deux.

Si l’on veut ramener cette pseudo-définition à notre exemple, on devine rapidement deux abstractions sous-jacentes : la notion de protocole, qui agit sur la manière dont les données sont interprétées, ainsi que la notion de commande qui, à partir des paramètres décodés, va exécuter une série d’opérations. La capture des données est ainsi dépendante du protocole et l’exécution des commandes n’est dépendante que des données obtenues.

Une chose est notable dans cette architecture : sans vraiment qu’on s’en rende compte, l’introduction a renversé le cycle de dépendances. Ainsi, avant ce changement, on avait[1] :

Le changement apporté nous donne :

Ce renversement de situation est à la fois intuitif et contre-intuitif. Intuitif car il parait naturel d’utiliser une abstraction en tant qu’interface entre un programme et sa source de données. Contre-intuitif, car soudain, l’implémentation ne dépends plus des détails, mais et les détails et l’implémentation dépendent de cette abstraction.

C’est là tout le principe de l’inversion de dépendance : les dépendances ne se font plus du haut niveau vers le bas niveau, car une couche intermédiaire est rajoutée entre les deux parties et c’est maintenant de cette couche intermédiaire dont elles vont toutes les deux dépendre.

Le but est évident, ainsi qu’il a déjà été dit dans un précédent billet : de par sa nature, une abstraction est moins sujette aux changements que les détails qu’elle cache. On peut ainsi changer complètement l’implémentation bas niveau sans que cela n’ait un impacte sur le code du haut niveau. Ce principe est ainsi utilisé dans de très nombreux cas : par exemple, lorsqu’il faut découpler une application d’une API utilisée (exemple : l’écriture d’un programme utilisant une interface graphique et devant fonctionner sur plusieurs plateforme différentes), lorsque l’application communique avec un périphérique dont la nature peut changer (un scanner SCSI qui devient un scanner USB pour des raisons de coût), etc.

Conclusion

L’image importante à retenir est cette idée d’abstraction qui permet de découpler une application de ses détails d’implémentation. Dans ce cadre, cette abstraction ne doit pas être construite en se basant sur les détails sous-jacent, afin d'offrir une résistance aux changements importante. C'est donc en deux parties que ce Principe d’Inversion de Dépendances est formulé :

1) Les modules haut niveau ne doit pas dépendre des modules bas niveau ; les deux doivent dépendre d’abstractions.
2) Les abstractions ne doivent pas dépendre des détails ; les détails doivent dépendre des abstractions.

Notes

[1] Christophe Moustier m'a fait remarquer que la dépendance Serveur/Client n'était pas claire dans ces schémas ; veuillez donc vous reporter à mon commentaire pour plus de précisions.

Trackbacks

Aucun trackback.

Les trackbacks pour ce billet sont fermés.

Commentaires

1. Le vendredi 1 décembre 2006 à 00:20, par Christophe Moustier

Gravatar

c'est marrant que tu aies écrit que le serveur dépendait du client dans ton 1er schéma...j'aurais plutôt dit l'inverse.

2. Le vendredi 1 décembre 2006 à 09:44, par Emmanuel Deloget

Gravatar

Ce que j'ai voulu exprimé, c'est que le serveur dépendant des données en provenance du client - sans données, il ne fait rien, même si il est vrai qu'il existe une dépendance forte entre le client et le serveur (le client ne peut pas fonctionner sans le serveur, l'inverse n'étant pas vrai). Je conçois que l'utilisation d'un pseudo-package "client" n'est pas très précise, mais je dois avouer que j'ai la flemme de refaire les schémas :) Je vais tout de même rajouter une note pour faire référence à ce commentaire.

Ajouter un commentaire

Si votre navigateur est compatible, vous pouvez vous aider de la barre d'outils placée au-dessus de la zone de saisie pour enrichir vos commentaires.