Refactoring progetto di C ++ legacy basato su codice di grandi dimensioni che si è mescolato con lo stile C

2

Sfondo:

Attualmente sto lavorando a un progetto C ++. È per uso aziendale, quindi questo progetto non è open source. Tuttavia, questo è un progetto con una base di codice abbastanza grande (circa 10.000 righe di codice) e un paio di librerie e binari esterni collegati. Il problema è che quasi l'80% della base di codice è scritto in stile C piuttosto che in stile C ++, ma l'interfaccia principale è scritta in C ++ 11 e deve essere compilata in C ++ 11. Una delle parti più scomode del codice di stile C è un sacco di istruzioni goto utilizzate, e posso dire che lo scopo dell'utilizzo di goto è quello di garantire che il codice sia sicuro per i thread e ripulire la spazzatura giusto in tempo. (Hanno funzionato) Quando ho imparato il C ++ per la prima volta, ho iniziato con C ++ 11 e penso che RAII sia la moda moderna per sostituire lo stile hacky per garantire che il codice sia sicuro da thread e che le risorse utilizzate vengano pulite in tempo.

Puzzle:

Sono stato assegnato per aggiungere una serie di nuove funzioni nel progetto. Alcune delle mie modifiche devono essere fatte in questi codici in stile C. E se ho cambiato uno di loro, perché non refactoring tutti in stile C ++ 1x? Il conflitto ha un tempo limitato, non riesco a completare il refactoring di tutti loro, nemmeno l'80%. E queste basi di codice in stile C usano anche alcune librerie in stile C, non so se ho cambiato il mio codice da stile C a stile C ++, queste librerie esterne funzioneranno come prima? Ci saranno librerie C ++ equivalenti per sostituire quelle librerie C deprecate? Quindi i miei enigmi sono: Dato un tempo limitato, come rifattorizzare in modo efficace un grande progetto di C ++ legacy basato su codice dallo stile C allo stile C ++? Qual è il trade off? Vale la pena considerare lo scambio?

    
posta Lingbo Tang 12.10.2017 - 01:44
fonte

4 risposte

6

Il codice C utilizza lo stile goto cleanup come questo:

int function(int argument)
{
  int result;
  opaque_handle handle = NULL;
  char* text = NULL;
  size_t size;

  handle = extlib_create();
  if (!handle) {
    result = extlib_failed;
    goto cleanup;
  }
  size = extlib_size(handle, argument);
  text = malloc(size);
  if (!text) {
    result = oom;
    goto cleanup;
  }
  extlib_read(handle, text, size, argument);
  // etc.

  result = success;
  goto cleanup;

cleanup:
  if (handle) extlib_destroy(handle);
  if (text) free(text);
  return result;
}

può sempre refactored in C ++ avvolgendo ogni risorsa in un wrapper RAII e magari usando alcuni wrapper di eccezioni.

class extlib_handle {
  opaque_handle handle;
public:
  explicit extlib_handle(opaque_handle handle) : handle(handle) {}
  extlib_handle(const extlib_handle&) = delete;
  extlib_handle& operator =(const extlib_handle&) = delete;
  ~extlib_handle() { extlib_destroy(handle); }
  operator opaque_handle() const { return handle; }
};

template <typename V>
bool resize_nothrow(V& v, typename V::size_type sz)
try {
  v.resize(sz);
  return true;
} catch (std::bad_alloc&) {
  return false;
}

int function(int argument)
{
  extlib_handle handle(extlib_create());
  if (!handle) return extlib_failed;

  size_t size = extlib_size(handle, argument);
  std::vector<char> text;
  if (!resize_nothrow(text, size)) return oom;

  extlib_read(handle, text.data(), size, argument);
  // etc.

  return success;
}

Questo refactoring non influisce in alcun modo su nessun codice esterno alla funzione e consente di creare lentamente una raccolta di wrapper RAII riutilizzabili.

Puoi usare questa tecnica per refactoring esattamente le parti del codice C che effettivamente tocchi, non altri. Toccando gli altri si introducono rischi non necessari (si potrebbe commettere un errore e aggiungere un errore nel codice che altrimenti non sarebbe stato toccato) questo rischio viene mitigato nel codice che si deve comunque toccare perché potrebbero esserci già nuovi bug comunque, quindi riceverà maggiore attenzione da parte dei tester e perché rende la scrittura di un nuovo codice meno soggetto a errori).

Come al solito, se puoi, scrivi dei test. Ma è probabile che il codice non sia suscettibile di test perché le dipendenze di terze parti non possono essere sostituite. Refactoring per che è un'attività che richiede molto tempo.

    
risposta data 12.10.2017 - 10:22
fonte
2

I don't know if I changed my code from C style to C++ style, will these external libraries work as before? Will there be equivalent C++ libraries to replace those deprecated C libraries?

Senza sapere quali librerie stai usando o quali funzioni in esse stai usando, è impossibile dirlo. Ovviamente puoi continuare a chiamare le funzioni C da C ++ e dovrebbero funzionare come prima se fornisci gli stessi input.

