Книги

Постигая Agile

22
18
20
22
24
26
28
30

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

Вы видите довольно много таких случаев. Проектирование для повторного использования слишком статично. Оно не работает таким образом и очень плохо для этого подходит. Его можно пытаться к чему-то применить, но это не имеет особого смысла в обозримом будущем. Повторное использование подходит для разработки гораздо лучше.

Из интервью с Ауке Джилдерда, Beautiful Teams (глава 8)

Мы уже говорили о том, как команда может начать формировать правильное мышление для XP, внедряя его практики.

Во-первых, она могла бы получить результаты «лучше-чем-ничего». Но в ходе использования практик люди начинают понимать, как те взаимодействуют, и представление каждого члена команды о том, как создавать ПО, начинает меняться. Это справедливо не только для Scrum, но и для ХР. И ключ к обладанию ХР лежит в инкрементальной архитектуре.

Набор утилит Unix – классический пример инкрементальной архитектуры, и это то, что позволило тысячам разработчиков добавлять к нему маленькие и большие части на протяжении многих лет. Утилиты Unix состоят из множества маленьких кусочков[69], разработанных различными программистами, намеревавшимися решить свои конкретные проблемы (они не старались создать огромную, монолитную операционную систему). На примере набора утилит Unix мы рассмотрим, как инкрементальная архитектура может помочь множеству людей (большинство из которых никогда не встречались) внести свой вклад в создание большой, устойчивой, высококачественной системы, продолжающей развиваться на протяжении десятилетий.

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

Можно написать программу, которая считывает данные из файла, обрабатывает строки, разделы, последовательности и формирует выходные данные. Но если вы знакомы с Unix-утилитами (такими как cat, ls, cut, awk, sed и т. д.), то знаете, что они созданы для решения подобных проблем. Вы пишете скрипт (или даже просто команду, которую выполняете в командной строке), считывающий данные из файла, выполняющий преобразования и записывающий результаты в другой файл.

Например, у вас есть большая, разделенная запятыми адресная книга в файле addr.txt. В ней восемь столбцов: имя, должность, адрес электронной почты, номер телефона, почтовый адрес, город, штат и почтовый индекс. Вы хотите найти имя, должность и номер телефона адресатов, проживающих в Миннесоте. Если полное имя и должность записаны в первых двух столбцах, номер телефона – в четвертом, а штат и почтовый индекс в последних столбцах, то эта команда в Unix будет производить правильный вывод и записывать его в файл output.txt[70]:

EGREP “,MN, [0–9]{5}([-] [0–9]{4})?$” ADDR.TXT | CUT-D, – FL,2,4 > OUTPUT.TXT

Разве Unix-утилиты создавали люди, желавшие упростить процесс обработки адресных книг? Конечно, нет. Утилиты для Unix основаны на философии простоты: каждая выдает результат, который любая другая утилита может использовать в качестве входных данных, и каждая делает одну конкретную работу. Утилита cut последовательно читает строки ввода, вырезает определенные поля или столбцы и вставляет их в выходные данные. Утилита командной строки grep проверяет каждую строку на входе и передает на выход только те, которые соответствуют заданному шаблону. Но эти две утилиты, особенно в сочетании с другими средствами Unix, могут выполнять практически неограниченное количество задач. Например, системный администратор может комбинировать их с такими утилитами, как find (она ищет файлы в файловой системе) или last (называет пользователей, последними работавших на этом компьютере), чтобы выполнить множество сложных задач системного администрирования.

Эти утилиты – основа того, благодаря чему Unix стала наиболее популярной операционной системой для интернет-сервисов и бизнес-применений. И в течение многих лет общая архитектура системы утилит развивалась среди людей, использующих эти утилиты каждый день, вместе с культурой системного администрирования. Со временем появлялись новые утилиты и возможности их использования. Так, сжатие файлов получило широкое распространение в 1980–1990-х годах. Когда в 1992 году gzip добавилась в набор утилит для Unix, для этого уже существовал простой шаблон. Еще до написания первой строки было ясно, как она будет получать данные и отдавать их (конвейер), выполняться (из командной строки) и даже в каком виде появится документация (страницы man). Утилиты могут легко расширяться, а поскольку все инструменты не связаны друг с другом, возможность сжатия и распаковки файлов легко добавляется, не требуя каких-либо изменений в других частях системы.

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

Когда модули взаимодействуют простым способом, система может расти инкрементально

Все утилиты Unix придерживаются очень строгого шаблона входных и выходных данных.

Символ «|» в командной строке обозначает конвейер, он передает выходные данные одной утилиты и вход другой. Символ «<» выполняет ввод из файла, а символ «>» – вывод в файл. Все стандартные утилиты Unix создают выходы, работающие с этими конвейерами, – такова часть договора, которого каждая из них придерживается. Договор между утилитами Unix очень простой, что позволяет легко расширить всю систему. Пока каждая дополнительная утилита придерживается того же договора, она будет соответствовать другим утилитам.

Важный инструмент для оказания помощи команде в создании системы, использующей поэтапный подход, – это очень простой договор между ее модулями. Как они взаимодействуют друг с другом, передают ли сообщения? Вызывают функции или методы? Предоставляют ли услуги доступа в сети? Чем проще и последовательней коммуникационный механизм между модулями, тем легче команде добавить новые.

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

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

Отличная архитектура возникает из простых взаимодействий

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

Когда система разработана так, что ее поведение формируется из взаимодействия отдельных модулей, работающих вместе, и кажется, что они «вылупились» из одного модуля, это называется возникающей архитектурой. Системы, созданные с использованием возникающей архитектуры, практически всегда состоят из маленьких, независимых, несвязанных модулей (подобных утилитам Unix или муравьям). Эти части объединяются для выполнения сложных задач, и поведение системы зависит как от взаимодействия между этими модулями, так и от них самих.

Глубокая иерархия модулей, вызывающих один другой, применяется редко. Вместо этого система будет использовать сообщения, очереди или другие способы коммуникации модулей, не требующие централизованного управления. Утилиты Unix – хороший пример такой организации работы[71]. Входной сигнал передается от одной утилиты к другой, что облегчает их объединение в цепочку. Это приводит к очень простому, последовательному использованию: вызывается первая утилита, потом вторая, затем третья. Можно создать и более сложное взаимодействие между инструментами, но это приводит к простой, неглубокой структуре вызовов (в отличие от глубокой, вложенной иерархии вызовов со слоями модулей).

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