IT Notes

Принцип DRY в действии

Введение

С принципом Don't Repeat Yourself (не повторяйся) я познакомился еще довольно давно. О нем в своих книгах уже писали все, кому не лень. Он звучит достаточно логично, и легко можно понять, когда нужно задуматься о его применении:

  • Если вы пишите или копируете один и тот же фрагмент кода много раз, то это верный признак, что вы повторяетесь. Этот фрагмент кода необходимо превратить в функцию или класс, а еще лучше задуматься о рефакторинге и при возможности максимально изолировать соответствующую логику программы;
  • Если вам периодически приходится делать какие-то однотипные системные действия. То создайте скрипт, который сделает это за вас, а затем настройте его запуск по расписанию.

Это достаточно очевидные случаи, которые встречаются регулярно. Но несколько иначе на принцип DRY меня заставила посмотреть книга Программист-прагматик. Путь от подмастерья к мастеру, которую я могу вам рекомендовать. В ней вы много раз услышите об этом принципе. Авторы доводят его практически до абсолюта. Об этом мы и поговорим.

Суть принципа DRY

Типичным примером повтора в большинстве случаев оказывается создание зависящих друг друга частей. Например, код на C++ принято разделять на h-файлы и cpp-файлы. Такой подход имеет свои преимущества, однако это означает, что если меняется заголовочный файл, то вы должны менять и соответствующий файл реализации.

То есть при более глубоком рассмотрении принцип DRY сводится к тому, что вы должны использовать минимальное число зависимостей. Но, очевидно, что совсем без зависимостей обойтись не удастся. Как же быть? Правильное решение заключается в организации централизованного доступа к этим зависимостям. Например, если вы в своем коде выводите отладочные сообщения с помощью std::cout, то разумнее всего создать подобную функцию:

void log( const char* const msg ) {
    std::cout << msg << std::endl;
}

// Теперь вместо этого:
// std::cout << "Hello, world!" << std::endl;
// Вот это:
log( "Hello, world!" );

Но какой в этом смысл? Какая будет разница, если мы просто поменяли одну строку на другую. Но разница есть. Она заключается в том, что когда-нибудь требования на формат выводимых сообщений может измениться. И если мы заранее вынесем эту зависимость в отдельную функцию, то эти изменения скажутся лишь на ней. Например, мы можем решить, что было бы здорово, если сообщения в нашем логе будут начинаться с >>>. Без проблем:

void log( const char* const msg ) {
    std::cout << ">>> " << msg << std::endl;
}

Понятно, что это лишь простейший вариант изменения. Мы могли бы решить записывать сообщения в файл или отправлять их по сети. Все, что угодно, но это никак не скажется на использующем эту функцию коде. По той же причине имеет смысл применять константы, а не "магические числа". А, например, в C++ очень много пользы может принести typedef. Более того, любое дублирование крайне уязвимо для ошибок. Если для изменения одного пункта логики программы вам требуется поработать с двумя и более разными фрагментами кода, то когда-нибудь вы забудете это сделать. И хорошо, если компилятор вам этого не простит, иначе на отладку может уйти очень много времени. Кстати, по поводу времени. Может возникнуть иллюзия, что сейчас я по-быстрому скопирую код тут и там, и все заработает. Возможно, так оно и будет на этот раз. И если сроки поджимают, то это тоже выход. Но при первой же возможности постарайтесь закрыть оставленную брешь. Иначе в нее будет утекать ваше время. И чем дольше вы будете делать вид, что не замечаете этого, тем шире станет пробоина. В конце концов ваше приложение просто "утонет".

Впрочем, мы опять вернулись к основам. Это только базовые принципы написания качественного кода, который будет удобно сопровождать. Однако мы можем выйти за рамки этого и перейти на следующую ступень.

Шаг вперед: автоматизация и генераторы кода

Рассмотрим пример. Предположим, что мы создаем клиент-серверное приложение (кстати, такое мы и правда уже делали на примере простого чата). Как и в случае с h-файлом и cpp-файлом, зависимость очевидна. Протокол работы такой системы должен быть однозначно согласован. Если меняется сервер, то клиент обязан под него подстраиваться, чтобы обеспечить взаимодействие. Явное нарушение принципа DRY.

Но какое тут может быть решение? Клиент и сервер все равно являются разными приложениями и работать они будут по разному. Они могут быть даже реализованы на разных языках программирования. Действительно, полностью устранить зависимость невозможно. Однако мы можем ее минимизировать хотя бы поверхностно. Причем, решение здесь такое же, как и в случае с функцией log() выше. Требуется изолировать общую часть, то есть протокол. Достичь этого можно разными путями. Например, для веб-сервисов используется формат wsdl с описание запросов и типов входных и выходных параметров. А уже из этого wsdl-файла с помощью специальных утилит мы можем сгенерировать каркасы приложений клиента и сервера, которые останется лишь заполнить реализацией. В случае серьезных изменений протокола нам все равно придется вносить некие изменения в реализации, но генератор ускорит этот процесс. А если изменения не так существенны, то и наше вмешательство практически не понадобится. Еще лучше включить процесс генерации каркасов из wsdl в процесс сборки. Тогда вы сможете гарантировать, что протокол клиента и сервера всегда находятся в актуальном состоянии.

Другой пример возможного нарушения принципа DRY связан с использованием баз данных. В этой ситуации мы тоже имеем дело со связью типа клиент-сервер. Но теперь появляется сразу несколько зависимостей между:

  1. Схемой таблиц, используемых в базе данных;
  2. SQL-кодом для организации запросов к этим таблицам;
  3. Классами и функциями на языке программирования.

Верный способ создания приложения, которое потом будет невозможно сопровождать, заключается в том, чтобы вынести SQL-запросы в высокоуровневые классы. Это никуда не годится. Первое, с чего мы должны начать, - это изоляция каждого элемента связи на своем уровне абстракции. Для каждой таблицы должна быть определена своя структура в языке программирования с соответствующими полями. А на каждую операцию добавления, чтения, обновления или удаления должен быть предусмотрен соответствующий метод или функция. Но лишь это не гарантирует нам выполнение принципа DRY. Если в таблицу БД добавляются новые столбцы, то нам придется самим следить за полями структур, которые должны им соответствовать. Вот мы уже и нарушили принцип.

Решение задачи c БД может быть построено на применении генераторов, как и в случае с wsdl, описанном выше. Например, мы можем взять за основу sql-файл и на его основе компоновать структуры. Или наоборот, отталкиваться от самих структур и по их описанию автоматически компоновать схему БД. А еще можно взять совершенно абстрактный файл с описанием и генерировать из него и sql, и структуры. У каждого из этих способов есть свои преимущества и недостатки, поэтому выбор нужно осуществлять по ситуации.

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

Заключение

Вот мы и рассмотрели одну из тем, посвященных принципам разработки программного обеспечения. На основе представленного материала можно сделать вывод, что принцип DRY является одним из базовых. На его основе держатся многие интуитивно-понятные правила и рекомендации написания качественного кода. Но и это еще не все. Соблюдение принципа DRY всегда и во всем позволит значительно уменьшить ошибки и сэкономить время на решение более важных задач, которые вас еще ждут.

Похожие публикации