Книга: Чистая архитектура. Искусство разработки программного обеспечения
Назад: V. Архитектура
Дальше: 16. Независимость

15. Что такое архитектура

Martin_Page_161_Image_0001.tif 

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

Но что такое «архитектура программного обеспечения»? Что делает архитектор программного обеспечения и когда он это делает?

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

Архитектура программной системы — это форма, которая придается системе ее создателями. Эта форма образуется делением системы на компоненты, их организацией и определением способов взаимодействий между ними.

Цель формы — упростить разработку, развертывание и сопровождение программной системы, содержащейся в ней.

Главная стратегия такого упрощения в том, чтобы как можно дольше иметь как можно больше вариантов.

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

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

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

Главное предназначение архитектуры — поддержка жизненного цикла системы. Хорошая архитектура делает систему легкой в освоении, простой в разработке, сопровождении и развертывании. Конечная ее цель — минимизировать затраты на протяжении срока службы системы и максимизировать продуктивность программиста.

Разработка

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

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

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

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

Развертывание

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

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

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

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

Эффективность работы

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

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

Это не означает, что архитектура, нацеленная на эффективную работу системы, нежелательна. Она желательна! Просто уравнение затрат больше ориентируется на разработку, развертывание и сопровождение.

Между тем существует еще одна роль, которую архитектура играет в работе системы: хорошая архитектура способствует высокой эффективности.

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

Сопровождение

Из всех аспектов программной системы сопровождение является самым дорогостоящим. Бесконечный парад новых особенностей и неизбежная череда дефектов и исправлений требуют огромных человеческих трудозатрат.

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

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

Сохранение разнообразия вариантов

Как говорилось в главе 2, всякая программная система имеет две ценности: поведение и структуру. Вторая из них больше, потому что именно она делает программное обеспечение гибким.

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

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

Любую программную систему можно разложить на два основных элемента: политику и детали. Политика воплощает все бизнес-правила и процедуры. Политика — вот истинная ценность системы.

Детали — это все остальное, что позволяет людям, другим системам и программистам взаимодействовать с политикой, никак не влияя на ее поведение. К ним можно отнести устройства ввода/вывода, базы данных, веб-системы, серверы, фреймворки, протоколы обмена данными и т.д.

Цель архитектора — создать такую форму для системы, которая сделает политику самым важным элементом, а детали — не относящимися к политике. Это позволит откладывать и задерживать принятие решений о деталях.

Например:

• Не нужно на ранних этапах разработки выбирать тип базы данных, потому что высокоуровневая политика не должна зависеть от этого выбора. У мудрого архитектора высокоуровневая политика не зависит от того, какая база данных будет выбрана впоследствии: реляционная, распределенная, иерархическая или просто набор файлов.

• Не нужно на ранних этапах разработки выбирать тип веб-сервера, потому что высокоуровневая политика не должна даже знать, что она доставляется через Веб. Если высокоуровневая политика не зависит от HTML, AJAX, JSP, JSF и прочих технологий, применяемых в веб-разработке, вы сможете отложить принятие решения о выборе веб-системы до более поздних этапов. Фактически вы даже не должны принимать решения, что система будет предоставлять свои услуги через Веб.

• Не нужно на ранних этапах разработки приспосабливать интерфейс REST, потому что высокоуровневая политика не должна зависеть от интерфейса с внешним миром. Также не нужно приспосабливаться под архитектуру микрослужб или SOA. Высокоуровневая политика не должна зависеть от подобных деталей.

• Не нужно на ранних этапах разработки приспосабливать механизмы внедрения зависимостей, потому что высокоуровневая политика не должна зависеть от способа разрешения зависимостей.

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

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

Чем дольше варианты будут оставаться открытыми, тем больше экспериментов можно провести, тем больше способов опробовать и тем большим объемом информации вы будете обладать, подойдя к моменту, когда принятие решения уже нельзя отложить.

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

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

Независимость от устройства

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

В те дни допускалась масса ошибок. Конечно, в те дни мы не знали, что это ошибки. Да и не могли знать.

Одна из таких ошибок — образование прямой зависимости кода от устройств ввода/вывода. Если нам требовалось вывести что-то на принтер, мы писали код, использующий инструкции ввода/вывода, который генерировал команды управления принтером. Наш код зависел от устройства.

Например, когда я писал программы для PDP-8, печатавшие что-то на телепринтере, я использовал примерно такой набор машинных инструкций:

PRTCHR, 0

        TSF

        JMP .-1

        TLS

        JMP I PRTCHR

PRTCHR — это подпрограмма, печатающая один символ на телепринтере. Ячейка памяти с нулем в начале подпрограммы использовалась для хранения адреса возврата. (Не спрашивайте, что это такое.) Инструкция TSF пропускает следующую за ней инструкцию, если телепринтер готов напечатать символ. Если телепринтер занят, TSF просто передавала управление следующей за ней инструкции JMP .-1, выполняющей переход обратно на инструкцию TSF. Если телепринтер готов, TSF передавала управление инструкции TLS, которая посылала символ в регистр A телепринтера. Затем инструкция JMP I PRTCHR возвращала управление вызывающему коду.

Первое время этот прием работал безупречно. Если нам нужно было прочитать данные с перфокарт, мы использовали код, напрямую обращающийся к устройству ввода с перфокарт. Если нужно было набить перфокарты, мы писали код, напрямую обращающийся к перфоратору. Такие программы работали замечательно. Могли ли мы знать, что это была ошибка?

