Il modello "metaprogramming" in Java è una buona idea?

29

C'è un file sorgente in un progetto piuttosto grande con diverse funzioni estremamente sensibili alle prestazioni (chiamate milioni di volte al secondo). In effetti, il precedente manutentore ha deciso di scrivere 12 copie di una funzione ciascuna leggermente diversa, in modo da risparmiare il tempo che sarebbe stato speso controllando i condizionali in una singola funzione.

Sfortunatamente, questo significa che il codice è un PITA da mantenere. Vorrei rimuovere tutto il codice duplicato e scrivere solo un modello. Tuttavia, il linguaggio, Java, non supporta i modelli e non sono sicuro che i generici siano adatti a questo.

Il mio piano attuale è di scrivere invece un file che genera le 12 copie della funzione (praticamente un expander template one-use-only). Fornirei ovviamente spiegazioni abbondanti sul motivo per cui il file deve essere generato a livello di codice.

La mia preoccupazione è che ciò porterebbe alla confusione dei futuri manutentori e forse introdurrà dei bug sgradevoli se dimenticheranno di rigenerare il file dopo averlo modificato, o (ancora peggio) se modificano invece il file generato dal programma. Sfortunatamente, a meno di riscrivere il tutto in C ++, non vedo alcun modo per sistemarlo.

I vantaggi di questo approccio superano gli svantaggi? Dovrei invece:

  • Ottieni il successo in termini di prestazioni e utilizza un'unica funzione gestibile.
  • Aggiungi spiegazioni sul motivo per cui la funzione deve essere ripetuta 12 volte e si presta gentilmente agli oneri di manutenzione.
  • Tentativo di utilizzare i generici come modelli (probabilmente non funzionano in questo modo).
  • Urlo al vecchio manutentore per la creazione di codice in modo da rendere le prestazioni dipendenti da una singola funzione.
  • Altro metodo per mantenere le prestazioni e la manutenibilità?

P.S. A causa della scarsa progettazione del progetto, la profilazione della funzione è piuttosto complicata ... tuttavia, il precedente manutentore mi ha convinto che il successo in termini di prestazioni è inaccettabile. Suppongo che questo significhi più del 5%, anche se questa è una supposizione completa da parte mia.

Forse dovrei elaborare un po '. Le 12 copie svolgono un compito molto simile, ma presentano minime differenze. Le differenze sono in vari punti in tutta la funzione, quindi purtroppo ci sono molte, molte, dichiarazioni condizionali. Vi sono effettivamente 6 "modalità" di funzionamento e 2 "paradigmi" di operazione (parole composte da me stesso). Per usare la funzione, si specifica la "modalità" e il "paradigma" dell'operazione. Questo non è mai dinamico; ogni pezzo di codice usa esattamente una modalità e un paradigma. Tutte le 12 coppie modalità-paradigma vengono utilizzate da qualche parte nell'applicazione. Le funzioni sono giustamente chiamate func1 a func12, con numeri pari che rappresentano il secondo paradigma e numeri dispari che rappresentano il primo paradigma.

Sono consapevole che questo è solo il peggior disegno di sempre se la manutenibilità è l'obiettivo. Ma sembra essere "abbastanza veloce" e questo codice non ha richiesto alcuna modifica per un po '... Vale anche la pena notare che la funzione originale non è stata cancellata (anche se è un codice morto per quanto posso dire) , quindi il refactoring sarebbe semplice.

    
posta Fengyang Wang 24.06.2014 - 20:00
fonte

10 risposte

23

Questa è una situazione molto brutta, è necessario refactoring questo ASAP - questo è il debito tecnico nel suo peggio - non si sa nemmeno quanto sia importante il codice veramente - si specula solo che è importante .

Per quanto riguarda le soluzioni APPENA POSSIBILE:
Qualcosa che può essere fatto è l'aggiunta di un passo di compilazione personalizzato. Se si utilizza Maven che è in realtà abbastanza semplice da fare, è probabile che anche altri sistemi di compilazione automatizzati possano far fronte a questo. Scrivi un file con un'estensione diversa di .java e aggiungi un passaggio personalizzato che cerca nella tua origine file simili e rigenera il file .java effettivo. Potresti anche voler aggiungere un enorme disclaimer sul file generato automaticamente che spiega di non modificarlo.

