Как вы делаете договор простым? Так же, как и модули: принимаете решения о том, как они взаимодействуют, в последний ответственный момент. И у нас уже есть инструмент, чтобы помочь в этом: разработка через тестирование. Когда программист создает модульные тесты, это помогает убрать сложность и заставляет его использовать этот модуль, прежде чем он написан. Можно применять один и тот же модуль тестирования утилит или платформ, чтобы написать простые интеграционные тесты, проверяющие, как различные модули взаимодействуют друг с другом. И разработчик напишет эти тесты, прежде чем добавит код для взаимодействия. Например, если есть взаимодействие между модулями, где выход одного из них передается в качестве входных данных в другой, выход которого поступает в третий, то разработчик напишет тест, который имитирует, как они «общаются» – то есть
Причина, по которой это помогает сохранить контракт между модулями простым, заключается в том, что легко понять, когда он начинает усложняться. При простом контракте этот вид интеграционного теста очень легко написать. Но если контракт сложный, то интеграционный тест становится для разработчика кошмаром. Сначала ему надо инициализировать множество разных, казалось бы, несвязанных объектов, затем кое-как преобразовать данные из одного формата в другой и вертеться, как уж на сковородке, чтобы обеспечить взаимодействие модулей. И так же, как создание модульных тестов, написание в первую очередь интеграционных тестов поможет избежать этих проблем, прежде чем они будут встроены в программное обеспечение.
Мы используем термин «возникает», когда сложное поведение появляется из простых систем. Возникновение – это распространенное явление: отдельные муравьи ведут себя примитивно, а весь муравейник демонстрирует гораздо более сложное поведение, возникающее в результате взаимодействия между муравьями. Таким образом, муравейник – это больше, чем сумма всех его обитателей.
Когда система разработана так, что ее поведение формируется из
Глубокая иерархия модулей, вызывающих один другой, применяется редко. Вместо этого система будет использовать сообщения, очереди или другие способы коммуникации модулей, не требующие централизованного управления. Утилиты Unix – хороший пример такой организации работы[71]. Входной сигнал передается от одной утилиты к другой, что облегчает их объединение в цепочку. Это приводит к очень простому, последовательному использованию: вызывается первая утилита, потом вторая, затем третья. Можно создать и более сложное взаимодействие между инструментами, но это приводит к простой, неглубокой структуре вызовов (в отличие от глубокой, вложенной иерархии вызовов со слоями модулей).
Если система спроектирована подобным образом (простые модули, взаимодействующие простым способом), то можно получить интересный эффект. Команде начинает казаться, что
Людям, потратившим годы на создание набора утилит Unix, знакомо это чувство возникающего поведения. Можно получить первое
ХР-команды, работающие с полной отдачей, считают естественным применение возникающей архитектуры. Все начинается с простоты: каждый модуль предназначен для конкретного использования. Разработка через тестирование гарантирует, что он остается простым и имеет только одно предназначение. Программист пишет тесты, чтобы убедиться: модуль выполняет одну функцию, – а потом создает код. И если он успешно проходит тесты, то его разработка прекращается. Поэтому никакого дополнительного поведения у модуля не появляется, и в нем нет кода, написанного без основания. Весь код в модуле необходим.