Divisez les grandes interfaces

9

J'utilise une grande interface avec environ 50 méthodes pour accéder à une base de données. L'interface a été écrite par un de mes collègues. Nous en avons discuté:

Moi: 50 méthodes, c'est trop. C'est une odeur de code.
Collègue: Que dois-je faire à ce sujet? Vous voulez l'accès DB - vous l'avez.
Moi: Oui, mais ce n'est pas clair et difficilement maintenable à l'avenir.
Collègue: OK, vous avez raison, ce n'est pas sympa. À quoi devrait ressembler l'interface alors?
Moi: que diriez-vous de 5 méthodes qui retournent des objets qui ont, comme, 10 méthodes chacun?

Mmmh, mais ça ne serait pas pareil? Cela conduit-il vraiment à plus de clarté? Cela en vaut-il la peine?

De temps en temps, je suis dans une situation où je veux une interface et la première chose qui me vient à l'esprit est une grande interface. Existe-t-il un modèle de conception général pour cela?


Mise à jour (répondant au commentaire de SJuan):

Le "genre de méthodes": c'est une interface pour récupérer les données d'une base de données. Toutes les méthodes ont la forme (pseudocode)

List<Typename> createTablenameList()

Les méthodes et les tableaux ne sont pas exactement dans une relation 1-1, l'accent est davantage mis sur le fait que vous obtenez toujours une sorte de liste provenant d'une base de données.

TobiMcNamobi
la source
12
Il manque des informations pertinentes (quel type de méthodes vous avez). Quoi qu'il en soit, je suppose: si vous divisez uniquement en nombre, alors votre collègue a raison, vous n'améliorez rien. Un critère de division possible serait par "l'entité" (presque l'équivalent d'un tableau) retourné (donc, a UserDaoet a CustomerDaoet a ProductDao)
SJuan76
En effet certaines tables sont sémantiquement proches d'autres tables formant des "cliques". Les méthodes aussi.
TobiMcNamobi
Est-il possible de voir le code? Je sais qu'il a 4 ans et vous l'avez probablement résolu maintenant: D Mais j'aimerais penser à ce problème. J'ai résolu quelque chose comme ça avant.
clankill3r
@ clankill3r En effet je n'ai plus accès à l'interface spécifique qui m'a fait poster la question ci-dessus. Le problème est cependant plus général. Imaginez une base de données avec environ 50 tables et pour chaque table une méthode commeList<WeatherDataRecord> createWeatherDataTable() {db.open(); return db.select("*", "tbl_weatherData");}
TobiMcNamobi

Réponses:

16

Oui, 50 méthodes sont une odeur de code, mais une odeur de code signifie y jeter un second coup d'œil, pas qu'elle soit automatiquement erronée. Si chaque client utilisant cette classe a potentiellement besoin des 50 méthodes, il peut ne pas être nécessaire de la diviser. Cependant, cela est peu probable. Mon point est que fractionner arbitrairement une interface peut être pire que de ne pas la diviser du tout.

Il n'y a pas un seul modèle pour le corriger, mais le principe qui décrit l'état souhaité est le principe de séparation d'interface (le «I» dans SOLID), qui stipule qu'aucun client ne devrait être forcé de dépendre de méthodes qu'il n'utilise pas .

La description du FAI vous donne un indice sur la façon de résoudre ce problème: regardez le client . Souvent, juste en regardant une classe, il semble que tout va ensemble, mais des divisions claires émergent lorsque vous regardez les clients utilisant cette classe. Tenez toujours compte des clients en premier lors de la conception d'une interface.

L'autre façon de déterminer si et où une interface doit être divisée est de faire une deuxième implémentation. Ce qui finit souvent par arriver, c'est que votre deuxième implémentation n'a pas besoin de beaucoup de méthodes, donc celles-ci doivent clairement être divisées en leur propre interface.

Karl Bielefeldt
la source
Ceci et la réponse d'utnapistim sont vraiment excellents.
TobiMcNamobi
13

Collègue: OK, vous avez raison, ce n'est pas sympa. À quoi devrait ressembler l'interface alors?

Moi: que diriez-vous de 5 méthodes qui retournent des objets qui ont, comme, 10 méthodes chacun?

Ce ne sont pas de bons critères (il n'y a en fait aucun critère dans cette déclaration). Vous pouvez les regrouper par (en supposant que votre application est une application de transactions financières, pour mes exemples):

  • fonctionnalité (insertions, mises à jour, sélections, transactions, métadonnées, schéma, etc.)
  • entité ( DAO utilisateur, DAO de dépôt, etc.)
  • domaine d'application (transactions financières, gestion des utilisateurs, totaux, etc.)
  • niveau d'abstraction (tout le code d'accès à la table est un module séparé; toutes les API sélectionnées sont dans leur propre hiérarchie, la prise en charge des transactions est séparée, tout le code de conversion dans un module, tout le code de validation dans un module, etc.)

Mmmh, mais ça ne serait pas pareil? Cela conduit-il vraiment à plus de clarté? Cela en vaut-il la peine?

Si vous choisissez les bons critères, certainement. Sinon, certainement pas :).

