Ontwerppatroon voor tolk in Java

1. Overzicht

In deze tutorial introduceren we een van de gedragsmatige GoF-ontwerppatronen: de tolk.

Eerst geven we een overzicht van het doel en leggen we het probleem uit dat het probeert op te lossen.

Vervolgens bekijken we het UML-diagram van Interpreter en de implementatie van het praktische voorbeeld.

2. Ontwerppatroon voor tolk

Kortom, het patroon definieert de grammatica van een bepaalde taal op een objectgeoriënteerde manier die door de tolk zelf kan worden beoordeeld.

Met dat in gedachten kunnen we technisch gezien onze aangepaste reguliere expressie bouwen, een aangepaste DSL-interpreter of we kunnen een van de menselijke talen ontleden, bouw abstracte syntaxisbomen en voer vervolgens de interpretatie uit.

Dit zijn slechts enkele van de mogelijke use-cases, maar als we een tijdje nadenken, zouden we er nog meer toepassingen van kunnen vinden, bijvoorbeeld in onze IDE's, omdat ze de code die we schrijven voortdurend interpreteren en ons dus voorzien van onschatbare hints.

Het tolkpatroon moet in het algemeen worden gebruikt wanneer de grammatica relatief eenvoudig is.

Anders kan het moeilijk worden om het te onderhouden.

3. UML-diagram

Bovenstaand diagram toont twee hoofdentiteiten: het Context en de Uitdrukking.

Nu moet elke taal op de een of andere manier worden uitgedrukt, en de woorden (uitdrukkingen) zullen een betekenis hebben op basis van de gegeven context.

Abstracte expressie definieert een abstracte methode die de context opneemtals parameter. Dankzij dat, elke uitdrukking heeft invloed op de context, verander de status en ga door met de interpretatie of retourneer het resultaat zelf.

Daarom zal de context de houder zijn van de globale staat van verwerking en zal deze tijdens het hele interpretatieproces worden hergebruikt.

Dus wat is het verschil tussen de TerminalExpression en NonTerminalExpression?

EEN NonTerminalExpression kan een of meer andere hebben Abstracte uitdrukkingen ermee geassocieerd, daarom kan het recursief worden geïnterpreteerd. Uiteindelijk, het interpretatieproces moet eindigen een TerminalExpression dat levert het resultaat op.

Het is de moeite waard om dat op te merken NonTerminalExpression is een composiet.

Ten slotte is het de rol van de klant om een ​​reeds gemaakt abstracte syntaxisboom, wat niets meer is dan een zin gedefinieerd in de gemaakte taal.

4. Implementatie

Om het patroon in actie te laten zien, bouwen we een eenvoudige SQL-achtige syntaxis op een objectgeoriënteerde manier, die vervolgens wordt geïnterpreteerd en ons het resultaat retourneert.

Eerst zullen we definiëren Selecteer uit, en Waar expressies, bouw een syntaxisboom in de klasse van de cliënt en voer de interpretatie uit.

De Uitdrukking interface heeft de interpretatiemethode:

Lijst interpreteren (Context ctx);

Vervolgens definiëren we de eerste uitdrukking, de Selecteer klasse:

class Select implementeert Expression {private String column; privé Vanaf van; // constructor @Override openbare lijst interpreteren (Context ctx) {ctx.setColumn (kolom); return from.interpret (ctx); }}

Het krijgt de kolomnaam die moet worden geselecteerd en een ander concreet Uitdrukking van het type Van als parameters in de constructor.

Merk op dat in het overschreven interpreteren() methode stelt het de toestand van de context in en geeft de interpretatie samen met de context door aan een andere uitdrukking.

Op die manier zien we dat het een NonTerminalExpression.

Een andere uitdrukking is de Van klasse:

class From implementeert Expression {private String-tabel; privé Waar waar; // constructors @Override public List interpret (Context ctx) {ctx.setTable (table); if (where == null) {return ctx.search (); } return where.interpret (ctx); }}

Nu is in SQL de where-component optioneel, daarom is deze klasse een terminal- of een niet-terminale expressie.

Als de gebruiker besluit een where-clausule niet te gebruiken, wordt de Van expressie wordt het beëindigd met de ctx.search () bel en stuur het resultaat terug. Anders wordt het verder geïnterpreteerd.

De Waar expression wijzigt opnieuw de context door het benodigde filter in te stellen en beëindigt de interpretatie met zoekoproep:

class Waar implementeert Expression {privé predikaatfilter; // constructor @Override openbare lijst interpreteren (Context ctx) {ctx.setFilter (filter); retourneer ctx.search (); }}

Voor het voorbeeld, de Context klasse bevat de gegevens die de databasetabel imiteren.

Merk op dat het drie sleutelvelden heeft die worden gewijzigd door elke subklasse van Uitdrukking en de zoekmethode:

class Context {privé statische kaart tabellen = nieuwe HashMap (); statische {Lijstlijst = nieuwe ArrayList (); list.add (nieuwe rij ("John", "Doe")); list.add (nieuwe rij ("Jan", "Kowalski")); list.add (nieuwe rij ("Dominic", "Doom")); tables.put ("mensen", lijst); } private String-tabel; private String-kolom; privé predikaat whereFilter; // ... List search () {List result = tables.entrySet () .stream () .filter (entry -> entry.getKey (). EqualsIgnoreCase (table)) .flatMap (entry -> Stream.of (entry .getValue ())) .flatMap (Verzameling :: stream) .map (Rij :: toString) .flatMap (columnMapper) .filter (whereFilter) .collect (Collectors.toList ()); Doorzichtig(); resultaat teruggeven; }}

Nadat de zoekopdracht is voltooid, wordt de context vanzelf gewist, dus de kolom, tabel en filter worden op de standaardwaarden ingesteld.

Op die manier heeft elke interpretatie geen invloed op de andere.

5. Testen

Laten we voor testdoeleinden eens kijken naar het InterpreterDemo klasse:

public class InterpreterDemo {public static void main (String [] args) {Expression query = new Select ("name", new From ("people")); Context ctx = nieuwe context (); Lijstresultaat = query.interpret (ctx); System.out.println (resultaat); Expression query2 = new Select ("*", new From ("people")); Lijst resultaat2 = query2.interpret (ctx); System.out.println (resultaat2); Expression query3 = new Select ("name", new From ("people", new Where (name -> name.toLowerCase (). StartsWith ("d")))); Lijst resultaat3 = query3.interpret (ctx); System.out.println (resultaat3); }}

Eerst bouwen we een syntaxisboom met gemaakte uitdrukkingen, initialiseren we de context en voeren we vervolgens de interpretatie uit. De context wordt hergebruikt, maar zoals we hierboven hebben laten zien, reinigt deze zichzelf na elke zoekoproep.

Door het programma uit te voeren, zou de output als volgt moeten zijn:

[John, Jan, Dominic] [John Doe, Jan Kowalski, Dominic Doom] [Dominic]

6. Minpunten

Wanneer de grammatica complexer wordt, wordt het moeilijker om vol te houden.

Het is te zien in het gepresenteerde voorbeeld. Het zou redelijk eenvoudig zijn om nog een uitdrukking toe te voegen, zoals Limiet, maar het zal niet al te gemakkelijk te onderhouden zijn als we zouden besluiten om het uit te breiden met alle andere uitdrukkingen.

7. Conclusie

Het ontwerppatroon van de tolk is geweldig voor relatief eenvoudige grammaticale interpretatie, die niet veel hoeft te evolueren en uit te breiden.

In bovenstaand voorbeeld hebben we laten zien dat het mogelijk is om met behulp van het interpreter-patroon objectgeoriënteerd een SQL-achtige query op te bouwen.

Ten slotte kunt u dit patroongebruik in JDK vinden, met name in java.util.Pattern, java.text.Format of java.text.Normalizer.

Zoals gewoonlijk is de volledige code beschikbaar op het Github-project.


$config[zx-auto] not found$config[zx-overlay] not found