So my puzzles are: Given limited time, how to refactor large code base legacy C++ project from C style to C++ style effectively? What is the trade off? Is the trade off worth considering?

Dato ciò che hai detto, penso che la migliore linea di azione è aggiungere le tue funzioni e se puoi pulire il codice che chiama le tue funzioni o che le tue funzioni chiamano, allora fallo. Non toccherei nulla che non sia direttamente collegato alle aggiunte che stai facendo.

Non consiglio di rimandare tutti i refactoring finché la direzione non deciderà di farlo. Nella mia esperienza, il management non deciderà mai di doverlo fare, anche se a beneficio dell'utente (come organizzazione). Se puoi farlo un pezzo alla volta mentre fai altre cose, di solito è un approccio migliore. (A meno che tu non sappia che la tua direzione è disposta a pagare il debito tecnico in futuro.)

Qui ci sono alcuni compromessi:

  1. Refactoring di tutto ora richiede più tempo di quello che hai, quindi non è realistico
  2. Rifare solo le cose che stai per toccare potrebbe far durare più tempo, ma ti verrà lasciato un codice più aggiornato e più facile da mantenere
  3. Attendere fino al termine del progetto e convincere la gestione a lasciarti fare qualche refactoring che non avrà alcun aspetto esteriore di nuove funzionalità è improbabile che abbia successo (secondo la mia esperienza).
  4. Non fare refactoring e aggiungendo semplicemente la tua nuova funzionalità ti lascia esattamente il debito tecnico di prima, e possibilmente di più ora che hai il debito tecnico originale e l'interfaccia con le cose più nuove.
risposta data 12.10.2017 - 07:03
fonte
1

All'università, il mio professore di informatica diceva: "Il nostro lavoro è veloce, economico e di alta qualità. Puoi scegliere due di questi".

Nel tuo caso, hai un tempo limitato (quindi il lavoro deve essere veloce), e solo uno sviluppatore sta lavorando su di esso (quindi non vengono spesi molti soldi per questo lavoro, quindi è economico). Ciò significa che la qualità soffrirà inevitabilmente. E quando la qualità soffre, è quando il costo aumenta, il più delle volte a un tasso esponenziale.

Il mio consiglio: scrivi tutti i test automatici che verificano la funzionalità del codice come prima puoi refactoring. Quindi, dopo aver rifattorizzato il codice, puoi confermare che il codice funziona ancora come prima del refactoring.

La cosa buona dei manager è che alla fine si preoccupano solo dei soldi (che è davvero il loro lavoro). Così, si può sempre difendere questa posizione facendo notare che i costi di mantenimento del vecchio codice e che fissa i bug nel nuovo codice che è stato scritto sulla parte superiore del vecchio codice saranno superiori ai costi di refactoring.

Non lasciare che la qualità ne risenta. Qualsiasi altra cosa può essere negoziabile e suscettibile di compromessi.

    
risposta data 12.10.2017 - 08:44
fonte
1

Ringrazia tutte le persone che forniscono i tuoi consigli alla mia domanda. Sulla base dei tuoi suggerimenti e della mia valutazione, ho preso questa decisione per questo caso.

Cosa c'è che non va e non è sbagliato con goto ?

Ho letto molti post per la formattazione di C ++ in alcuni standard, uno dei punti più comuni è evitare di usare goto , è il modo di assemblare per risolvere il problema. Se hai più goto s nel tuo codice in più ambiti (se nidificati e poi peggio), specialmente per progetti di codice di grandi dimensioni, lascerai un codice meno leggibile e meno gestibile. Tuttavia, se si desidera fornire la flessibilità necessaria per gestire il flusso di controllo, goto è accettabile in alcuni casi. Sebbene i linguaggi moderni usassero le istruzioni condizionali per rendere più dettagliato il branching, se qualcuno ha usato goto in precedenza, potrebbe anche solo tenerli. In effetti, alcuni progetti ben funzionanti ben noti utilizzavano anche goto , per esempio: git.

Scrivi il codice di stile C in C ++:

Anche in questo caso il C ++ è un linguaggio con più paradigmi. Uno degli scopi è progettato per supportare diversi stili e renderli compatibili. Quindi, se il codice di stile C esistente in un progetto C ++ funziona, dovremmo lasciarli come prima.

Scrittura test prima del refactoring:

Come fai a sapere se il tuo refactoring funziona come ha fatto il tuo codice base? Scrivi test di automazione per garantire che il codice refactored produca il comportamento previsto. Se non hai questi test, non fare nulla su di loro. "Lasci i grosses lordi". Dato un tempo limitato, non posso scrivere un test dettagliato, cosa dovrei fare? Scrivi il micro test e apporta micro cambiamenti passo dopo passo. Scrivi solo un test per la funzione che desideri aggiungere nel progetto precedente.

    
risposta data 12.10.2017 - 22:40
fonte

Leggi altre domande sui tag