Combien de modèles de conception et de niveaux d'abstraction sont nécessaires? [fermé]

29

Comment puis-je savoir que mon logiciel contient trop d'abstraction et trop de modèles de conception, ou inversement, comment savoir s'il devrait en avoir plus?

Les développeurs avec lesquels je travaille programment différemment sur ces points.

Certains résument chaque petite fonction, utilisent des modèles de conception dans la mesure du possible et évitent la redondance à tout prix.

Les autres, dont moi, essaient d'être plus pragmatiques et écrivent du code qui ne convient pas parfaitement à tous les modèles de conception, mais qui est beaucoup plus rapide à comprendre car moins d'abstraction est appliquée.

Je sais que c'est un compromis. Comment puis-je savoir quand il y a suffisamment d'abstraction dans le projet et comment savoir s'il en a besoin de plus?

Exemple, lorsqu'une couche de mise en cache générique est écrite à l'aide de Memcache. Avons-nous vraiment besoin Memcache, MemcacheAdapter, MemcacheInterface, AbstractCache, CacheFactory, CacheConnector, ... ou est plus facile à maintenir et encore du bon code lorsque vous utilisez seulement la moitié de ces classes?

Trouvé cela sur Twitter:

entrez la description de l'image ici

( https://twitter.com/rawkode/status/875318003306565633 )

Daniel W.
la source
58
Si vous traitez les modèles de conception comme des choses que vous sortez d'un seau et utilisez pour assembler des programmes, vous en utilisez trop.
Blrfl
5
Vous pourriez penser à des modèles de conception comme des modèles vocaux que les gens utilisent. Parce que dans un sens, les idiomes, les métaphores, etc. sont tous des modèles de conception. Si vous utilisez un idiome à chaque phrase ... c'est probablement trop souvent. Mais ils peuvent aider à clarifier les pensées et aider à la compréhension de ce qui serait autrement un long mur de prose. Il n'y a pas vraiment de bonne façon de répondre "à quelle fréquence dois-je utiliser des métaphores?" - c'est au jugement de l'auteur.
Prime
8
Il n'y a aucun moyen qu'une seule réponse SE puisse couvrir adéquatement ce sujet. Cela prend littéralement des années d'expérience et de mentorat pour comprendre. C'est clairement trop large.
jpmc26
5
Suivez le principe de conception de base dans l'industrie aéronautique: "Simplifiez et ajoutez plus de légèreté". Lorsque vous découvrez que la meilleure façon de corriger un bogue est simplement de supprimer le code contenant le bogue, car il ne fait toujours rien d'utile même s'il était exempt de bogue, vous commencez à faire le bon design!
alephzero

Réponses:

52

Combien d'ingrédients sont nécessaires pour un repas? De combien de pièces avez-vous besoin pour construire un véhicule?

Vous savez que vous avez trop peu d'abstraction lorsqu'un petit changement d' implémentation conduit à une cascade de changements partout dans votre code. Des abstractions appropriées aideraient à isoler la partie du code qui doit être modifiée.

Vous savez que vous avez trop d'abstraction lorsqu'un petit changement d' interface conduit à une cascade de changements partout dans votre code, à différents niveaux. Au lieu de changer l'interface entre deux classes, vous vous retrouvez à modifier des dizaines de classes et d'interfaces simplement pour ajouter une propriété ou changer le type d'un argument de méthode.

En dehors de cela, il n'y a vraiment aucun moyen de répondre à la question en donnant un chiffre. Le nombre d'abstractions ne sera pas le même d'un projet à l'autre, d'un langage à un autre, et même d'un développeur à un autre.

Arseni Mourzenko
la source
28
Si vous n'avez que deux interfaces et des centaines de classes les implémentant, changer les interfaces entraînerait une cascade de changements, mais cela ne signifie pas qu'il y a trop d'abstraction, car vous n'avez que deux interfaces.
Tulains Córdova
Le nombre d'abstractions ne sera même pas le même pour différentes parties d'un même projet!
T. Sar - Rétablir Monica le
Astuce: le passage de Memcache à un autre mécanisme de mise en cache (Redis?) Est un changement d' implémentation .
Ogre Psalm33
Vos deux règles (directives, peu importe comment vous voulez les appeler) ne fonctionnent pas, comme l'a démontré Tulains. Ils sont aussi terriblement incomplets même s'ils l'ont fait. Le reste du message est une non-réponse, en disant un peu plus que nous ne pouvons pas fournir une réponse de portée raisonnable. -1
jpmc26
Je dirais que dans le cas de deux interfaces et des centaines de cours pour leur application, vous très probablement avoir débordé vos abstractions. J'ai certainement vu cela dans des projets qui réutilisent une abstraction très vague dans de nombreux endroits ( interface Doer {void prepare(); void doIt();}), et il devient douloureux de refactoriser lorsque cette abstraction ne correspond plus. L'élément clé de la réponse est que le test s'applique lorsqu'une abstraction doit changer - si elle ne le fait jamais, elle ne provoque jamais de douleur.
James_pic
24

Le problème avec les modèles de conception peut se résumer avec le proverbe "lorsque vous tenez un marteau, tout ressemble à un clou." Le fait d' appliquer un modèle de conception n'améliore en rien votre programme. En fait, je dirais que vous créez un programme plus compliqué si vous ajoutez un modèle de conception. La question reste de savoir si vous faites ou non un bon usage du modèle de conception, et c'est le cœur de la question, "Quand avons-nous trop d'abstraction?"

Si vous créez une interface et une super classe abstraite pour une seule implémentation, vous avez ajouté deux composants supplémentaires à votre projet qui sont superflus et inutiles. Le point de fournir une interface est de pouvoir la gérer de manière égale tout au long de votre programme sans savoir comment cela fonctionne. Le but d'une super classe abstraite est de fournir un comportement sous-jacent pour les implémentations. Si vous n'avez qu'une seule implémentation, vous obtenez toutes les interfaces de complication et les classes abstact fournissent et aucun des avantages.

De même, si vous utilisez un modèle Factory et que vous vous retrouvez à convertir une classe afin d'utiliser des fonctionnalités uniquement disponibles dans la super classe, le modèle Factory n'ajoute aucun avantage à votre code. Vous avez seulement ajouté une classe supplémentaire à votre projet qui aurait pu être évitée.

TL; DR Je veux dire que l'objectif d'abstraction n'est pas abstrait en soi. Il sert un objectif très pratique dans votre programme, et avant de décider d'utiliser un modèle de conception ou de créer une interface, vous devriez vous demander si ce faisant, le programme est plus facile à comprendre malgré la complexité supplémentaire ou le programme est plus robuste malgré la complexité supplémentaire (de préférence les deux). Si la réponse est non ou peut-être, alors prenez quelques minutes pour réfléchir à la raison pour laquelle vous vouliez le faire et si cela peut être fait de manière meilleure à la place sans nécessairement la nécessité d'ajouter une abstraction à votre code.

Neil
la source
L'analogie du marteau serait le problème de ne connaître qu'un seul modèle de conception. Les modèles de conception doivent créer une boîte à outils complète à sélectionner et à appliquer le cas échéant. Vous ne sélectionnez pas un marteau pour casser une noix.
Pete Kirkham
@PeteKirkham Certes, mais même un ensemble complet de modèles de conception à votre disposition peut ne pas convenir correctement à un problème particulier. Si un marteau n'est pas le mieux adapté pour casser un écrou, ni un tournevis, ni un ruban à mesurer parce que le marteau vous manque, cela ne fait pas d'un marteau le bon choix pour le travail, il fait juste c'est le plus approprié des outils à votre disposition. Cela ne signifie pas pour autant que vous devriez utiliser un marteau pour casser une noix. Heck, si nous sommes francs, ce dont vous auriez vraiment besoin, c'est d'un casse-noisette, pas d'un marteau ..
Neil
Je veux une armée d'écureuils entraînés pour mes noix qui se cassent.
icc97
6

TL: DR;

Je ne pense pas qu'il y ait un nombre "nécessaire" de niveaux d'abstraction au-dessous desquels il y a trop peu ou au-dessus desquels il y a trop. Comme dans la conception graphique, une bonne conception POO doit être invisible et doit être considérée comme acquise. Un mauvais design ressort toujours comme un pouce endolori.

Longue réponse

Très probablement, vous ne saurez jamais sur combien de niveaux d'abstractions vous construisez.

La plupart des niveaux d'abstraction nous sont invisibles et nous les tenons pour acquis.

Ce raisonnement m'amène à cette conclusion:

L'un des principaux objectifs de l'abstraction est de sauver le programmeur de la nécessité de garder à l'esprit tout le fonctionnement du système à tout moment. Si la conception vous oblige à en savoir trop sur le système pour ajouter quelque chose, il y a probablement trop peu d'abstraction. Je pense qu'une mauvaise abstraction (mauvaise conception, conception anémique ou suringénierie) peut également vous forcer à en savoir trop pour ajouter quelque chose. Dans un extrême, nous avons un design basé sur une classe divine ou un groupe de DTO, dans l'autre extrême, nous avons des cadres OR / persistance qui vous font sauter à travers d'innombrables cerceaux pour créer un monde bonjour. Les deux cas vous obligent à trop en savoir trop.

Une mauvaise abstraction adhère à une cloche de Gauss dans le fait qu'une fois que vous avez dépassé un point idéal, cela commence à vous gêner. Une bonne abstraction, en revanche, est invisible et il ne peut pas y en avoir trop parce que vous ne remarquez pas qu'elle est là. Pensez au nombre de couches sur les couches d'API, de protocoles réseau, de bibliothèques, de bibliothèques de systèmes d'exploitation, de systèmes de fichiers, de couches matérielles, etc. sur lesquelles votre application est construite et tient pour acquise.

L'autre objectif principal de l'abstraction est la compartimentation, de sorte que les erreurs ne pénètrent pas au-delà d'une certaine zone, contrairement à la double coque et les réservoirs séparés empêchent un navire d'inonder complètement lorsqu'une partie de la coque a un trou. Si des modifications de code finissent par créer des bogues dans des zones apparemment sans rapport, il y a de fortes chances qu'il y ait trop peu d'abstraction.

Tulains Córdova
la source
2
"Très probablement, vous ne saurez jamais combien de niveaux d'abstractions vous construisez." - En fait, l'intérêt d'une abstraction est que vous ne savez pas comment elle est implémentée, IOW vous ne savez pas (et ne pouvez pas ) combien de niveaux d'abstractions elle cache.
Jörg W Mittag
4

Les modèles de conception sont simplement des solutions courantes aux problèmes. Il est important de connaître les modèles de conception, mais ce ne sont que des symptômes d'un code bien conçu (un bon code peut toujours être vide de l'ensemble de quatre modèles de conception), pas la cause.

