Книга: Чистый код: создание, анализ и рефакторинг. Библиотека программиста
Назад: 11. Системы
Дальше: 13. Многопоточность

12. Формирование архитектуры

Джефф Лангр

12_01.tif 

Четыре правила

Разве не хотелось бы вам знать четыре простых правила, выполнение которых помогало бы повысить качество проектирования? Четыре правила, помогающих составить представление о важнейших особенностях структуры и архитектуры кода, упрощающих применение таких принципов, как SRP (принцип единой ответственности) и DIP (принцип обращения зависимостей)? Четыре правила, способствующих формированию хороших архитектур?

Многие полагают, что четыре правила простой архитектуры [XPE] Кента Бека оказывают значительную помощь в проектировании программных продуктов.

Согласно Кенту, архитектура может считаться «простой», если она:

• обеспечивает прохождение всех тестов,

• не содержит дублирующегося кода,

• выражает намерения программиста,

• использует минимальное количество классов и методов.

Правила приведены в порядке их важности.

Правило № 1: выполнение всех тестов

Прежде всего система должна делать то, что задумано ее проектировщиком. Система может быть отлично спланирована «на бумаге», но если не существует простого способа убедиться в том, что она действительно решает свои задачи, то результат выглядит сомнительно.

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

К счастью, стремление к контролируемости системы ведет к архитектуре с компактными узкоспециализированными классами. Все просто: классы, соответствующие принципу SRP, проще тестировать. Чем больше тестов мы напишем, тем дальше продвинемся к простоте тестирования. Таким образом, обеспечение полной контролируемости системы помогает повысить качество проектирования.

Жесткая привязка усложняет написание тестов. Таким образом, чем больше тестов мы пишем, тем интенсивнее используем такие принципы, как DIP, и такие инструменты, как внедрение зависимостей, интерфейсы и абстракции, для минимизации привязок.

Как ни удивительно, выполнение простого и очевидного правила, гласящего, что для системы необходимо написать тесты и постоянно выполнять их, влияет на соответствие системы важнейшим критериям объектно-ориентированного программирования: устранению жестких привязок и повышению связности. Написание тестов улучшает архитектуру системы.

Правила № 2–4: переработка кода

Когда у вас появился полный набор тестов, можно заняться чисткой кода и классов. Для этого код подвергается последовательной переработке (рефакторингу). Мы добавляем несколько строк кода, делаем паузу и анализируем новую архитектуру. Не ухудшилась ли она по сравнению с предыдущим вариантом? Если ухудшилась, то мы чистим код и тестируем его, чтобы убедиться, что в нем ничего не испорчено. Наличие тестов избавляет от опасений, что чистка кода нарушит его работу!

В фазе переработки применяется абсолютно все, что вы знаете о качественном проектировании программных продуктов. В ход идут любые приемы: повышение связности, устранение жестких привязок, разделение ответственности, изоляция системных областей ответственности, сокращение объема функций и классов, выбор более содержательных имен и т.д. Также применяются три критерия простой архитектуры: устранение дубликатов, обеспечение выразительности и минимизация количества классов и методов.

Отсутствие дублирования

Дублирование — главный враг хорошо спроектированной системы. Его последствия — лишняя работа, лишний риск и лишняя избыточная сложность. Дублирование проявляется во многих формах. Конечно, точное совпадение строк кода свидетельствует о дублировании. Похожие строки часто удается «причесать» так, чтобы сходство стало еще более очевидным; это упростит рефакторинг. Кроме того, дублирование может существовать и в других формах — таких, как дублирование реализации. Например, класс коллекции может содержать следующие методы:

int size() {}

boolean isEmpty() {}

Методы могут иметь разные реализации. Допустим, метод isEmpty может использовать логический флаг, а size — счетчик элементов. Однако мы можем устранить дублирование, связав isEmpty с определением size:

boolean isEmpty() {

   return 0 == size();

}

Чтобы создать чистую систему, необходимо сознательно стремиться к устранению дубликатов, пусть даже всего в нескольких строках кода. Для примера рассмотрим следующий код:

  public void scaleToOneDimension(

       float desiredDimension, float imageDimension) {

    if (Math.abs(desiredDimension - imageDimension) < errorThreshold)

       return;

    float scalingFactor = desiredDimension / imageDimension;

    scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);

 

    RenderedOp newImage = ImageUtilities.getScaledImage(

       image, scalingFactor, scalingFactor);

    image.dispose();

    System.gc();

    image = newImage;

}

public synchronized void rotate(int degrees) {

    RenderedOp newImage = ImageUtilities.getRotatedImage(

       image, degrees);

    image.dispose();

    System.gc();

    image = newImage;

}

Чтобы обеспечить чистоту системы, следует устранить незначительное дублирование между методами scaleToOneDimension и rotate:

  public void scaleToOneDimension(

       float desiredDimension, float imageDimension) {

    if (Math.abs(desiredDimension - imageDimension) < errorThreshold)

       return;

    float scalingFactor = desiredDimension / imageDimension;

    scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);

    replaceImage(ImageUtilities.getScaledImage(

       image, scalingFactor, scalingFactor));

  }

 

  public synchronized void rotate(int degrees) {

    replaceImage(ImageUtilities.getRotatedImage(image, degrees));

  }

 

  private void replaceImage(RenderedOp newImage) {

    image.dispose();

    System.gc();

    image = newImage;

  }

