системой быстрых неудач. Если вы хотите получить отказоустойчивую систему и вам важно обнаружить первопричину возможных проблем, то нужно, чтобы вероятный сбой случился как можно раньше (поэтому метод и назвали «быстрые неудачи»). Быстрые неудачи обеспечивают петлю обратной связи, которая позволяет использовать полученные знания в проекте. Эту идею можно применить не только к способу разработки, но и к создаваемому программному продукту.
Неудача – это один из принципов XP, который мы обсуждали в главе 6. Непрерывная интеграция обеспечивает быструю неудачу в проекте в том случае, когда два члена команды одновременно добавляют несовместимый или конфликтующий код. Если члены команды имеют совместимое с ХР мировоззрение, то они будут положительно воспринимать интеграционные неудачи, потому что это помогает выявлять и устранять проблемы на ранних этапах, когда сделать это намного легче.
Рис. 7.9. Когда проблемы проектирования выявлены в самом начале проекта, их легче исправить, и это предотвращает появление запутанных клуджей в дальнейшем. Таков один из способов, которым непрерывная интеграция улучшает общую архитектуру
Разработка через тестирование помогает команде создавать небольшие независимые части, которые легче интегрировать, поэтому непрерывная интеграция – это меньшее бремя для XP-команды. Она постоянно тестирует каждый модуль, чтобы убедиться, что части работают. Когда все члены команды непрерывно интегрируют свой код в общую базу кода, это не дает им углубляться в свою задачу и создавать модули, которые не вполне подходят друг другу.
Допустим, вы трудитесь над проектом программного обеспечения и в процессе работы многое узнаете о проблеме, которую это ПО должно решить. Большинству команд такой сценарий хорошо знаком, и ваша команда должна принимать решения в последний ответственный момент, чтобы обойти эту проблему.
Рис. 7.10. Когда разработчики не привыкли создавать простой несвязанный код, в итоге они получают монолитную архитектуру
Традиционные водопадные команды часто сталкиваются с проблемами, развивающимися по такому сценарию. Предварительное выяснение требований, их рассмотрение большой аудиторией, а затем создание всего кода сразу приводят к тому, что его трудно изменять. У команды нет особых стимулов менять архитектуру решения, потому что такие изменения находятся под жестким контролем. Разработчики никогда не выработают привычки (наподобие постоянного рефакторинга или отслеживания кода «с душком»), заставляющие их создавать легко изменяемый код.
Это почти автоматически ведет к монолитной архитектуре, то есть дизайну, при котором программное обеспечение состоит из больших взаимосвязанных модулей, имеющих много взаимозависимостей и трудноотделимых друг от друга.
В начале книги мы говорили о том, чем ПО отличается от строительных работ. Самое опасное, что вы можете натворить в строительстве, – это взять кувалду и разрушить стены. В программном обеспечении удаление кода не нанесет большого ущерба – его можно восстановить из хранилища контроля версий. Гораздо опаснее, если вы напишете плохой код, а потом создадите другой, который зависит от первого. Добавление зависимостей (иногда называемых «связками», потому что они соединяют вместе два куска кода) делает сложным изменение одной из частей без того, чтобы по крайней мере оценить влияние этого изменения на другую часть.
Монолитная архитектура часто характеризуется сильно связанным кодом, где типичный модуль имеет много связей с другими частями системы. Попытка изменить сильно связанный код становится затруднительной – часто эта связка дает эффект «стрельбы дробью» и другие антипаттерны. Именно такие (обычно недокументированные) связки приводят к ошибкам при переделке.
Связанный код можно сделать несвязанным, разорвав соединения между модулями (а лучше вовсе не добавлять эти связи в начале работы).
Код «с душком» и рефакторинг помогут создать несвязанный код. Вернемся к примеру рефакторинга с кодом улья, где мы извлекли два метода из блока кода C# для перемещения пчел между полем и ульем. Мы увидели, что эти два метода могут повторно использоваться в другой части программы, которая должна перемещать пчел таким же образом. Но что если эти методы – часть более крупного модуля, который нуждается в большой инициализации? Программист с хорошими навыками, попытавшись повторно использовать эти простые методы, почувствует непродуманный код и проведет рефакторинг, чтобы удалить дополнительную инициализацию. Теперь этот код можно применить в двух различных частях программы, и для этого не нужно «знать», как его вызывать, потому что он отделен от тех частей кода, которые к нему обращаются. И если необходимо, чтобы он был вызван из третьей части кода, то он будет отделен и от этой части.
Систему легче поддерживать, если она собрана из небольших независимых модулей, то есть каждая часть кода отделена от любой другой, насколько это возможно (степень этой зависимости может быть удивительной), так что между ними существует очень мало зависимостей. Это главный принцип создания программы, которая может быть изменена без серьезной доработки. Если ваша архитектура не отягощена лишними связями, то вам редко придется прибегать к «стрельбе дробью» или производить целую череду взаимосвязанных изменений. И когда вы начнете так поступать и выработаете в себе привычку не откладывать рефакторинг на завтра, чтобы удалить эти сцепления, код будет становиться все лучше и лучше: каждый модуль станет выполнять только одну задачу и окажется отделен от посторонних модулей.
Инкрементальная архитектура и целостные XP-практики
В главе 6 мы говорили о 10 из 13 основных XP-практик, разделив их на категории: программирование, интеграция, планирование и командные практики. Существуют еще три основные XP-практики, и мы придумали для них отдельную категорию: целостные практики. Мы намеренно отложили их рассмотрение, поскольку о них надо говорить как о комплексе мер и в контексте всех прочих практик. Название «целостные» было выбрано, потому что они тесно взаимосвязаны и не работают одна без другой (в отличие, скажем, от парного программирования или недельных циклов, где команды могут продвигаться маленькими шажками).
Первая целостная ХР-практика, о которой мы будем говорить, – это инкрементальная архитектура. Она самая трудная для понимания. В этой главе мы постараемся помочь вам понять основы этой практики и то, как она влияет на проект и команду в целом.
Вы можете найти замечательные примеры инкрементальной архитектуры во многих зрелых, качественных проектах с открытым исходным кодом, таких как Linux, Apache HTTP Server и Firefox. Эти проекты создаются вокруг стабильного ядра. Разработчики используют архитектуру, основанную на плагинах (или других способах отделения кода от ядра), чтобы создавать дополнительные функции, и только самые повторяющиеся, наиболее стабильные из них встраиваются в ядро. Такая архитектура приводит к весьма слабо связанному коду, и программисты, работающие над этими проектами, взяли за правило часто делать рефакторинг, писать много тестов и выполнять непрерывную интеграцию. (Следует отметить, что многие практики, включенные в XP, возникли или были отточены разработчиками, участвующими в проектах с открытым исходным кодом.)
Каждый из таких проектов создан программистами, добавлявшими отдельные модули, работающие независимо друг от друга и созданные специально для взаимодействия с ядром проекта. Создается впечатление, что исходный код проекта постепенно вырастает из этого ядра. Любой разработчик может добавить свои собственные модули независимо от остальной команды и рассчитывать на то, что все пишут почти полностью разъединенный код, поэтому риск столкновения новых модулей со старыми невелик. Однако работа ведется не в вакууме, и члены команды хорошо осведомлены об этом. Все дополнения происходят в том же исходном коде, где эти команды проводят автоматизированную или ручную непрерывную интеграцию. (Некоторые непрерывные интеграции и серверы сборки, доступные сегодня, возникли в ходе этих проектов.) Кроме того, каждый разработчик старается избежать кода «с душком» и антипаттернов и чувствует большую ответственность за их исправление, если они будут выявлены.
В результате применения этих отличных практик наряду с правильным поведением разработчиков исходный код растет инкрементально. Это пример, как очень большая команда с участниками, находящимися в разных странах, может создать качественное программное обеспечение[64]. Фактически инкрементальная архитектура – это создание проектного решения в последний ответственный момент, что дает возможность избежать одной из самых распространенных ловушек, в которую попадают даже опытные, высококвалифицированные программисты, – попытки создать все и сразу.
Команды, попадающие в такую ловушку, выработали дурные привычки, приводящие к монолитной или замысловатой архитектуре программного обеспечения. Например, они могут сосредоточиться на решении проблемы, которая крупнее текущей задачи или вовсе с ней не связана (фокусируя внимание на крайних случаях, а не на работе каждого конкретного модуля). Или посвящать массу времени обсуждению будущих дополнений и добавлять слишком много хуков. Они могут также создавать большие абстрактные платформы для решения мелких конкретных задач. Что объединяет все эти ситуации? Речь идет о командах, которые принимают чересчур много архитектурных решений и делают это слишком рано.
Однако когда команда выработала привычки, которые ведут к созданию несвязанного кода, состоящего из небольших, надежных, независимых модулей, ей не нужно продумывать архитектуру заранее. Они могут создавать архитектуру высокого уровня, не думая обо всех возможных сценариях, которые могут в нее не вписаться. И уверены, что если обнаружится непредвиденный пограничный случай, то найдутся инструменты, при помощи которых можно будет решить проблему позднее. Это раскрепощает людей и позволяет им свободно обдумывать проблемы, а также дает возможность создавать код по частям, по мере того как накапливается понимание задачи. Такая идея лежит в основе инкрементальной архитектуры, что приводит к очень гибкому, поддающемуся изменениям коду. Но такой подход эффективен, когда