Professionisti vs utilizzando un file generato una sola volta: i tuoi sviluppatori non avranno le loro modifiche su .java funzionante. Se effettivamente eseguono il codice sulla loro macchina prima di commetterli, scopriranno che le loro modifiche non hanno alcun effetto (hah). E poi forse leggeranno il disclaimer. Hai assolutamente ragione nel non fidarti dei tuoi compagni di squadra e del tuo sé futuro ricordando che questo particolare file deve essere cambiato in un modo diverso. Permette anche il collaudo automatico come wel, dato che JUnit compilerà il tuo programma prima di eseguire test (e rigenererà anche il file)

EDIT

A giudicare dai commenti, la risposta è venuta fuori come se fosse un modo per rendere questo lavoro indefinito e forse OK per la distribuzione in altre parti critiche del tuo progetto.

In parole semplici: non lo è.

Il fardello extra di creare il tuo mini-linguaggio, scrivere un generatore di codice e mantenerlo, per non parlare dell'insegnamento ai futuri manutentori, a lungo andare è infernale. Quanto sopra consente solo un più sicuro modo per gestire il problema mentre si lavora su una soluzione a lungo termine . Quello che prenderò è sopra di me.

    
risposta data 24.06.2014 - 20:16
fonte
16

Il colpo di manutenzione è reale o ti infastidisce? Se ti dà solo fastidio, lascia stare.

Il problema delle prestazioni è reale o lo è stato solo lo sviluppatore precedente pensa ? I problemi di prestazioni, il più delle volte, non sono dove si pensa che siano, anche quando viene utilizzato un profiler. (Io uso questa tecnica per trovarli in modo affidabile.)

Quindi c'è una possibilità che tu abbia una brutta soluzione per un non-problema, ma potrebbe anche essere una brutta soluzione per un vero problema. In caso di dubbio, lascia stare.

    
risposta data 24.06.2014 - 20:16
fonte
10

Assicurati davvero che in condizioni reali, produzione, condizioni (consumo di memoria realistico, che innesca la Garbage Collection realisticamente, ecc.), tutti quei metodi separati fanno davvero una differenza di prestazioni (invece di avere un solo metodo). Questa operazione richiederà alcuni giorni, ma potrebbe farti risparmiare settimane semplificando il codice su cui lavori da ora in poi.

Inoltre, se fai scopri che hai bisogno di tutte le funzioni, potresti usare Javassist per generare il codice in modo programmatico. Come altri hanno sottolineato, puoi automatizzarlo con Maven .

    
risposta data 24.06.2014 - 22:04
fonte
6

Perché non includere una sorta di preprocessore \ generazione di codice per questo sistema? Un passaggio personalizzato aggiuntivo che esegue ed emette file sorgente java aggiuntivi prima di compilare il resto del codice. Questo è il modo in cui funziona spesso includere i client Web al di fuori dei file wsdl e xsd.

Ovviamente manterrai la manutenzione di quel generatore di codice \ preprocessore, ma non avrai la manutenzione duplicata del codice base di cui preoccuparti.

Da quando è stato emesso il codice Java in fase di compilazione, non è prevista alcuna penalizzazione delle prestazioni per il codice aggiuntivo. Ma ottieni semplificazione della manutenzione avendo il modello invece di tutto il codice duplicato.

I generici Java non danno alcun vantaggio in termini di prestazioni a causa della cancellazione dei caratteri nella lingua, al posto del semplice casting.

Dalla mia comprensione dei modelli C ++, sono compilati in diverse funzioni per ogni invocazione di template. Ti ritroverai con un time code mid-compile duplicato per ogni tipo che immagazzini in un vettore std :: ad esempio.

    
risposta data 24.06.2014 - 20:14
fonte
2

Nessun metodo Java sensato può essere abbastanza lungo da avere 12 varianti ... e JITC odia metodi lunghi - semplicemente rifiuta di ottimizzarli correttamente. Ho visto un fattore di accelerazione di due semplicemente suddividendo un metodo in due più brevi. Forse questa è la strada da percorrere.