Les abstractions sont comme des clôtures. Ils aident à séparer les régions de votre programme en morceaux testables et interchangeables (exigences pour créer du code non rigide non rigide). Et un peu comme les clôtures:

  • Vous voulez des abstractions aux points d'interface naturels pour minimiser leur taille.

  • Vous ne voulez pas les changer.

  • Vous voulez qu'ils séparent les choses qui peuvent être indépendantes.

  • En avoir un au mauvais endroit est pire que de ne pas l'avoir.

  • Ils ne devraient pas avoir de grosses fuites .

dlasalle
la source
4

Refactoring

Je n'ai pas vu le mot "refactoring" mentionné une seule fois jusqu'à présent. Alors, c'est parti:

N'hésitez pas à implémenter une nouvelle fonctionnalité aussi directement que possible. Si vous n'avez qu'une seule classe simple, vous n'avez probablement pas besoin d'une interface, d'une superclasse, d'une usine, etc. pour cela.

Si et quand vous remarquez que vous développez la classe de manière à ce qu'elle grossisse trop, c'est le moment de la déchirer. À ce moment-là, il est très logique de réfléchir à la façon dont vous devriez réellement faire cela.

Les motifs sont un outil mental

Les modèles, ou plus précisément le livre "Design Patterns" du gang de quatre, sont excellents, entre autres, car ils créent un langage permettant aux développeurs de penser et de parler. Il est facile de dire "observateur", "usine" ou "façade" et tout le monde sait exactement ce que cela signifie, tout de suite.

