Время от времени на форумах по программированию всплывает вопрос о том, что такое хороший дизайн проекта. В одной из наиболее любимых мной книг Agile Principles, Patterns, and Practices in C# — by Robert C. Martin автор приводит признаки плохого дизайна (если переводить более дословно – “запахи загнивающего проекта”). Эти признаки могут быть прекрасной основой для выведения признаков хорошего дизайна, что мы и сделаем в конце данного поста. Итак, начнём с рассмотрения признаков плохого дизайна.
Признаки плохого дизайна
Диагноз “загнивания” программы устанавливается в случае обнаружения любого из следующих признаков плохого дизайна.
- Закрепощенность: система с трудом поддается изменениям, поскольку любое минимальное изменение вызывает эффект “снежного кома”, затрагивающего другие компоненты системы
- Хрупкость: в результате осуществляемого изменения система ломается в нескольких других местах, нередко даже в тех, которые не имеют прямого отношения к непосредственно изменяемому компоненту
- Монолитность: достаточно трудно разделить систему на компоненты, которые могли бы повторно использоваться в других системах
- Вязкость: сделать что-то правильно намного сложнее, чем выполнить какие-либо некорректные действия или решить задачу с помощью хака
- Неоправданная сложность: проект содержит элементы, которые не нужны на данный момент (и возможно никогда не понадобятся)
- Неоправданное дублирование кода: проект содержит дублирование кода, которое может быть устранено с помощью простого рефакторинга
- Неясность: проект трудно читать и понимать.
Закрепощенность
Закрепощенность проявляется в том случае, когда программа с трудом поддается даже простым изменениям. Дизайн становится закрепощенным, если одно изменение влечет за собой каскад последующих изменений в зависимых модулях. Чем больше модулей подвержено изменениям, тем более закрепощенным считается дизайн проекта.
Большинство разработчиков, так или иначе, сталкиваются с этой проблемой. Их просят сделать простые на первый взгляд изменения. Они внимательно изучают характер будущих изменений, а затем выполняют обоснованную оценку требуемого в этом случае объема работы. Позднее, в процессе реализации изменений, разработчики сталкиваются с непредвиденными последствиями этих изменений. В частности, подвергаются переработке огромные блоки кода, причем в процесс модификации вовлекается намного больше модулей, чем планировалось в результате первоначальной оценки. В конце концов, внедрение изменений занимает намного больше времени, чем планировалось изначально. Если спросить разработчиков о том, почему не оправдались их расчеты, они будут жаловаться на то, что задача оказалась намного сложнее, чем предполагалось изначально.
Хрупкость
Хрупкость – это когда при внесении одного изменения программа “ломается” во многих местах. Зачастую новые проблемы возникают в тех областях, которые казалось бы не связаны с изменяемым компонентом. В процессе исправления этих ошибок возникают новые ошибки, в результате чего команда разработчиков начинает походить на собаку, гоняющуюся за собственным хвостом.
По мере возрастания хрупкости программного модуля вероятность появления непредвиденных проблем приближается к 100%. Несмотря на всю кажущуюся абсурдность подобного утверждения, подобные модули встречаются нередко. Задачи переделки таких модулей могут бесконечно висеть в баг трекере, но никто из разработчиков не хочет за них браться.
Монолитность
Дизайн проекта считается монолитным, если он содержит компоненты, которые могли бы применяться в других системах, однако усилия и степень риска, связанные с выделением этих компонентов из первоначальной системы, слишком велики. К сожалению, такое встречается весьма часто.
Вязкость
Вязкость может проявляться в двух формах: по отношению к ПО и к среде.
В случае необходимости внесения изменений разработчики, как правило, видят несколько вариантов решения задачи. Некоторые из них сохраняют исходный дизайн проекта, а другие — нет (т.е. относятся к разряду хакерских приемов). Если предлагаемые дизайном методы сложнее в применении, чем хакерские приемы, то говорят, что вязкость проекта высока. В этом случае легко допустить ошибку, а правильные действия выполнить не так уж и просто. В идеале дизайн проекта должен быть таким, чтобы внесение изменений, не ухудщающих этот дизайну, осуществлялось легко и понятно.
Симптом вязкости среды наблюдается в случае, если среда разработки характеризуется словами “медленный” и “неэффективный”. Например, если компиляция занимает очень долгое время, у разработчика может возникнуть желание изменять программный код таким образом, чтобы избежать полной рекомпиляции (даже если изменение ухудшает дизайн проекта). Если системе управления исходным кодом требуется несколько часов, чтобы обновить в репозитории всего несколько файлов, то разработчики могут захотеть сделать изменения, требующие как можно меньше обновлений в репозитории, даже если это приведет к ухудшению дизайна проекта.
В обоих случаях вязкий проект представляет собой проект, в котором трудно сохранить качественный дизайн. Мы же хотим создавать такие системы, в которых можно без особых усилий сохранять качественный дизайн проекта и улучшать его.
Неоправданная сложность
Проект имеет неоправданную сложность, если содержит элементы, не использующиеся в настоящий момент времени. Это часто происходит в том случае, когда разработчики предсказывают изменения в требованиях и проводят мероприятия, направленные на то, чтобы справиться с этими потенциальными изменениями в будущем. На первый взгляд кажется, что это неплохо. В конце концов, подготовка к предстоящим изменениям должна сохранить наш код гибким и предотвратить кошмарные изменения, которые могут возникнуть впоследствии.
К сожалению, эффект от таких мероприятий может быть совсем противоположным. При подготовке к большому количеству возможных изменений в требованях и непредвиденных ситуаций, проект начинает “замусориваться” частями кода, которые не используются, и возможно никогда не понадобятся. На практике некоторые из таких подготовительных мероприятий оправдываются и окупаются, но большинство — нет. Между тем проект несет на себе груз неиспользуемых элементов, а программа получается сложной и трудно понимаемой.
Неоправданное дублирование кода
Операции “вырезки” и “вставки” могут быть полезными при редактировании текста, но в то же время они могут быть опасными операциями в случае редактирования кода. Очень часто программные системы выстраиваются на десятках или сотнях повторяющихся элементов кода. Это происходит примерно следующим образом:
Предположим, что Ральфу необходимо написать код, выполняющий некие функции. Он просматривает другие части кода, где, по его мнению, такие функции уже выполнялись и обнаруживает подходящий фрагмент. Он копирует и вставляет этот код в свой модуль и производит необходимые изменения.
Ральфу неизвество, что этот фрагмент кода был изначально позаимствован Тодом из модуля, написанного Лили. Причем Лили было нужно, чтобы ее код выполнял поиск натуральных чисел. Она быстро обнаружила, что код, выполняющий поиск целых чисел, тоже может применяться в этой ситуации. Затем она просто вырезала последний фрагмент кода и включила его в свой модуль, изменив соответствующим образом последний.
Если один и тот же код появляется несколько раз в несколько различных формах, разработчики теряют абстракцию. Поиск всех повторений, а также их устранение с применением соответствующих абстракций может и не включаться в первые пункты списка приоритетов, но следует учитывать то, что в противном случае затрачивается немало усилий и времени для того, чтобы система получалась легкой для понимания и изменения в дальнейшем.
Когда в проекте есть дублирование кода, это может сильно усложнить работу по изменению системы. Ошибки, обнаруженные в одном из блоков повторяющегося кода должны быть исправлены во всех местах, содержащих этот код. Тем не менее, поскольку каждое повторение может в незначительной степени отличаться от всех остальных, то исправления не всегда могут быть одинаковыми, и в такой ситуации легко допустить ошибку.
Неясность
Неясность программного модуля проявляется в сложности его понимания. Код может быть написан либо в четкой и выразительной манере, либо быть непонятным и запутанным. Код, эволюционирующий с течением времени, обычно становится все более неясным. Необходимо постоянно следить за тем, чтобы код оставался “прозрачным” и выразительным, сводя возможную неясность к минимуму.
В начале написания нового модуля его код может казаться разработчикам достаточно “прозрачным” и понятным, т.к. они, погружаяются в проблему с головой, и код может казаться проще и понятнее, чем есть на самом деле. Позднее они могут вернуться к модулю и удивиться, что могли написать нечто настолько ужасное. Чтобы это предотвравить, разработчики должны представлять себя на месте будущих читателей этого кода и приложить усилия для необходимого рефакторинга кода, так чтобы будущие читатели этого кода смогли его понять. Кроме того, желательно, чтобы кто-то еще просмотрел составленный ими код (code review).
Признаки хорошего дизайна
Таким образом, поле того, как мы перечислили признаки плохого дизайна, мы можем инвертировать их и получить признаки хорошего дизайна. Давайте попробуем:
- Гибкость – система легко поддается изменениям
- Прочность – единичное изменение не приводит к появлению неожиданных ошибок в зависимых модулях
- Подвижность – систему легко разделить на компоненты, которые могут быть повторно использованы в других местах
- Гладкость – легко внести изменения, не ухудшая дизайн проекта, не используя хаки
- Простота – система не содержит частей, которые просто усложняют и замусоривают код и скорее всего никогда не понадобятся
- Отсутствие дублирования кода
- Прозрачность – архитектура и код проекта легки для понимания
Литература
Agile Principles, Patterns, and Practices in C# (ссылку на книгу в русском переводе не даю и не рекомендую, т.к. перевод там ужасный).