Je suis assez nouveau sur les principes de conception SOLID . Je comprends leur cause et leurs avantages, mais je n'arrive pas à les appliquer à un projet plus petit que je souhaite refactoriser comme un exercice pratique pour utiliser les principes SOLIDES. Je sais qu'il n'est pas nécessaire de changer une application qui fonctionne parfaitement, mais je veux quand même la refactoriser afin d'acquérir une expérience de conception pour de futurs projets.
L'application a la tâche suivante (en fait beaucoup plus que cela mais restons simple): elle doit lire un fichier XML qui contient les définitions de table / colonne / vue etc. de la base de données et créer un fichier SQL qui peut être utilisé pour créer un schéma de base de données ORACLE.
(Remarque: veuillez vous abstenir de discuter des raisons pour lesquelles j'en ai besoin ou pourquoi je n'utilise pas XSLT et ainsi de suite, il y a des raisons, mais elles sont hors sujet.)
Pour commencer, j'ai choisi de ne regarder que les tableaux et les contraintes. Si vous ignorez les colonnes, vous pouvez le déclarer de la manière suivante:
Une contrainte fait partie d'une table (ou plus précisément, fait partie d'une instruction CREATE TABLE), et une contrainte peut également référencer une autre table.
Tout d'abord, je vais expliquer à quoi ressemble l'application en ce moment (sans appliquer SOLID):
À l'heure actuelle, l'application possède une classe "Table" qui contient une liste de pointeurs vers des contraintes appartenant à la table et une liste de pointeurs vers des contraintes faisant référence à cette table. Chaque fois qu'une connexion est établie, la connexion arrière sera également établie. La table a une méthode createStatement () qui à son tour appelle la fonction createStatement () de chaque contrainte. Cette méthode utilisera elle-même les connexions à la table propriétaire et à la table référencée afin de récupérer leurs noms.
Évidemment, cela ne s'applique pas du tout à SOLID. Par exemple, il existe des dépendances circulaires, qui gonflent le code en termes de méthodes "ajouter" / "supprimer" requises et de certains destructeurs d'objets volumineux.
Il y a donc quelques questions:
- Dois-je résoudre les dépendances circulaires à l'aide de l'injection de dépendances? Si c'est le cas, je suppose que la contrainte doit recevoir le propriétaire (et éventuellement la table référencée) dans son constructeur. Mais comment pourrais-je alors parcourir la liste des contraintes pour une seule table?
- Si la classe Table enregistre à la fois son état (par exemple, nom de table, commentaire de table, etc.) et les liens vers les contraintes, s'agit-il d'une ou deux "responsabilités", en pensant au principe de responsabilité unique?
- Dans le cas 2. a raison, dois-je simplement créer une nouvelle classe dans la couche métier logique qui gère les liens? Dans l'affirmative, 1. ne serait évidemment plus pertinent.
- Les méthodes "createStatement" doivent-elles faire partie des classes Table / Contrainte ou dois-je également les déplacer? Si oui, où? Une classe Manager pour chaque classe de stockage de données (ie Table, Contrainte, ...)? Ou plutôt créer une classe de gestionnaire par lien (similaire à 3.)?
Chaque fois que j'essaie de répondre à l'une de ces questions, je me retrouve à tourner en rond quelque part.
Le problème devient évidemment beaucoup plus complexe si vous incluez des colonnes, des index et ainsi de suite, mais si vous m'aidez avec la simple chose Table / Contrainte, je peux peut-être résoudre le reste par moi-même.
la source
Réponses:
Vous pouvez partir d'un point de vue différent pour appliquer ici le "principe de responsabilité unique". Ce que vous nous avez montré n'est (plus ou moins) que le modèle de données de votre application. SRP signifie ici: assurez-vous que votre modèle de données est uniquement responsable de la conservation des données - ni plus ni moins.
Ainsi, lorsque vous allez lire votre fichier XML, créer un modèle de données à partir de celui-ci et écrire du SQL, ce que vous ne devez pas faire est d'implémenter quoi que ce soit dans votre
Table
classe qui soit spécifique à XML ou SQL. Vous voulez que votre flux de données ressemble à ceci:Donc , le seul endroit où le code spécifique de XML doit être placé est une classe nommée, par exemple,
Read_XML
. Le seul endroit pour le code spécifique SQL devrait être une classe commeWrite_SQL
. Bien sûr, vous allez peut-être diviser ces 2 tâches en plusieurs sous-tâches (et diviser vos classes en plusieurs classes de gestionnaire), mais votre "modèle de données" ne devrait pas prendre de responsabilité de cette couche. N'ajoutez donc pas de acreateStatement
à aucune de vos classes de modèle de données, car cela donne à votre modèle de données la responsabilité du SQL.Je ne vois aucun problème lorsque vous décrivez qu'une table est responsable de la tenue de toutes ses parties (nom, colonnes, commentaires, contraintes ...), c'est l'idée derrière un modèle de données. Mais vous avez décrit que "Table" est également responsable de la gestion de la mémoire de certaines de ses parties. C'est un problème spécifique à C ++, auquel vous ne seriez pas confronté si facilement dans des langages comme Java ou C #. La façon C ++ de se débarrasser de ces responsabilités consiste à utiliser des pointeurs intelligents, en déléguant la propriété à une couche différente (par exemple, la bibliothèque de boost ou à votre propre couche de pointeur "intelligente"). Mais attention, vos dépendances cycliques peuvent "irriter" certaines implémentations de pointeurs intelligents.
Quelque chose de plus sur SOLID: voici un bel article
http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game
expliquant SOLID par un petit exemple. Essayons d'appliquer cela à votre cas:
vous aurez besoin non seulement de classes
Read_XML
etWrite_SQL
, mais aussi d'une troisième classe qui gère l'interaction de ces 2 classes. Appelons cela unConversionManager
.En appliquant le principe de DI pourrait signifier ici: ConversionManager ne devrait pas créer des instances de
Read_XML
etWrite_SQL
par lui - même. Au lieu de cela, ces objets peuvent être injectés via le constructeur. Et le constructeur devrait avoir une signature comme celle-ciConversionManager(IDataModelReader reader, IDataModelWriter writer)
où
IDataModelReader
est une interface dontRead_XML
hérite, etIDataModelWriter
la même chose pourWrite_SQL
. CelaConversionManager
ouvre les extensions (vous fournissez très facilement différents lecteurs ou écrivains) sans avoir à le changer - nous avons donc un exemple pour le principe Open / Closed. Pensez à ce que vous devrez changer lorsque vous souhaitez prendre en charge un autre fournisseur de base de données - idéalement, vous n'avez rien à changer dans votre modèle de données, il suffit de fournir un autre SQL-Writer à la place.la source
Eh bien, vous devez appliquer le S de SOLID dans ce cas.
Une table contient toutes les contraintes qui y sont définies. Une contrainte contient toutes les tables auxquelles elle fait référence. Modèle simple et simple.
Ce que vous vous en tenez à cela, c'est la possibilité d'effectuer des recherches inverses, c'est-à-dire de déterminer par quelles contraintes une table est référencée.
Donc, ce que vous voulez réellement, c'est un service d'indexation. C'est une tâche complètement différente et doit donc être effectuée par un objet différent.
Pour le décomposer en une version très simplifiée:
Quant à l'implémentation de l'index, il y a 3 façons de procéder:
getContraintsReferencing
méthode pourrait vraiment simplement explorer l'ensembleDatabase
desTable
instances et explorer leursConstraint
s pour obtenir le résultat. Selon le coût et la fréquence à laquelle vous en avez besoin, cela peut être une option.Table
etConstraint
, lorsqu'ils changent. Une solution légèrement plus simple consisterait àIndex
créer un "index de clichés" de l'ensembleDatabase
avec lequel vous pourriez alors vous défaire. Cela n'est bien sûr possible que si votre application fait une grande distinction entre «temps de modélisation» et «temps d'interrogation». S'il est plutôt probable de faire ces deux en même temps, alors ce n'est pas viable.la source
Le remède contre les dépendances circulaires est de promettre que vous ne les créerez jamais. Je trouve que le codage test-first est un puissant moyen de dissuasion.
Quoi qu'il en soit, les dépendances circulaires peuvent toujours être brisées en introduisant une classe de base abstraite. Ceci est typique pour les représentations graphiques. Ici, les tables sont des nœuds et les contraintes de clé étrangère sont des arêtes. Créez donc une classe Table abstraite et une classe Contrainte abstraite et peut-être une classe Colonne abstraite. Ensuite, toutes les implémentations peuvent dépendre des classes abstraites. Ce n'est peut-être pas la meilleure représentation possible, mais c'est une amélioration par rapport aux classes mutuellement couplées.
Mais, comme vous le pensez, la meilleure solution à ce problème peut ne nécessiter aucun suivi des relations d'objet. Si vous souhaitez uniquement traduire XML en SQL, vous n'avez pas besoin d'une représentation en mémoire du graphe de contraintes. Le graphe de contraintes serait bien si vous vouliez exécuter des algorithmes de graphe, mais vous ne l'avez pas mentionné, donc je suppose que ce n'est pas une exigence. Vous avez juste besoin d'une liste de tables et d'une liste de contraintes et d'un visiteur pour chaque dialecte SQL que vous souhaitez prendre en charge. Générez les tables, puis générez les contraintes externes aux tables. Jusqu'à ce que les exigences changent, je n'aurais aucun problème à coupler le générateur SQL au DOM XML. Économisez demain pour demain.
la source