Quelle est la meilleure méthode pour passer du désordre de gabarit à l'architecture de classes vierges (C ++)?

StackOverflow https://stackoverflow.com/questions/324043

  •  11-07-2019
  •  | 
  •  

Question

Supposons une bibliothèque de modèles volumineuse avec environ 100 fichiers contenant environ 100 modèles avec plus de 200 000 lignes de code. Certains modèles utilisent l’héritage multiple pour simplifier l’utilisation de la bibliothèque elle-même (c’est-à-dire hériter de certains modèles de base et ne devoir appliquer que certaines règles commerciales).

Tout ce qui existe (développé sur plusieurs années), & "; fonctionne &"; et est utilisé pour des projets.

Cependant, la compilation de projets utilisant cette bibliothèque prend un temps de plus en plus long et il faut beaucoup de temps pour localiser la source de certains bogues. La réparation provoque souvent des effets secondaires inattendus ou est assez difficile, car certains modèles interdépendants doivent être modifiés. Les tests sont presque impossibles en raison de la quantité de fonctions.

Maintenant, j'aimerais vraiment simplifier l'architecture en utilisant moins de modèles et des classes plus petites et plus spécialisées.

Existe-t-il une méthode éprouvée pour mener à bien cette tâche? Quel serait un bon endroit pour commencer?

Était-ce utile?

La solution

Je ne suis pas sûr de comprendre comment / pourquoi les modèles sont le problème et pourquoi les classes non modélisées sont une amélioration. Cela ne signifie-t-il pas simplement même plus de plus classes, moins de sécurité de type et un potentiel de bogues plus important?

Je peux comprendre la simplification de l’architecture, la refactorisation et la suppression des dépendances entre les différentes classes et modèles, mais en supposant automatiquement que & "moins de modèles amélioreront l’architecture &"; est imparfait imo.

Je dirais que les modèles potentiellement vous permettent de construire une architecture beaucoup plus propre que celle que vous obtiendriez sans eux. Tout simplement parce que vous pouvez rendre des classes séparées totalement indépendantes. Sans modèles, les fonctions de classe qui appellent une autre classe doivent connaître la classe ou une interface dont elle hérite à l'avance. Avec les modèles, ce couplage n'est pas nécessaire.

La suppression des modèles entraînerait uniquement davantage de dépendances, pas moins. La sécurité de type ajoutée des modèles peut être utilisée pour détecter beaucoup de bogues au moment de la compilation (Saupoudrez votre code généreusement avec static_assert à cette fin)

Bien sûr, le temps de compilation ajouté peut constituer une raison valable d’éviter les modèles dans certains cas, et si vous n’avez que quelques programmeurs Java, habitués à penser de manière & «traditionnelle»! ; Les termes de la programmation orientée objet, les modèles peuvent les confondre, ce qui peut constituer une autre raison valable d'éviter les modèles.

Mais du point de vue de l'architecture, je pense qu'éviter les modèles est un pas dans la mauvaise direction.

Refactorisez l'application, bien sûr, cela semble nécessaire. Mais ne jetez pas l'un des outils les plus utiles pour produire un code extensible et robuste simplement parce que la version d'origine de l'application l'a mal utilisé. Surtout si vous êtes déjà préoccupé par la quantité de code, la suppression de modèles entraînera très probablement plus lignes de code.

Autres conseils

