Conception des bases de données SOLID

Responsabilité unique et normalisation

Cet article est le second de la suite d'articles Conception des bases de données SOLID rédigés par Chris Travers. Il présente l'application du principe de la responsabilité unique à la conception des bases de données relationnelles. L'auteur fait également un détour sur la normalisation des bases de données. Chris Travers est un blogueur très actif et nous avons souhaité partager avec la communauté francophone ses contributions afin que chacun en tire profit.

Les commentaires et les suggestions d'amélioration sont les bienvenus, alors, après votre lecture, n'hésitez pas. 3 commentaires Donner une note à l'article (4).

Article lu   fois.

Les deux auteur et traducteur

Traducteur :

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Cet article traitera du principe de la responsabilité unique dans la conception objet-relationnelle, et sa relation avec la normalisation des données et la programmation orientée-objet. Bien que la responsabilité unique soit un principe orienté-objet assez facile à appliquer, je pense qu'il est important de l'explorer en profondeur, car il permet de fournir un cadre plus clair pour aborder la conception objet-relationnelle.

Plus tard dans cet article, j'utiliserai des bouts de code développés ailleurs dans d'autres domaines. Ce ne sont pas des versions complètes de ce qui a été écrit, mais des versions suffisantes pour montrer les fondements de la structure de données et de l'interface.

II. Relations et classes : les similarités ou ressemblances

Les objets et les classes sont superficiellement presque similaires, au point où l'on pourrait considérer les relations comme des ensembles de classes. En effet, cette équivalence est la base de la conception des bases de données objet-relationnelles.

Les objets sont des structures de données utilisées pour stocker un état. Les objets ont une identité et sont étroitement liés à l'interface. Les relations sont les structures de données qui enregistrent l'état. Et si elles répondent à la deuxième forme normale, elles ont une identité sous la forme d'une clé primaire. Les bases de données objet-relationnelles fournissent alors une interface et ainsi dans une base de données relationnelle, les relations et les classes contiennent des ensembles d'objets de certaines classes.

III. Relations et classes : les différences

Aussi similaires que les structures de classes et d'objets soient, une erreur très commune est de simplement considérer un système de gestion de bases de données relationnelle comme un magasin de simples objets. Cela tend à la conception d'une base de données fragile qui conduira à une application relativement fragile. Par conséquent plusieurs d'entre nous considèrent cette approche comme une sorte d'anti-modèle.

La raison pour laquelle cela ne fonctionne pas très bien n'est pas le fait que l'équivalence de base est mauvaise, mais le fait que les relations et les classes sont utilisées de manière très différente. Sur la couche applicative, les classes sont utilisées pour modèle et pour le contrôle (comportement), tandis que dans la base de données, les relations et les lignes sont utilisées pour modéliser l'information. Ainsi, lier les structures de bases de données et les classes applicatives de cette manière surchargerait considérablement les structures de données, transformant ainsi les structures dans les rapports entre objets plutôt que des objets de comportement.

Les relations doivent donc être considérées non seulement comme des classes, mais comme des classes spécialisées utilisées pour le stockage et le reporting. De cette manière, elles ont fondamentalement des exigences différentes de celles des classes de comportement dans une application, et donc elles ont différentes raisons de changer. Une classe applicative change généralement quand il y a un besoin pour un changement de comportement, tandis qu'une relation ne devrait changer que lorsqu'il y a un changement dans la conservation des données et dans le reporting.

Les relations ont toujours eu tendance à être dissociées de l'interface, et cela fournit une grande puissance. Alors que les classes ont tendance à être assez opaques, les relations ont tendance à être très transparentes. La raison est la suivante : tandis que les deux représentent des informations sur l'état que ce soit l'état de la demande ou d'autres faits, les objets traditionnellement encapsulent le comportement (et ainsi agissent dans la construction des blocs de comportement), les relations encapsulent les informations et construisent des blocs d'information. Ainsi, les structures de données doivent être transparentes tandis que la conception orientée objet tend à moins pousser la transparence et plus caractériser l'abstraction.