Но с большими колодами перфокарт было трудно управляться. Карты могли теряться, мяться, рваться, перемешиваться или проскакивать мимо. Могли теряться отдельные карты или добавляться лишние. То есть целостность данных была большой проблемой.

Магнитные ленты решили эту проблему. Мы получили возможность переносить образы карт на ленту. После сброса данных на ленту записи уже не могли перемешаться. Они не могли потеряться по неосторожности, а также не могли добавиться новые пустые записи. Лента оказалась гораздо надежнее. А кроме того, операции чтения/записи с лентой выполнялись намного быстрее, и намного проще стало делать резервные копии.

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

К концу 1960-х годов мы выучили урок и придумали, как обрести независимость от устройств. Операционные системы, современные в те дни, абстрагировали устройства ввода/вывода за фасадами программных функций, которые обрабатывали отдельные записи, выглядящие как перфокарты. Программы должны были обращаться к службам операционной системы, представляющим абстрактные устройства хранения единичных записей. Операторы могли сообщить операционной системе, к каким физическим устройствам должны подключаться эти абстрактные службы — устройствам чтения перфокарт, накопителям на магнитной ленте или любым другим устройствам хранения записей.

Теперь одна и та же программа могла читать и записывать данные на перфокарты или магнитные ленты без всяких изменений. Так родился принцип открытости/закрытости (Open-Closed Principle; OCP), но в те дни он еще не имел названия.

Нежелательная почта

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

Примерно такого содержания:

Здравствуйте, мистер Мартин,

Поздравляем!

Мы выбрали ВАС из всех живущих на Вичвуд-лейн, чтобы сделать фантастическое предложение…

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

Такие рулоны весили по 500 фунтов и содержали тысячи писем. Клиенты могли присылать сотни рулонов, и каждый приходилось печатать отдельно.

Сначала у нас была машина IBM 360, печатающая на единственном последовательном принтере. За смену мы могли напечатать несколько тысяч писем. К сожалению, печать таких писем стоила очень дорого. В те дни аренда IBM 360 обходилась в десятки тысяч долларов в месяц.

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

360-я могла заполнить ленту за десять минут или что-то около того — вполне достаточно, чтобы напечатать несколько рулонов писем. Эти ленты затем выносились из машинного зала и монтировались в приводы, подключенные к автономным принтерам. У нас их было пять штук, и эти пять принтеров работали круглые сутки без перерывов и выходных, печатая сотни тысяч рекламных писем каждую неделю.

Ценность независимости от устройства оказалась огромной! Мы могли писать программы, не зная и не заботясь о том, какие устройства будут использоваться, и отлаживать их, используя локальный принтер, подключенный к компьютеру. После этого мы могли просто приказать операционной системе выполнить «печать» на магнитную ленту и получить сотни тысяч готовых форм.

Наши программы обрели форму. Эта форма отделила политику от деталей. Политика заключалась в форматировании записей с именами и адресами. А деталями стали устройства. Мы смогли отложить принятие решения о выборе устройства.

Физическая адресация

В начале 1970-х годов я работал над большой системой учета для местного профсоюза водителей грузовиков. У нас имелся диск емкостью 25 Мбайт, на котором хранились записи с информацией об агентах, работодателях и членах профсоюза. Разные записи имели разные размеры, поэтому первые несколько цилиндров на диске форматировались так, чтобы размер одного сектора в точности соответствовал размеру записи с информацией об агенте. Следующие несколько цилиндров форматировались под записи с информацией о работодателях. И последние несколько цилиндров — под записи с информацией о членах профсоюза.

Мы писали программы, детально зная структуру диска. Мы знали, что на диске имеется 200 цилиндров и 10 головок, что каждый цилиндр имеет несколько десятков секторов на головку. Мы знали, какие цилиндры отводятся для хранения записей того или иного вида. И все это жестко «зашивалось» в код.

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

Представляете, во что выливался переход на использование нового дискового устройства, в котором на одну головку или на один цилиндр больше? Мы писали специальную программу, которая читала данные в старом формате со старого диска и записывала их на новый диск, преобразуя номера цилиндров/головок/секторов. Нам также приходилось изменять все жестко зашитые номера в коде, которые были повсюду! Все бизнес-правила скрупулезно учитывали схему цилиндр/головка/сектор.

Однажды в наши ряды влился более опытный программист. Увидев наше «творчество», он побледнел и уставился на нас как на инопланетян. Затем осторожно посоветовал нам изменить схему адресации и использовать относительные адреса.

Наш более умудренный опытом коллега предложил рассматривать диск как большой линейный массив секторов с последовательными номерами. Такая интерпретация позволяла написать крошечную подпрограмму преобразования, которая, зная физическую структуру диска, могла бы «на лету» транслировать относительные адреса в номера физических цилиндров/головок/секторов.

К счастью для нас, мы приняли его совет. Мы изменили высокоуровневую политику системы, добавив независимость от физической структуры диска. Это позволило нам отделить решение об организации записей на диске от прикладной задачи.

Заключение

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

SOA (Service-Oriented Architecture): сервис-ориентированная архитектура (). — Примеч. пер.

Назад: V. Архитектура
Дальше: 16. Независимость