arrière-plan
Je travaille sur un projet C # en cours. Je ne suis pas un programmeur C #, principalement un programmeur C++. J'ai donc été affecté aux tâches essentiellement faciles et refactorisantes.
Le code est un gâchis. C'est un énorme projet. Au fur et à mesure que notre client aient demandé des communiqués fréquents avec de nouvelles fonctionnalités et des corrections de bugs, tous les autres développeurs ont été forcés de prendre une approche de la force brute tout en codant. Le code est hautement restreint et tous les autres développeurs sont d'accord avec cela.
Je ne suis pas ici pour débattre si ils l'ont fait correctement. Comme je me refactore, je me demande si je le fais de bonne manière que mon code refactored semble complexe! Voici ma tâche comme exemple simple.
problème
Il y a six classes: A
, B
, C
, D
, E
et F
. Toutes les classes ont une fonction ExecJob()
. Les six implémentations sont très similaires. Fondamentalement, au début A::ExecJob()
a été écrit. Ensuite, une version légèrement différente a été requise qui a été mise en œuvre dans B::ExecJob()
par copie-coller-modification de A::ExecJob()
. Lorsqu'une autre version légèrement différente était requise, C::ExecJob()
a été écrit et ainsi de suite. Les six implémentations ont du code commun, puis des lignes de code différentes, puis à nouveau du code commun, etc. Voici un exemple simple des implémentations:
A::ExecJob()
{
S1;
S2;
S3;
S4;
S5;
}
B::ExecJob()
{
S1;
S3;
S4;
S5;
}
C::ExecJob()
{
S1;
S3;
S4;
}
Où SN
est un groupe de mêmes déclarations.
Pour les faire courir, j'ai créé une autre classe et déplacé le code commun dans une fonction. Utilisation du paramètre pour contrôler quel groupe d'instructions doit être exécutée:
Base::CommonTask(param)
{
S1;
if (param.s2) S2;
S3;
S4;
if (param.s5) S5;
}
A::ExecJob() // A inherits Base
{
param.s2 = true;
param.s5 = true;
CommonTask(param);
}
B::ExecJob() // B inherits Base
{
param.s2 = false;
param.s5 = true;
CommonTask(param);
}
C::ExecJob() // C inherits Base
{
param.s2 = false;
param.s5 = false;
CommonTask(param);
}
Notez que, cet exemple emploie seulement trois classes et des déclarations simplifiées. En pratique, la fonction CommonTask()
fonction est très complexe avec toutes ces vérifications de paramètre et de nombreuses autres déclarations. En outre, en vrai code, Il y a plusieurs fonctions CommonTask()
-.
Bien que toutes les implémentations partageaient du code commun et ExecJob()
Les fonctions ont l'air plus mignon, il existe deux problèmes qui me dérangent:
CommonTask()
, les six (et peuvent être plus à l'avenir) sont nécessaires pour être testés.CommonTask()
est déjà complexe. Cela sera plus complexe au fil du temps.Est-ce que je le fais de la bonne façon?
Oui, vous êtes absolument dans le bon chemin!
Dans mon expérience, j'ai remarqué que lorsque les choses sont compliquées, les changements se produisent en petites étapes. Ce que vous avez fait est le étape 1 dans le processus d'évolution (ou processus de refactoring). Voici l'étape 2 et l'étape 3:
étape 2
class Base {
method ExecJob() {
S1();
S2();
S3();
S4();
S5();
}
method S1() { //concrete implementation }
method S3() { //concrete implementation }
method S4() { //concrete implementation}
abstract method S2();
abstract method S5();
}
class A::Base {
method S2() {//concrete implementation}
method S5() {//concrete implementation}
}
class B::Base {
method S2() { // empty implementation}
method S5() {//concrete implementation}
}
class C::Base {
method S2() { // empty implementation}
method S5() { // empty implementation}
}
Il s'agit du "modèle de conception de modèles" et il est d'une longueur d'avance dans le processus de refactorisation. Si la classe de base change, les sous-classes (A, B, C) n'ont pas besoin d'être affectées. Vous pouvez ajouter de nouvelles sous-classes relativement facilement. Cependant, tout de suite de l'image ci-dessus, vous pouvez voir que l'abstraction est cassée. La nécessité d'une "mise en œuvre vide" est un bon indicateur; Cela montre qu'il y a quelque chose qui ne va pas avec votre abstraction. Cela aurait pu être une solution acceptable à court terme, mais il semble y avoir un meilleur.
étape
interface JobExecuter {
void executeJob();
}
class A::JobExecuter {
void executeJob(){
helper = new Helper();
helper->S1();
helper->S2();
helper->S3();
helper->S4();
helper->S5();
}
}
class B::JobExecuter {
void executeJob(){
helper = new Helper();
helper->S1();
helper->S3();
helper->S4();
helper->S5();
}
}
class C::JobExecuter {
void executeJob(){
helper = new Helper();
helper->S1();
helper->S3();
helper->S4();
}
}
class Base{
void ExecJob(JobExecuter executer){
executer->executeJob();
}
}
class Helper{
void S1(){//Implementation}
void S2(){//Implementation}
void S3(){//Implementation}
void S4(){//Implementation}
void S5(){//Implementation}
}
Il s'agit du "modèle de conception de stratégie" et semble un bon ajustement pour votre cas. Il existe différentes stratégies pour exécuter le travail et chaque classe (A, B, C) implémente-la différemment.
Je suis sûr qu'il y a une étape 4 ou une étape 5 dans ce processus ou beaucoup de meilleures approches de refactorisation. Cependant, celui-ci vous permettra d'éliminer le code dupliqué et de vous assurer que les modifications sont localisées.
Vous faites réellement la bonne chose. Je dis ça parce que:
Vous voyez ce type de code partageant un lot dans la conception entraînée par événement (.NET surtout). Le moyen le plus maintenu est de garder votre comportement partagé en tant que petits morceaux que possible.
Laissez le code de haut niveau réutiliser une bouquet de petites méthodes, laissez le code de haut niveau hors de la base partagée.
Vous aurez beaucoup de plaque de chaudière dans vos implémentations de feuille/de béton. Ne paniquez pas, ça va. Tout ce code est direct, facile à comprendre. Vous devrez vous réorganiser de temps en temps lorsque cela se casse, mais il sera facile de changer.
Vous verrez beaucoup de motifs dans le code de niveau élevé. Parfois, ils sont réels, la plupart du temps ils ne le sont pas. Les "configurations" des cinq paramètres là-bas look similaire, mais ils ne sont pas. Ils sont trois stratégies entièrement différentes.
Voulez-vous également noter que vous pouvez faire tout cela avec la composition et ne jamais vous soucier de l'héritage. Vous aurez moins de couplage.
Si j'étais vous, j'ajouterai probablement une étape de plus au début: une étude basée sur UML.
Refactoring Le code fusionner toutes les parties communes ensemble n'est pas toujours le meilleur mouvement, sonne plus comme une solution temporaire qu'une bonne approche.
Dessine un programme UML, gardez les choses simples mais efficaces, gardez à l'esprit des concepts de base sur votre projet comme "Qu'est-ce qui est censé faire ce logiciel?" "Quelle est la meilleure façon de garder cette pièce de logiciel abstrait, modulaire, extensible, ... etc, etc." "Comment puis-je mettre en œuvre l'encapsulation à son meilleur?"
Je ne dis que ceci: Ne vous souciez pas du code en ce moment, il vous suffit de vous soucier de la logique lorsque vous avez une logique claire à l'esprit, tout le reste peut devenir une tâche très facile, à la fin de tout ce genre. des problèmes que vous faites face est simplement causé par une mauvaise logique.
La toute première étape, peu importe où cela va, devrait enfreindre la méthode apparemment importante A::ExecJob
en petits morceaux.
Par conséquent, au lieu de
A::ExecJob()
{
S1; // many lines of code
S2; // many lines of code
S3; // many lines of code
S4; // many lines of code
S5; // many lines of code
}
vous obtenez
A::ExecJob()
{
S1();
S2();
S3();
S4();
S5();
}
A:S1()
{
// many lines of code
}
A:S2()
{
// many lines of code
}
A:S3()
{
// many lines of code
}
A:S4()
{
// many lines of code
}
A:S5()
{
// many lines of code
}
D'ici, il y a beaucoup de moyens possibles d'aller. Ma prise: faire une classe de base de votre classe hiérachie et exécujob virtuelle et il devient facile de créer B, C, ... sans trop de copier-coller - il suffit de remplacer Execjob (maintenant à cinq doublures) avec une modification version.
B::ExecJob()
{
S1();
S3();
S4();
S5();
}
Mais pourquoi autant de classes du tout? Peut-être que vous pouvez les remplacer tous avec une seule classe qui a un constructeur qui peut être raconté quelles actions sont nécessaires dans ExecJob
.
Je suis d'accord avec les autres réponses que votre approche est le long des lignes de droite, bien que je ne pense pas que l'héritage est la meilleure façon de mettre en œuvre du code commun - Je préfère la composition. Du C++ FAQ qui peut l'expliquer beaucoup mieux que je ne pouvais jamais: http://www.parashift.com/c++-faq/priv-inherit-vs-compos. html
Premièrement, vous devez vous assurer que l'héritage est vraiment le bon outil ici pour le travail - uniquement parce que vous avez besoin d'un lieu commun pour les fonctions utilisées par vos classes A
à F
ne signifie pas qu'un commun La classe de base est la bonne chose ici - Parfois, une classe d'assistance séparée fait mieux le travail. C'est peut-être, ce n'est peut-être pas. Cela dépend d'avoir une relation "is-a" entre A à F et votre classe de base commune, impossible à dire des noms artificiels A-F. ICI Vous trouvez un article de blog traitant de cette rubrique.
Supposons que vous décidez que la classe de base commune est la bonne chose dans votre cas. Ensuite, la deuxième chose que je ferais est de vous assurer que vos fragments de code S1 à S5 sont utilisés chacun dans une méthode distincte S1()
à S5()
de votre classe de base. Ensuite, les fonctions "Execjob" devraient ressembler à ceci:
A::ExecJob()
{
S1();
S2();
S3();
S4();
S5();
}
B::ExecJob()
{
S1();
S3();
S4();
S5();
}
C::ExecJob()
{
S1();
S3();
S4();
}
Comme vous le voyez maintenant, puisque S1 à S5 ne sont que des appels de méthodes, aucun code ne bloque plus, la duplication de code a été presque complètement supprimée, et vous n'avez plus besoin de vérification de paramètre, évitant ainsi le problème de la complexité croissante que vous pourriez obtenir. autrement.
Enfin, mais seulement en tant que troisième étape (!), Vous penserez peut-être de combiner toutes ces méthodes Execjob dans l'une de votre classe de base, où l'exécution de ces pièces peut être contrôlée par des paramètres, comme vous le suggérez, ou en utilisant modèle de méthode de modèle. Vous devez décider vous-même si cela vaut la peine d'effort dans votre cas, sur la base du code réel.
Mais IMHO, la technique de base pour décomposer de grandes méthodes en petites méthodes est beaucoup plus importante pour éviter la duplication de code que d'appliquer des modèles.