Donc, mon avis serait que chaque développeur devrait avoir une connaissance passagère au moins des modèles du livre original, simplement pour pouvoir parler des concepts OO sans avoir à expliquer les bases. Devriez-vous réellement utiliser les modèles chaque fois qu'une possibilité de le faire apparaît? Préférablement pas.

Bibliothèques

Les bibliothèques sont probablement le seul domaine où il peut être afin d'errer du côté de trop de choix basés sur des modèles au lieu de trop peu. Changer quelque chose d'une classe "fat" en quelque chose avec plus de modèles dérivés (généralement cela signifie des classes plus nombreuses et plus petites) changera radicalement l'interface; et c'est la seule chose que vous ne voulez généralement pas changer dans une bibliothèque, car c'est la seule chose qui présente un réel intérêt pour l'utilisateur de votre bibliothèque. Ils ne se soucieraient pas moins de la façon dont vous traitez vos fonctionnalités en interne, mais ils se soucient beaucoup s'ils doivent constamment changer de programme lorsque vous faites une nouvelle version avec une nouvelle API.

AnoE
la source
2

Le point de l'abstraction doit être avant tout la valeur apportée au consommateur de l'abstraction, c'est-à-dire le client de l'abstraction, les autres programmeurs et souvent vous-même.

Si, en tant que client qui consomme les abstractions, vous trouvez que vous devez mélanger et faire correspondre de nombreuses abstractions différentes pour faire votre travail de programmation, alors il y a potentiellement trop d'abstractions.

