Сложный страшный код для создания тестовых данных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Car car = new Car() car.plateNumber = 'IDDQD' // engine Engine engine = new Engine() engine.power = 100 engine.vin = 'ASDAF123' car.engine = engine // model Model model = new Model() model.mark = 'SLR' model.manufacturer = 'Mercedes Benz' model.title = 'C5' car.model = model // ... |
Тут я даже не стал показывать пример заполнения каких-либо списков, это просто превращается в муку. Но я же не поплакать этот пост пишу!
Ищем решение
Я думаю многие писали такое и не испытывали положительных эмоций. Есть конечно другой вариант — написать простой билдер в стиле Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Car car = car() .setPlateNumber("IDDQD") .setEngineParams( 100, "ASDAF123" ) .setRepairs( crashRepair( "3.10.2012", "Fatal crash", "487511577896511" ), scheduledRepair( "5.7.2012", "Scheduled repair", machineComponents( "Engine", "Injector" ) ) ).build(); |
Уже намного лучше! Мы даже смогли без боли в нижней части позвоночника сформировать список Repairs. Но появились специфичные проблемы:
- Потеряли смысл аттрибутов, например что такое «487511577896511» в crashRepair совсем непонятно
- Мы начали использовать громоздкие конструкции setAttributeValue
- Для поддержки такого решения требуется множество статических функций: car(..), crashRepair(..), machineComponents(..), scheduledRepair(..)
Я думаю многим хотелось бы видеть тестовые данные вот в таком виде:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
Car car = Car.build { plateNumber 'TY290DD' year 2011 engine { vin 'IDKFA' power 120 } model { title 'C5' manufacturer 'Mercedes Benz' mark 'SLR' } repairs { scheduledRepair { date '5.7.2012' summary 'Scheduled repair' machineComponents 'Engine', 'Injector' } crashRepair { date '3.10.2012' summary 'Fatal crash' insuranceNumber '487511577896511' } } } |
Вот это то, что мы искали! Прекрасный пример с минимальной избыточностью и прекрасной структурой!
DSL и Groovy
Groovy — прекрасный динамический язык, позволяющий творить чудеса (хотя это часто кажется безумием).
Начнём с основ, с замыканий (Closure). Это объекты, по сути являющиеся функциями, которые имеют контекст выполнения. В них возможно использовать внешние переменные и методы вызывающего кода. Для этого у замыкания есть свойство delegate. Фактически это объект, у которого запрашиваются недостающие функции и переменные. Вот таким нехитрым образом можно заставить замыкание использовать наш объект для вызовов:
1 2 3 4 5 6 7 |
class DslSupport { def static <T> T build(Closure closure, T delegate) { closure.delegate = delegate closure.call() return delegate } } |
И можно использовать например так:
1 |
Car car = DslSupport.build({ plateNumber 'IDDQD' }, new Car()) |
Здесь в замыкании вызывается метод plateNumber у объекта car, в который передаётся строка ‘IDDQD’.
Не супер конечно, но уже есть идеи как сделать конфетку.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Car { String plateNumber = '' Integer year = 0 def plateNumber(String plateNumber) { this.plateNumber = plateNumber } def year(Integer year) { this.year = year } def static build(Closure carDefinition) { DslSupport.build(carDefinition, new Car()) } } |
И теперь мы видим уже вполне симпатичный код:
1 2 3 4 |
Car car = Car.build { plateNumber 'TY290DD' year 2011 } |
Ну и полный пример для ленивых:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
class DslSupport { def static <T> T build(Closure closure, T delegate) { closure.delegate = delegate closure.call() return delegate } } class Engine { String vin = '' Integer power = 0 def power(Integer power) { this.power = power } def vin(String vin) { this.vin = vin } } class Model { protected def manufacturer = '' protected def mark = '' protected def title = '' def manufacturer(String manufacturer) { this.manufacturer = manufacturer } def title(String title) { this.title = title } def mark(String mark) { this.mark = mark } } class Repair { Date date = null String summary = '' def date(String date) { this.date = new java.text.SimpleDateFormat('dd.MM.yyyy').parse(date) } def summary(String summary) { this.summary = summary } } class CrashRepair extends Repair { String insuranceNumber = '' def insuranceNumber(String insuranceNumber) { this.insuranceNumber = insuranceNumber } } class ScheduledRepair extends Repair { List<String> machineComponents = [] def machineComponents(String... machineComponents) { this.machineComponents.addAll(Arrays.asList(machineComponents)) } } class Repairs { List<Repair> repairs = [] def crashRepair(Closure closure) { repairs.add(DslSupport.build(closure, new CrashRepair())) } def scheduledRepair(Closure closure) { repairs.add(DslSupport.build(closure, new ScheduledRepair())) } } class Car { String plateNumber = '' Integer year = 0 Engine engine = null Model model = null Repairs repairs = null def plateNumber(String plateNumber) { this.plateNumber = plateNumber } def engine(Closure engineDefinition) { this.engine = DslSupport.build(engineDefinition, new Engine()) } def year(Integer year) { this.year = year } def model(Closure modelDefinition) { this.model = DslSupport.build(modelDefinition, new Model()) } def repairs(Closure repairsDefinition) { this.repairs = DslSupport.build(repairsDefinition, new Repairs()) } def static build(Closure carDefinition) { DslSupport.build(carDefinition, new Car()) } } |
Думаю в работе это может пригодиться, ведь даже в Java проектах многие леняться использовать Builder для тестов. Хотя многие ленятся писать тесты, ну тут уж я им ничем не помогу.
P.S. ещё в качестве исходных данных можно использовать объекты в сериализованном виде: JSON или XML, в использовании не очень удобно, но иногда этого хватает. Только стоит помнить, что интроспекции IDE ничего не смогут проверить в ваших данных. А если вы ищете статически типизированное решение, то лучше паттерна Builder ничего нет.