Как я уже писал раньше, одно из первых болезненных откровений по прибытию на канадскую контору было то, что я пишу отвратительный и никому не понятный код. С тех пор прошло некоторое время, я прочитал стопку профессиональной литературы, и.. продолжил писать отвратительный и никому не понятный код. В чем засада?
Я пытался разобраться, и вот набрел на какие мысли. В прочитанных книгах, в общем-то, было мало нового. Паттерны, проектирование, принципы вменяемого объектно-ориентированного программирования — SOLID. Большая часть этого мелькала аж в университете и всё равно не сказалась на моём профессионализме.
Мне кажется проблема заключается в том, что начиная с какого-то момента, между правильными и неправильными программистскими решениями пропадает быстрая обратная связь. Другими словами, между «хорошо» и «плохо» нет очевидной разницы.
Вот написал я кусок кода, который умеет кофе заваривать и поглаживать меня по животу. А тут один из SOLID принципов — Single Responsibility (принцип единственной обязанности) — и говорит: неправильно ты, дядя Федор, код пишешь. Этот кусок должен либо кофе пить, либо живот чесать, но ни то и другое.
Ну а что плохого? Ведь в моём контексте это очень удобно. Кофе я пью по утрам, и по утрам же люблю, чтобы мне живот почесывали. И самое поганое, в ближайшем будущем действительно ничего плохого не случится. Через год это будет всё еще кофе, а не водка, и будет всё еще почесывание живота, а не обрезание.
Вот только в кофейную чашку со временем оказалась встроена ложка для помешивания чая, который так любит жена, поэтому теперь при каждом глотке ложка стучит в очки для чтения, которые не снять, потому что падали, и пришлось примотать по-быстрому скотчем. Ну и чесалка теперь путается в майке и тем самым проливает горячий кофе на чресла. Зима настала, приходится одеваться теплее, а под шерстяные майки чесалка не рассчитана.
И между этим абсурдом «сейчас» и решением оставить кофеварку и чесалку вместе «тогда» нет очевидной связи. А слепо следовать принципам и паттернам проектирования тяжело, да и награды никакой.
Вы спросите причём тут test-driven development (TDD)? Всё просто, он дает быструю обратную связь.
TDD строится на трех простых китах. Китятах даже. Если нам нужно написать новый кусок кода, то:
- Напишем тест для будущего куска. Тест, разумеется, упадет.
- Напишем новый код. Минимально, лишь бы тест был успокоился.
- Порефакторим. Код всё равно под тестами, можно спокойно привести его в читаемый вид.
Повторяем эти пункты столько, сколько нужно. Один цикл должен уложиться максимум в пару минут.
Идея писать тест первым выглядит еще более абсурдно, чем кофеварка, почесывающая живот, и это так. Но есть прикол. Тестировать «грязный» код очень тяжело. Что-то из области фантастики даже. Код «под паттернами» тестируется запросто.
Когда мы пишем тест первым, хочешь не хочешь, а приходится как-то лаконично формулировать, чем именно код должен заниматься. Например, варить кофе. Тестировать кофе и живот одновременно — мозг взорвется. Лучше по-отдельности. Вот тебе и вынужденный принцип единственной ответственности.
Более того, нужно как-то планировать интерфейс, через который мы будет тестировать. Сунуть палец тестера в трубку с кипятком, чтобы убедиться, что кофеварка включилась — больно. Проверить, лампочку «кофеварка включилась» — нет. Вот и инкапсуляция подтянулась.
Я пробую TDD дома на учебном проекте уже несколько недель, и это взрывает мозг. В хорошем смысле. Код получается совсем другой. Не просто качественнее или некачественнее, больше или меньше, а другой стиль. И понятнее, да. И естественным образом находятся места для паттернов, без которых получилось бы сложнее. Например, пару дней назад, уже в рабочем проекте, я впервые в жизни сознательно выбрал паттерн Visitor, чтобы разделить логику хранения GPS данных и логику удаления GPS, которые уже устарели. В прошлой жизни очистка жила в самом хранилище и вызывалась по внутреннему таймеру.
Иногда стимул для того, чтобы делать правильную архитектуру, вылазит совсем уж в неожиданных местах. Чтобы потренироваться в TDD дома, решил я написать простенький прокси-сервер. И тут же, еще на пустом проекте, появляется первый вопрос:
— ок, а о чем должен быть первый тест?
Десять потерянных минут жизни и удивительный ответ:
— ну, можно, например, проверить, что сервер есть чем запустить.
Сегодняшняя версия этого теста выглядит вот так:
1 2 3 4 5 |
it("should be able to start", function () { server = new WebServer(port); expect(server.start).toBeA(Function); expect(server.start()).toBe(undefined); }); |
Тест с виду абсолютно бесполезный, но его ценность именно в этих десяти минутах размышлений. За все годы в вэб-девелопменте, ни один из написанных мною серверов не имел метода start 🙂
Через несколько итераций получился сервер, который умеет только запускаться, останавливаться и принимать запросы. Обрабатывают запросы уже совсем другие компоненты. Снова Single Responsibility Principle. По-другому и не получилось бы.
Проектик пережил уже 2 больших рефакторинга, и оба прошли безболезненно. Компоненты сервера изолированы друг от друга, поменять их легко, а куча тестов вокруг сразу скажет, что упало.
В общем, нашелся способ, которая поможет мне расти дальше. Очень рекомендую.
Спасибо, познавательно.