OTOH che ha più copie può avere senso anche se ci fossero identici. Dato che ognuno di essi viene utilizzato in diversi luoghi, vengono ottimizzati per casi diversi (il JITC li analizza e quindi posiziona i casi rari su un percorso eccezionale.

Direi che generare codice non è un grosso problema, supponendo che ci sia una buona ragione. L'overhead è piuttosto basso e i file con nome corretto portano immediatamente alla loro fonte. Molto tempo fa, mentre generavo il codice sorgente, ho inserito // DO NOT EDIT su ogni riga ... Immagino che sia abbastanza sufficiente.

    
risposta data 25.06.2014 - 02:34
fonte
1

Non c'è assolutamente nulla di sbagliato nel "problema" che hai citato. Da quello che so, questo è il tipo esatto di design e approccio utilizzato dal server DB per avere buone prestazioni.

Hanno molti metodi speciali per assicurarsi che possano massimizzare le prestazioni per tutti i tipi di operazioni: join, select, aggregate, ecc ... quando si applicano determinate condizioni.

In breve, la generazione di codice come quella che pensi sia una cattiva idea. Forse dovresti guardare questo diagramma per vedere come un DB risolve un problema simile al tuo:

    
risposta data 29.06.2014 - 09:30
fonte
1

Potresti voler verificare se puoi ancora utilizzare alcune astrazioni sensibili e utilizzare ad es. il modello di metodo del modello per scrivere codice comprensibile per la funzionalità comune e spostare le differenze tra i metodi in " operazioni primitive "(secondo la descrizione del modello) in 12 sottoclassi. Ciò potrebbe migliorare la manutenibilità e la testabilità e potrebbe effettivamente avere le stesse prestazioni del codice corrente, poiché la JVM può in linea di tempo integrare le chiamate al metodo per le operazioni primitive. Certo, devi verificarlo in un test delle prestazioni.

    
risposta data 01.07.2014 - 08:54
fonte
0

Puoi risolvere il problema dei metodi specializzati usando il linguaggio di Scala.

Scala può usare metodi inline, questo (in combinazione con un uso semplice della funzione di ordine superiore) rende possibile evitare la duplicazione del codice gratuitamente - questo sembra il problema principale menzionato nella risposta.

Ma anche Scala ha macro sintattiche, che rendono possibile fare molte cose con il codice al momento della compilazione in un modo sicuro dal punto di vista del tipo.

E il problema comune dei tipi primitivi di boxe quando usati in generici è possibile risolvere anche in Scala: può fare la specializzazione generica per le primitive per evitare il pugilato automaticamente usando @specialized annotazione - questa cosa è incorporata nel linguaggio stesso. Quindi, in pratica, scriverai un metodo generico in Scala e verrà eseguito con la velocità dei metodi specializzati. E se hai bisogno anche di aritmetica generica veloce, è facilmente fattibile usando "pattern" di Typeclass per iniettare le operazioni e i valori per diversi tipi numerici.

Inoltre, Scala è fantastico in molti altri modi. E non devi riscrivere tutto il codice su Scala, perché Scala < - > L'interoperabilità Java è eccellente. Assicurati di utilizzare SBT (scala build tool) per la costruzione del progetto.

    
risposta data 27.04.2015 - 09:36
fonte
-1

Se la funzione in questione è di grandi dimensioni, trasformare i bit "mode / paradigm" in un'interfaccia e quindi passare un oggetto che implementa quell'interfaccia come parametro per la funzione può funzionare. GoF chiama questo modello "Strategia", iirc. Se la funzione è piccola, l'overhead aumentato può essere significativo ... qualcuno ha ancora parlato di profilazione? ... più persone dovrebbero menzionare la profilazione.

    
risposta data 24.06.2014 - 20:29
fonte
-2

Quanti anni ha il programma? Molto probabilmente l'hardware più recente rimuoverebbe il collo di bottiglia e potresti passare a una versione più gestibile. Ma ci deve essere un motivo per la manutenzione, quindi a meno che il tuo compito non sia migliorare la base di codice, lascia che sia come funziona.

    
risposta data 24.06.2014 - 22:47
fonte

Leggi altre domande sui tag