Il est à noter que parce que ces systèmes sont conçus pour faire des choses différentes, il y a beaucoup de DBA qui suggèrent d'encapsuler la base de données entière derrière une API, définie par des procédures stockées. Le problème typique lié à cette approche est que le couplage faible de l'application à l'interface est difficile. Quand l'interface de la base de données est étroitement couplée à l'interface de l'application, on finit avec des problèmes à plusieurs niveaux et il a tendance à sacrifier une bonne conception de l'application à une bonne conception de la base de données relationnelle.

IV. Principe de la responsabilité unique dans le développement d'applications

Le principe de responsabilité unique stipule que chaque classe doit avoir une responsabilité unique qui devrait être entièrement encapsulée. Une responsabilité est définie comme une raison de changer. L'exemple canonique de ce principe est une classe qui pourrait formater et imprimer un rapport. Parce que les deux modifications de données peuvent nécessiter un changement de la classe, ce serait une violation du principe en cause. Dans un monde idéal, nous aimerions séparer l'impression et la mise en forme de sorte que les changements de mise en forme ne nécessitent pas de modifications lorsque les changements de données sont faits et vice versa.

Le problème de l'exemple canonique est qu'il n'est pas autonome. Si vous modifiez les données dans le rapport, il faudra presque certainement des changements de mise en forme. Vous pouvez essayer d'automatiser ces changements, mais seulement dans certaines conditions, et vous pouvez abstraire les interfaces (inversion de dépendances). Mais à la fin, si vous modifiez les données dans le rapport, des changements de mise en forme seront nécessaires.

En outre, une « raison de changer » est épistémologiquement problématique. Les raisons prévues sont rarement, voire jamais atomiques, et il y a donc une vraie question à y réfléchir. En termes de mise en forme d'un rapport, voulons-nous sortir de la classe qui s'adapte à la taille du papier de sorte que si nous voulons aller des « US letters » au « format A4 », nous n'ayons plus besoin de changer le reste de la mise en forme de la page ?

La séparation parfaite des responsabilités dans cet exemple est donc impossible, comme il est probablement toujours -vous pouvez seulement changer les règles de gestion à un certain point avant que les interfaces doivent changer. Et quand cela arrive, le flux en cascade de changements nécessaires peut être assez important.

La base de données est cependant tout à fait différente en ce que la responsabilité de code au niveau de la base de données (y compris DDL et DML) est limitée à la proposition selon laquelle nous devrions construire des réponses de faits connus. Cela fait une énorme différence en termes de responsabilité unique, et il est possible d'élaborer des définitions mathématiques pour la responsabilité unique.

V. Définition de la troisième forme normale

La définition de Codd stipule qu'une relation (table) est en 3FN, si et seulement si les deux conditions suivantes sont réunies :

  • la relation R (table) est en 2FN ;
  • chaque attribut non clé de R est non transitivement dépendante de la super clé de R.

Un attribut non clé est un attribut qui ne fait pas partie de la super clé. À la base, ce que la 3FN stipule est que chaque relation doit contenir une super clé et des valeurs fonctionnellement et directement dépendantes de cette super clé.

Cela deviendra plus important lorsque nous regardons comment les anomalies des données se raccordent avec la responsabilité unique.

VI. Normalisation et responsabilité unique

Le processus de normalisation d'une base de données est un exercice pour créer des bases de données relationnelles où des anomalies de données n'existeront pas. Les anomalies dans les données se produisent soit lorsque la modification des données requiert la modification d'autres données pour maintenir la précision (où aucun changement de faits n'est enregistré), soit lorsque les données existantes peuvent projeter des faits actuels ou historiques non existants (anomalies de jointure).

