Interpreter Pattern

Interpreter Pattern

Das Interpreter Pattern ist ein Verhaltensmuster, das dazu verwendet wird, eine Sprache oder ein Ausdrucksformat zu interpretieren. Es ermöglicht, Ausdrücke zu analysieren und in eine Aktion umzuwandeln. Dieses Muster wird häufig bei der Implementierung von Parsern und Compilern verwendet. Es ist besonders nützlich, wenn eine Grammatik definiert und dann die Eingaben entsprechend dieser Grammatik interpretiert werden sollen.

Was ist das Interpreter Pattern?

Das Interpreter Pattern definiert eine Möglichkeit, Ausdrücke zu interpretieren, die in einer bestimmten Sprache geschrieben sind. Ein Interpreter ist dafür verantwortlich, diese Ausdrücke zu verarbeiten und in konkrete Aktionen umzuwandeln. Das Muster verwendet eine rekursive Struktur, um komplexe Ausdrücke zu verarbeiten. Jeder Teil des Ausdrucks wird durch ein eigenes Objekt (oder eine Klasse) repräsentiert, das die Bedeutung dieses Teils interpretiert.

Komponenten des Interpreter Patterns

Das Interpreter Pattern besteht aus mehreren wichtigen Komponenten:

  1. AbstractExpression (Abstrakte Ausdrucksklasse): Eine Schnittstelle oder abstrakte Klasse, die eine Methode zur Interpretation eines Kontextes definiert.
  2. TerminalExpression (Terminalausdruck): Eine Klasse, die einen konkreten Wert oder Ausdruck darstellt. Sie kann einen Wert wie eine Zahl oder ein Schlüsselwort repräsentieren.
  3. NonTerminalExpression (Nicht-Terminalausdruck): Eine Klasse, die einen komplexeren Ausdruck repräsentiert, der aus mehreren Ausdrücken zusammengesetzt ist.
  4. Context (Kontext): Ein Objekt, das Informationen enthält, die zum Interpretieren der Ausdrücke benötigt werden.
  5. Client (Benutzer): Der Client erstellt den Kontext und die Ausdrücke, die dann vom Interpreter verarbeitet werden.

Beispiel des Interpreter Patterns in C++

Nehmen wir an, wir wollen eine einfache Sprache erstellen, die mathematische Ausdrücke wie „3 + 5“ oder „10 – 2“ interpretiert. Jeder Ausdruck kann aus Terminals wie Zahlen und Operatoren bestehen. Wir können das Interpreter Pattern verwenden, um diese Ausdrücke zu analysieren und zu berechnen.

#include <iostream>
#include <memory>
#include <string>
#include <map>
#include <sstream>

// Abstrakte Expression: Definiert eine Methode zur Interpretation eines Kontextes
class Expression {
public:
    virtual int interpret(std::map<std::string, int>& context) = 0;
    virtual ~Expression() = default;
};

// Terminal Expression: Repräsentiert eine Zahl
class NumberExpression : public Expression {
private:
    int number;

public:
    NumberExpression(int number) : number(number) {}

    int interpret(std::map<std::string, int>& context) override {
        return number;
    }
};

// Non-terminal Expression: Repräsentiert eine Addition
class AddExpression : public Expression {
private:
    std::shared_ptr<Expression> left;
    std::shared_ptr<Expression> right;

public:
    AddExpression(std::shared_ptr<Expression> left, std::shared_ptr<Expression> right)
        : left(left), right(right) {}

    int interpret(std::map<std::string, int>& context) override {
        return left->interpret(context) + right->interpret(context);
    }
};

// Non-terminal Expression: Repräsentiert eine Subtraktion
class SubtractExpression : public Expression {
private:
    std::shared_ptr<Expression> left;
    std::shared_ptr<Expression> right;

public:
    SubtractExpression(std::shared_ptr<Expression> left, std::shared_ptr<Expression> right)
        : left(left), right(right) {}

    int interpret(std::map<std::string, int>& context) override {
        return left->interpret(context) - right->interpret(context);
    }
};

