Sto scrivendo un compilatore, per il quale ho ideato un'architettura piuttosto classica: è composta da passaggi sequenziali collegati insieme, a partire da un lexer e da un parser, che continua con un macro processore, quindi un passaggio di verifica di tipo / analisi semantica e infine un generatore di codice intermedio (e forse un ottimizzatore IR che verrà in seguito).
Il mio approccio attuale è il seguente. Il parser sta creando un AST, in cui ogni tipo di nodo AST eredita da una classe di base "AST". Pianifico che le funzioni virtuali di AST implementeranno la funzionalità dei passaggi successivi. Per fornire un esempio semplificato:
-
macroExpand()
trova, valuta e sostituisce tutte le macro in AST, in modo ricorsivo; -
typeCheck()
esegue il controllo dei tipi, l'inferenza di tipo e l'analisi semantica generale / controllo degli errori sull'albero ormai desugared, completando ogni nodo con annotazioni di tipo (che sono implementate come variabili membro); -
codeGen()
, infine, genera una sorta di IR dall'AST annotato.
Tuttavia, temo che avere tutte queste funzionalità in una singola classe vìoli il principio della responsabilità unica.
Riconosco che l'espansione macro non è particolarmente adatta: stavo pensando di integrare l'analisi semantica e la generazione di codice in un solo insieme di funzioni invece di separarle, semplicemente perché non penso di dover attraversare l'intero albero due volte e dovrei comunque esaminare i tipi al momento del codegen, anche se li ho pre-controllati e pre-controllati in precedenza.
Ma anche con questo cambiamento strutturale, ci sono ancora almeno due serie di metodi completamente diversi sulle mie classi AST. Non vedo ancora alcun motivo particolare per cui questo di per sé sarebbe negativo nel mio caso specifico, ma sono abbastanza sicuro che il principio della responsabilità unica sia stato scoperto per una buona ragione.
Un modo per rimediare (?) a questo problema sarebbe utilizzare il modello di visitatore e scrivere gerarchie di classi di visitatori separate per l'AST per ogni scopo (espansione macro, analisi semantica e / o generazione di codice) . Ma davvero non ho voglia di farlo. (In tutta onestà, non mi piace l'idea.) Finora sembra solo introdurre complessità e onere inutili (costringendomi a mantenere le gerarchie di classi parallele).
Attualmente sto scrivendo questo compilatore in C ++, ma sono abbastanza sicuro che se usassi un linguaggio che permettesse la modifica e l'aumento delle classi successive (ad esempio le categorie Objective-C), sicuramente utilizzare questa caratteristica del linguaggio e decorerei semplicemente le mie classi AST di base con il set di metodi necessario, indipendentemente dall'interfaccia "core" e dall'implementazione di dette classi.
Potrei simulare che in C ++ inserendo tutte le dichiarazioni del metodo in un file di intestazione, ma scrivendo l'implementazione di ogni categoria di funzioni in diversi file di implementazione. Ciò, tuttavia, contraddice il solito pratica "una classe, un file".
Per riassumere, la mia domanda è: il mio attuale approccio nel dare due o tre funzionalità diverse a una classe è davvero pessimo?
-
In tal caso, alcune delle mie correzioni suggerite sono considerate buone prassi, oppure ...
-
Se non lo è, puoi suggerire qualcosa di meglio?