Design minimale rispetto all'uso previsto

3

Recentemente durante l'esecuzione di una revisione del codice mi sono imbattuto in qualcosa del genere:

Codice vecchio:

 WriteLabour(useNewVersion);
 WritePart();
 WriteNonLabour(useNewVersion);

Nuovo codice:

WriteLabour();
WritePart(useNewVersion);
WriteNonLabour();

Ora lascia che ti dia qualche informazione. Questo è un codice molto vecchio, non molto orientato agli oggetti. Ci sono sempre due versioni del file, la versione corrente e la versione precedente. Il flag useNewVersion determina se la versione corrente o precedente deve essere scritta. Il file è destinato al consumo da parte di un cliente e quando viene visualizzata una nuova versione, ci inviano le nuove specifiche, cambiamo il codice per generare il file secondo le nuove specifiche e questa diventa la nuova versione. Quella che era la nuova versione ora diventa la versione precedente, la capacità di scrivere la versione anche precedente a quella viene gettato via per sempre (il codice viene cancellato).

Il programmatore stava aggiornando il codice per la nuova specifica appena ricevuta. Ha trovato dopo aver cancellato il codice per la versione da dimenticare che WriteLabour () e WriteNonLabour () non avevano più codice specifico per la versione, mentre WriteParts () ora doveva avere un codice specifico per la versione. Così ha cambiato le firme del metodo come ho indicato sopra.

Il mio suggerimento nella revisione del codice era che sarebbe stato bello mantenere coerenti le firme dei metodi e fornire sempre il flag useNewVersion, anche se il codice all'interno del metodo non utilizzava il flag. Per me questo ha un senso intuitivo, perché è molto probabile che le future versioni del file richiederanno il passaggio del flag e l'interfaccia per il chiamante rimarrà coerente. La sua argomentazione contro il mio suggerimento era che passare il flag useNewVersion a metodi che non ne avevano bisogno era un design gonfiato. Chissà quali altri parametri potrebbero aver bisogno dei metodi, dovremmo passarli tutti in anticipo?

Il mio istinto era che la sua argomentazione era errata in questo caso particolare, ma non potevo contrastarla in modo efficace. La mia domanda quindi è se ci sono decisioni di progettazione che sappiamo siano in arrivo o da esperienze passate, è sbagliato incorporarle nel progetto attuale anche se potrebbero essere attualmente inutilizzate? Specificamente in termini di interfacce pubbliche, firme dei metodi ecc., Non codice privato.

Ma è importante mantenere il codice snello e non sovrascrivere la progettazione. Non voglio essere l'astronauta dell'architettura.

Nota educata: per favore non deragliare l'argomento suggerendo il rifacimento del codice. Questo non dovrebbe essere una critica del design del nostro codice legacy. Quel codice non è all'altezza degli standard, e lo so.

    
posta Ali 09.10.2015 - 18:27
fonte

4 risposte

7

Penso che quello con cui hai a che fare è una collisione di alcuni principi.

Da un lato, hai il principio del minimo stupore che ti incoraggerebbe a mantenere coerenti le firme dei tuoi metodi.

D'altra parte, YAGNI (non ne avrai bisogno) ti dice di lasciarlo fuori perché non è necessario.

E questi principi devono essere valutati rispetto ai costi di refactoring.

Sembra che questo codice legacy ti abbia battuto in un approccio particolare per affrontarlo. Ti sei abituato a dover specificare useNewVersion e numb alla perdita di informazioni da quel particolare design. Per essere un po 'ingiusto, quello che hai suggerito al tuo collega di lavoro è stato "Sono abituato ad essere randellato in testa e dover capire quale versione dovrei chiamare. Non cambiamo i nostri modi perché sono abituato ad essere colpire ".

E la risposta dei tuoi colleghi è stata l'effetto di "Guarda, guarda, non dobbiamo più picchiarlo tutto il tempo!" Purtroppo, ha reimplementato lo stesso approccio randagio con un metodo diverso, quindi in questo caso non ottiene pieno credito.

Che cosa dovresti fare? Non credo che dovresti specificare useNewVersion come parametro di funzione. È un design scadente e costringe lo sviluppatore a pensare a cose di cui non dovrebbe preoccuparsi. Credo nel trattare le funzioni come se fossero contratti. "Se chiami questo metodo, farà XYZ. Non devi preoccuparti di come funziona XYZ, si prenderà cura di questo per te."

Quindi WriteLabour() e WriteNonLabour() non dovrebbero avere parametri useNewVersion .

    
risposta data 09.10.2015 - 19:08
fonte
8

"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away."

Antoine de Saint-Exupery