Vous avez besoin de tests automatisés. Ainsi, dans dix ans, lorsque votre successeur aura le même problème, il pourra refactoriser le code (probablement pour ajouter plus de modèles, car il pense que cela simplifiera l'utilisation de la bibliothèque) et savoir qu'il répond à tous les tests. cas. De même, les effets secondaires des corrections de bugs mineurs seront immédiatement visibles (en supposant que vos cas de test soient satisfaisants).

Autre que cela, & "; divide et conqueor &";

Écrire des tests unitaires.

Le nouveau code doit être identique à l'ancien.

C'est un conseil au moins.

Modifier:

Si vous déconseillez l'ancien code que vous avez remplacé par la nouvelle fonctionnalité, peut passer progressivement au nouveau code.

Eh bien, le problème est que la façon de penser des modèles est très différente de la méthode basée sur l'héritage orientée objet. Il est difficile de répondre à autre chose que & "; Remanier le tout et recommencer à zéro &";

Bien sûr, il peut exister un moyen simple pour un cas particulier. Nous ne pouvons pas en savoir plus sans en savoir plus sur ce que vous avez.

Le fait que la solution de gabarit soit si difficile à gérer est de toute façon un signe de mauvaise qualité.

Quelques points (mais remarque: ce ne sont pas des pervers. Si vous voulez changer de code sans modèle, cela peut aider):

Recherchez vos interfaces statiques . Où les modèles dépendent-ils des fonctions existantes? Où ont-ils besoin de typedefs?

Placez les parties communes dans une classe de base abstraite. Un bon exemple est lorsque vous tombez sur l’idiot du CRTP. Vous pouvez simplement le remplacer par une classe de base abstraite dotée de fonctions virtuelles.

Rechercher des listes entières . Si vous constatez que votre code utilise des listes intégrales telles que list<1, 3, 3, 1, 3>, vous pouvez les remplacer par std::vector, si tous les codes qui les utilisent peuvent vivre avec l'utilisation de valeurs d'exécution au lieu d'expressions constantes.

Traits de type de recherche . Il y a beaucoup de code impliqué pour vérifier si une typedef existe, ou si une méthode existe dans un code basé sur un modèle typique. Les classes de base abstraites résolvent ces deux problèmes en utilisant des méthodes virtuelles pures et en héritant des typedefs de la base. Souvent, les typedefs ne sont nécessaires que pour déclencher des fonctionnalités hideuses telles que SFINAE , qui seraient alors superflues également.

Modèles d'expression de recherche . Si votre code utilise des modèles d’expression pour éviter de créer des temporaires, vous devrez les éliminer et utiliser la méthode traditionnelle de renvoi / transfert de temporaires aux opérateurs impliqués.

Objets de la fonction de recherche . Si vous constatez que votre code utilise des objets fonction, vous pouvez les modifier pour qu'ils utilisent également des classes de base abstraites. Vous pouvez également les appeler void run(); pour les appeler (ou si vous souhaitez continuer à utiliser operator(), mieux vaut le faire!! Il peut également être virtuel. ).

Si je comprends bien, vous êtes particulièrement préoccupé par les temps de construction et la facilité de maintenance de votre bibliothèque?

D'abord, n'essayez pas de " réparer " tout à la fois.

Deuxièmement, comprenez ce que vous corrigez. La complexité des modèles existe souvent pour une raison, par exemple. pour faire respecter certaines utilisations, et pour que le compilateur vous aide à ne pas vous tromper. Cette raison peut parfois être poussée trop loin, mais jeter 100 lignes parce que & "Personne ne sait vraiment ce qu’ils font &"; ne devrait pas être pris à la légère. Tout ce que je suggère ici peut introduire de très méchants bugs, vous avez été prévenu.

Troisièmement, envisagez d’abord des solutions moins coûteuses: par exemple. machines plus rapides ou outils de construction distribués. Au moins, jetez toute la RAM que les cartes vont prendre et jetez les vieux disques. Cela peut faire une différence. Un lecteur pour le système d'exploitation, un lecteur pour la construction est un RAID mans pas cher.

La bibliothèque est-elle bien documentée? C’est votre meilleure chance de le faire. Recherchez des outils tels que doxygen qui vous aident à créer une telle documentation.

Tous considérés? OK, maintenant quelques suggestions pour les temps de construction;)

Comprenez le C ++ modèle de génération : chaque fichier .cpp est compilé individuellement. Cela signifie que beaucoup de fichiers .cpp avec plusieurs en-têtes = construction énorme. Ce n’est PAS un conseil de tout mettre dans un fichier .cpp! Cependant, une astuce (!) Qui peut considérablement accélérer la construction consiste à créer un seul fichier .cpp contenant un tas de fichiers .cpp et à ne nourrir que ce & "Maître &"; fichier au compilateur. Vous ne pouvez pas le faire à l'aveuglette, vous devez comprendre les types d'erreur que cela pourrait introduire.

Si vous n'en avez pas encore, procurez-vous une machine de génération distincte dans laquelle vous pouvez effectuer la télégestion. Vous devrez faire beaucoup de builds presque complets pour vérifier si vous avez cassé certains include. Vous voudrez exécuter ceci sur une autre machine, cela ne vous empêche pas de travailler sur autre chose. À long terme, vous en aurez besoin pour les constructions d’intégration quotidiennes;)