Idéalement, la superposition devrait regrouper un certain nombre d'abstractions inférieures et les remplacer par une abstraction simple et de niveau supérieur que ses consommateurs peuvent utiliser sans avoir à traiter avec l'une de ces abstractions sous-jacentes. S'ils doivent faire face aux abstractions sous-jacentes, alors la couche fuit (par voie d'être incomplète). Si le consommateur doit faire face à trop d'abstractions différentes, la superposition manque peut-être.

Après avoir considéré la valeur des abstractions pour les programmeurs consommateurs, nous pouvons alors nous tourner pour évaluer et considérer l'implémentation, comme celle de DRY-ness.

Oui, il s'agit de faciliter la maintenance, mais nous devons d'abord considérer le sort de la maintenance de nos consommateurs, en fournissant des abstractions et des couches de qualité, puis envisager de faciliter notre propre maintenance en termes d'aspects de mise en œuvre, comme éviter la redondance.


Exemple, lorsqu'une couche de mise en cache générique est écrite à l'aide de Memcache. Avons-nous vraiment besoin de Memcache, MemcacheAdapter, MemcacheInterface, AbstractCache, CacheFactory, CacheConnector, ... ou est-ce plus facile à maintenir et toujours bon code lorsque vous n'utilisez que la moitié de ces classes?

Nous devons examiner le point de vue du client, et si sa vie est facilitée, c'est bien. Si leur vie est plus complexe, c'est mal. Cependant, il se pourrait qu'il y ait une couche manquante qui regroupe ces éléments dans quelque chose de simple à utiliser. En interne, cela peut très bien améliorer la maintenance de l'implémentation. Cependant, comme vous le pensez, il est également possible qu'il soit simplement trop conçu.

Erik Eidt
la source
2

L'abstraction est conçue pour rendre le code plus facile à comprendre. Si une couche d'abstraction va rendre les choses plus confuses - ne le faites pas.

L'objectif est d'utiliser le nombre correct d'abstractions et d'interfaces pour:

  • minimiser le temps de développement
  • maximiser la maintenabilité du code

Résumé seulement si nécessaire

  1. Lorsque vous découvrez que vous écrivez une super classe
  2. Quand il permettra une réutilisation importante du code
  3. Si abstraire fera le code deviendra beaucoup plus clair et plus facile à lire

