Qual è il modo migliore per passare dal disordine dei modelli all'architettura delle classi pulite (C ++)?

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

  •  11-07-2019
  •  | 
  •  

Domanda

Supponendo una vasta libreria di modelli con circa 100 file contenenti circa 100 modelli con complessivamente oltre 200.000 righe di codice. Alcuni modelli utilizzano l'ereditarietà multipla per rendere piuttosto semplice l'utilizzo della libreria stessa (ovvero ereditare da alcuni modelli di base e dover solo implementare determinate regole aziendali).

Tutto ciò che esiste (cresciuto in diversi anni), " funziona " e viene utilizzato per i progetti.

Tuttavia, la compilazione di progetti che utilizzano quella libreria richiede una quantità crescente di tempo e impiega un po 'di tempo per individuare l'origine di alcuni bug. La correzione spesso causa effetti collaterali imprevisti o è piuttosto difficile, perché alcuni modelli interdipendenti devono essere modificati. I test sono quasi impossibili a causa dell'enorme quantità di funzioni.

Ora, vorrei davvero semplificare l'architettura per utilizzare meno modelli e classi più piccole più specializzate.

Esiste un modo provato per svolgere tale compito? Quale sarebbe un buon punto di partenza?

È stato utile?

Soluzione

Non sono sicuro di vedere come / perché i modelli sono il problema e perché le semplici classi non basate su modelli sarebbero un miglioramento. Ciò non significherebbe solo più classi, meno sicurezza dei tipi e un potenziale così grande di bug?

Riesco a comprendere la semplificazione dell'architettura, il refactoring e la rimozione delle dipendenze tra le varie classi e modelli, ma supponendo automaticamente che " un numero minore di modelli renderà migliore l'architettura " è difettoso imo.

Direi che i modelli potenzialmente ti consentono di costruire un'architettura molto più pulita di quella che avresti senza di loro. Semplicemente perché puoi rendere le classi separate totalmente indipendenti. Senza modelli, le funzioni delle classi che richiamano in un'altra classe devono conoscere in anticipo la classe o un'interfaccia che eredita. Con i modelli, questo accoppiamento non è necessario.

La rimozione dei modelli porterebbe solo a più dipendenze, non a meno. La sicurezza dei tipi aggiuntiva di modelli può essere utilizzata per rilevare molti bug in fase di compilazione (cospargere il codice liberamente con static_assert per questo scopo)

Naturalmente, il tempo di compilazione aggiunto può essere un motivo valido per evitare i template in alcuni casi, e se hai solo un gruppo di programmatori Java, che sono abituati a pensare in " tradizionale " ; Termini OOP, i modelli potrebbero confonderli, che può essere un altro motivo valido per evitare i modelli.

Ma dal punto di vista dell'architettura, penso che evitare i modelli sia un passo nella direzione sbagliata.

Rifattorizza l'applicazione, certo, sembra che sia necessario. Ma non buttare via uno degli strumenti più utili per la produzione di codice estensibile e robusto solo perché la versione originale dell'app lo ha usato in modo improprio. Soprattutto se sei già preoccupato della quantità di codice, la rimozione dei modelli porterà molto probabilmente a più righe di codice.

Altri suggerimenti

