Автоматизация для людей: Очищаем скрипты сборки от запахов

Практики создания согласованных, повторяемых и легко поддерживаемых сборок

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

Дувол Поль, руководитель технического отдела, Stelligent Incorporated

Поль Дувол (Paul Duvall) является руководителем технического отдела в Stelligent Incorporated. Эта фирма помогает компаниям следить за качеством программных продуктов с помощью эффективных стратегий тестирования и методов непрерывной интеграции, которые позволяют командам раньше и чаще контролировать и улучшать качество кода. Поль также известный автор the Инструментария UML™ 2 и в настоящее время является соавтором книги Непрерывная интеграция: улучшение качества программ и уменьшение риска (Addison-Wesley).



22.01.2007

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

Часто даже великие программисты испытывают трудности при сборке; такое случается, если они недавно узнали как писать процедурный код - написание больших монолитных билд-файлов, копировать-и-вставлять скрипт-код, сложно с помощью кода задавать атрибуты и т.д. Меня всегда удивляло почему так получается (может быть потому что сборка не компилируется в нечто, что потребитель мог бы со временем использовать?) Однако, все мы знаем, что сборка - основной момент в создании кода, который потребитель будет использовать, и если эти скрипты являются одним большим шаром зловония, создание программ быстро превращается в испытание.

К счастью, вы можете с легкостью применять целый ряд практик для сборки (будет это Ant, Maven или даже что-нибудь собственное), которые будут иметь большое значение для того, чтобы сборка была согласованная, повторяемая и легко поддерживаемая. Чтобы быстро научиться создавать лучшие скрипты для сборки, надо посмотреть, как не стоит их создавать, понять, почему не стоит так делать, и затем посмотреть правильный способ создания. В данной статье я применяю именно такой подход. Я объясню следующие 9 запахов сборки, которые вы должны избегать, почему вы должны их избегать и как их устранить:

  • Сборка только в IDE
  • Копировать-и-вставить
  • Длинные объекты
  • Большие файлы сборки
  • Не чистить
  • Сильно закодированные значения
  • Успешная сборка при неуспешных тестах
  • Волшебные машины
  • Недостаток стиля

Об этой серии статей

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

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

Избегаем аромата сборки-только-в-IDE

Сборка-только-в-IDE отличается тем, что она может выполнятся только в IDE разработчика и к сожалению это похоже самый распространенный запах сборки. Проблема сборки-только-в-IDE относится к типу "на моей машине работает", когда программа работает в среде разработчика и не в какой другой. Что еще... из-за того, что сборки-только-в-IDE плохо автоматизируются, они с огромным трудом интегрируются в среду Непрерывной Интеграции (Continuous Integration). По сути в большинстве случаев такие сборки невозможно атоматизировать без вмешательства человека.

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

Рисунок 1. IDE связи в сборке
IDE связи в сборке

Сборка-толькло-в-IDE препятствует автоматизации, и единственный способ избавится от этого зловония - сделать скриптуемую сборку. На эту тему есть достаточно документации и множество книг (см. Ресурсы), а с такими проектами как Maven очень просто определить сборку прямо из рабочей версии. В любом случае, собирайте свою платформу для сборки и сделайте свой проект скриптуемым как можно скорее.


Копировать-и-вставить - как дешевые духи

Дублирование кода является общей проблемой для программных проектов. На самом деле даже популярные проекты с открытым кодом продублированы на 20-30%. И на сколько дублирование кода усложняет его поддержку, на столько же усложняется поддержка скриптов сборки. Представьте, например, что вам надо установить ссылку на определенные файлы с помощью класса fileset в Ant, как показано в листинге 1:

Листинг 1. Скрипт в Ant для операций копировать-и-вставить
<fileset dir="./brewery/src" >
  <include name="**/*.java"/>
  <exclude name="**/*.groovy"/>
</fileset>

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

Класс patternset в Ant, показанный в листинге 2, позволяет мне сослаться на логическое имя, которое представляет нужные мне файлы. Теперь, когда мне надо добавить (или удалить) дополнительные файлы вfileset, я буду делать это только один раз.

Листинг 2. Скрипт в Ant для операций копировать-и-вставить
<patternset id="sources.pattern">
  <include name="**/*.java"/>
  <exclude name="**/*.groovy"/>
</patternset>
...
<fileset dir="./brewery/src">
  <patternset refid="sources.pattern"/>