// Kontext: Hält Variablen und deren Werte
class Context {
private:
    std::map<std::string, int> variables;

public:
    void assign(const std::string& name, int value) {
        variables[name] = value;
    }

    int get(const std::string& name) {
        return variables[name];
    }
};

// Client-Code
int main() {
    // Erstelle einen einfachen Ausdruck: 3 + 5
    std::shared_ptr<Expression> expression = std::make_shared<AddExpression>(
        std::make_shared<NumberExpression>(3),
        std::make_shared<NumberExpression>(5)
    );

    std::map<std::string, int> context;
    std::cout << "Ergebnis: " << expression->interpret(context) << std::endl;

    // Erstelle einen komplexeren Ausdruck: (10 - 2) + 3
    std::shared_ptr<Expression> complexExpression = std::make_shared<AddExpression>(
        std::make_shared<SubtractExpression>(
            std::make_shared<NumberExpression>(10),
            std::make_shared<NumberExpression>(2)
        ),
        std::make_shared<NumberExpression>(3)
    );

    std::cout << "Ergebnis des komplexen Ausdrucks: " << complexExpression->interpret(context) << std::endl;

    return 0;
}

Erklärung des C++-Beispiels

  1. Expression (Abstrakte Expression): Die abstrakte Klasse Expression definiert die Methode interpret(), die von allen konkreten Ausdrucksobjekten implementiert wird. Diese Methode verarbeitet den Ausdruck im Kontext.
  2. TerminalExpression (Terminalausdruck): Die Klasse NumberExpression ist eine konkrete Implementierung eines Terminalausdrucks. Sie stellt eine Zahl dar und gibt sie als Ergebnis zurück.
  3. NonTerminalExpression (Nicht-Terminalausdruck): Die Klassen AddExpression und SubtractExpression repräsentieren komplexere Ausdrücke. Sie bestehen aus zwei Unterausdrücken (links und rechts) und führen eine Addition oder Subtraktion durch.
  4. Context (Kontext): Der Kontext speichert Variablen und deren Werte. In diesem einfachen Beispiel wird der Kontext verwendet, um Variablen zu speichern und später in komplexeren Ausdrücken zu referenzieren.
  5. Client (Benutzer): Der Client erstellt die verschiedenen Ausdrucksobjekte und interpretiert sie, um das Ergebnis zu berechnen. Der Client muss sich nicht um die Details der Berechnungen kümmern.

Vorteile des Interpreter Patterns

  1. Flexibilität: Das Muster ermöglicht es, neue Ausdrücke leicht hinzuzufügen, ohne bestehende Codeänderungen vorzunehmen. Man muss einfach neue Ausdrucksklassen erstellen.
  2. Wiederverwendbarkeit: Einzelne Teile eines Ausdrucks können wiederverwendet werden, da sie in verschiedene Ausdrücke eingebaut werden können.
  3. Erweiterbarkeit: Neue Regeln oder Operationen können einfach eingeführt werden, indem man neue Klassen für nicht-terminalen Ausdruck hinzufügt.

Nachteile des Interpreter Patterns

  1. Komplexität: Bei komplexeren Grammatiken kann die Anzahl der Klassen schnell steigen, was zu einer schwer wartbaren Struktur führen kann.
  2. Leistungseinbußen: Die rekursive Natur des Musters kann bei sehr tief verschachtelten Ausdrücken die Leistung beeinträchtigen.

Fazit

Das Interpreter Pattern ist ein nützliches Muster, um eine Grammatik zu definieren und Ausdrücke in dieser Grammatik zu interpretieren. Es entkoppelt die Interpretation von den Details der Ausdrücke und bietet eine strukturierte Möglichkeit, komplexe Ausdrücke zu verarbeiten. Das Beispiel in C++ verdeutlicht, wie einfach man Ausdrücke erstellen und verarbeiten kann, indem man sie in eine Reihe von Objekten aufteilt.

Zurück zur Liste der Pattern: Liste der Design-Pattern

VG WORT Pixel