(TLDR) Per costruire un parser per una grammatica ricorsiva per composizione di singoli parser (ad esempio con un framework di un combinatore di parser), ci sono spesso dipendenze circolari tra alcuni dei singoli parser. Mentre le dipendenze circolari sono generalmente un segno di cattiva progettazione, è un caso valido in cui la dipendenza circolare è inevitabile? In tal caso, quale soluzione sarebbe preferibile trattare con la dipendenza circolare? O i parser combinator sono solo una cattiva idea? (/ TLDR)
Ci sono altre domande che chiedono informazioni sull'integrazione delle dipendenze con le dipendenze circolari. In genere, la risposta è modificare il design per evitare la circolarità.
- Come risolvere la dipendenza circolare?
- Come gestire la "dipendenza circolare" nell'iniezione delle dipendenze
- La circular Dependency Injection è una buona pratica?
Mi sono imbattuto in un caso tipico in cui trovo dipendenze circolari: se voglio avere servizi diversi per ispezionare una struttura ricorsiva.
Ho pensato ad altri esempi, ma finora il meglio che ho trovato è un parser per una grammatica ricorsiva. Usiamo json come esempio, perché è semplice e ben noto.
- Un "valore" json può essere una stringa (
".."
), un oggetto ({..}
) o un array ([..]
). - Una stringa è semplice da analizzare e non ha altri figli.
- Un oggetto è costituito da chiavi e valori e dalla sintassi dell'oggetto circostante.
- Un array è costituito da valori e dalla sintassi dell'array circostante.
- Una "chiave" all'interno di un oggetto è fondamentalmente la stessa di una stringa.
Ora creerò un numero di oggetti parser:
- Un parser del valore, che dipende dagli altri 3 parser.
- Un parser di oggetti, che dipende dal parser di stringhe e dal parser di valori.
- Un parser di array, che dipende dal parser di valori.
- Un parser di stringhe.
Voglio gestire i 4 parser con un contenitore per le dipendenze. Oppure, anche se non voglio un contenitore, dobbiamo ancora capire in che ordine creare i diversi parser. C'è un problema di pollo e uova.
Ci sono soluzioni note a questo, che possono essere osservate nelle librerie di parser esistenti. Finora ho visto soprattutto la soluzione "stub".
- Evita la dipendenza circolare ..
.. passando il parser del valore come argomento al metodo parse () del parser di oggetti e dell'array.
Funziona, ma svuota la firma del metodo parse (). Immagina che vogliamo che questo sia qualcosa come un parser combinator, che può essere riutilizzato per altre grammatiche. Vorremmo un'interfaccia parser generica e indipendente dalla grammatica specifica, quindi non possiamo aver bisogno che venga passato un parser specifico.
- Utilizza uno stub.
Invece di richiedere ogni dipendenza nel costruttore, potremmo usare un metodo set () o add () su uno dei parser. Per esempio. per prima cosa creiamo un parser di valore vuoto ("stub"), quindi aggiungiamo l'oggetto, gli array e gli parser di stringa tramite il metodo add ().
- Utilizza un proxy.
Invece di creare il parser del valore attuale, creiamo un oggetto proxy, con un riferimento al contenitore. Solo quando il metodo parse () viene chiamato la prima volta sul parser del valore proxy, viene creato il parser del valore reale.
Ora va tutto bene, e suppongo sia solo questione di gusti, quale soluzione preferisco.
Ma come si adatta alla tipica risposta high horse che le dipendenze circolari sono un segno di una cattiva progettazione? L'esempio sembra totalmente valido e c'è un'intera classe di problemi come questo.