Come eseguire il refactoring del codice su un codice comune?


16

sfondo

Sto lavorando a un progetto C # in corso. Non sono un programmatore C #, principalmente un programmatore C ++. Quindi mi hanno assegnato compiti sostanzialmente semplici e di refactoring.

Il codice è un casino. È un grande progetto. Poiché i nostri clienti richiedevano rilasci frequenti con nuove funzionalità e correzioni di bug, tutti gli altri sviluppatori sono stati costretti ad adottare un approccio a forza bruta durante la codifica. Il codice è altamente non mantenibile e tutti gli altri sviluppatori sono d'accordo.

Non sono qui per discutere se hanno fatto bene. Mentre sto eseguendo il refactoring, mi chiedo se lo sto facendo nel modo giusto poiché il mio codice refactored sembra complesso! Ecco il mio compito come semplice esempio.

Problema

Ci sono sei classi: A, B, C, D, Ee F. Tutte le classi hanno una funzione ExecJob(). Tutte e sei le implementazioni sono molto simili. Fondamentalmente, all'inizio è A::ExecJob()stato scritto. Quindi è stata richiesta una versione leggermente diversa che è stata implementata in B::ExecJob()copia-incolla-modifica di A::ExecJob(). Quando è stata richiesta un'altra versione leggermente diversa, è C::ExecJob()stata scritta e così via. Tutte e sei le implementazioni hanno un codice comune, quindi alcune righe di codice diverse, quindi ancora un codice comune e così via. Ecco un semplice esempio delle implementazioni:

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

Dov'è SNun gruppo di stesse identiche dichiarazioni.

Per renderli comuni, ho creato un'altra classe e spostato il codice comune in una funzione. Utilizzo del parametro per controllare quale gruppo di istruzioni deve essere eseguito:

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);
}

Si noti che in questo esempio vengono utilizzate solo tre classi e istruzioni semplificate. In pratica, la CommonTask()funzione sembra molto complessa con tutti quei parametri che controllano e ci sono molte più istruzioni. Inoltre, nel codice reale, ci sono diverse CommonTask()funzioni dall'aspetto.

Sebbene tutte le implementazioni condividano codice comune e le ExecJob()funzioni siano più carine, esistono due problemi che mi danno fastidio:

  • Per qualsiasi modifica CommonTask(), sono necessarie tutte e sei le funzionalità (e potrebbero essere più in futuro) da testare.
  • CommonTask()è già complesso. Diventerà più complesso nel tempo.

Lo sto facendo nel modo giusto?


Il libro Refactoring di Martin Fowler ha molte tecniche specifiche per il refactoring del codice che potresti trovare utili.
Allan,

Risposte:


14

Sì, sei assolutamente sulla strada giusta!

Nella mia esperienza, ho notato che quando le cose sono complicate, i cambiamenti avvengono a piccoli passi. Quello che hai fatto è il passaggio 1 nel processo di evoluzione (o processo di refactoring). Ecco il passaggio 2 e il passaggio 3:

Passo 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}
}

Questo è il "Modello di progettazione del modello" ed è un passo avanti nel processo di refactoring. Se la classe base cambia, le sottoclassi (A, B, C) non devono essere influenzate. È possibile aggiungere nuove sottoclassi in modo relativamente semplice. Tuttavia, subito dall'immagine sopra puoi vedere che l'astrazione è rotta. La necessità di una "implementazione vuota" è un buon indicatore; mostra che c'è qualcosa che non va nella tua astrazione. Potrebbe essere stata una soluzione accettabile a breve termine, ma sembra esserci una soluzione migliore.

Passaggio 3

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}
}

Questo è il "modello di progettazione strategica" e sembra adatto al tuo caso. Esistono diverse strategie per eseguire il lavoro e ogni classe (A, B, C) lo implementa in modo diverso.

Sono sicuro che ci sia un passaggio 4 o 5 in questo processo o approcci di refactoring molto migliori. Tuttavia, questo ti consentirà di eliminare il codice duplicato e assicurarti che le modifiche siano localizzate.


Il problema principale che vedo con la soluzione delineata nella "Fase 2" è che l'implementazione concreta di S5 esiste due volte.
user281377

1
Sì, la duplicazione del codice non viene eliminata! E questo è un altro indicatore dell'astrazione che non funziona. Volevo solo mettere il passo 2 là fuori per mostrare come penso al processo; un approccio graduale alla ricerca di qualcosa di meglio.
Guven,

1
+1 Ottima strategia (e non sto parlando del modello )!
Jordão,

7

In realtà stai facendo la cosa giusta. Lo dico perché:

  1. Se è necessario modificare il codice per una funzionalità di attività comune, non è necessario modificarlo in tutte e 6 le classi che conterrebbero il codice se non lo si scrive in una classe comune.
  2. Il numero di righe di codice verrà ridotto.