Quelques exemples:

  • regardez les objets ADODB pour un exemple simpliste de primitives OO (votre API DB le propose probablement déjà)

  • regardez le modèle de données Django ( https://docs.djangoproject.com/en/dev/topics/db/models/ ) pour une idée de modèle de données avec un haut niveau d'abstraction (en C ++ vous aurez probablement besoin d'un peu plus de plaque de chaudière mais c'est une bonne idée). Cette implémentation est conçue avec un rôle de «modèle» à l'esprit, dans le modèle de conception MVC.

  • regardez l'API sqlite pour une idée d'API plate ( http://www.sqlite.org/c3ref/funclist.html ), constituée uniquement de primitives fonctionnelles (API C).

utnapistim
la source
3

De temps en temps, je suis dans une situation où je veux une interface et la première chose qui me vient à l'esprit est une grande interface. Existe-t-il un modèle de conception général pour cela?

C'est un design anti-motif appelé classe monolithique . Avoir 50 méthodes dans une classe ou une interface est une violation probable du SRP . La classe monolithique naît parce qu'elle essaie d'être tout pour tout le monde.

DCI résout le problème de méthode. Essentiellement, les nombreuses responsabilités d'une classe pourraient être réparties en rôles (déchargés dans d'autres classes) qui ne sont pertinents que dans certains contextes. L'application des rôles peut être réalisée de plusieurs façons, y compris les mixins ou les décorateurs . Cette approche maintient les classes concentrées et allégées.

Que diriez-vous de 5 méthodes qui retournent des objets qui ont, comme, 10 méthodes chacun?

Cela suggère d'instancier tous les rôles lorsque l'objet est lui-même instancié. Mais pourquoi instancier des rôles dont vous pourriez ne pas avoir besoin? Au lieu de cela, instanciez un rôle dans le contexte dans lequel vous en avez réellement besoin.

Si vous trouvez que le refactoring vers DCI n'est pas évident, vous pouvez opter pour un modèle de visiteur plus simple . Il offre un avantage similaire sans mettre l'accent sur la création de contextes de cas d'utilisation.

EDIT: Ma réflexion à ce sujet a changé certains. J'ai fourni une réponse alternative.

Mario T. Lanza
la source
1

Il me semble que toutes les autres réponses ne sont pas pertinentes. Le fait est qu'une interface devrait idéalement définir un morceau atomique de comportement. C'est le je dans SOLID.

Une classe devrait avoir une seule responsabilité, mais cela pourrait toujours inclure plusieurs comportements. Pour rester avec un objet client de base de données typique, cela peut offrir une fonctionnalité CRUD complète. Ce serait quatre comportements: créer, lire, mettre à jour et supprimer. Dans un monde purement SOLIDE, le client de base de données implémenterait non pas IDatabaseClient mais unstead ICreator, IReader, IUpdater et IDeleter.

Cela aurait un certain nombre d'avantages. Premièrement, juste en lisant la déclaration de classe, on en apprendrait instantanément beaucoup sur la classe, les interfaces qu'elle implémente racontent toute l'histoire. Deuxièmement, si l'objet client devait être passé comme argument, on a maintenant différentes options utiles. Il pourrait être adopté en tant que lecteur IR et on pourrait être sûr que l'appelé ne pourrait lire. Différents comportements pourraient être testés séparément.

Cependant, en ce qui concerne les tests, la pratique courante consiste simplement à gifler une interface sur une classe qui est une réplique 1 à 1 de l'interface de classe complète. Si vous ne vous souciez que des tests, cela peut être une approche valable. Il vous permet de faire des mannequins assez rapidement. Mais ce n'est presque jamais SOLIDE et vraiment un abus d'interfaces à des fins spécifiques.

Alors oui, 50 méthodes sont une odeur mais cela dépend de l'intention et du but, que ce soit mauvais ou non. Ce n'est certainement pas idéal.

Martin Maat
la source
0

Les couches d'accès aux données ont généralement de nombreuses méthodes attachées à une classe. Si vous avez déjà travaillé avec Entity Framework ou d'autres outils ORM, vous verrez qu'ils génèrent des centaines de méthodes. Je suppose que vous et votre collègue le mettez en œuvre manuellement. Ce n'est pas nécessaire une odeur de code, mais ce n'est pas joli à regarder. Sans connaître votre domaine, c'est difficile à dire.

Roman Mik
la source
Méthodes ou propriétés?
JeffO
0

J'utilise des protocoles (appelez-les interfaces si vous le souhaitez) presque universellement pour tous les API avec FP et OOP. (Vous vous souvenez de la matrice? Il n'y a pas de concrétions!) Il existe bien sûr des types concrets mais dans le cadre d'un programme, chaque type est considéré comme quelque chose qui joue un rôle dans un certain contexte.

Cela signifie que les objets transmis dans les programmes, dans les fonctions, etc. peuvent être considérés comme des entités abstraites avec des ensembles de comportements nommés. L'objet peut être considéré comme jouant un rôle qui est un ensemble de protocoles. Une personne (type concret) pourrait être un homme, un père, un mari, un ami et un employé, mais je ne peux pas imaginer beaucoup de fonctions qui considéreraient l'entité comme la somme de plus de 2 d'entre elles.

Je suppose qu'il est possible qu'un objet complexe puisse respecter un certain nombre de protocoles différents, mais vous auriez toujours du mal à accéder à une API à 50 méthodes. La plupart des protocoles ont 1 ou 2 méthodes, peut-être 3, mais jamais 50! Toute entité disposant de 50 méthodes doit être constituée de l'ensemble de composants plus petits, chacun ayant ses propres responsabilités. L'entité dans son ensemble présenterait une interface plus simple qui résume la somme totale des apis en son sein.

Plutôt que de penser en termes d'objets et de méthodes, commencez à penser en termes d'abstractions et de contrats et quels rôles un sujet joue dans un certain contexte.

Mario T. Lanza
la source