Po rozgrzewce kontynuuję implementację wykonywania wyrażeń w projekcie DSLExecutor. Dzisiaj przyjrzę się wyrażeniom drugiego (z trzech) typu: BatchExpression (reprezentującego zbiór wyrażeń). A zanim przyjrzę się wyrażeniom, muszę najpierw… przyjrzeć się wyrażeniom.
Jak podałem w założeniach: “zbiór wyrażeń zawiera jedno wyrażenie wyróżnione – to, którego wartość zwracana stanie się wartością zwracaną całego zbioru”. Implementację tego założenia pokazałem tutaj. Wykonanie zbioru wyrażeń polega więc na wykonaniu wszystkich wyrażeń pobocznych, a następnie wykonaniu wyrażenia zwracającego i zwrócenia jego wyniku. No właśnie, koncepcja jest prosta: aby wykonać wyrażenie, trzeba wykonać wyrażenie. Gorzej z implementacją, bo mamy tu do czynienia z rekurencją.
Rekurencja w pojedynczej klasie
Rekurencja nie jest problemem, o ile nie jest rozprzestrzeniona na kilka klas. Gdybym całą implementację wykonywania wyrażeń wrzucił do jednej klasy, ExpressionExecutor wyglądałby mniej więcej tak:
class ExpressionExecutor_Fat { object Execute(IExpression expression) { ... // Obsługa ConstantExpression ... // Obsługa FunctionExpression var be = expression as IBatchExpression; if (batchExpression != null) { return ExecuteBatch(be); } } object ExecuteBatch(IBatchExpression be) { foreach (var se in be.SideExpressions) { Execute(se); } return Execute(be.ResultExpression); } object ExecuteConstant … object ExecuteFunction … }
Wygląda czytelnie, działa, i… narusza Single responsibility principle, Open/closed principle, i potencjalnie rośnie w nieskończoność (w miarę dodawania nowych rodzajów wyrażeń).
Rekurencja pomiędzy klasami
Spróbujmy w takim razie – zgodnie z początkowym zamiarem – przenieść wykonanie BatchExpression do osobnej klasy:
class BatchExpressionExecutor { BatchExpressionExecutor(IExpressionExecutor ee) ... object Execute(IBatchExpression be) { foreach (var se in be.SideExpressions) { _expressionExecutor.Execute(se); } return _expressionExecutor.Execute(be.ResultExpression); } }
Tu jednak trafiamy na problem będący konsekwencją rekurencji rozprzestrzenionej na kilka klas: cykliczną zależność pomiędzy ExpressionExecutor i BatchExpressionExecutor. Zobaczmy jak wyglądają konstruktory tych klas:
ExpressionExecutor(IConstantExpressionExecutor cee, IFunctionExpressionExecutor fee, IBatchExpressionExecutor bee) { _constantExpressionExecutor = cee; _functionExpressionExecutor = fee; _batchExpressionExecutor = bee; }
BatchExpressionExecutor(IExpressionExecutor ee) { _expressionExecutor = ee; }
Aby stworzyć instancję ExpressionExecutor, muszę mieć instancję BatchExpressionExecutor. A żeby stworzyć BatchExpressionExecutor, muszę… no właśnie.
Przerwanie bezpośredniej cyklicznej zależności
Aby przerwać to błędne koło, muszę złamać bezpośrednią zależność, wprowadzić coś pomiędzy te dwie klasy. Po chwili zastanowienia postanowiłem użyć leniwej inicjalizacji:
Lazy<IExpressionExecutor> _expressionExecutorFactory; BatchExpressionExecutor(Lazy<IExpressionExecutor> eeFactory) { _expressionExecutorFactory = eeFactory; }
Rozwiązanie działa (pełna implementacja klasy BatchExpressionExecutor: tutaj), ale nie jest pozbawione wad:
- Przede wszystkim, BatchExpressionExecutor wie, że ExpressionExecutor od niego zależy. Łamie to zasadę polegania na abstrakcji.
- Tworzenie instancji ExpressionExecutor jest możliwe, ale pokraczne:
IExpressionExecutor ee = null; var eeFactory = new Lazy<IExpressionExecutor>(() => ee); var bee = new BatchExpressionExecutor(eeFactory); ee = new ExpressionExecutor(bee, /* pozostałe zależności */);
- Na szczęście użytkownik DSLExecutora nie będzie samodzielnie tworzył takich instancji.
- Ciekawostka: niektóre kontenery Dependency Injection (w tym: StructureMap, Autofac, Unity, ale na przykład nie Simple Injector) automatycznie wspierają tworzenie zależności przyjętych za pośrednictwem Lazy (przykład).
- eeFactory.Value w ciele konstruktora BatchExpressionExecutor zwróci null, a poza konstruktorem – nie. Może to powodować dezorientację.
Jak widać, osiągnięty kształt klasy BatchExpressionExecutor jest kompromisem. Zdecydowałem się na niego, ponieważ gdybym wepchnął całą implementację wykonywania wyrażeń do klasy ExpressionExecutor, to:
- W klasie tej znajdowałyby się różne poziomy abstrakcji przetwarzania: wykonywanie i abstrakcyjnego IExpression, i konkretnych BatchExpression (i innych wyrażeń).
- Rozmiar tej klasy zwiększałby się niekontrolowanie w miarę dodawania nowych rodzajów wyrażeń, a co za tym idzie, byłaby ona nieczytelna.
- Pisanie testów jednostkowych dla tej klasy byłoby utrudnione.
To tyle jeśli chodzi o wykonywanie zbiorów wyrażeń. Nie lubię takich kompromisów, więc jeśli masz lepszy pomysł, Czytelniku, koniecznie daj znać. A ja tymczasem zabieram się za ostatni (póki co) z zaimplementowanych typów wyrażeń: wywołania funkcji. Ale o tym następnym razem – zapraszam!