3

Vedi questo tipo di codice condividere molto con la progettazione guidata dagli eventi (soprattutto .NET). Il modo più gestibile è mantenere il tuo comportamento condiviso nel minor numero possibile.

Lascia che il codice di alto livello riutilizzi un mucchio di piccoli metodi, lascia il codice di alto livello fuori dalla base condivisa.

Avrai molta piastra della caldaia nelle tue implementazioni foglia / calcestruzzo. Non fatevi prendere dal panico, va bene. Tutto quel codice è diretto, facile da capire. Dovrai riorganizzarlo di tanto in tanto quando le cose si rompono, ma sarà facile cambiarle.

Vedrai molti schemi nel codice di alto livello. A volte sono reali, il più delle volte non lo sono. Le "configurazioni" dei cinque parametri lassù sembrano simili, ma non lo sono. Sono tre strategie completamente diverse.

Inoltre, tieni presente che puoi fare tutto questo con la composizione e non preoccuparti mai dell'eredità. Avrai meno accoppiamento.


3

Se fossi in te probabilmente aggiungerei 1 ulteriore passo all'inizio: uno studio basato su UML.

Rifattorizzare il codice fondendo insieme tutte le parti comuni non è sempre la mossa migliore, sembra più una soluzione temporanea che un buon approccio.

Disegna uno schema UML, mantieni le cose semplici ma efficaci, tieni a mente alcuni concetti di base sul tuo progetto come "cosa dovrebbe fare questo software?" "Qual è il modo migliore per mantenere questo software astratto, modulare, estensibile, ecc. ecc.?" "come posso implementare al meglio l'incapsulamento?"

Sto solo dicendo questo: non preoccuparti del codice in questo momento, devi solo preoccuparti della logica, quando hai in mente una logica chiara tutto il resto può diventare un compito davvero facile, alla fine tutto questo tipo dei problemi che stai affrontando è semplicemente causato da una cattiva logica.


Questo dovrebbe essere il primo passo, prima di eseguire qualsiasi refactoring. Fino a quando il codice non sarà compreso abbastanza per essere mappato (uml o qualche altra mappa dei jolly), il refactoring sarà l'architettura nel buio.
Kzqai,

3

Il primo passo, non importa dove stia andando, dovrebbe rompere il metodo apparentemente grande A::ExecJob in pezzi più piccoli.

Pertanto, invece di

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
}

hai capito

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
}

Da qui in poi, ci sono molti modi possibili per andare. La mia opinione su di esso: rendi A la classe base della tua gerarchia di classi ed ExecJob virtuali e diventa facile creare B, C, ... senza troppa copia-incolla - basta sostituire ExecJob (ora un cinque-liner) con un modificato versione.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

Ma perché ci sono così tante lezioni? Forse puoi sostituirli tutti con una singola classe che ha un costruttore a cui si può dire in quali azioni sono necessarie ExecJob.



1

In primo luogo, è necessario assicurarsi che l'ereditarietà è davvero lo strumento giusto qui per il lavoro - solo perché avete bisogno di un luogo comune per le funzioni usate per le vostre classi Aa Fnon significa che una classe base comune è la cosa giusta qui - a volte un aiutante separata la classe fa meglio il lavoro. Potrebbe essere, potrebbe non esserlo. Dipende dall'avere una relazione "is-a" tra A e F e la tua classe base comune, impossibile da pronunciare con nomi artificiali AF. Qui trovi un post sul blog che tratta questo argomento.

Supponiamo che tu decida che la classe base comune è la cosa giusta nel tuo caso. Quindi la seconda cosa che farei è assicurarmi che i tuoi frammenti di codice da S1 a S5 siano implementati ciascuno in un metodo separato rispetto S1()alla S5()tua classe base. Successivamente le funzioni "ExecJob" dovrebbero apparire così:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

Come vedi ora, poiché da S1 a S5 sono solo chiamate di metodo, nessun codice viene più bloccato, la duplicazione del codice è stata quasi completamente rimossa e non è più necessario alcun controllo dei parametri, evitando il problema della crescente complessità che potresti ottenere altrimenti.

Infine, ma solo come terzo passo (!), Potresti pensare di combinare tutti quei metodi ExecJob in una delle tue classi base, dove l'esecuzione di quelle parti può essere controllata da parametri, proprio come l'hai suggerita, o usando modello di metodo modello. Devi decidere tu stesso se vale la pena nel tuo caso, in base al codice reale.

Ma IMHO la tecnica di base per scomporre i grandi metodi in piccoli metodi è molto, molto più importante per evitare la duplicazione del codice che applicare i pattern.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.