Ce processus se produit en décomposant clés et super clés et leurs dépendances, telles que les données soient suivies dans les petites unités autonomes. Commençant à la 3FN, certains voient les relations former une responsabilité unique de gestion des données directement dépendante de leur super-clé. De ce point précédent, la structure des relations changerait (en supposant qu'aucune décision de décomposer une relation dans une forme normale de niveau supérieur) si et seulement si un changement est fait à une donnée suivie et qui soit directement tributaire d'une super clé.

La responsabilité de la couche base de données est le stockage des données et la synthèse des réponses. Vu que les relations de stockage gèrent eux même la première forme normale, la normalisation est une condition préalable à une bonne conception de l'objet-relationnel.

Le seul inconvénient se trouve ici, cependant, les prérequis d'atomicité de la première forme normale (1FN) doivent être interprétés de façon légèrement différente dans les configurations objet-relationnelles, car plusieurs structures de données complexes peuvent être atomiques par rapport à une conception purement relationnelle. Dans une base de données relationnelle pure, les types de données qui peuvent être utilisés sont relativement faibles et par conséquent doivent être décomposés. Par exemple, nous pourrions enregistrer une adresse IP ainsi que le masque de réseau sur quatre entiers pour l'adresse et un entier pour le masque de réseau, ou nous pourrions stocker en un seul entier (32 bits) plus un autre entier pour le masque de réseau, mais la seconde approche pose des problèmes d'affichage que la première ne pose pas. Dans cette base de données objet-relationnelle, nous pourrions enregistrer l'adresse IP comme un tableau de quatre entiers pour IPv4 ou si nous avons besoin de meilleures performances, nous pourrions construire un type personnalisé. Si le stockage n'est pas un problème, et que la facilité de maintenance est là, nous pourrions même définir les relations, les domaines et notamment organiser les adresses IP, puis stocker les tuples dans une colonne avec des interfaces fonctionnelles appropriées.

Aucune de ces approches ne viole nécessairement la première forme normale (1FN), aussi longtemps que le type de données impliqué encapsule complètement et correctement le comportement requis. Lorsque cette encapsulation est problématique, elle viole la première forme normale (1FN) parce qu'elle ne peut plus traiter les valeurs atomiques. Dans tous les cas, la valeur spécifique a une correspondance 1 à 1 à une adresse IP.

En outre, là où les besoins sont différents, le stockage, l'interface de l'application et les classes devraient l'être aussi (ce qui peut être manipulé avec vues actualisables, interfaces objet-relationnelles et autres).

VII. Les interfaces objet-relationnelles et la responsabilité unique

Pour des bases de données relationnelles pures, la normalisation est suffisante pour adresser la responsabilité unique. La conception objet-relationnelle apporte une complexité additionnelle parce que les comportements peuvent être encapsulés dans les interfaces d'objets. Il y a deux cas fondamentaux où ceci peut faire une différence, en termes de modèle de composition et en termes de données encapsulées dans les colonnes.

Un modèle de composition dans PostgreSQL arriverait typiquement quand nous utilisons l'héritage pour gérer les champs qui se produisent d'une manière fonctionnellement dépendante de plusieurs autres champs de la base de données. Par exemple, nous pourrions avoir une table abstraite et avoir des tables variées qui héritent de la table abstraite, et il est possible qu'elles soient des parties d'autres grandes tables. Un cas commun où la composition fait une grande différence est dans la gestion des notes. Les gens peuvent vouloir associer des notes à tout type de données dans la base de données et ainsi personne ne peut dire que le texte ou la note sont mutuellement dépendants.

Une approche relationnelle pure est soit d'avoir plusieurs tables de gestion des notes indépendantes, soit d'avoir une unique table globale dans laquelle on enregistre les notes de n'importe quoi et donc avoir plusieurs tables de jointures pour ajouter des dépendances par jointure. Le problème avec cette approche est que la donnée « note » est dépendante logiquement et non mathématiquement de la jointure et ainsi, il n y a pas de moyen d'exprimer cela sans beaucoup de complexité dans la conception de la base de données.

Une approche objet-relationnelle pourrait être d'avoir plusieurs tables de notes, mais qui héritent d'une structure de table commune des notes. Cette table peut alors être étendue, les interfaces ajoutées si besoin, et cela devrait remplir le principe de responsabilité unique même si nous ne pourrions être capables de dire qu'il y a une dépendance fonctionnelle naturelle sur la table elle-même.

Le second cas doit se faire avec le stockage de l'information complexe dans les colonnes. Ici, la stabilité et la robustesse du code sont très importantes, et les approches traditionnelles du principe de la responsabilité unique s'appliquent directement aux types de données utilisés.

VIII. Exemple : la configuration d'une base de données et la configuration du service SMTP sur une machine

L'un de mes projets actuels est de mettre en place une base de données de configuration réseau pour une entreprise d'hébergement « LedgerSMB ». J'aide dans la réalisation de ce projet. Pour des raisons de concurrence, je ne peux divulguer le code entier ici. Cependant, ce que je voudrais faire est de montrer une très brève approche que j'utilise pour résoudre un cas spécifique.

L'un de mes défis dans la base de données de configuration d'un réseau est que les dépendances fonctionnelles directes pour une machine donnée peuvent devenir très vite complexes quand on considère que les agents logiciels de réseau ne sont pas exécutés plusieurs fois sur une machine donnée. De plus, nous voulons souvent être sûrs que certains types de composants logiciels sont configurés pour certains types de machines et ainsi les contraintes peuvent exister.

La largeur et la complexité de certaines tables de configuration peuvent poser un problème de gestion dans le temps pour la raison qu'ils ne peuvent pas être brisés de toute évidence en morceaux maniable de colonnes.

Une solution possible est de décomposer la classe de stockage dans plusieurs petits morceaux. Chacun d'eux  expriment un ensemble de dépendances fonctionnelles sur une clé spécifique, encapsulant totalement une responsabilité unique. La classe de stockage globale existe pour gérer les contraintes entre les morceaux et gérer le stockage physique. Les données peuvent ensuite être présentées sous forme d'une table unifiée, ou plusieurs tables jointes (et cela fonctionne même lorsque les vues ont une complexité significative). De cette manière, les sous-tables plus petites peuvent avoir la responsabilité de la gestion de la configuration des composants logiciels spécifiques.

Nous pourrions par conséquent avoir les tables comme suit :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
-- abstract table, contains no data
CREATE TABLE mi_smtp_config (
    mi_id bigint,
    smtp_hostname text,
    smtp_forward_to text
);

CREATE TABLE machine_instance (
   mi_id bigserial,
   mi_name text not null,
   inservice_date date not null,
   retire_date date.
   ....
) INHERITS (mi_smtp_config, ...); 

L'avantage majeur de cette approche est que nous pouvons facilement vérifier et ajouter des champs configurés avec leurs composants logiciels, sans parcourir un grand nombre de tables. Ceci fournit également des interfaces additionnelles pour des données en rapport. Par exemple, le code suivant :

 
Sélectionnez
select * from mi_smtp_config ;

est directement équivalent à celui suivant :

 
Sélectionnez
select (mi::mi_smtp_config) * from machine_instance mi; 

IX. Conclusion

Lorsque nous pensons aux relations spécialisées « classe de faits » à l'opposé des « classes de comportement » dans les applications, l'idée du principe de la responsabilité unique fonctionne très bien avec les bases de données relationnelles, en particulier lorsqu'elle est associée à d'autres procédés d'encapsulation comme les procédures stockées et les vues.

Dans la conception objet-relationnelle, le principe peut être utilisé comme un guide général pour la décomposition des relations dans les classes plus petites, ou dans la création intelligente des types de données pour attributs et cela devient possible pour résoudre un nombre de problèmes dans ce sillage sans casser les règles de normalisation.

X. Remerciements

Nous remercions, Chris Travers, de nous avoir aimablement autorisés à publier son article grâce à Alassane Diakité et Deepin. Nous remercions aussi  yimson pour sa traduction, Nous tenons à remercier également milkoseck qui a bien voulu corriger la traduction de cet article. Et enfin Lana Bauer pour sa disponibilité constante dans le suivi pour la publication de ce chef d'œuvre.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2013 Chris Travers - Developpez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.