Appliquer les principes SOLID

13

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:

  1. 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?
  2. 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?
  3. 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.
  4. 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.

Tim Meyer
la source
3
Quelle langue utilisez-vous? Pourriez-vous publier au moins un code squelette? Il est très difficile de discuter de la qualité du code et des remaniements possibles sans voir le code réel.
Péter Török
J'utilise C ++ mais j'essayais de le garder hors de la discussion car vous pourriez avoir ce problème dans n'importe quelle langue
Tim Meyer
Oui, mais l'application de modèles et de refactorings dépend de la langue. Par exemple, @ back2dos a suggéré AOP dans sa réponse ci-dessous, qui ne s'applique évidemment pas au C ++.
Péter Török
Veuillez consulter programmers.stackexchange.com/questions/155852/… pour en savoir plus sur les principes SOLID
LCJ

Réponses:

8

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 Tableclasse qui soit spécifique à XML ou SQL. Vous voulez que votre flux de données ressemble à ceci:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

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 comme Write_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 a createStatementà 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_XMLet Write_SQL, mais aussi d'une troisième classe qui gère l'interaction de ces 2 classes. Appelons cela un ConversionManager.

  • En appliquant le principe de DI pourrait signifier ici: ConversionManager ne devrait pas créer des instances de Read_XMLet Write_SQLpar 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-ci

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

IDataModelReaderest une interface dont Read_XMLhérite, et IDataModelWriterla même chose pour Write_SQL. Cela ConversionManagerouvre 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.

Doc Brown
la source
Bien qu'il s'agisse d'un exercice très raisonnable de SOLID, notez (positivement) qu'il viole "la vieille école Kay / Holub OOP" en exigeant des getters et setters un modèle de données assez anémique. Cela me rappelle également l'infâme diatribe de Steve Yegge .
user949300
2

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:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

Quant à l'implémentation de l'index, il y a 3 façons de procéder:

  • la getContraintsReferencingméthode pourrait vraiment simplement explorer l'ensemble Databasedes Tableinstances et explorer leurs Constraints pour obtenir le résultat. Selon le coût et la fréquence à laquelle vous en avez besoin, cela peut être une option.
  • il pourrait également utiliser un cache. Si votre modèle de base de données peut changer une fois défini, vous pouvez maintenir le cache en envoyant des signaux à partir des instances respectives Tableet Constraint, lorsqu'ils changent. Une solution légèrement plus simple consisterait à Indexcréer un "index de clichés" de l'ensemble Databaseavec 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.
  • Une autre option serait d'utiliser AOP pour intercepter tous les appels de création et maintenir l'index en conséquence.
back2dos
la source
Réponse très détaillée, j'aime votre solution jusqu'à présent! Que penseriez-vous si j'effectuais DI pour la classe Table, en lui donnant une liste de contraintes pendant la construction? J'ai une classe TableParser de toute façon, qui pourrait agir comme une usine ou travailler avec une usine dans ce cas.
Tim Meyer
@Tim Meyer: DI n'est pas nécessairement une injection constructeur. L'ID peut également être effectuée par les fonctions membres. Si la table doit obtenir toutes ses parties via le constructeur, cela dépend si vous souhaitez que ces parties soient ajoutées uniquement au moment de la construction et ne changent jamais plus tard, ou si vous souhaitez créer une table étape par étape. Cela devrait être la base de votre décision de conception.
Doc Brown
1

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.

Kevin Cline
la source
C'est là que "(en fait beaucoup plus que cela mais restons simple)" entre en jeu. Par exemple, dans certains cas, je dois supprimer une table, je dois donc vérifier si des contraintes font référence à cette table.
Tim Meyer