Utilizzo del pre-processore per separare la logica in diverse versioni del prodotto

3

Nel codice di base .NET di un prodotto, esistono dichiarazioni di pre-processore #if per definire se alcune funzionalità dovrebbero essere disponibili. Mi piace così:

shared logic

#if version1
    some logic
#endif
#if version2
    some other logic
#endif

Personalmente, mi concentro su questa struttura. Trovo più difficile leggere, e per qualche motivo mi rende nervoso.
Mi è stato detto che il ragionamento alla base di questo è che ci sono troppi cambiamenti che si verificano in tutte le versioni. Se le versioni fossero archiviate in repository separati o in qualche altro metodo, eventuali modifiche future dovevano essere implementate più volte.

Trovo l'argomento "troppo da cambiare" un po 'vago, ma ammetto che la mia esperienza con questo tipo di design è limitata.

Questa è davvero la procedura migliore o l'approccio più comune quando si tratta di gestire più versioni di prodotti con funzionalità diverse? C'è un'alternativa migliore?

    
posta Phil 14.11.2014 - 14:39
fonte

3 risposte

4

Eviterei di avere basi di codice separate. Tutto ciò che ramifica e fonde crea un sacco di lavori di manutenzione non necessari. Ancora peggio quando non sono sincronizzati diventa difficile determinare quali differenze siano state intenzionali e quali per errore.

Preferisco sostituire le istruzioni del preprocessore #if con le ordinarie istruzioni if , in questo modo tutte le varianti del codice devono compilare e gli strumenti di refactoring visualizzano tutto il codice.

Al di là di questo semplice cambiamento generale dovresti esaminare il codice di refactoring, ma come farlo dipende dalla natura delle differenze.

Alcuni suggerimenti:

  • Non mescolare varianti diverse all'interno di un singolo metodo. Ad esempio potresti sostituire il tuo codice con:

    SharedPart();
    if(cond1)
        Variant1();
    if(cond2)
        Variant2();
    
  • Utilizza delegati, interfacce o metodi virtuali da inviare alle diverse varianti anziché% dichiarazioni diif.

  • Inietta * un'interfaccia contenente la configurazione anziché utilizzare lo stato globale. Ciò consente di scrivere test unitari per tutte le varianti senza ricompilare o riavviare.
  • Per le funzionalità che sono abilitate o disabilitate, creerei un enum di Feature , insieme a un'interfaccia * iniettata che determina se la funzionalità è disponibile. L'interfaccia potrebbe essere simile a:

    interface IGateKeeper
    {
        bool IsEnabled(Feature feature);
        void Require(Feature feature);
    }
    

    dove Require controlla se la funzione è abilitata e genera altrimenti un'eccezione significativa.

* Se non hai familiarità con l'integrazione delle dipendenze (DI) e l'inversione del controllo (IoC), acquisiscili.

    
risposta data 14.11.2014 - 15:33
fonte
3

Sono stato su questa barca. Non è divertente. Non può sempre essere una questione di utilizzo di un test di runtime:

if (IsConfigOne)
    ConfigOneImpl();
if (IsConfigTwo)
    ConfigTwoImpl();

A causa delle dipendenze che si escludono a vicenda (cioè, non posso fare riferimento a ConfigOneImpl() quando sono in ConfigTwo perché ciò farebbe riferimento agli assembly che non ho in ConfigTwo ...).

In generale, è meglio costruire una classe astratta che implementa la maggior parte delle funzionalità e classi concrete create per ogni singola implementazione. Nel tuo caso, potresti avere qualcosa di simile a questo:

public abstract class SomeThing {
    public void Logic() {
        CommonLogic();
        SpecificLogic();
    }
    protected void CommonLogic() { /* ... */ }
    protected abstract void SpecificLogic();
}

public class SomeThingImplConfig1 : SomeThing {
    protected void SpecificLogic() { /* ... */ }
}

Ho incontrato altri casi in cui questo non funziona e si trovava in una posizione in cui non potevo adattare o modificare le cose perché o non possedevo quelle classi o ero vincolato da altri vincoli, ho provato a isolare un codice specifico in classi parziali. Ad esempio, ho dovuto creare 3 diverse configurazioni di un set di classi che potevano funzionare con routine di supporto non gestite, equivalenti in puro codice gestito e equivalenti per Silverlight, quindi sono stato in grado di isolare quelli usando 4 partials, uno per tutti i condivisi codice e interfacce e 1 per ciascuna delle configurazioni specifiche.

Infine, alcune classi non erano disponibili in Silverlight (System.Drawing, IIRC), ma in Silverlight c'erano equivalenti che avevano membri con lo stesso nome (ma non erano compatibili attraverso un'interfaccia). In queste circostanze, potrei usare un alias di tipo C # per far sì che gli oggetti Silverlight seguano gli equivalenti System.Drawing:

#if SILVERLIGHT_CONFIG
using Rectangle = System.Windows.Rect;
#endif
    
risposta data 14.11.2014 - 16:27
fonte
2

Dipende se il tuo problema di versione è un problema in fase di compilazione o in fase di esecuzione.

Ad esempio, potresti voler supportare un formato di messaggio legacy nella tua API. Questo è un problema di runtime perché entrambi i percorsi di codice sono ancora disponibili all'interno dell'applicazione e possono essere chiamati arbitrariamente. Richiede una soluzione run-time: DI, ifs, delegati, ecc. Sono davvero ben coperti dalla risposta di CodesInChaos.

Se vuoi solo costruire una versione precedente del tuo codice per scopi legacy, allora la soluzione è chiaramente una soluzione in fase di compilazione. Anche in una base di codice molto ampia, preferirei comunque dipendere da un sistema di controllo sorgente per questo e non per le condizioni del pre-processore.

Sceglierei questa strada perché, secondo la mia esperienza, il ragionamento che hai menzionato è errato. Non importa in che modo memorizzi le tue molte basi di codice. Che si tratti di file diversi o dello stesso, è necessario mantenere più versioni dello stesso codice. L'unione non dovrebbe essere un problema, in quanto le modifiche alla logica incompatibile non rifletteranno mai a una su una base di codice diversa. Deve essere implementato in modo indipendente. Se lo fa, la tua logica non è incompatibile, il che indica un problema di architettura / fattorizzazione nel tuo codice. Aka, non stai riutilizzando tutto quello che puoi.

Dall'altro lato, occuparsi del doppio del codice in una funzione, separato dalle condizioni del pre-processore senza alcun concetto di scope, trasforma rapidamente il codice in un immenso e illeggibile pasticcio. Si può finire per avere condizioni che attraversano ambiti e l'unico modo per capire cosa sta realmente accadendo è fare da soli il lavoro del pre-processore. Dopo tutto, c'è una ragione per cui lo sviluppatore C ++ considera il pre-processore come a demone che non avrebbe mai dovuto esistere . L'unica soluzione a questo problema è l'utilizzo del tempo di esecuzione se le condizioni impongono l'utilizzo della logica scoped. Eviterei comunque di andare in questo modo perché possono avere un strong impatto negativo sulle prestazioni se vengono utilizzate in un percorso critico.

Alla fine, tutto si riduce a utilizzare lo strumento giusto per il lavoro e uno strumento di sostituzione automatica del testo non è lo strumento giusto per gestire il controllo delle versioni del codice.

    
risposta data 14.11.2014 - 17:39
fonte

Leggi altre domande sui tag