Hai bisogno di test automatizzati, in questo modo tra dieci anni quando il tuo successore ha lo stesso problema, può refactoring il codice (probabilmente per aggiungere più template perché pensa che semplificherà l'utilizzo della libreria) e sa che continua a soddisfare tutti i test casi. Allo stesso modo gli effetti collaterali di eventuali correzioni minori saranno immediatamente visibili (supponendo che i casi di test siano buoni).

Oltre a questo, " divide and conqueor "

Scrivi unit test.

Dove il nuovo codice deve fare lo stesso del vecchio codice.

Questo è almeno un suggerimento.

Modifica:

Se deprechi il vecchio codice che hai sostituito con la nuova funzionalità che hai può passare gradualmente al nuovo codice poco a poco.

Bene, il problema è che il modo di pensare modello è molto diverso dal modo basato sull'eredità orientato agli oggetti. È difficile rispondere ad altro che & Quot; riprogettare il tutto e ricominciare da capo & Quot ;.

Naturalmente, potrebbe esserci un modo semplice per un caso particolare. Non possiamo dirlo senza sapere di più su ciò che hai.

Il fatto che la soluzione del modello sia così difficile da mantenere è comunque indice di un design scadente.

Alcuni punti (ma nota: questi sono non davvero cattivi. Se vuoi passare a un codice non-template, tuttavia, questo può aiutarti):


Cerca le tue interfacce statiche . Da dove dipendono i modelli da quali funzioni esistono? Dove hanno bisogno dei typedef?

Metti le parti comuni in una classe base astratta. Un buon esempio è quando ti capita di imbatterti nel linguaggio CRTP. Puoi semplicemente sostituirlo con una classe base astratta con funzioni virtuali.

Cerca elenchi di numeri interi . Se trovi che il tuo codice utilizza elenchi integrali come list<1, 3, 3, 1, 3>, puoi sostituirli con std::vector, se tutti i codici che li usano possono vivere con il lavoro con valori di runtime invece di espressioni costanti.

Tratti del tipo di ricerca . C'è molto codice coinvolto che controlla se esiste qualche typedef o se esiste un metodo nel tipico codice di modello. Le basi astratte risolvono questi due problemi usando metodi virtuali puri ed ereditando i typedef alla base. Spesso, i typedef sono necessari solo per attivare funzionalità orribili come SFINAE , che sarebbe quindi superflua.

Modelli di espressioni di ricerca . Se il tuo codice utilizza modelli di espressioni per evitare la creazione di provvisori, dovrai eliminarli e utilizzare il modo tradizionale di restituire / passare i provvisori agli operatori coinvolti.

Oggetti funzione di ricerca . Se trovi che il tuo codice usa oggetti funzione, puoi cambiarli per usare anche classi base astratte e avere qualcosa come void run(); per chiamarli (o se vuoi continuare a usare operator(), meglio così! Può anche essere virtuale ).

A quanto ho capito, ti preoccupi di più dei tempi di costruzione e della manutenibilità della tua libreria?

Per prima cosa, non provare a " fix " tutto in una volta.

In secondo luogo, capire cosa si corregge. La complessità del modello esiste spesso per un motivo, ad es. per imporre un certo utilizzo e fare in modo che il compilatore ti aiuti a non commettere errori. Questo motivo a volte potrebbe essere portato troppo lontano, ma buttando via 100 righe perché & Quot; nessuno sa davvero cosa fanno & Quot; non dovrebbe essere preso alla leggera. Tutto ciò che suggerisco qui può introdurre bug davvero cattivi, sei stato avvisato.

Terzo, considera prima le correzioni più economiche: ad es. macchine più veloci o strumenti di costruzione distribuiti. Almeno, getta tutta la RAM che le schede prenderanno e getta i vecchi dischi. Fa una differenza. Un disco per sistema operativo, un disco per build è un RAID mans economico.

La biblioteca è ben documentata? Questa è la tua migliore possibilità per farlo. Cerca strumenti come doxygen che ti aiutino a creare tale documentazione.

Tutti considerati? OK, ora alcuni suggerimenti per i tempi di costruzione;)


Comprendi il C ++ modello di costruzione : ogni .cpp viene compilato singolarmente. Ciò significa che molti file .cpp con molte intestazioni = build enorme. Questo NON è un consiglio per mettere tutto in un unico file .cpp, però! Tuttavia, un trucco (!) Che può accelerare immensamente una build è quello di creare un singolo file .cpp che includa un mucchio di file .cpp e alimentare solo quel & Quot; master & Quot; file al compilatore. Non puoi farlo alla cieca, però - devi capire i tipi di errori che questo potrebbe introdurre.

