Utiliser la classe abstraite en C # comme définition


21

En tant que développeur C ++, je suis assez habitué aux fichiers d'en-tête C ++ et je trouve utile d'avoir une sorte de "documentation" forcée à l'intérieur du code. J'ai généralement du mal à lire du code C # à cause de cela: je n'ai pas ce genre de carte mentale de la classe avec laquelle je travaille.

Supposons qu'en tant qu'ingénieur logiciel, je conçois le cadre d'un programme. Serait-il trop fou de définir chaque classe comme une classe abstraite non implémentée, de manière similaire à ce que nous ferions avec les en-têtes C ++, et de laisser les développeurs l'implémenter?

Je suppose qu'il peut y avoir des raisons pour lesquelles quelqu'un pourrait trouver cela comme une terrible solution, mais je ne sais pas pourquoi. Que faudrait-il considérer pour une solution comme celle-ci?


13
C # est un langage de programmation totalement différent de C ++. Certaines syntaxes semblent similaires, mais vous devez comprendre C # pour y faire du bon travail. Et vous devriez faire quelque chose contre la mauvaise habitude que vous avez de vous fier aux fichiers d'en-tête. Je travaille avec C ++ depuis des décennies, je n'ai jamais lu les fichiers d'en-tête, je les ai seulement écrits.
Bent le

17
L'une des meilleures choses de C # et Java est la liberté des fichiers d'en-tête! Utilisez simplement un bon IDE.
Erik Eidt

9
Les déclarations dans les fichiers d'en-tête C ++ ne finissent pas par faire partie du binaire généré. Ils sont là pour le compilateur et l'éditeur de liens. Ces classes abstraites C # feraient partie du code généré, sans aucun avantage.
Mark Benningfield

16
La construction équivalente en C # est une interface, pas une classe abstraite.
Robert Harvey

6
@DaniBarcaCasafont "Je considère les en-têtes comme un moyen rapide et fiable de voir ce que sont les méthodes et les attributs d'une classe, quels types de paramètres attend-elle et ce qu'elle renvoie". Lorsque vous utilisez Visual Studio, mon alternative à cela est le raccourci Ctrl-MO.
wha7ever

Réponses:


44

La raison pour laquelle cela a été fait en C ++ était de rendre les compilateurs plus rapides et plus faciles à implémenter. Ce n'était pas une conception pour faciliter la programmation .

Le but des fichiers d'en-tête était de permettre au compilateur d'effectuer une première passe super rapide pour connaître tous les noms de fonctions attendus et leur allouer des emplacements de mémoire afin qu'ils puissent être référencés lorsqu'ils sont appelés dans des fichiers cp, même si la classe qui les définissait avait pas encore analysé.

Il n'est pas recommandé de répliquer une conséquence des anciennes limitations matérielles dans un environnement de développement moderne!

La définition d'une interface ou d'une classe abstraite pour chaque classe réduira votre productivité; qu'auriez-vous pu faire d'autre avec ce temps ? De plus, les autres développeurs ne suivront pas cette convention.

En fait, d'autres développeurs peuvent supprimer vos classes abstraites. Si je trouve une interface dans le code qui répond à ces deux critères, je la supprime et la refactorise hors du code:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

L'autre chose est, il existe des outils inclus avec Visual Studio qui font ce que vous souhaitez accomplir automatiquement:

  1. le Class View
  2. le Object Browser
  3. Dans, Solution Explorervous pouvez cliquer sur des triangles pour dépenser des classes pour voir leurs fonctions, paramètres et types de retour.
  4. Il y a trois petits menus déroulants sous les onglets de fichiers, le plus à droite répertorie tous les membres de la classe actuelle.

Faites un essai ci-dessus avant de consacrer du temps à la réplication des fichiers d'en-tête C ++ en C #.


De plus, il y a des raisons techniques de ne pas le faire ... cela rendra votre binaire final plus grand qu'il ne devrait l'être. Je vais répéter ce commentaire de Mark Benningfield:

Les déclarations dans les fichiers d'en-tête C ++ ne finissent pas par faire partie du binaire généré. Ils sont là pour le compilateur et l'éditeur de liens. Ces classes abstraites C # feraient partie du code généré, sans aucun avantage.


Aussi, mentionné par Robert Harvey, techniquement, l'équivalent le plus proche d'un en-tête en C # serait une interface, pas une classe abstraite.


2
Vous vous retrouveriez également avec beaucoup d'appels de répartition virtuels inutiles (pas bien si votre code est sensible aux performances).
Lucas Trzesniewski

5
voici le principe de séparation d'interface auquel il est fait référence.
NH.

2
On peut également simplement réduire la classe entière (clic droit -> Décrire -> Réduire aux définitions) pour en avoir une vue plongeante.
jpmc26

"qu'est-ce que tu aurais pu faire d'autre avec ce temps" -> Uhm, copier-coller et postwork vraiment très mineur? Cela me prend environ 3 secondes, voire pas du tout. Bien sûr, j'aurais pu utiliser ce temps pour retirer une pointe de filtre en prévision du roulement d'une cigarette. Pas vraiment un point pertinent. [Avertissement: j'aime les deux, le C # idiomatique, ainsi que le C ++ idiomatique, et quelques autres langages]
phresnel

1
@phresnel: J'aimerais vous voir essayer de répéter les définitions d'une classe et hériter de la classe originale de la classe abstraite en 3 secondes, confirmée sans erreur, pour toute classe suffisamment grande pour ne pas avoir une vue d'ensemble (comme c'est le cas d'utilisation proposé par l'OP: soi-disant classes sans complication autrement compliquées). Presque intrinsèquement, la nature compliquée de la classe d'origine signifie que vous ne pouvez pas simplement écrire la définition de classe abstraite par cœur (car cela signifierait que vous la connaissez déjà par cœur), puis considérez le temps nécessaire pour le faire pour une base de code entière .
Flater

14

Tout d'abord, comprenez qu'une classe purement abstraite n'est vraiment qu'une interface qui ne peut pas faire d'héritage multiple.

Écrire une classe, extraire l'interface, est une activité morte du cerveau. Tant et si bien que nous avons un refactoring pour cela. C'est dommage. En suivant ce modèle «chaque classe obtient une interface», non seulement cela produit du désordre, mais il manque complètement le point.

Une interface ne doit pas être considérée comme une simple reformulation formelle de tout ce que la classe peut faire. Une interface doit être considérée comme un contrat imposé par le code client utilisateur détaillant ses besoins.

Je n'ai aucun problème à écrire une interface qui n'a actuellement qu'une seule classe l'implémentant. En fait, je m'en fiche si aucune classe ne l'implémente encore. Parce que je pense à ce dont mon code a besoin. L'interface exprime ce que demande le code utilisateur. Tout ce qui arrivera plus tard peut faire ce qu'il veut tant qu'il répond à ces attentes.

Maintenant, je ne fais pas cela chaque fois qu'un objet en utilise un autre. Je le fais lors du franchissement d'une frontière. Je le fais quand je ne veux pas qu'un objet sache exactement à quel autre objet il parle. C'est la seule façon dont le polymorphisme fonctionnera. Je le fais quand je m'attends à ce que l'objet dont parle mon code client soit susceptible de changer. Je ne fais certainement pas cela lorsque j'utilise la classe String. La classe String est agréable et stable et je ne ressens pas le besoin de me prémunir contre tout changement sur moi.

Lorsque vous décidez d'interagir directement avec une implémentation concrète plutôt que par le biais d'une abstraction, vous prédisez que l'implémentation est suffisamment stable pour ne pas changer.

Ce droit là est la façon dont je tempère le principe d'inversion de dépendance . Vous ne devriez pas appliquer aveuglément et fanatiquement ceci à tout. Lorsque vous ajoutez une abstraction, vous dites vraiment que vous ne faites pas confiance au choix d'implémenter la classe pour être stable pendant la durée de vie du projet.

