Rompendo la grande classe in classi più piccole quando hanno bisogno di uno stato comune?

5

Sto scrivendo un parser per un linguaggio abbastanza complicato in C ++. La classe Parser riceve una lista di token e costruisce l'AST. Sebbene sia stata completata solo una parte del parser, il file Parser.cpp è già più di 1.5k linee e la classe ha circa 25 funzioni. Quindi, ho intenzione di rompere la grande classe Parser in classi più piccole, in modo da poter avere classi separate per l'analisi di costrutti linguistici diversi.

Ad esempio, desidero avere la classe ExprParser che analizza le espressioni, una classe TypeParser che analizza i tipi. Sembra essere molto più pulito. Il problema è che le funzioni di analisi devono avere accesso a uno stato che include la posizione del token corrente e diverse funzioni di aiuto di parsing. In C #, è possibile implementare funzioni correlate in classi diverse usando classi parziali. C'è qualche schema di progettazione specifico o un modo consigliato per questo?

    
posta Fish 18.05.2016 - 17:37
fonte

3 risposte

4

Crea una classe Scanner o Tokenizer, che prende i dati di input (il testo da analizzare) e mantiene la posizione del token corrente o dello stato simile. Può anche fornire alcune funzioni di aiuto condivise. Quindi fornire un riferimento (o un puntatore condiviso) all'oggetto Scanner a tutti i singoli oggetti xyzParser , in modo che tutti possano accedere allo stesso scanner. Lo "scanner" sarà responsabile solo dell'accesso ai dati tramite le funzioni tokenize di base, i singoli parser saranno responsabili della logica di analisi effettiva.

Funzionerà più facilmente se lo scanner non ha bisogno di sapere quali parser esistono. Se lo scanner ha realmente bisogno di saperlo, si potrebbe considerare di risolvere la dipendenza ciclica introducendo classi di base "interfaccia" astratte, o implementando una sorta di meccanismo di richiamata o evento, in cui lo scanner può notificare qualsiasi tipo di osservatore. p>     

risposta data 18.05.2016 - 18:27
fonte
1

Lo schema di progettazione dello stato, forse? È praticamente un'eredità diretta, con la parent - abstract - class che contiene un riferimento all'attuale oggetto "state", cioè parser.

Il modello accoppiato con i delegati, i metodi di estensione, ecc. dovrebbe dare molta flessibilità.

Fai attenzione a dividere arbitrariamente una classe. Queste classi più piccole hanno anche bisogno di integrità OO. Non mi riferisco alle lezioni parziali qui.

Mi piace particolarmente questo video demo pulito e semplice

    
risposta data 18.05.2016 - 18:06
fonte
1

Molto probabilmente, l'implementazione della grammatica come più parser interdipendenti renderà il tuo codice più complicato. Il flusso di dati diventerà meno ovvio e duplicherà qualche comportamento. Va bene se una classe è grande.

Tuttavia, molte lingue possono essere facilmente suddivise in diversi livelli e gestirne separatamente potrebbe essere ragionevole. Ad esempio:

  • potresti estrarre la tokenizzazione dal parser principale. C ha una fase separata di tokenizzazione e preprocessore.
  • Potresti eseguire alcune operazioni di post-elaborazione in una fase separata che costruisce l'AST finale. Questo è particolarmente sensibile se il parser controlla anche la semantica, ad es. risoluzione delle definizioni dei simboli o esecuzione di controlli di tipo. Questi dovrebbero essere separati dall'analisi.
  • Se la tua lingua ha una strong dictomia di espressione di istruzioni, potresti avere parser separati per ciascuno, con il parser dell'istruzione che richiama il parser di espressioni come necessario. Markdown è un esempio di una lingua con una grammatica basata sulla linea (indentazione) su una grammatica a livello di blocco (paragrafi, titoli, elenchi) su una grammatica in linea (enfasi, collegamenti). Alcuni parser utilizzano un semplice approccio di discesa ricorsiva per la sintassi del livello di istruzione, come i costrutti del flusso di controllo o le definizioni di livello superiore, ma passano a un algoritmo LR per le espressioni per gestire correttamente la precedenza e l'associatività.

Ho trovato piuttosto vantaggioso estrapolare operazioni di analisi di basso livello in una classe separata: gestire il buffer di input, controllare lookaheads, estrarre token, gestire errori, è meglio farlo da una classe personalizzata piuttosto che fare affidamento sul strutture fornite dalla lingua (in particolare, std::istream non è adatto alla maggior parte dei problemi). Se stai utilizzando un algoritmo di analisi diverso da Discorsione ricorsiva, dovresti anche gestire queste operazioni in una classe separata.

    
risposta data 18.05.2016 - 18:34
fonte

Leggi altre domande sui tag