Se non ne hai ancora uno, ottieni una macchina per la costruzione separata in cui puoi remotare. Dovrai fare un sacco di build quasi complete per verificare se hai rotto alcune inclusioni. Avrai voglia di eseguirlo su un'altra macchina, che non ti impedisce di lavorare su qualcos'altro. A lungo termine, ne avrai comunque bisogno per le build di integrazione quotidiana;)

Utilizza intestazioni precompilate . (scala meglio con macchine veloci, vedi sopra)

Controlla la tua politica di inclusione dell'intestazione . Mentre ogni file dovrebbe essere & Quot; indipendente & Quot; (vale a dire includere tutto ciò che deve essere incluso da qualcun altro), non includere liberamente. Sfortunatamente, non ho ancora trovato uno strumento per trovare istruzioni #incldue non necessarie, ma potrebbe essere utile dedicare un po 'di tempo a rimuovere le intestazioni inutilizzate in & Quot; hotspot & Quot; file.

Crea e usa dichiarazioni anticipate per i modelli che usi. Spesso, è possibile includere un'intestazione con dichiarazioni forwad in molti punti e utilizzare l'intestazione completa solo in alcune specifiche. Questo può aiutare molto a compilare i tempi. Controlla <iosfwd> come funziona la libreria standard per i flussi di I / O.

sovraccarichi di modelli per alcuni tipi : se disponi di un modello di funzione complesso che è utile solo per pochissimi tipi come questo:

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

È possibile dichiarare i sovraccarichi nell'intestazione e spostare il modello nel corpo:

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

questo sposta il modello lungo in una singola unità di compilazione.
Sfortunatamente, questo è solo di uso limitato per le classi.

Verifica se il linguaggio PIMPL può sposta il codice dalle intestazioni in file .cpp.

La regola generale che si nasconde dietro è separare l'interfaccia della tua libreria dall'implementazione . Usa commenti, detail spazi dei nomi e separa .impl.h intestazioni per isolare mentalmente e fisicamente ciò che dovrebbe essere conosciuto all'esterno da come è realizzato. Questo espone il valore reale della tua libreria (incapsula effettivamente la complessità?) E ti dà la possibilità di sostituire & Quot; facili target & Quot; prima.


Consigli più specifici - e quanto utile quello dato is - dipende in gran parte dalla libreria attuale.

Buona fortuna!

Come accennato, i test unitari sono una buona idea. Infatti, piuttosto che violare il codice introducendo & Quot; semplice & Quot; i cambiamenti che potrebbero emergere, si concentrano solo sulla creazione di una serie di test e sulla correzione della non conformità ai test. Avere un'attività per aggiornare i test quando vengono alla luce dei bug.

Oltre a ciò, suggerirei di aggiornare i tuoi strumenti, se possibile, per aiutarti a risolvere i problemi relativi al modello di debug.

Mi sono imbattuto spesso in modelli legacy che erano enormi e richiedevano molto tempo e memoria per istanziare, ma non era necessario. In quei casi, il modo più semplice per eliminare il grasso era prendere tutto il codice che non si basava su nessuno degli argomenti del modello e nasconderlo in funzioni separate definite in una normale unità di traduzione. Ciò ha avuto anche l'effetto collaterale positivo di innescare un minor numero di ricompilazioni quando questo codice doveva essere leggermente modificato o la documentazione modificata. Sembra piuttosto ovvio, ma è davvero sorprendente la frequenza con cui le persone scrivono un modello di classe e pensano che TUTTO ciò che deve essere definito nell'intestazione, piuttosto che solo il codice che necessita delle informazioni basate sul modello.

Un'altra cosa che potresti voler considerare è la frequenza con cui ripulisci le gerarchie dell'ereditarietà creando i template " mixin " stile anziché aggregazioni di eredità multipla. Scopri quanti posti puoi ottenere facendo diventare uno degli argomenti del modello il nome della classe base da cui dovrebbe derivare (il modo in cui boost::enable_shared_from_this funziona). Ovviamente questo in genere funziona bene solo se i costruttori non accettano argomenti, poiché non devi preoccuparti di inizializzare nulla correttamente.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top