Wykonywanie wyrażeń: ExpressionExecutor i wartości stałe

Zgodnie z zapowiedzią, dzisiaj wreszcie zajmę się implementacją klas wykonujących wyrażenia. Będzie trochę kodu i trochę rozterek związanych z wydajnością osiąganą kosztem czystości kodu.

Jak już pisałem, na razie przewiduję trzy typy wyrażeń:

  • wyrażenie reprezentujące stałą wartość (ConstantExpression),
  • wyrażenie reprezentujące wywołanie funkcji (FunctionExpression),
  • wyrażenie reprezentujące zbiór wyrażeń (BatchExpression).

Jako że przewiduję, że zbiór ten będzie się powiększał, dla każdego typu chcę wydzielić osobną klasę obsługi ([typ_wyrażenia]Executor). Aby jednak użytkownik – mając obiekt typu IExpression – nie musiał samodzielnie wybierać, której klasy musi użyć, postanowiłem wprowadzić klasę, która przyjmuje instancję IExpression, rozpoznaje konkretny typ wyrażenia i wywołuje odpowiedni Executor.

ExpressionExecutor

Klasa ta wygląda tak:

class ExpressionExecutor
{
  ... // Konstruktor i zależności (konkretne Executory)

  object Execute(IExpression expression)
  {
    var ce = expression as IConstantExpression;

    if (ce != null)
    {
      return _constantExpressionExecutor.Execute(ce);
    }

    var fe = … // To samo dla IFunctionExpression
    var be = … // To samo dla IBatchExpression

    throw new NotSupportedException();
  }
}

Warty wyjaśnienia jest sposób rzutowania IExpression na konkretny typ wyrażenia. Wygodniej i krócej byłoby napisać:

if (expression is IConstantExpression)
{
  var ce = (IConstantExpression)constantExpression;

  return _constantExpressionExecutor.Execute(ce);
}

To rozwiązanie jest jednak mniej wydajne. Użycie operatora is skutkuje wykonaniem rzutowania, którego wynik przepada. Wymagane jest więc drugie rzutowanie – którego można było uniknąć, używając as.

Skoro już wspominam o wydajności, wypada dopowiedzieć, że rzutowania można było uniknąć całkowicie, stosując wzorzec wizytator. Jego najprostsza implementacja wyglądałaby mniej więcej tak:

interface IExpression
{
  object Accept(ExpressionExecutor visitor);
}

class ExpressionExecutor
{
  ... // Konstruktor i zależności (konkretne Executory)

  object Execute(IExpression expression)
    => expression.Accept(this);

  object Visit(ConstantExpression expression)
    => _constantExpressionExecutor.Execute(expression);

  object Visit(FunctionExpression expression) ...
  object Visit(BatchExpression expression) ...
}

class ConstantExpression : IExpression
{
  object Accept(ExpressionExecutor visitor)
    => visitor.Visit(this);
}

Takie rozwiązanie wygląda dość kusząco, ale póki co wolę zamknąć rozpoznanie typu i wybór konkretnego Executora w jednej klasie (pozostawiając wyrażenia w nieświadomości faktu, że są w ogóle wykonywane). Kiedy już będę bardziej pewien ostatecznego zbioru wyrażeń, może zdecyduję się na ten wzorzec. (Ciekawostka: w .NET obiekty Expression są obsługiwane właśnie przez wizytatory.)

Przejdźmy do klas wykonujących konkretne wyrażenia.

ConstantExpressionExecutor

Na pierwszy ogień niech pójdą wyrażenia reprezentujące stałą wartość (dla przypomnienia: odpowiadająca im klasa to ConstantExpression). Wykonanie takiego wyrażenia jest trywialne – wystarczy zwrócić przechowywaną przez nie rzeczoną wartość stałą:

class ConstantExpressionExecutor
{
  object Execute(IConstantExpression expression)
    => expression.Value;
}

Ale to tylko rozgrzewka przed pozostałymi typami wyrażeń. Z nimi rozprawimy się w kolejnych postach – zapraszam!

2 thoughts on “Wykonywanie wyrażeń: ExpressionExecutor i wartości stałe”

  1. Dzięki :)
    Staram się nie zanudzać szczegółami projektu, a właśnie pokazywać, że podobne rozwiązania są stosowane powszechnie – cieszę się, że się udaje.

Comments are closed.