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 toutes les petites fonctions, 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 savoir quand il y a suffisamment d'abstraction dans le projet et comment puis-je savoir qu'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 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?
Trouvé cela sur Twitter:
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 implémentation changement 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 quand un petit changement 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.
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 de 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 but de fournir une interface est de pouvoir la gérer de manière égale tout au long de votre programme sans savoir comment elle 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 implémentation de n, vous obtenez toutes les interfaces de complication et les classes abstacts et aucun des avantages.
De même, si vous utilisez un modèle Factory et que vous vous retrouvez à lancer 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é les complexité 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 d'une meilleure manière à la place sans nécessairement la nécessité d'ajouter une abstraction à votre code.
Je ne pense pas qu'il y ait un nombre "nécessaire" de niveaux d'abstraction en dessous desquels il y a trop peu ou au-dessus desquels il y a trop. Comme dans la conception graphique, une bonne OOP conception doit être invisible et doit être tenue pour acquise. Une mauvaise conception ressort toujours comme un pouce endolori.
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é d'avoir tout le fonctionnement du système à l'esprit tout le temps. Si la conception vous oblige à en savoir trop sur le système afin d'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ème 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 de submerger complètement lorsqu'une partie de la coque a un trou. Si les modifications de code finissent par créer des bogues dans des zones apparemment sans rapport alors il y a des chances qu'il y ait trop peu d'abstraction.
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 dépourvu de l'ensemble de modèles de conception gang de quatre ), 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 (conditions requises pour créer un 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 gros fuites .
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 penser à comment vous devriez réellement le faire.
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 opinion 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. en fait tilisez les modèles chaque fois qu'une possibilité de le faire apparaît? Probablement pas.
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.
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 gérer les 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 regarder le point de vue du client, et si sa vie est facilitée, alors 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.
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:
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:
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.
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.
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 simples 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. Et les modèles de conception peuvent aider car ils fournissent un modèle de solution standard, et le lecteur voit "OK, je reçois les objets d'une usine" et sait ce que cela signifie. Mais si simplement instancier une classe concrète fait le travail, je préfère cela.
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).
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.
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.