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!
Bardzo pouczający post. Oby więcej takich ciekawostek, jak ta o Expressionach!
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.