Der Interpreter (englisch interpreter pattern) ist ein Entwurfsmuster aus dem Bereich der Softwareentwicklung und gehört zu der Kategorie der Verhaltensmuster (englisch behavioural patterns). Das Muster ist eines der sogenannten GoF-Muster.
Das Interpretermuster definiert eine Repräsentation für die Grammatik einer Sprache und die Möglichkeit, Sätze dieser Sprache zu interpretieren.[1]
Wenn ähnliche Probleme oft genug gelöst werden müssen, ist es häufig sinnvoll, das Problem mit einer einfachen Sprache zu beschreiben. Beispiele für ein solches Problem sind das Auswerten von regulären Ausdrücken und die Berechnung von logischen oder mathematischen Formeln.
UML-Diagramm
Bestandteile
Die abstrakte Klasse AbstrakterAusdruck schreibt eine Methode Interpretiere() vor, die von allen abgeleiteten, konkreten Klassen implementiert werden muss und den entsprechenden Ausdruck auswertet.
Ein TerminalerAusdruck steht für einen Ausdruck, der keinen Unterausdruck hat, d. h. für einen festen Wert innerhalb eines gemäß der Grammatik geformten Satzes. Z. B. steht Zahl für einen Zahlenwert innerhalb einer mathematischen Formel.
Ein NonterminalerAusdruck steht für einen Ausdruck, der aus Unterausdrücken besteht, zum Beispiel Addition oder Multiplikation, die beide zwei Operanden als Unterausdrücke benötigen. Ein Unterausdruck kann sowohl ein TerminalerAusdruck als auch ein NichtterminalAusdruck sein.
Für den zu interpretierenden Satz wird gemäß der Grammatik ein Syntaxbaum aus Nichtterminal- und Terminalausdrücken aufgebaut. Dies kann durch einen externen Parser oder den Klienten selbst geschehen. Der Klient wertet diesen Syntaxbaum aus, indem er für den obersten Ausdruck die Methode Interpretiere() aufruft.
Im Kontext werden die konkreten Werte der Terminalausdrücke gekapselt, mit denen der Satz interpretiert werden soll, z. B. die Belegung von Variablen.
Vorteile
Die Grammatik kann durch dieses Entwurfsmuster leicht geändert oder erweitert, derselbe Satz oder Ausdruck durch Ändern des Kontextes immer wieder auf neue Art und Weise interpretiert werden.
Nachteile
Für komplexe Grammatiken und sehr große Sätze ist das Interpretermuster ungeeignet, da die Klassenhierarchie zu groß wird und die Effizienz bei großen Syntaxbäumen leidet. Sollen komplexe Grammatiken verarbeitet werden, eignet sich Rekursiver Abstieg oder Parsergenerator besser. Große Syntaxbäume werden üblicherweise in andere Strukturen konvertiert und zum Beispiel mit Hilfe von Zustandsautomaten bearbeitet.
Beispiele
Diese C++11 Implementierung basiert auf dem vor C++98 Beispielcode im Buch Entwurfsmuster.
expression ::= plus | minus | variable | number
plus ::= expression expression '+'
minus ::= expression expression '-'
variable ::= 'a' | 'b' | 'c' | ... | 'z'
number ::= '-'? ('0' | '1' | '2' | ... | '9')+
Beispiele für Ausdrücke, die dieser Grammatik entsprechen, sind
1 1 +
a 2 + 3 -
5 4 - a b + +
Der Interpreter sieht für jeden Terminal-Ausdruck und für jeden Nichtterminal-Ausdruck eine konkrete Klasse vor, die eine gemeinsame Schnittstelle implementiert. Diese Schnittstelle schreibt vor, dass die jeweilige Klasse einen zu ihr passenden Ausdruck in einem Kontext interpretieren können muss. Der Kontext ist hier die Belegung der Variablen:
importjava.util.*;/** * Interface for all expression types */interfaceIExpressable{/** * Interprets the passed variables * @param variables * @return Result */intinterpret(finalHashMap<String,Integer>variables);}/** * Class for a non-terminal expression */classPlusimplementsIExpressable{/** Left operation */privateIExpressableleftOperand=null;/** Right operation */privateIExpressablerightOperand=null;/** * Constructor * @param left Left expression * @param right Right expression */publicPlus(finalIExpressableleft,finalIExpressableright){leftOperand=left;rightOperand=right;}/* (non-Javadoc) * @see interpreter.IExpressable#interpret(java.util.HashMap) */@Overridepublicintinterpret(finalHashMap<String,Integer>variables){returnleftOperand.interpret(variables)+rightOperand.interpret(variables);}/** * Converts the content to a readable string by overloading the Object * method. * @return String representation */publicStringtoString(){returnleftOperand.toString()+" + "+rightOperand.toString();}}/** * Class for a non-terminal expression */classMinusimplementsIExpressable{/** Left operation */privateIExpressableleftOperand=null;/** Right operation */privateIExpressablerightOperand=null;/** * Constructor * @param left Left expression * @param right Right expression */publicMinus(finalIExpressableleft,finalIExpressableright){leftOperand=left;rightOperand=right;}/* (non-Javadoc) * @see interpreter.IExpressable#interpret(java.util.HashMap) */@Overridepublicintinterpret(finalHashMap<String,Integer>variables){returnleftOperand.interpret(variables)-rightOperand.interpret(variables);}}/** * Class for a terminal expression */classVariableimplementsIExpressable{/** Variable name */privateStringname=null;/** * Constructor * @param name */publicVariable(finalStringname){this.name=name;}/* (non-Javadoc) * @see interpreter.IExpressable#interpret(java.util.HashMap) */@Overridepublicintinterpret(finalHashMap<String,Integer>variables){returnvariables.get(name);}}/** * Class for a terminal expression */classNumberimplementsIExpressable{/** Number object */privateintnumber=0;/** * Constructor * @param number */publicNumber(finalintnumber){this.number=number;}/* (non-Javadoc) * @see interpreter.IExpressable#interpret(java.util.HashMap) */@Overridepublicintinterpret(finalHashMap<String,Integer>variables){returnnumber;}}
Der Interpreter beschäftigt sich nicht mit dem Parsen des ursprünglichen Ausdrucks und dem Erzeugen des Syntax-Baumes, der dem Ausdruck entspricht.[2] Der Vollständigkeit halber hier die Implementierung eines einfachen Parsers. Er ist unvollständig in dem Sinne, dass er manche nicht-gültige Ausdrücke nicht verwirft! (Aber alle gültigen Ausdrücke parst er korrekt und erzeugt den Syntax-Baum dafür.)
classParser{/** * Parser method * @param expression * @return Parsed result */staticpublicIExpressableparseExpression(finalStringexpression){IExpressablesyntaxTree=null;PatternnumberPattern=Pattern.compile("[+-]?\\d+");Stack<IExpressable>expressionStack=newStack<IExpressable>();for(Stringtoken:expression.split(" ")){if(token.equals("+")){IExpressablesubExpression=newPlus(expressionStack.pop(),expressionStack.pop());expressionStack.push(subExpression);}elseif(token.equals("-")){IExpressablesubExpression=newMinus(expressionStack.pop(),expressionStack.pop());expressionStack.push(subExpression);}elseif(numberPattern.matcher(token).matches()){expressionStack.push(newNumber(Integer.parseInt(token.trim())));}elseexpressionStack.push(newVariable(token));}syntaxTree=expressionStack.pop();returnsyntaxTree;}}/** * Test class */publicclassInterpreterExample{/** * Test method for the interpreter * @param arguments */publicstaticvoidmain(finalString[]arguments){finalStringexpression="w x z - + -2 +";finalHashMap<String,Integer>variables=newHashMap<String,Integer>();variables.put("w",5);variables.put("x",33);variables.put("z",10);finalIExpressabletree=Parser.parseExpression(expression);System.out.println(tree.interpret(variables));}}
Es ist nun recht einfach, die Grammatik zu erweitern und die erweiterten Ausdrücke zu interpretieren. Um eine Quadrier-Funktion sqr einzubauen (einen unären Operator), muss nur eine neue Klasse eingeführt werden:
/** * Class for a non-terminal expression */classSqrFunctionimplementsIExpressable{/** Operand */IExpressableoperand=null;/** * Constructor * @param operand */publicSqrFunction(finalIExpressableoperand){this.operand=operand;}/* (non-Javadoc) * @see interpreter.IExpressable#interpret(java.util.HashMap) */@Overridepublicintinterpret(finalHashMap<String,Integer>variables){inttmp=operand.interpret(variables);returntmp*tmp;}}
Der (unvollständige) Parser kann folgendermaßen erweitert werden, um auch sqr-Ausdrücke zu parsen:
Der Syntaxbaum wird durch ein Kompositum beschrieben.
Ein Visitor kann das Verhalten aller Nichtterminalsymbole in sich kapseln, um die Anzahl der Klassen zu verringern und/oder das Verhalten dieser austauschbar zu gestalten.
Mit Hilfe des Flyweight können Terminalsymbole gemeinsam genutzt werden.
Ein Iterator kann verwendet werden, um den Syntaxbaum zu traversieren.