Мы уже говорили о том, как команда может начать формировать правильное мышление для XP, внедряя его практики.
Во-первых, она могла бы получить результаты «лучше-чем-ничего». Но в ходе использования практик люди начинают понимать, как те взаимодействуют, и представление каждого члена команды о том, как создавать ПО, начинает меняться. Это справедливо не только для Scrum, но и для ХР. И ключ к обладанию ХР лежит в инкрементальной архитектуре.
Набор утилит Unix – классический пример инкрементальной архитектуры, и это то, что позволило тысячам разработчиков добавлять к нему маленькие и большие части на протяжении многих лет. Утилиты Unix состоят из множества маленьких кусочков[69], разработанных различными программистами, намеревавшимися решить свои конкретные проблемы (они не старались создать огромную, монолитную операционную систему). На примере набора утилит Unix мы рассмотрим, как инкрементальная архитектура может помочь множеству людей (большинство из которых никогда не встречались) внести свой вклад в создание большой, устойчивой, высококачественной системы, продолжающей развиваться на протяжении десятилетий.
Вот пример того, как работают утилиты Unix. Допустим, у вас есть масса больших текстовых файлов с множеством данных – может быть, это адреса для списка рассылки, конфигурационные файлы операционной системы или числа, которые должны быть обработаны. Нужно быстро придумать способ их преобразовать (например, вытащить только имена и номера телефонов, изменить определенные разделы конфигурации нужным образом или найти значения данных, соответствующих шаблону). Как бы вы это сделали?
Можно написать программу, которая считывает данные из файла, обрабатывает строки, разделы, последовательности и формирует выходные данные. Но если вы знакомы с Unix-утилитами (такими как cat, ls, cut, awk, sed и т. д.), то знаете, что они созданы для решения подобных проблем. Вы пишете скрипт (или даже просто команду, которую выполняете в командной строке), считывающий данные из файла, выполняющий преобразования и записывающий результаты в другой файл.
Например, у вас есть большая, разделенная запятыми адресная книга в файле
Разве Unix-утилиты создавали люди, желавшие упростить процесс обработки адресных книг? Конечно, нет. Утилиты для Unix основаны на
Эти утилиты – основа того, благодаря чему Unix стала наиболее популярной операционной системой для интернет-сервисов и бизнес-применений. И в течение многих лет общая архитектура системы утилит развивалась среди людей, использующих эти утилиты каждый день, вместе с культурой системного администрирования. Со временем появлялись новые утилиты и возможности их использования. Так, сжатие файлов получило широкое распространение в 1980–1990-х годах. Когда в 1992 году gzip добавилась в набор утилит для Unix, для этого уже существовал простой шаблон. Еще до написания первой строки было ясно, как она будет получать данные и отдавать их (конвейер), выполняться (из командной строки) и даже в каком виде появится документация (страницы man). Утилиты могут легко расширяться, а поскольку все инструменты не связаны друг с другом, возможность сжатия и распаковки файлов легко добавляется, не требуя каких-либо изменений в других частях системы.
Уже более сорока лет расширение набора утилит Unix происходит именно таким способом. Тысячи разработчиков внесли свой вклад в отдельные утилиты или улучшили существующие. Unix-система росла естественным образом. А вместе с ней развивались культура и база знаний. Это позволило людям использовать Unix для выполнения все более сложных задач, что, в свою очередь, помогло найти новые способы ее совершенствования.
Все утилиты Unix придерживаются очень строгого шаблона входных и выходных данных.
Символ «|» в командной строке обозначает конвейер, он передает выходные данные одной утилиты и вход другой. Символ «<» выполняет ввод из файла, а символ «>» – вывод в файл. Все стандартные утилиты Unix создают выходы, работающие с этими конвейерами, – такова часть договора, которого каждая из них придерживается. Договор между утилитами Unix очень простой, что позволяет легко расширить всю систему. Пока каждая дополнительная утилита придерживается того же договора, она будет соответствовать другим утилитам.
Важный инструмент для оказания помощи команде в создании системы, использующей поэтапный подход, – это очень простой договор между ее модулями. Как они взаимодействуют друг с другом, передают ли сообщения? Вызывают функции или методы? Предоставляют ли услуги доступа в сети? Чем проще и последовательней коммуникационный механизм между модулями, тем легче команде добавить новые.
Как вы делаете договор простым? Так же, как и модули: принимаете решения о том, как они взаимодействуют, в последний ответственный момент. И у нас уже есть инструмент, чтобы помочь в этом: разработка через тестирование. Когда программист создает модульные тесты, это помогает убрать сложность и заставляет его использовать этот модуль, прежде чем он написан. Можно применять один и тот же модуль тестирования утилит или платформ, чтобы написать простые интеграционные тесты, проверяющие, как различные модули взаимодействуют друг с другом. И разработчик напишет эти тесты, прежде чем добавит код для взаимодействия. Например, если есть взаимодействие между модулями, где выход одного из них передается в качестве входных данных в другой, выход которого поступает в третий, то разработчик напишет тест, который имитирует, как они «общаются» – то есть
Причина, по которой это помогает сохранить контракт между модулями простым, заключается в том, что легко понять, когда он начинает усложняться. При простом контракте этот вид интеграционного теста очень легко написать. Но если контракт сложный, то интеграционный тест становится для разработчика кошмаром. Сначала ему надо инициализировать множество разных, казалось бы, несвязанных объектов, затем кое-как преобразовать данные из одного формата в другой и вертеться, как уж на сковородке, чтобы обеспечить взаимодействие модулей. И так же, как создание модульных тестов, написание в первую очередь интеграционных тестов поможет избежать этих проблем, прежде чем они будут встроены в программное обеспечение.
Мы используем термин «возникает», когда сложное поведение появляется из простых систем. Возникновение – это распространенное явление: отдельные муравьи ведут себя примитивно, а весь муравейник демонстрирует гораздо более сложное поведение, возникающее в результате взаимодействия между муравьями. Таким образом, муравейник – это больше, чем сумма всех его обитателей.
Когда система разработана так, что ее поведение формируется из
Глубокая иерархия модулей, вызывающих один другой, применяется редко. Вместо этого система будет использовать сообщения, очереди или другие способы коммуникации модулей, не требующие централизованного управления. Утилиты Unix – хороший пример такой организации работы[71]. Входной сигнал передается от одной утилиты к другой, что облегчает их объединение в цепочку. Это приводит к очень простому, последовательному использованию: вызывается первая утилита, потом вторая, затем третья. Можно создать и более сложное взаимодействие между инструментами, но это приводит к простой, неглубокой структуре вызовов (в отличие от глубокой, вложенной иерархии вызовов со слоями модулей).
Если система спроектирована подобным образом (простые модули, взаимодействующие простым способом), то можно получить интересный эффект. Команде начинает казаться, что