Les classes de base abstraites sont utilisées de deux manières.
Vous spécialisez votre objet abstrait, mais tous les clients utiliseront la classe dérivée via son interface de base.
Vous utilisez une classe de base abstraite pour éliminer la duplication au sein des objets de votre conception, et les clients utilisent les implémentations concrètes via leurs propres interfaces.!
Solution pour 1 - Modèle de stratégie
Si vous avez la première situation, vous avez en fait une interface définie par les méthodes virtuelles dans la classe abstraite que vos classes dérivées implémentent.
Vous devriez envisager d'en faire une véritable interface, de changer votre classe abstraite pour qu'elle soit concrète, et de prendre une instance de cette interface dans son constructeur. Vos classes dérivées deviennent alors des implémentations de cette nouvelle interface.
Cela signifie que vous pouvez désormais tester votre classe précédemment abstraite à l'aide d'une instance fictive de la nouvelle interface et de chaque nouvelle implémentation via l'interface désormais publique. Tout est simple et testable.
Solution pour 2
Si vous avez la deuxième situation, alors votre classe abstraite fonctionne comme une classe auxiliaire.
Jetez un œil aux fonctionnalités qu'il contient. Voyez si une partie peut être poussée sur les objets qui sont manipulés pour minimiser cette duplication. S'il vous reste encore quelque chose, envisagez d'en faire une classe auxiliaire que votre implémentation concrète prendra dans son constructeur et supprimera sa classe de base.
Cela conduit à nouveau à des classes concrètes qui sont simples et facilement testables.
Comme règle
Privilégiez un réseau complexe d'objets simples à un simple réseau d'objets complexes.
La clé du code testable extensible est de petits blocs de construction et un câblage indépendant.
Mise à jour: comment gérer les mélanges des deux?
Il est possible d'avoir une classe de base remplissant ces deux rôles ... c'est-à-dire: elle a une interface publique et a des méthodes d'assistance protégées. Si tel est le cas, vous pouvez factoriser les méthodes d'assistance en une seule classe (scénario 2) et convertir l'arbre d'héritage en un modèle de stratégie.
Si vous trouvez que certaines méthodes implémentent directement votre classe de base et que d'autres sont virtuelles, vous pouvez toujours convertir l'arbre d'héritage en un modèle de stratégie, mais je le considérerais également comme un bon indicateur que les responsabilités ne sont pas correctement alignées et peuvent besoin de refactoring.
Mise à jour 2: les classes abstraites comme tremplin (2014/06/12)
L'autre jour, j'ai eu une situation où j'ai utilisé l'abstrait, alors j'aimerais explorer pourquoi.
Nous avons un format standard pour nos fichiers de configuration. Cet outil particulier a 3 fichiers de configuration tous dans ce format. Je voulais une classe fortement typée pour chaque fichier de paramètres afin que, grâce à l'injection de dépendances, une classe puisse demander les paramètres qui lui importaient.
J'ai implémenté cela en ayant une classe de base abstraite qui sait analyser les formats de fichiers de paramètres et les classes dérivées qui ont exposé ces mêmes méthodes, mais a encapsulé l'emplacement du fichier de paramètres.
J'aurais pu écrire un "SettingsFileParser" que les 3 classes ont encapsulé, puis délégué à la classe de base pour exposer les méthodes d'accès aux données. J'ai choisi de ne pas le faire encore car cela conduirait à 3 classes dérivées avec plus de code de délégation en elles qu'autre chose.
Cependant ... à mesure que ce code évolue et que les consommateurs de chacune de ces classes de paramètres deviennent plus clairs. Chaque utilisateur de paramètres demandera certains paramètres et les transformera d'une certaine manière (comme les paramètres sont du texte, ils peuvent les envelopper dans des objets de les convertir en nombres, etc.). Dans ce cas, je vais commencer à extraire cette logique dans des méthodes de manipulation de données et à les repousser dans les classes de paramètres fortement typées. Cela conduira à une interface de niveau supérieur pour chaque ensemble de paramètres, qui finit par ne plus savoir qu'il s'agit de «paramètres».
À ce stade, les classes de paramètres fortement typées n'auront plus besoin des méthodes «getter» qui exposent l'implémentation sous-jacente des «paramètres».
À ce stade, je ne voudrais plus que leur interface publique inclue les méthodes d'accesseur des paramètres; je vais donc changer cette classe pour encapsuler une classe d'analyseur de paramètres au lieu d'en dériver.
La classe Abstract est donc: un moyen pour moi d'éviter le code de délégation pour le moment, et un marqueur dans le code pour me rappeler de changer le design plus tard. Je n'y arriverai peut-être jamais, donc ça peut vivre longtemps ... seul le code peut le dire.
Je trouve que cela est vrai avec n'importe quelle règle ... comme "pas de méthodes statiques" ou "pas de méthodes privées". Ils indiquent une odeur dans le code ... et c'est bien. Il vous permet de rechercher l'abstraction que vous avez manquée ... et vous permet de continuer à apporter de la valeur à votre client dans l'intervalle.
J'imagine des règles comme celle-ci définissant un paysage, où le code maintenable vit dans les vallées. Lorsque vous ajoutez un nouveau comportement, c'est comme une pluie tombant sur votre code. Au début, vous le placez partout où il atterrit .. puis vous refactorisez pour permettre aux forces de bonne conception de pousser le comportement jusqu'à ce que tout finisse dans les vallées.