Utilisez des en-têtes précompilés . (balance mieux avec des machines rapides, voir ci-dessus)

Vérifiez votre politique d'inclusion d'en-tête . Alors que chaque fichier doit être & Quot; indépendant & Quot; (c'est-à-dire inclure tout ce dont il a besoin d'être inclus par quelqu'un d'autre), ne pas inclure libéralement. Malheureusement, je n'ai pas encore trouvé d'outil pour rechercher des instructions #incldue inutiles, mais il serait peut-être utile de perdre du temps à supprimer les en-têtes non utilisés dans & "; Hotspot &"; fichiers.

Créer et utiliser des déclarations en aval pour les modèles que vous utilisez. Souvent, vous pouvez inclure un en-tête avec des déclarations forwad à plusieurs endroits et utiliser l’en-tête complet dans quelques cas seulement. Cela peut grandement aider à compiler le temps. Vérifiez dans l’en-tête <iosfwd> comment la bibliothèque standard effectue cela pour les flux i / o.

surcharges pour les modèles pour quelques types : si vous avez un modèle de fonction complexe qui n'est utile que pour très peu de types comme celui-ci:

// .h
template <typename FLOAT> // float or double only
FLOAT CalcIt(int len, FLOAT * values) { ... }

Vous pouvez déclarer les surcharges dans l'en-tête et déplacer le modèle dans le corps:

// .h
float CalcIt(int len, float * values);
double CalcIt(int len, double * values);

// .cpp
template <typename FLOAT> // float or double only
FLOAT CalcItT(int len, FLOAT * values) { ... }

float CalcIt(int len, float * values) { return CalcItT(len, values); }
double CalcIt(int len, double * values) { return CalcItT(len, values); }

Ceci déplace le long modèle vers une seule unité de compilation.
Malheureusement, ceci n’a qu’une utilité limitée pour les cours.

Vérifiez si l' idiome PIMPL peut déplacez le code des en-têtes dans les fichiers .cpp.

La règle générale qui se cache derrière cette est de séparer l'interface de votre bibliothèque de la mise en oeuvre . Utilisez des commentaires, detail nameapces et séparez .impl.h les en-têtes pour isoler mentalement et physiquement ce qui doit être connu de l'extérieur et la façon dont il est accompli. Cela expose la valeur réelle de votre bibliothèque (encapsule-t-il réellement la complexité?) Et vous donne une chance de remplacer & "Cibles faciles &"; premier.


Conseils plus spécifiques - et quelle utilité celle donnée is - dépend en grande partie de la bibliothèque réelle.

Bonne chance!

Comme mentionné, les tests unitaires sont une bonne idée. En effet, plutôt que de casser votre code en introduisant & "; Simple &"; modifications susceptibles de se répercuter, il suffit de créer une suite de tests et de remédier aux non-conformités. Organisez une activité pour mettre à jour les tests lorsque des bugs sont découverts.

Au-delà de cela, je vous suggère de mettre à niveau vos outils, si possible, pour vous aider à résoudre les problèmes liés au modèle.

J'ai souvent rencontré d'anciens modèles qui étaient énormes et qui nécessitaient beaucoup de temps et de mémoire pour être instanciés, mais ce n'était pas nécessaire. Dans ces cas, le moyen le plus simple de réduire le gras consistait à prendre tout le code qui ne reposait sur aucun des arguments de modèle et à le masquer dans des fonctions distinctes définies dans une unité de traduction normale. Cela a également eu pour effet secondaire de provoquer moins de recompilations lorsque ce code a dû être légèrement modifié ou que la documentation a été modifiée. Cela semble assez évident, mais il est vraiment surprenant de constater à quelle fréquence les gens écrivent un modèle de classe et pensent que TOUT doit être défini dans l’en-tête, et pas seulement le code qui nécessite les informations basées sur un modèle.

Vous pouvez également prendre en compte la fréquence à laquelle vous nettoyez les hiérarchies d'héritage en créant les modèles & "; mixin &"; style au lieu d'agrégations d'héritage multiple. Voyez combien d'endroits vous pouvez vous en tirer en faisant de l'un des arguments du modèle le nom de la classe de base dont il doit dériver (la façon dont fonctionne boost::enable_shared_from_this) Bien sûr, cela ne fonctionne généralement que si les constructeurs ne prennent aucun argument, car vous n'avez pas à vous soucier d'initialiser quoi que ce soit correctement.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top