Se riordini il codice con parametri inutilizzati, altre persone lo leggono più tardi perderanno tempo a cercare di capire perché sono lì. Intendevi metterli li? Dimenticati di rimuoverli? C'è un bug a causa della mancanza di codice? Creano rumore che nasconde le informazioni reali nel codice. Questo rende il codice più difficile da leggere e rende le persone più nervose a rompere le cose quando la cambiano.

Inoltre, molti compilatori moderni contrassegneranno parametri e variabili inutilizzati come avvertenze. Le buone pratiche dicono che tutti gli avvertimenti dovrebbero essere trattati come errori. In questo contesto, i parametri non utilizzati sono un errore del compilatore e pertanto devono essere rimossi.

TL; DR: non inserire parametri inutilizzati nel tuo codice "nel caso in cui ne avessi bisogno in seguito".

    
risposta data 09.10.2015 - 18:37
fonte
5

Spero che questa risposta non ricada nel "deragliare l'argomento proponendo la categoria del ridimensionamento per il codice". Tuttavia, vorrei suggerire di sostituire il flag booleano useNewVersion con un parametro version integrale che tutte le funzioni dovrebbero accettare. L'implementazione della funzione può quindi prendere decisioni come questa.

final int newVersion = 424;
final int oldVersion = 423;
if (version == newVersion) {
    // Do what is appropriate for the “new” version
} else if (version == oldVersion) {
    // Do what is appropriate for the “old” version
} else {
    throw new VersionNotSupportedException(version);
}

Se l'azione da intraprendere per le due versioni attualmente supportate sembra essere identica, la funzione può comunque fare buon uso del parametro.

final int newVersion = 424;
final int oldVersion = 423;
if (version != newVersion && version != oldVersion) {
    throw new VersionNotSupportedException(version);
}
// Do what is appropriate for both versions

Il problema con il flag useNewVersion è che "nuovo" non ha realmente alcun significato e, pertanto, il tuo collega non ha completamente torto affermando che non c'è modo in cui possa agire in modo sensato, quindi il parametro non dovrebbe essere passato in primo luogo. D'altra parte, le interfacce stabili sono una buona cosa da avere. Considero anche auspicabile che tutte le funzioni seguano una convenzione comune, come prendere la versione come primo parametro. Può aiutare a ridurre la confusione quando si guarda una chiamata di funzione. Se puoi già prevedere che avrai di nuovo bisogno del parametro in una versione futura, aggiungere e rimuovere ripetutamente il tempo non è molto efficace. Soprattutto se si propagano le informazioni sulla versione attraverso diversi livelli di chiamate di funzione.

Hai affermato che il flag useNewVersion viene caricato da una configurazione utente. Va bene e non devi cambiarlo. Basta modificare il codice interno per tradurre quella bandiera booleana in un numero intero. Puoi avere una funzione centrale che fa questo.

int getCurrentVersion() {
    final int newVersion = 424;
    final int oldVersion = 423;
    final boolean useNewVersion = loadFromDatabase("useNewVersion");
    return useNewVersion ? newVersion : oldVersion;
}

L'utilizzo di un'enumerazione anziché di un intero potrebbe anche essere un'opzione.

Questo mi sembra uno sforzo di refactoring molto moderato per me che aiuterebbe a rendere il tuo codice più solido e coerente. Se ora ricevi una nuova specifica dal tuo cliente e ti dimentichi di aggiornare alcune funzioni, ti verrà detto immediatamente perché il nuovo numero di versione non viene riconosciuto da loro. Con la bandiera booleana, avresti invece invertito silenziosamente il significato di "vecchio" e "nuovo" e una funzione che avevi dimenticato di aggiornare avrebbe fatto felicemente la cosa sbagliata.

    
risposta data 10.10.2015 - 01:32
fonte
1

... if there are design decisions we know are coming or by past experience, is it wrong to incorporate them into the current design even though they may be presently unused?

Evitiamo il livello di prevedibilità della necessità o meno della funzione. Questo è un rischio che starai prendendo. Se assumiamo che le funzionalità verranno aggiunte, la domanda è quando sono più facili da aggiungere, ora o dopo. Se sono molto più facili da aggiungere ora, potresti voler riconsiderare il design, ma hai indicato che non è questo il problema.

Ora tutto ciò che devi fare è che hai tempo o tempo dopo? A un certo punto sembra che tu stia anticipando molte nuove funzionalità e sappiamo tutti che vorranno prima o poi. Scegliendo di aggiungerli ora quando ne hai il tempo, potresti mitigare il rischio se ti servono o meno.

Penso che sia sempre più facile aggiungere le cose in seguito, rispetto al tentativo di identificare il codice inutilizzato / non necessario in un secondo momento e dover capire come sbarazzarsi di esso. O avere buone note e / o memoria. Non penso che ne valga la pena.

    
risposta data 09.10.2015 - 20:47
fonte

Leggi altre domande sui tag