Tout cela suppose que vous essayez de suivre le principe ouvert et fermé . Ce principe n'est important que lorsque les coûts associés à la modification directe du code établi sont importants. L'une des principales raisons pour lesquelles les gens ne sont pas d'accord sur l'importance du découplage des objets est que tout le monde ne subit pas les mêmes coûts lors des changements directs. Si retester, recompiler et redistribuer l'intégralité de votre base de code est trivial pour vous, résoudre un besoin de changement avec une modification directe est probablement une simplification très intéressante de ce problème.

Il n'y a tout simplement pas de réponse cérébrale à cette question. Une interface ou une classe abstraite n'est pas quelque chose que vous devez ajouter à chaque classe et vous ne pouvez pas simplement compter le nombre de classes d'implémentation et décider qu'elle n'est pas nécessaire. Il s'agit de gérer le changement. Ce qui signifie que vous anticipez l'avenir. Ne soyez pas surpris si vous vous trompez. Restez simple comme vous pouvez sans vous reculer dans un coin.

Alors s'il vous plaît, n'écrivez pas d'abstractions juste pour nous aider à lire le code. Nous avons des outils pour ça. Utilisez des abstractions pour découpler ce qui doit être découplé.


5

Oui, ce serait terrible parce que (1) Il introduit du code inutile (2) Il confondra le lecteur.

Si vous voulez programmer en C #, vous devez simplement vous habituer à lire C #. Le code écrit par d'autres personnes ne suivra pas ce modèle de toute façon.


1

Tests unitaires

Je vous suggère fortement d'écrire des tests unitaires au lieu d'encombrer le code avec des interfaces ou des classes abstraites (sauf lorsque cela est justifié pour d'autres raisons).

Un test unitaire bien écrit décrit non seulement l'interface de votre classe (comme le ferait un fichier d'en-tête, une classe abstraite ou une interface), mais il décrit également, par exemple, la fonctionnalité souhaitée.

Exemple: où vous pourriez avoir écrit un fichier d'en-tête myclass.h comme ceci:

class MyClass
{
public:
  void foo();
};

Au lieu de cela, en c #, écrivez un test comme celui-ci:

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

Ce test très simple transmet les mêmes informations que le fichier d'en-tête. (Nous devrions avoir une classe nommée "MyClass" avec une fonction sans paramètre "Foo") Alors qu'un fichier d'en-tête est plus compact, le test contient beaucoup plus d'informations.

Une mise en garde: un tel processus consistant à demander à un ingénieur logiciel senior de fournir (échouer) des tests pour que d'autres développeurs résolvent violemment les conflits avec des méthodologies comme TDD, mais dans votre cas, ce serait une énorme amélioration.


Comment faites-vous les tests unitaires (tests isolés) = Mocks nécessaires, sans interfaces? Quel cadre prend en charge la simulation d'une implémentation réelle, la plupart que j'ai vu utilise une interface pour échanger l'implémentation avec l'implémentation fictive.
Bulan

Je ne pense pas que les moqueries soient nécessairement nécessaires aux fins de l'OP. N'oubliez pas qu'il ne s'agit pas de tests unitaires en tant qu'outils pour TDD, mais de tests unitaires en remplacement des fichiers d'en-tête. (Ils pourraient bien sûr évoluer en tests unitaires "habituels" avec des simulateurs, etc.)
Guran

Ok, si je ne considère que le côté OP, je comprends mieux. Lisez votre réponse comme une réponse d'application plus générale. Thx pour clarifier!
Bulan

@Bulan, le besoin de moqueries étendues est souvent révélateur d'un mauvais design
TheCatWhisperer

@TheCatWhisperer Oui, je suis bien conscient de cela, donc aucun argument ici, ne peut pas voir que j'ai fait cette déclaration n'importe où non plus: DI parlait de tests en général, que tous les Mock-framework que j'ai utilisés utilisent des interfaces pour échanger sur l'implémentation réelle, et s'il y avait une autre technique sur la façon de vous moquer si vous n'avez pas d'interfaces.
Bulan
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.