Aggiunta di complessità per rimuovere il codice duplicato


24

Ho diverse classi che ereditano tutte da una classe base generica. La classe base contiene una raccolta di diversi oggetti di tipo T.

Ogni classe figlio deve essere in grado di calcolare i valori interpolati dalla raccolta di oggetti, ma poiché le classi figlio usano tipi diversi, il calcolo varia leggermente da classe a classe.

Finora ho copiato / incollato il mio codice da una classe all'altra e ho apportato piccole modifiche a ciascuno. Ma ora sto cercando di rimuovere il codice duplicato e sostituirlo con un metodo di interpolazione generico nella mia classe di base. Tuttavia, ciò si sta rivelando molto difficile e tutte le soluzioni che ho pensato sembrano troppo complesse.

Sto iniziando a pensare che il principio DRY non si applichi tanto in questo tipo di situazione, ma suona come una bestemmia. Quanta complessità è eccessiva quando si tenta di rimuovere la duplicazione del codice?

MODIFICARE:

La migliore soluzione che posso trovare è qualcosa del genere:

Classe base:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

Classe A per bambini:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

Classe B per bambini:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}

9
Una eccessiva micro-applicazione di concetti come DRY e Code Reuse porta a peccati molto più grandi.
Affe,

1
Stai ricevendo alcune buone risposte generali. La modifica per includere una funzione di esempio potrebbe aiutarci a determinare se la stai spingendo troppo in questa particolare istanza.
Karl Bielefeldt,

Questo non è davvero una risposta, più di un'osservazione: se non si può facilmente spiegare ciò che una classe base ceduti-out lo fa , potrebbe essere meglio non avere uno. Un altro modo di vederlo è (suppongo che tu abbia familiarità con SOLID?) "Un probabile consumatore di questa funzionalità richiede la sostituzione di Liskov"? Se non esiste un probabile caso commerciale per un consumatore generalizzato di funzionalità di interpolazione, una classe di base non ha alcun valore.
Tom W,

1
La prima cosa è raccogliere la tripletta X, Y, Z in un tipo Posizione e aggiungere l'interpolazione a quel tipo come membro o forse un metodo statico: Posizione interpolata (Posizione altro, rapporto).
Kevin Cline,

Risposte:


30

In un certo senso, hai risposto alla tua domanda con quell'osservazione nell'ultimo paragrafo:

Sto iniziando a pensare che il principio DRY non si applichi tanto in questo tipo di situazione, ma suona come una bestemmia .

Ogni volta che trovi qualche pratica non molto pratica per risolvere il tuo problema, non cercare di usarla religiosamente (la parola blasfemia è una specie di avvertimento per questo). La maggior parte delle pratiche ha i suoi capricci e perché e anche se coprono il 99% di tutti i casi possibili, c'è ancora quell'1% in cui potresti aver bisogno di un approccio diverso.

In particolare, per quanto riguarda DRY , ho anche scoperto che a volte è meglio avere anche diversi pezzi di codice duplicato ma semplice di una mostruosità gigantesca che ti fa stare male quando lo guardi.

Detto questo, l'esistenza di questi casi limite non dovrebbe essere usata come una scusa per la sciatta codifica copia e incolla o la completa mancanza di moduli riutilizzabili. Semplicemente, se non hai idea di come scrivere un codice sia generico che leggibile per qualche problema in qualche lingua, probabilmente è meno male avere un po 'di ridondanza. Pensa a chiunque debba mantenere il codice. Vivrebbero più facilmente con ridondanza o offuscamento?

Un consiglio più specifico sul tuo esempio particolare. Hai detto che questi calcoli erano simili ma leggermente diversi . È possibile che si desideri provare a suddividere la formula di calcolo in sottformule più piccole e quindi tutti i calcoli leggermente diversi chiamano queste funzioni di supporto per eseguire i calcoli secondari. Eviteresti la situazione in cui ogni calcolo dipende da un codice troppo generalizzato e avresti ancora un certo livello di riutilizzo.