</fileset>

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


Привкус длинных объектов

В своей книге Refactoring Мартин Фоулер (Martin Fowler) описывает код с Длинными методами с довольно приятным запахом так: "чем длинне процедура, тем сложнее ее понять." Длинные методы, по существу, влекут слишком много обязательств. Когда приходит время сборки, запах сборки Длинных Объектов представляет скрипт, который сложнее понять и поддерживать. В листинге 3 показан относительно длинный объект:

Листинг 3. Длинный объект
  <target name="run-tests">
    <mkdir dir="${classes.dir}"/>
    <javac destdir="${classes.dir}" debug="true">
      <src path="${src.dir}" />
      <classpath refid="project.class.path"/>
    </javac>
    <javac destdir="${classes.dir}" debug="true">
      <src path="${test.unit.dir}"/>
      <classpath refid="test.class.path"/>
    </javac>
    <mkdir dir="${logs.junit.dir}" />
    <junit fork="yes" haltonfailure="true" dir="${basedir}" printsummary="yes">
      <classpath refid="test.class.path" />
      <classpath refid="project.class.path"/>
      <formatter type="plain" usefile="true" />
      <formatter type="xml" usefile="true" />
      <batchtest fork="yes" todir="${logs.junit.dir}">
        <fileset dir="${test.unit.dir}">
          <patternset refid="test.sources.pattern"/>
        </fileset>
      </batchtest>
    </junit>    
    <mkdir dir="${reports.junit.dir}" />
    <junitreport todir="${reports.junit.dir}">
      <fileset dir="${logs.junit.dir}">
        <include name="TEST-*.xml" />
        <include name="TEST-*.txt" />
      </fileset>
      <report format="frames" todir="${reports.junit.dir}" />
    </junitreport>
  </target>

Этот длинный объект (поверьте, я видел намного длиннее) представляет четыре различных процесса: компиляция исходного кода, компиляция тестов, запуск тестов JUnit и создание отчета JUnitReport. Целая куча обязанностей, не говоря уже о добавлении к сложности всего того XML в одном месте. Этот объект можно разбить на четыре - независимых, логических, как показано в листинге 4:

Листинг 4. Извлечение объектов
 <target name="compile-src">
    <mkdir dir="${classes.dir}"/>
    <javac destdir="${classes.dir}" debug="true">
      <src path="${src.dir}" />
      <classpath refid="project.class.path"/>
    </javac>
  </target>
  
  <target name="compile-tests">
    <mkdir dir="${classes.dir}"/>
    <javac destdir="${classes.dir}" debug="true">
      <src path="${test.unit.dir}"/>
      <classpath refid="test.class.path"/>
    </javac>
  </target>

  <target name="run-tests" depends="compile-src,compile-tests">
    <mkdir dir="${logs.junit.dir}" />
    <junit fork="yes" haltonfailure="true" dir="${basedir}" printsummary="yes">
      <classpath refid="test.class.path" />
      <classpath refid="project.class.path"/>
      <formatter type="plain" usefile="true" />
      <formatter type="xml" usefile="true" />
      <batchtest fork="yes" todir="${logs.junit.dir}">
        <fileset dir="${test.unit.dir}">
          <patternset refid="test.sources.pattern"/>
        </fileset>
      </batchtest>
    </junit>    
  </target>

  <target name="run-test-report" depends="compile-src,compile-tests,run-tests">
      <mkdir dir="${reports.junit.dir}" />
	  <junitreport todir="${reports.junit.dir}">
      <fileset dir="${logs.junit.dir}">
        <include name="TEST-*.xml" />
        <include name="TEST-*.txt" />
      </fileset>
      <report format="frames" todir="${reports.junit.dir}" />
    </junitreport>
  </target>

Как вы можете видеть, благодаря тому, что каждый объект имеет одну задачу, код в листинге 4 стал намного проще. Разделяя объекты в зависимости от цели, вы можете упростить и тем самым обеспечить возможность использования объектов в различных компонентах, т.е. повторно использовать, если необходимо.


Большие файлы сборки также имеют сильный запах

Фоулер также определяет Большие Классы как запах кода. В скриптах сборки похожий запах имеют большие файлы сборки, которые дико сложно прочитать. Непросто понять какие объекты что делают и какие между ними связи. Это опять же вносит трудности в поддержку; более того, в огромных файлах сборки часто встречается операция вырезать-и-вставить.