Ne résumez pas lorsque

  1. Cela n'aura aucun avantage en termes de réutilisation de code ou de clarté
  2. Cela rendra le code beaucoup plus long / plus complexe sans aucun avantage

Quelques exemples

  • Si vous ne devez avoir qu'un seul cache dans tout votre programme, ne faites pas de résumé, sauf si vous pensez que vous risquez de vous retrouver avec une superclasse
  • Si vous avez trois types de tampons différents, utilisez une abstraction d'interface commune à chacun d'eux
sdfgeoff
la source
2

Je pense que cela pourrait être une méta-réponse controversée, et je suis un peu en retard à la fête, mais je pense qu'il est très important de le mentionner ici, car je pense que je sais d'où vous venez.

Le problème avec la façon dont les modèles de conception sont utilisés, c'est que lorsqu'ils sont enseignés, ils présentent un cas comme celui-ci:

Vous avez ce scénario spécifique. Organisez votre code de cette façon. Voici un exemple intelligent, mais quelque peu artificiel.

Le problème est que lorsque vous commencez à faire de la vraie ingénierie, les choses ne sont pas aussi simples. Le modèle de conception que vous lisez ne correspond pas tout à fait au problème que vous essayez de résoudre. Sans oublier que les bibliothèques que vous utilisez violent totalement tout ce qui est indiqué dans le texte expliquant ces modèles, chacun à sa manière. Et par conséquent, le code que vous écrivez "se sent mal" et vous posez des questions comme celle-ci.

En plus de cela, je voudrais citer Andrei Alexandrescu, en parlant de génie logiciel, qui déclare:

Le génie logiciel, peut-être plus que toute autre discipline du génie, présente une riche multiplicité: vous pouvez faire la même chose de tant de façons correctes, et il y a des nuances infinies entre le bien et le mal.

C'est peut-être un peu exagéré, mais je pense que cela explique parfaitement une raison supplémentaire pour laquelle vous pourriez vous sentir moins confiant dans votre code.

C'est dans des moments comme celui-ci que la voix prophétique de Mike Acton, responsable du moteur de jeu chez Insomniac, me crie dans la tête:

CONNAISSEZ VOS DONNÉES

Il parle des entrées de votre programme et des sorties souhaitées. Et puis il y a ce joyau de Fred Brooks du mois de l'homme mythique:

Montrez-moi vos organigrammes et cachez vos tables, et je continuerai à être mystifié. Montrez-moi vos tableaux et je n'aurai généralement pas besoin de vos organigrammes; ils seront évidents.

Donc, si j'étais vous, je raisonnerais sur mon problème en fonction de mon cas d'entrée typique, et s'il atteint la sortie correcte souhaitée. Et posez des questions comme celle-ci:

  • Les données de sortie de mon programme sont-elles correctes?
  • Est-il produit efficacement / rapidement pour mon cas d'entrée le plus courant?
  • Mon code est-il assez facile à raisonner localement, pour moi et mes coéquipiers? Sinon, puis-je le simplifier?

Lorsque vous faites cela, la question de "combien de couches d'abstraction ou de motifs de conception sont nécessaires" devient beaucoup plus facile à répondre. De combien de couches d'abstraction avez-vous besoin? Autant que nécessaire pour atteindre ces objectifs, et pas plus. "Qu'en est-il des modèles de conception? Je n'en ai pas utilisé!" Eh bien, si les objectifs ci-dessus ont été atteints sans application directe d'un modèle, c'est bien. Faites-le fonctionner et passez au problème suivant. Commencez par vos données, pas par le code.

nasser-sh
la source
2

L'architecture logicielle invente les langues

Avec chaque couche logicielle, vous créez le langage dans lequel vous (ou vos collègues) souhaitez exprimer leur solution de couche supérieure suivante (donc, je vais ajouter quelques analogues en langage naturel dans mon article). Vos utilisateurs ne veulent pas passer des années à apprendre à lire ou à écrire cette langue.

Cette vue m'aide à décider des problèmes architecturaux.