10
Un altro punto su simili ma leggermente diversi è che anche se sembrano simili nel codice, non significa che debbano essere simili negli "affari". Dipende da quello che è ovviamente, ma a volte è una buona idea tenere le cose separate perché anche se sembrano uguali, potrebbero essere basate su decisioni / requisiti aziendali diversi. Quindi, potresti volerli considerare come calcoli molto diversi anche se il loro codice potrebbe sembrare simile. (Non una regola o altro, ma solo qualcosa da tenere a mente quando si decide se le cose devono essere combinate o refactored :)
Svish

@Svish Punto interessante. Non ci ho mai pensato in quel modo.
Phil

17

le classi secondarie utilizzano tipi diversi, il calcolo varia leggermente da una classe all'altra.

La cosa principale è innanzitutto: identificare chiaramente quale parte sta cambiando e quale parte NON sta cambiando tra le classi. Una volta identificato, il problema è stato risolto.

Fallo come primo esercizio prima di iniziare il factoring. Tutto il resto andrà a posto automaticamente dopo quello.


2
Ben messo. Questo potrebbe essere solo un problema della funzione ripetuta troppo grande.
Karl Bielefeldt,

8

Credo che quasi tutte le ripetizioni di più di un paio di righe di codice possano essere prese in considerazione in un modo o nell'altro, e quasi sempre dovrebbero esserlo.

Tuttavia, questo refactoring è più facile in alcune lingue rispetto ad altre. È abbastanza facile in linguaggi come LISP, Ruby, Python, Groovy, Javascript, Lua, ecc. Di solito non è troppo difficile in C ++ usando i template. Più doloroso in C, dove l'unico strumento può essere la macro del preprocessore. Spesso doloroso in Java e talvolta semplicemente impossibile, ad es. Provare a scrivere codice generico per gestire più tipi numerici incorporati.

In linguaggi più espressivi, non c'è dubbio: refactoring più di un paio di righe di codice. Con linguaggi meno espressivi devi bilanciare il dolore del refactoring con la lunghezza e la stabilità del codice ripetuto. Se il codice ripetuto è lungo o rischia di cambiare frequentemente, tendo a refacturing anche se il codice risultante è in qualche modo difficile da leggere.

Accetterò il codice ripetuto solo se è breve, stabile e il refactoring è troppo brutto. Fondamentalmente, escludo quasi tutta la duplicazione, a meno che non scriva Java.

È impossibile dare una raccomandazione specifica per il tuo caso perché non hai pubblicato il codice o hai persino indicato la lingua che stai utilizzando.


Lua non è un acronimo.
DeadMG

@DeadMG: annotato; sentiti libero di modificare. Ecco perché ti abbiamo dato tutta quella reputazione.
Kevin Cline,

5

Quando dici che la classe base deve eseguire un algoritmo, ma l'algoritmo varia per ogni sottoclasse, questo suona come un candidato perfetto per The Template Pattern .

Con questo, la classe base esegue l'algoritmo e quando si tratta della variazione per ogni sottoclasse, si passa a un metodo astratto che è responsabilità della sottoclasse implementare. Pensa al modo in cui la pagina ASP.NET si differenzia dal tuo codice per implementare Page_Load, ad esempio.


4

Sembra che il tuo "metodo di interpolazione generico" in ogni classe stia facendo troppo e dovrebbe essere trasformato in metodi più piccoli.

A seconda della complessità del calcolo, perché ogni "pezzo" del calcolo non può essere un metodo virtuale come questo

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

E sovrascrivere i singoli pezzi quando è necessario apportare quella "leggera variazione" nella logica del calcolo.

(Questo è un esempio molto piccolo e inutile di come è possibile aggiungere molte funzionalità ignorando i metodi di piccole dimensioni)


3