В ходе выделения общности конструкций на этом микроскопическом уровне начинают проявляться нарушения принципа SRP. Таким образом, только что сформированный метод можно переместить в другой класс. Это расширяет видимость метода. Другой участник группы может найти возможность дальнейшего абстрагирования нового метода и его использования в другом контексте. Таким образом, принцип «повторного использования даже в мелочах» может привести к значительному сокращению сложности системы. Понимание того, как обеспечить повторное использование в мелочах, абсолютно необходимо для его обеспечения в большом масштабе.

Паттерн ШАБЛОННЫЙ МЕТОД [GOF] относится к числу стандартных приемов устранения высокоуровневого дублирования. Пример:

public class VacationPolicy {

   public void accrueUSDivisionVacation() {

      // Код вычисления продолжительности отпуска

      // по количеству отработанных часов

      // ...

      // Код проверки минимальной продолжительности отпуска

      // по стандартам США

      // ...

      // Код внесения отпуска в платежную ведомость

      // ...

   }

 

   public void accrueEUDivisionVacation() {

      // Код вычисления продолжительности отпуска

      // по количеству отработанных часов

      // ...

      // Код проверки минимальной продолжительности отпуска

      // по европейским стандартам

      // ...

      // Код внесения отпуска в платежную ведомость

      // ...

   }

}

Код accrueUSDivisionVacation и accrueEuropeanDivisionVacation в основном совпадает, если не считать проверки минимальной продолжительности. Этот фрагмент алгоритма изменяется в зависимости от типа работника.

Для устранения этого очевидного дублирования можно воспользоваться паттерном ШАБЛОННЫЙ МЕТОД:

abstract public class VacationPolicy {

   public void accrueVacation() {

      calculateBaseVacationHours();

      alterForLegalMinimums();

      applyToPayroll();

   }

 

   private void calculateBaseVacationHours() { /* ... */ };

   abstract protected void alterForLegalMinimums();

   private void applyToPayroll() { /* ... */ };

}

 

public class USVacationPolicy extends VacationPolicy {

   @Override protected void alterForLegalMinimums() {

      // Логика для США

   }

}

 

public class EUVacationPolicy extends VacationPolicy {

   @Override protected void alterForLegalMinimums() {

      // Логика для Европы

   }

}

Субклассы «заполняют пробел» в обобщенном алгоритме accrueVacation; они предоставляют только ту информацию, которая различается в специализированных версиях алгоритма.

Выразительность

Большинству читателей доводилось работать с запутанным кодом. Многие из них создавали запутанный код сами. Легко написать код, понятный для нас самих, потому что в момент его написания мы глубоко понимаем решаемую проблему. У других программистов, которые будут заниматься сопровождением этого кода, такого понимания не будет.

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

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

Относительно небольшой размер функций и классов также помогает выразить ваши намерения. Компактным классам и функциям проще присваивать имена; они легко пишутся и в них легко разобраться.

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

Хорошо написанные модульные тесты тоже выразительны. Они могут рассматриваться как разновидность документации, построенная на конкретных примерах. Читая код тестов, разработчик должен составить хотя бы общее представление о том, что делает класс.

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

Так что уделите немного внимания качеству исполнения своего продукта. Немного поразмыслите над каждой функцией и классом. Попробуйте улучшить имена, разбейте большие функции на меньшие и вообще проявите заботу о том, что вы создали. Неравнодушие — воистину драгоценный ресурс.

Минимум классов и методов

Даже такие фундаментальные концепции, как устранение дубликатов, выразительность кода и принцип единой ответственности, могут зайти слишком далеко. Стремясь уменьшить объем кода наших классов и методов, мы можем наплодить слишком много крошечных классов и методов. Это правило рекомендует ограничиться небольшим количеством функций и классов.

Многочисленность классов и методов иногда является результатом бессмысленного догматизма. В качестве примера можно привести стандарт кодирования, который требует создания интерфейса для каждого без исключения класса. Или разработчиков, настаивающих, что поля данных и поведение всегда должны быть разделены на классы данных и классы поведения. Избегайте подобных догм, а в своей работе руководствуйтесь более прагматичным подходом.

Наша цель — сделать так, чтобы система была компактной, но при этом одновременно сохранить компактность функций и классов. Однако следует помнить, что из четырех правил простой архитектуры это правило обладает наименьшим приоритетом. Свести к минимуму количество функций и классов важно, однако прохождение тестов, устранение дубликатов и выразительность кода все же важнее.

Заключение

Может ли набор простых правил заменить практический опыт? Нет, конечно. С другой стороны, правила, описанные в этой главе и в книге, представляют собой кристаллизованную форму многих десятилетий практического опыта авторов. Принципы простой архитектуры помогают разработчикам следовать по тому пути, который им пришлось бы самостоятельно прокладывать в течение многих лет.

Литература

[XPE]: Extreme Programming Explained: Embrace Change, Kent Beck, Addison-Wesley, 1999.

[GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al., Addison-Wesley, 1996.

Назад: 11. Системы
Дальше: 13. Многопоточность