Чтобы уменьшить размер файлов сборки, вы можете найти части скрипта, которые логически соотносятся и выделить эти части в более мелкие файлы сборки, которые будут выполняться одним общим файлом сборки (например, в Ant вы можете вызвать другие файлы сборки с помощью программного модуля ant).

Чаще я разбиваю скрипты сборки с помощью функции ядра и проверяю, что они могут выполняться как автономные (stand-alone) скрипты (подумайте о компонентном представлении для сборки). Например, я хочу определить четыре типа тестов разработки в моей сборки в Ant: элемент, компонент, система и функциональность. Далее я также хочу запустить четыре типа автоматических инспектора: стандарт написания кода, анализ связей, влияние неисправностей и сложность кода. Я разделяю реализацию тестов и инспекторов в два разных файла сборки, вместо того, чтобы поместить исполнение этих тестов и инспекторов в один монолитный скрипт сборки (вместе с компиляцией, интеграцией базы данных и развертки), см. рисунок 2:

Рисунок 2. Разделение файлов сборки
Разделение файлов сборки

Более мелкие и лаконичные файлы сборки намного легче поддерживать и понимать; по сути, этот же шаблон справедлив и для кода. Похоже, у нас есть шаблон, правда?


Если не чистить

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

К счастью, вот оно решение: вы можете с легкостью избавиться от предложений, удалив все созданные директории и файлы с любой предыдущей сборки. Это просто действие сократит предложения и позволит сделать точным статус успеха или неуспеха. Листинг 5 демонстрирует пример чистки среды сборки с помощью delete в Ant для удаления любых файлов и директорий, использованных в предыдущей сборке:

Листинг 5. Сделайте чистку
<target name="clean">
  <delete dir="${logs.dir}" quiet="true" failonerror="false"/>    
  <delete dir="${build.dir}" quiet="true" failonerror="false"/>    
  <delete dir="${reports.dir}" quiet="true" failonerror="false"/>    
  <delete file="cobertura.ser" quiet="true" failonerror="false"/>     
</target>

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


Зловоние от сильной закодированности

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

Для примера в листинге 6 модуль run-simian имеет несколько сильно закодированных путей и значений, а именно директорию _reports:

Листинг 6. Сильно закодированные значения
  <target name="run-simian">
    <taskdef resource="simiantask.properties" 
      classpath="simian.classpath" classpathref="simian.classpath" />
    <delete dir="./_reports" quiet="true" />
    <mkdir dir="./_reports" />
    <simian threshold="2" language="java" 
      ignoreCurlyBraces="true" ignoreIdentifierCase="true" ignoreStrings="true" 
      ignoreStringCase="true" ignoreNumbers="true"  ignoreCharacters="true">
      <fileset dir="${src.dir}"/>
      <formatter type="xml" toFile="./_reports/simian-log.xml" />
    </simian>
    <xslt taskname="simian"
      in="./_reports/simian-log.xml" 
      out="./_reports/Simian-Report.html" 
      style="./_config/simian.xsl" />
  </target>

У меня будут трудности с сильно закодированной директорией _reports, если я решу положить отчеты Simian в другую директорию; к тому же если другие инструменты используют эту директорию где-нибудь в скрипте, кто-то мог вполне написать это название с ошибкой, из-за чего отчеты появятся в разных директориях. Для простоты и удобства я советую определять свойство значения, которое будет указывать на директорию. Тогда по всему скрипту я могу сослаться на это свойство, значит и изменения надо будет сделать лишь в одном месте. Листинг 7 показывает переделанный run-simian:

Листинг 7. Использование свойств
  <target name="run-simian">
    <taskdef resource="simiantask.properties" 
      classpath="simian.classpath" classpathref="simian.classpath" />
    <delete dir="${reports.simian.dir}" quiet="true" />
    <mkdir dir="${reports.simian.dir}" />
    <simian threshold="${simian.threshold}" language="${language.type}" 
      ignoreCurlyBraces="true" ignoreIdentifierCase="true" ignoreStrings="true" 
      ignoreStringCase="true" ignoreNumbers="true"  ignoreCharacters="true">
      <fileset dir="${src.dir}"/>
      <formatter type="xml" toFile="${reports.simian.dir}/${simian.log.file}" />
    </simian>
    <xslt taskname="simian"
      in="${reports.simian.dir}/${simian.log.file}" 
      out="${reports.simian.dir}/${simian.report.file}" 
      style="${config.dir}/${simian.xsl.file}" />
  </target>

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