Secondo me, hai ragione, in un certo senso, che DRY può essere portato troppo lontano. Se è probabile che due parti di codice simili si evolvano in direzioni molto diverse, puoi causare problemi cercando di non ripetere te stesso inizialmente.

Tuttavia, hai anche ragione a diffidare di tali pensieri blasfemi. Prova molto a riflettere sulle tue opzioni prima di decidere di lasciarlo da solo.

Ad esempio, sarebbe meglio inserire quel codice ripetuto in una classe / metodo di utilità, piuttosto che nella classe base? Vedi Preferire la composizione rispetto all'ereditarietà .


2

DRY è una linea guida da seguire, non una regola infrangibile. Ad un certo punto devi decidere che non vale la pena avere livelli X di ereditarietà e modelli Y in ogni classe che stai usando solo per dire che non c'è ripetizione in questo codice. Un paio di buone domande da porsi sarebbero: mi ci vorrà più tempo per estrarre questi metodi simili e implementarli come uno, quindi dovrebbe cercare in tutti loro se dovesse sorgere la necessità di un cambiamento o è il loro potenziale che avvenga un cambiamento che potrebbe annullare il mio lavoro estraendo questi metodi in primo luogo? Sono nel punto in cui l'astrazione aggiuntiva sta iniziando a capire dove o cosa fa questo codice è una sfida?

Se riesci a rispondere sì a una di queste domande, allora hai un valido motivo per lasciare un codice potenzialmente duplicato


0

Devi farti la domanda, "perché dovrei rifattorizzarla"? Nel tuo caso in cui hai un codice "simile ma diverso" se apporti una modifica a un algoritmo, devi assicurarti di riflettere anche quella modifica negli altri punti. Questa è normalmente una ricetta per il disastro, invariabilmente qualcun altro mancherà un punto e introdurrà un altro bug.

In questo caso, il refactoring degli algoritmi in uno gigante lo renderà troppo complicato, rendendolo troppo difficile per la futura manutenzione. Quindi, se non riesci a fattorizzare sensibilmente le cose comuni, un semplice:

// this code is similar to class x function b

Il commento sarà sufficiente. Problema risolto.


0

Nel decidere se è meglio avere un metodo più grande o due metodi più piccoli con funzionalità sovrapposte, la prima domanda da $ 50.000 è se la parte sovrapposta del comportamento potrebbe cambiare e se qualsiasi modifica dovrebbe essere applicata allo stesso modo ai metodi più piccoli. Se la risposta alla prima domanda è sì ma la risposta alla seconda domanda è no, i metodi dovrebbero rimanere separati . Se la risposta ad entrambe le domande è sì, allora bisogna fare qualcosa per garantire che ogni versione del codice rimanga sincronizzata; in molti casi, il modo più semplice per farlo è avere una sola versione.

Ci sono alcuni posti in cui Microsoft sembra essere andata contro i principi DRY. Ad esempio, Microsoft ha esplicitamente scoraggiato che i metodi accettino un parametro che indicherebbe se un errore deve generare un'eccezione. Sebbene sia vero che un parametro "fallimenti genera eccezioni" è brutto nell'API di "utilizzo generale" di un metodo, tali parametri possono essere molto utili nei casi in cui un metodo Try / Do deve essere composto da altri metodi Try / Do. Se si suppone che un metodo esterno generi un'eccezione quando si verifica un errore, qualsiasi chiamata al metodo interno che fallisce dovrebbe generare un'eccezione che può essere propagata dal metodo esterno. Se il metodo esterno non dovrebbe generare un'eccezione, nemmeno quello interno. Se un parametro viene utilizzato per distinguere tra try / do, quindi il metodo esterno può passarlo al metodo interno. Altrimenti, sarà necessario che il metodo esterno chiami i metodi "try" quando dovrebbe comportarsi come "try", e i metodi "do" quando dovrebbe comportarsi come "do".

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.