Bad code just happens, inevitably
На днях участвовал в семинаре по рефакторингу кода. Был одним из 4-х докладчиков и выступил, на мой взгляд, не очень удачно, хоть и готовился достаточно основательно. Думаю, виной тому тот факт, что теоретическую часть разобрали в выступлениях прямо передо мной, а всё остальное аудитории было не столь интересно.
Так или иначе, решил изложить суть своего выступления письменно, благо это у меня получается обычно куда лучше, а суть всё же какая-то была.
Итак, тема выступления - Рефакторинг.
Все мы знаем, что повторное использование кода - это очень хорошо. Ещё бы, ведь писать дважды одно и то же нам, как правило, лень. К тому же программерское сообщество нас в этом знании полностью поддерживает
"Don't repeat yourself",
"Duplication is evil" - говорят нам книжки и
pragmatic-программеры,
"Once and only once" - говорят нам
XP-программисты. Что ж, я, разумеется, склонен согласиться с устоявшимся мнением. К тому же, я всегда считал, что автоматизация любого явления в этом мире - это хорошо. Никому не интересен рутинный труд, заключающийся в копировании чего бы-то ни было, а тем более программного кода. Поэтому, лучше один раз написать метод, а потом использовать его хоть 100 раз (при этом, разумеется, во всём нужно знать меру). На этом простейшем принципе и базируется основной способ автоматизации при разработке ПО - реюзабилити кода. Таким образом, повторное использование кода - это один из самых полезных аспектов, предоставляемых хорошим дизайном приложения, недаром Алан Перлис назвал его Священным Граалем разработки ПО.
Я подхожу к мысли о том, что рефакторинг позволяет сделать код чище, проще и элегантнее. А простой и незаграмождённый код куда легче расширять, поддерживать и повторно использовать. Выгода рефакторинга неоспорима, если требуется внести какие-либо изменения в программную структуру.
Тем не менее, практики рефакторинга исходно предполагают, что вы имеете дело с несовершенным дизайном. Но ведь и создать идеальный дизайн практически невозможно. В любом, даже самом простейшем проекте, всегда будет код, который можно сделать лучше. Пределов совершенству не существует в принципе. Поэтому программисты, которые взяли в свои руки знамя рефакторинга всегда принимают на веру и его священный закон
"Плохой код просто возникает, это неизбежно". И это действительно так.
Вспомните себя. Как вы пишете код? Большинство из вас допускает типичные ошибки при проектировании приложения. И в этом нет вашей вины. Ведь невозможно с самого начала заложить совершенную концепцию на все случаи жизни. А если требования к конечному продукту не зафиксированны жёстко, то ситуация усложняется во много раз. И это вполне нормально... Все мы допускаем ошибки проектирования. Чаще всего они связаны со следующими действиями:
Мы оптимизируем код преждевременно
Мы стремимся хранить как можно больше промежуточных значений, чтобы не вычислять их повторно
Мы боимся писать очень маленькие методы (ещё бы, ведь стек-то не бесконечный!)
И самое банальное: мы создаём жирные иерархии классов, потому что предполагаем их мнимое повторное использование для каких-то других целей
Рефакторинг наставляет вас на путь истинный. Он заставляет исправлять ошибки, допущенные ранее. При этом практики рефакторинга вовсе не утверждают, что код, который мы собираемся менять, написан с ошибкой. При создании он вполне мог себя оправдывать. А в условиях изменившихся требований, подобный дизайн стал неприемлем для дальнейшей модификации программной архитектуры. И поэтому его необходимо модифицировать таким образом, чтобы он соответствовал ей на текущий момент времени. Как бы пространно это ни звучало, существует специальное определение симптомов, которые могут сигнализировать нам о том, что код вероятно нуждается в рефакторинге. В программерских кругах это определение получило название
"запахов" или
"душков". Существуют специальные практики выявления кода с душком, которые чаще всего применяют для поддержания наиболее приемлемой архитектуры кода. (Подробнее о "запахах" можно узнать из книги Мартина Фаулера "Рефакторинг"). При этом определение проблемных мест в коде вручную - процесс сложный и долгий, требующий больших затрат аналитической составляющей нашего мыслительного центра. Поэтому для большинства современных технологий существуют специальные инструменты, облегчающие поиск "пахнущих" мест в коде. Как пишет Фаулер, "если что-то воняет, его, определённо, нужно тщательно проверить, а при необходимости, заменить".
Я рассмотрел несколько инструментов, облегчающих поиск проблемных мест в коде на ruby. Тщательно я изучил только 3 инструмента, которые ставятся и запускаются одной командой (именно это меня в них и привлекло):
Flog - утилита, позволяющая определить наиболее мучительные для понимания места в коде. Она анализирует ABC-метрики (Assignments, Branches, Calls) и считает количество баллов, которое набирает тот или иной метод. Чем больше баллов, тем труднее для осознания код, тем труднее его будет модифицировать и поддерживать
Flay - инструмент, анализирующий структуру ruby-кода, который на настоящий момент времени просто выявляет дублирование.
Reek - тулза с говорящим названием, позволяет находить в коде несколько основных типов запахов.
Подробнее с этими инструментами можно познакомиться по ссылкам в конце статьи.
Далее мой рассказ касается примера простого рефакторинга, который я сделал, основываясь на показаниях этих инструментов.
Предположим, инструменты рефакторинга просигнализировали нам о проблемных местах в коде и мы выбрали метод с наибольшим количеством баллов:
Из примера в принципе ясно, что метод получает какие-то данные из POST-запроса и возвращает результат их декодирования в зависимости от используемого формата. Наверное, не самый удачный пример, поскольку метод не очень длинный и, по большей части достаточно понятный. Однако, представим ситуацию когда нам понадобилось добавить поддержку ещё одного формата декодирования результата. Здесь, в общем, ленивому программисту, привыкшему автоматизировать свою работу, сразу становится ясно, что метод берёт на себя слишком много ответственности, и вердикт неоспорим -декодирование результата нужно вынести в отдельный метод. Так мы и поступим.
Однако, этим мы ещё не решили проблему добавления нового метода декодирования - по прежнему, для его добавления нужно будет написать "уродливый" код с проверкой формата. Практики рефакторинга советуют заменить подобные проверки полиморфизмом и созданием иерархии полиморфных классов. Однако, поскольку мы работаем с ruby, где возможно всё, и создавать лишние классы нам лень, то лучше просто выделим класс декодирования и отдельные методы декодирования для каждого формата:
В итоге исходный код получился более прозрачным для понимания, а добавить новый формат декодирования стало проще. К тому же, инструменты вычисления "пахнущих" мест скажут нам, что код стал существенно лучше. Все довольны, галактика спасена.
Другой пример, которого я коснулся в своей презентации, направлен на уменьшение дублирования кода.
Вероятно, программируя на ruby on rails все вы сталкивались с ситуацией, когда большая часть ваших контроллеров выполняет одну и ту же функцию, а именно функцию CRUD (Create Update Delete). При этом код создания, изменения и удаления данных редко отличается и содержит какую-то сложную логику. И тут вам, как истинным ленивым программистам, наверняка хотелось как-то скрасить ситуацию. Следующий пример как раз направлен на то, чтобы улучшить запах от бесконечного числа контроллеров, выполняющих функции CRUD.
Теперь представим, что код, выполняющий создание модели выглядит следующим образом:
Сперва выполняется поиск соответствий и присваивания между полями данных, которые были присланы с клиента и колонками таблицы модели. Затем создаётся экземпляр и происходит проверка корректности сохранения. В итоге возвращается результат - либо ошибка, либо сообщение о том, что новая запись была создана.
Итак, мы захотели избавиться от дублирования. Первым шагом станет классическое выделение метода:
В примере выше мы выделили сразу 2 метода (simple_create и set_column_values). В жизни рефакторинг лучше производить мелкими шагами и делать по одному действию за раз, при этом каждый раз выполняя тесты чтобы убедиться, что ничего не сломалось.
В контроллере мы просто создаём метод create, где будем вызывать simple_create и передавать туда класс модели, которую хотим инициализировать. Данное изменение уже спасёт нас от кучи дублирующегося кода, однако, нам всё равно придётся создавать методы create во всех контроллерах. Подобного дублирования так же хочется избежать. Сделать это можно с помощью средств мета-программинга ruby и подмешивания методов в базовый класс контроллера:
Как видно из кода, мы подмешали метод acts_as_simple_crud в класс контроллера. Вызов этого метода подмешает метод create в инстанс. В итоге, всё что нам останется сделать - это написать в контроллере:
Всё в рамках мета-программирования ruby, предельно просто и понятно. То же самое можно произвести с любыми другими задачами, идея которых уже прошла стадию обобщения и кричит нам о том, что дублирование кода превращается в кошмар.
Статья сырая и, разумеется, требует капитального рефакторинга (который я непременно проведу), но надеюсь, что основные мысли я довёл:
существование плохого кода неизбежно, но это не конец света
техники рефакторинга пригодятся любому, кто собирается повторно использовать свой (или чужой) код
при осуществлении рефакторинга необходимо использование специальных инструментов
если вы такой же ленивый программист как я (впрочем, как и большинство нормальных программистов), то рефакторинг - это методология, которая сохранит вам массу времени
рефакторинг - прекрасный способ учиться на ошибках, которые, возможно, когда-то ошибками и не считались
Оригинал презентации можно посмотреть здесь
Использованные источники:
http://www.refactoring.com/
http://www.martinfowler.com
http://www.exciton.cs.rice.edu
http://everydayrails.com/2010/09/27/rails-refactoring-tools.html
http://www.rubyinside.com/3-tools-for-drying-your-ruby-code-1305.html
http://ruby.sadi.st
http://github.com/kevinrutherford/reek
http://www.redhillconsulting.com.au/products/simian/Ярлыки: программинг, рефакторинг, статьи, ruby, ruby on rails