Успешная сборка, когда тесты дурно пахнут (или неудачно завершаются)

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

В листинге 8 содержится пример с таким запахом. Обратите внимание, что атрибут haltonfailure в задаче junit в Ant установлен на false (это его значение по умолчанию). Это значит, что сборка не завершится с ошибкой, даже если один из тестов JUnit провалится.

Листинг 8. Запах: удачная сборка, хотя тесты провалились
<junit fork="yes" haltonfailure="false" dir="${basedir}" printsummary="yes">
  <classpath refid="test.class.path" />
  <classpath refid="project.class.path"/>
  <formatter type="plain" usefile="true" />
  <formatter type="xml" usefile="true" />
  <batchtest fork="yes" todir="${logs.junit.dir}">
  <fileset dir="${test.unit.dir}">
    <patternset refid="test.sources.pattern"/>
  </fileset>
  </batchtest>
</junit>

Есть пара подходов, чтобы избежать этого запаха. Первый очень простой - установить атрибут haltonfailure значение true. Это убережет сборку от успешного завершения, если какой-нибудь тест провалится.

Единственное, что мне не нравится в этом решении, это что я не могу видеть какой процент тестов не завершились успешно и всю картину целиком. Поэтому второй подход заключается в установке свойства, если какой-либо из тестов провалился. Затем я настраиваю Ant, чтобы он завершал с ошибкой сборку, когда он выполнит все тесты. Оба подхода будут работать. В листинге 9 показан второй способ с использованием свойства tests.failed:

Листинг 9. Тесты завершают сборку с ошибкой
<junit dir="${basedir}" haltonfailure="false" printsummary="yes" 
  errorProperty="tests.failed" failureproperty="tests.failed">
  <classpath>
    <pathelement location="${classes.dir}" />
  </classpath>
  <batchtest fork="yes" todir="${logs.junit.dir}" unless="testcase">
    <fileset dir="${src.dir}">
      <include name="**/*Test*.java" />
   </fileset>
  </batchtest>
  <formatter type="plain" usefile="true" />
  <formatter type="xml" usefile="true" />
</junit>
<fail if="tests.failed" message="Test(s) failed." />

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


Запах волшебной машины

Из всех запахов, покрывающих статью, этот возможно самый вонючий. Волшебные машины - это одни-из волшебных частей аппаратного обеспечения, которые могут осуществлять сборку только на программах данной компании. Этот сценарий не такой искусственный, как могло бы показаться. Я встречался с такими мастерами (wizard)-чудовищами несколько раз за свою карьеру. Такие машины превращаются в демонов, хотя бы когда теряются связи.

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

Рисунок 3. Волшебная машина
Волшебная машина

Сильно закодированные ссылки на машине, пути, в которые входят определенные диски (такие как С:), и определенные средства на машине - все это красные флаги, которые заколдовывают машину. Как только вы увидите ссылку на диск С: или вызов особого инструмента (например, grep), изменяйте скрипт немедленно. Если вы поймали себя на мысли, что "директория C:\Program Files\ есть на любой машине" или что-то вроде того, подумайте еще раз.


Зловоние от плохого стиля

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

  • имена свойств
  • имена объектов
  • имена директорий
  • имена для переменных среды
  • структурированное расположение текста
  • длина линий

Лично я предпочитаю использовать правила других языков на сколько это возможно, когда работаю со стилем. К счастью, группа из нескольких человек создали ресурс Элементы стиля в Ant (см. Ресурсы). В этом документе авторы описывают правила такие как название объектов с использованием нижнего региста и разделительной точкой, длину строк и расположение текста. Какой бы ресурс вы не выбрали, следование правилам стилистики поможет вам сделать легко поддерживаемые файлы сборки на долго.


Никогда еще сборки не пахли так ароматно

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

Ресурсы

Научиться

Получить продукты и технологии

  • Apache Ant: мать платформы Java для сборки.
  • Maven: мощная платформа, использующая лучшее от Ant.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=191373
ArticleTitle=Автоматизация для людей: Очищаем скрипты сборки от запахов
publish-date=01222007