Lisibilité

Cette langue doit être facilement comprise (rendant le code de la couche suivante lisible). Le code est lu beaucoup plus souvent qu'écrit.

Un concept doit être exprimé avec un mot - une classe ou une interface doit exposer le concept. (Les langues slaves ont généralement deux mots différents pour un verbe anglais, vous devez donc apprendre deux fois le vocabulaire. Toutes les langues naturelles utilisent des mots uniques pour plusieurs concepts).

Les concepts que vous exposez ne doivent pas contenir de surprises. Il s'agit principalement de conventions de dénomination telles que les méthodes get et set, etc. Mais si simplement instancier une classe concrète fait le travail, je préfère cela.

Convivialité

La langue doit être facile à utiliser (ce qui facilite la formulation de «phrases correctes»).

Si toutes ces classes / interfaces MemCache deviennent visibles pour la couche suivante, cela crée une courbe d'apprentissage abrupte pour l'utilisateur jusqu'à ce qu'il comprenne quand et où utiliser lequel de ces mots pour le concept unique de cache.

Exposer uniquement les classes / méthodes nécessaires permet à votre utilisateur de trouver plus facilement ce dont il a besoin (voir la citation DocBrowns d'Antoine de Saint-Exupéry). L'exposition d'une interface au lieu de la classe d'implémentation peut faciliter cela.

Si vous exposez une fonctionnalité où un modèle de conception établi peut s'appliquer, il vaut mieux suivre ce modèle de conception que d'inventer quelque chose de différent. Votre utilisateur comprendra les API suivant un modèle de conception plus facilement qu'un concept complètement différent (si vous connaissez l'italien, l'espagnol vous sera plus facile que le chinois).

Sommaire

Introduisez des abstractions si cela facilite l'utilisation (et vaut la peine de maintenir l'abstraction et l'implémentation).

Si votre code a une sous-tâche (non triviale), résolvez-la "de la manière attendue", c'est-à-dire suivez le modèle de conception approprié au lieu de réinventer un type de roue différent.

Ralf Kleberhoff
la source
1

La chose importante à considérer est de savoir combien le code consommateur qui gère réellement votre logique métier a besoin de connaître ces classes liées à la mise en cache. Idéalement, votre code ne devrait se soucier que de l'objet cache qu'il souhaite créer et peut-être d'une fabrique pour créer cet objet si une méthode constructeur n'est pas suffisante.

Le nombre de modèles utilisés ou le niveau d'héritage ne sont pas trop importants tant que chaque niveau peut être justifié par d'autres développeurs. Cela crée une limite informelle car chaque niveau supplémentaire est plus difficile à justifier. La partie la plus importante est le nombre de niveaux d'abstraction affectés par les modifications des exigences fonctionnelles ou commerciales. Si vous pouvez modifier un seul niveau pour une seule exigence, vous n'êtes probablement pas trop abstrait ou mal résumé, si vous changez le même niveau pour plusieurs modifications non liées, vous êtes probablement sous-extrait et devez séparer davantage les préoccupations.

Ryathal
la source
-1

Tout d'abord, la citation de Twitter est fausse. Les nouveaux développeurs doivent faire un modèle, les abstractions les aident généralement à "se faire une idée". À condition que les abstractions aient un sens, bien sûr.

Deuxièmement, votre problème n'est pas trop ou trop peu d'abstractions, c'est qu'apparemment personne ne peut décider de ces choses. Personne ne possède le code, aucun plan / conception / philosophie unique n'est mis en œuvre, n'importe quel gars suivant peut faire ce qu'il veut pour ce moment. Quel que soit le style que vous choisissez, il doit en être un.

Martin Maat
la source
2
Evitons de rejeter l'expérience comme "fausse". Trop d'abstractions sont un vrai problème. Malheureusement, les gens ajoutent l'abstraction dès le départ, parce que "les meilleures pratiques", plutôt que de résoudre un problème réel. De plus, personne ne peut décider "de ces choses" ... les gens quittent les entreprises, les gens adhèrent, personne ne s'approprie leur boue.
Rawkode