Domain Specific Language для TDD

Domain Specific Language (DSL) – это язык специального назначения, который предназначен для решения какой-либо задачи в терминах самой задачи.

DSL — это не какой-то новый язык программирования в общем смысле этого слова. Вы создаете этот язык путем определения домена и разрешенных операций над доменными сущностями. Если вы разрабатываете приложения по философии Domain Driven Design, у вас в целом получается DSL  автоматически (но конечно не такой красивый, как если бы вы это делали целенаправленно). Это такой положительный побочный результат. Вы создаете правила и следуете им, так как они делают решение задачи легче. Вместо кучи строк кода, которые могут менять внутреннее состояние объекта, посылать сообщения, проверять значения и условия – вы пишете один оператор, который является значимым для задачи, а весь инфраструктурный код прячется.

DSL  должен быть декларативным и, по возможности, «текучим» (fluent interface – как пример, LINQ). Т.е. целю будет сообщить ЧТО надо делать, без упоминания КАК.

DSL не зависит от конкретного языка. Можно создавать его на любом языке программирования и конечный результат будет в целом одинаков (с поправкой на синтаксис).

Кстати, SQL тоже может рассматриваться как вариант DSL, можно – так как «язык» создан для вполне определенной предметной области.

Собственно, на этом вводную часть статьи можно считать законченной. По теории можно шерстить вики и гугл, мы рассмотрим DSL на примере.

Как DSL может быть применено к TDD

С DSL написание тестов становится реально быстрее и проще! Конечно, для этого придется потратить некоторое время, разрабатывая удобный язык, но зато сколько  сил экономится в будущем! Для примера, возьмем достаточно простой тест из второй части статьи про DDD и TDD. Помните, там был такой вот тестовый метод:

Так вот его можно записать в более короткой форме и более понятно, чтобы он читался как текст, а не как обрывки непонятно чего.

Тест получился короче и, на мой взгляд, читабельнее.  Мы применили fluent  интерфейс, это то место, где много всего через точку написано. Для того чтобы такой код заработал, мы будем использовать методы расширения (Extension Methods). Дополнять доменную модель созданными в тесте методами плохая идея, потому что в результате будет много конструкторов  с уже предопределенными значениями класса, перегруженных методов, которые меняют внутренности класса, а это вообще-то работа доменных сервисов в реальном коде.

Итак, как реализовать новые методы, использованные в тесте:

Создаем новый статичный класс в тестовом проекте и называем его SchoolExtension.

1.  Первый метод, который мы напишем – будет WithClassFor.

Так как мы работаем с методами расширения, то метод WithClassFor  так же будет статичным. Первым параметром передается класс, который мы будем расширять + служебное слово this.

Последним параметром будет Action, т.к. мы используем лямбда выражения. Внутри этого метода будет создан новый экземпляр класса Class и добавлен в класс School. Если требуются дополнительные действия с Class, то они должны быть описаны с помощью лямбда выражения. Получает вот так:

2. Следующий метод расширения создадим для класса Class метода AddStudents, который принимает массив параметров.

Правила составления всё те же, а код простецкий. Массив переводим с помощью LINQ в список, и для каждого элемента осуществляем добавление в переменную cls.Кстати, это тоже пример fluent интерфейса.

Теперь солюшен должен компилироваться без ошибок. Запускаем тесты и видим что они зеленые!

Дополнительные улучшения

Можно еще немного улучшить читабельность тестов, оставив только значимую для теста информацию.

Можно заметить, что лямбда выражение исчезло и осталось только количество школьников в классе. Это достаточно хорошо, но слишком радикально. Я думаю, что недели через две вы уже забудете, что это за цифры, и потребуется жать ctrl+p чтобы вспомнить что там за параметры, либо лезть в объявление метода. Поэтому мы пойдем другим путем.

В такой записи уже точно ничего не забудешь, я гарантирую это! =) Тут четко видно, что и как задается. Нет лишней информации, все только по делу: номер класса и количество студентов в классе. Нам не важно, что там за ученики, важно только их количество.

Для такого использования надо перегрузить метод WithClassFor чтобы он принимал Action. Но делегат Action будет параметризованным классом инициализации.

Правильно мыслите, теперь надо будет создать новый класс ClassInit, который будет помогать в инициализации основного объекта Class. Когда свойства этого класса будут меняться, надо будет каким-то образом менять значения и в экземпляре Class. Согласно этим соображениям, новый класс должен принимать класса Class как параметр конструктора. Следуя этой идее и дальше, а так же исходя из использования класса, могу сказать, что в свойствах хватит реализации только Set’а.

ClassInit может выглядеть и так:

Student объявлен как поле класса для сохранения времени объявления и памяти. Так-так, ClassInit появился, но мы ведь работаем с Action<ClassInit>, а не с реальным классом. Как это все организовать в методе WithClassFor?

Да почти так же как и до этого!

Похоже на магию. =)

Создаем новый  экземпляр Сlass и передаем его параметром в ClassInit. Лямбда выражения не вычисляются до того как результат действительно кто-то не попросит. Так что, когда  мы задаем значения полям в тесте, ничего не происходит. Ничто никуда не присваивается. Все пойдет по своим местам в классе только по команде Invoke. Можете провести аналогии с пошаговым режимом в играх ;) Думаю, теперь понятно, почему мы не создаем переменную для ClassInit, и как заданные значения попадают в реальный экземпляр класса.

Запускаем тест. И он снова зеленый! magic-magic =)

Используя описанный подход вы сможете сделать тесты меньшего размера, но при этом читаться они будут намного легче. Без всего информационного шума, что создают все эти new и тд, можно легко увидеть что действительно важно для прохождения теста.

Исходники.

Hard’n’heavy!

Один комментарий на “Domain Specific Language для TDD

Оставить комментарий