Чистая архитектура. Искусство разработки программного обеспечения Мартин Роберт

        maybeConnectCavern(cavern, NORTH);

        maybeConnectCavern(cavern, SOUTH);

        maybeConnectCavern(cavern, EAST);

        maybeConnectCavern(cavern, WEST);

      }

    String playerCavern = anyCavern();

    game.setPlayerCavern(playerCavern);

    game.setWumpusCavern(anyOther(playerCavern));

    game.addBatCavern(anyOther(playerCavern));

    game.addBatCavern(anyOther(playerCavern));

    game.addBatCavern(anyOther(playerCavern));

    game.addPitCavern(anyOther(playerCavern));

    game.addPitCavern(anyOther(playerCavern));

    game.addPitCavern(anyOther(playerCavern));

    game.setQuiver(5);

  }

  // здесь следует еще много кода...

}

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

Заключение

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

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

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

Глава 27. Службы: большие и малые

Сервис-ориентированные «архитектуры» и микросервисные «архитектуры» приобрели особую популярность в последнее время. Эта популярность обусловлена, в том числе, следующими причинами:

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

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

Сервисная архитектура?

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

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

Наглядной аналогией является организация функций. Архитектура монолитной или компонентной системы определяется некоторыми вызовами функций, пересекающими архитектурные границы и следующими правилу зависимостей. Однако многие другие функции в системе просто отделяют одно поведение от другого и не являются архитектурно значимыми.

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

Преимущества служб?

Знак вопроса в заголовке указывает, что в этом разделе мы поставим под сомнение популярное мнение о сервис-ориентированной архитектуре. Давайте рассмотрим предполагаемые преимущества по одному.

Заблуждение о независимости

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

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

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

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

Заблуждение о возможности независимой разработки и развертывания

Другое предполагаемое преимущество — возможность передачи служб во владение отдельным командам. Каждая такая команда может отвечать за разработку, сопровождение и эксплуатацию службы, как это предполагает стратегия интеграции разработки и эксплуатации (DevOps). Такая независимость разработки и развертывания, как предполагается, является масштабируемой. Считается, что крупные корпоративные системы могут состоять из десятков, сотен и даже тысяч служб, разрабатываемых и развертываемых независимо. Разработку, сопровождение и эксплуатацию системы можно разделить между аналогичным количеством независимых команд.

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

Во-вторых, как было показано в разделе «Заблуждение о независимости», службы не всегда могут разрабатываться, развертываться и эксплуатироваться независимо. Их разработку приходится координировать в той же мере, в какой они связаны данными или поведением.

Проблема с животными

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

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

Схема на рис. 27.1 демонстрирует, как наши предполагаемые архитекторы распределили ответственность приложения между микрослужбами. Служба TaxiUI взаимодействует непосредственно с клиентом, заказывающим такси с помощью своего мобильного устройства. Служба TaxiFinder исследует параметры разных поставщиков услуг TaxiSupplier и выбирает возможных

Рис. 27.1. Организация системы-агрегатора услуг такси в виде комплекса служб

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

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

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

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

Один из поставщиков услуг согласился участвовать в этой программе. Другие, скорее всего, последуют его примеру, а третьи могут отклонить предложение.

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

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

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

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

Спасение в объектах

Как бы мы решили эту проблему в архитектуре, состоящей из компонентов? Неуклонное следование принципам проектирования SOLID вынудило бы нас создать набор классов, которые можно было бы полиморфно расширять под потребности новых возможностей.

Схема на рис. 27.2 демонстрирует эту стратегию. Классы на этой схеме примерно соответствуют службам на рис. 27.1. Но обратите внимание на границы. Отметьте также, что все зависимости оформлены в соответствии с правилом зависимостей.

Рис. 27.2. Использование объектно-ориентированного подхода для преодоления проблемы сквозных задач

Большая часть логики оригинальных служб сосредоточена в базовых классах объектной модели. Однако часть логики, связанная с поездками, выделена в компонент Rides. Реализация новой услуги по доставке котят помещена в отдельный компонент Kittens. Эти два компонента переопределяют абстрактные базовые классы из оригинальных компонентов с применением шаблонов, таких как «Шаблонный метод» или «Стратегия».

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

Ясно, что в этой схеме после реализации Kittens придется изменить TaxiUI. Но все остальное останется в прежнем виде. В систему добавится лишь новый файл jar, Gem или DLL, который будет динамически загружаться во время выполнения системы.

То есть компонент Kittens существует отдельно от других и может разрабатываться и развертываться независимо.

Службы на основе компонентов

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

Представьте службу на Java как набор абстрактных классов в одном или нескольких jar-файлах. Каждую новую возможность или расширение существующей возможности можно реализовать как отдельный jar-файл, содержащий классы, наследующие абстрактные классы из уже имеющихся jar-файлов. Для развертывания новой возможности в этом случае больше не потребуется повторно развертывать службы, достаточно лишь добавить новые jar-файлы в пути загрузки этих служб. Иными словами, добавление новой возможности выполняется в соответствии с принципом открытости/закрытости (Open-Closed Principle; OCP).

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

Рис. 27.3. Каждая служба состоит из компонентов, что позволяет добавлять новые возможности, реализованные в виде новых производных классов

Сквозные задачи

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

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

Рис. 27.4. Службы должны иметь компонентные архитектуры,  следующие правилу зависимостей

Заключение

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

Служба может быть единственным компонентом, полностью окруженным архитектурной границей. Но точно так же служба может состоять из нескольких компонентов, разделенных архитектурными границами. В редких[54] случаях клиенты и службы могут быть настолько связаны, что не могут иметь архитектурной значимости.

Глава 28. Границы тестов

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

Тесты как компоненты системы

С тестами связано много неясностей. Являются ли они частью системы? Должны ли они отделяться от системы? Какие виды тестов бывают? Являются ли модульные и интеграционные тесты разными тестами? Какое место во всем этом занимают приемочные тесты, функциональные тесты, тесты для фреймворка Cucumber, тесты TDD, тесты BDD, тесты для компонентов и т.д.?

Обсуждение данной конкретной темы не является целью этой книги, и, к счастью, этого не требуется. С архитектурной точки зрения все тесты одинаковы. Будь то маленькие тесты TDD, или большие тесты FitNesse, Cucumber, SpecFlow, или JBehave — все они архитектурно эквивалентны.

Тесты по самой своей природе следуют правилу зависимостей; они очень детальны и конкретны; и они всегда зависят от тестируемого кода. Фактически тесты можно считать самым внешним кругом архитектуры. Ничто в системе не зависит от тестов, но тесты всегда зависят от внутренних компонентов системы.

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

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

Проектирование для простоты тестирования

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

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

Эта ситуация может приобретать особую остроту. Изменения в общих системных компонентах могут нарушить работу сотен и даже тысяч тестов. Эта ситуация известна как «проблема хрупких тестов».

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

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

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

Программный интерфейс для тестирования

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

Цель API тестирования — отделить тесты от приложения. Под отделением подразумевается не только отделение тестов от пользовательского интерфейса: цель — отделить структуру тестов от структуры приложения.

Структурная зависимость

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

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

Роль API тестирования — скрыть структуру приложения от тестов. Это позволит развивать прикладной код, не влияя на тесты. Это также позволит развивать тесты, не влияя на прикладной код.

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

Безопасность

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

Заключение

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

Глава 29. Чистая встраиваемая архитектура

Автор: Джеймс Греннинг (James Grenning)

Некоторое время тому назад я прочитал статью The Growing Importance of Sustaining Software for the DoD[55] («Растущее значение устойчивого программного обеспечения для министерства обороны») в блоге Дуга Шмидта. В ней Дуг сделал следующее заявление:

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

Этот момент кое-что прояснил для меня. Дуг упомянул два термина, которые я мог или не мог бы счесть очевидными. Программное обеспечение — это то, что имеет долгий срок службы, но встроенные микропрограммы (firmware) устаревают в процессе развития аппаратного обеспечения. Если у вас есть опыт разработки встраиваемых систем, вы должны знать, что оборудование постоянно совершенствуется. В то же время добавляются новые функции в «программное обеспечение», и его сложность постоянно растет.

Я хотел бы добавить к заявлению Дуга:

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

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

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

• «Микропрограммы хранятся в энергонезависимых запоминающих устройствах, таких как ПЗУ, ППЗУ или флеш-память» (https://ru.wikipedia.org/wiki/Встроенное_программное_обеспечение).

• «Микропрограмма — это программа, или набор инструкций, заключенная в аппаратном устройстве» (https://techterms.com/definition/firmware).

• «Микропрограмма — это программное обеспечение, встроенное в устройство» (https://www.lifewire.com/what-is-firmware-2625881).

• Микропрограмма — это «программное обеспечение (программа или данные), записанное в постоянное запоминающее устройство (ПЗУ)» (http://www.webopedia.com/TERM/F/firmware.html).

Заявление Дуга помогло мне заметить ошибочность всех этих общепринятых определений микропрограмм или, по крайней мере, их неактуальность. Название «микропрограмма» не подразумевает, что код хранится в ПЗУ. Принадлежность к категории микропрограмм не зависит от места хранения; в большей степени она зависит от сложности изменения в процессе совершенствования оборудования. Оборудование действительно совершенствуется (приостановитесь и взгляните на свой телефон), поэтому мы должны структурировать свой встраиваемый код с учетом этой данности.

Я ничего не имею против микропрограмм или разработчиков микропрограмм (я сам занимался созданием микропрограмм). Но мы действительно должны меньше писать микропрограммы и больше — программное обеспечение. На самом деле я разочарован тем, что разработчики микропрограмм пишут их как микропрограммы!

Инженеры, не занимающиеся разработкой встраиваемого программного обеспечения, тоже пишут микропрограммы! Вы тоже фактически пишете микропрограммы, когда внедряете SQL в свой код или когда ставите его в зависимость от платформы. Разработчики приложений для Android пишут микропрограммы, когда не отделяют бизнес-логику от Android API.

Я участвовал в разработке многих проектов, где грань между прикладным кодом (программным обеспечением) и кодом, взаимодействующим с оборудованием (микропрограммой), была размыта до полного исчезновения. Например, в конце 1990-х годов мне посчастливилось участвовать в перепроектировании подсистемы коммуникации с целью перехода от технологии мультиплексирования с разделением по времени (Time-Division Multiplexing; TDM) к технологии передачи голоса по протоколу IP (Voice Over IP; VOIP). Технология VOIP широко используется в наши дни, а технология TDM считалась современной в 1950 — 1960-х годах и широко применялась в 1980 — 1990-х годах.

Всякий раз, когда у нас появлялся вопрос к инженеру-системотехнику о том, как вызов должен реагировать в той или иной ситуации, он исчезал и спустя какое-то время появлялся с очень подробным ответом. «Откуда ты это узнал?» — спрашивали мы. «Из кода продукта», — отвечал он. Запутанный и устаревший код служил справочником по новому продукту! Существующая реализация не имела разделения между TDM и бизнес-логикой, выполняющей вызовы. Весь продукт целиком зависел от оборудования/технологий, и этот клубок нельзя было распутать. Весь продукт фактически был микропрограммой.

Рассмотрим другой пример: управляющие сообщения поступают в систему через последовательный порт. Неудивительно, что в такой системе имеется обработчик/диспетчер сообщений. Обработчик сообщений знает их форматы, может их анализировать и передавать коду, который сгенерирует ответ. Ничто из перечисленного не вызывает удивления, кроме того, что обработчик/диспетчер сообщений находится в том же файле, что и код, взаимодействующий с микросхемой UART[56]. Обработчик сообщений инфицирован деталями, имеющими отношение к микросхеме UART. Он мог бы быть программным обеспечением с потенциально большим сроком службы, но вместо этого он стал микропрограммой. Обработчику сообщений отказано в праве быть программным обеспечением — и это неправильно!

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

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

Тест на профпригодность

Почему так много программного обеспечения превращается в микропрограммы? Похоже, что основная причина заключается в стремлении получить действующий встраиваемый код и практически не уделяется внимания его структурированию для увеличения срока службы. Кент Бек описывает три шага в создании программного обеспечения (далее в кавычках приводятся слова Кента, а курсивом выделены мои комментарии):

1. «Сначала заставьте его работать». Вы останетесь не у дел, если он не работает.

2. «Затем перепишите его правильно». Реорганизуйте код, чтобы вы и другие смогли понимать и развивать его, когда потребуется что-то изменить или понять.

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

Большая часть встраиваемых систем, которые мне приходилось видеть, похоже, писалась с единственной мыслью в голове: «Заставьте его работать», — и иногда с навязчивой идеей: «Заставьте его работать быстро», — воплощаемой введением микрооптимизаций при каждом удобном случае. В своей книге The Mythical Man-Month[57] Фред Брукс предлагает «планировать отказ от первой версии». Кент и Фред советуют практически одно и то же: узнайте, как это работает, и найдите лучшее решение.

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

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

В качестве примера взгляните на следующие функции, находящиеся в одном файле маленькой встраиваемой системы, написанные в ходе прохождения теста на профпригодность:

ISR(TIMER1_vect) { ... }

ISR(INT2_vect) { ... }

void btn_Handler(void) { ... }

float calc_RPM(void) { ... }

static char Read_RawData(void) { ... }

void Do_Average(void) { ... }

void Get_Next_Measurement(void) { ... }

void Zero_Sensor_1(void) { ... }

void Zero_Sensor_2(void) { ... }

void Dev_Control(char Activation) { ... }

char Load_FLASH_Setup(void) { ... }

void Save_FLASH_Setup(void) { ... }

void Store_DataSet(void) { ... }

float bytes2float(char bytes[4]) { ... }

void Recall_DataSet(void) { ... }

void Sensor_init(void) { ... }

void uC_Sleep(void) { ... }

В таком порядке функции были объявлены в файле с исходным кодом. А теперь разделим их и сгруппируем по решаемым задачам:

функции, реализующие предметную логику:

• float calc_RPM(void) { ... }

• void Do_Average(void) { ... }

• void Get_Next_Measurement(void) { ... }

• void Zero_Sensor_1(void) { ... }

• void Zero_Sensor_2(void) { ... }

функции, обслуживающие аппаратную платформу:

• ISR(TIMER1_vect) { ... }*

• ISR(INT2_vect) { ... }

• void uC_Sleep(void) { ... }

функции, реагирующие на нажатия кнопок:

• void btn_Handler(void) { ... }

• void Dev_Control(char Activation) { ... }

функция, читающая данные из аппаратного аналогово-цифрового преобразователя:

• static char Read_RawData(void) { ... }

функции, записывающие значения в долговременное хранилище:

• char Load_FLASH_Setup(void) { ... }

• void Save_FLASH_Setup(void) { ... }

• void Store_DataSet(void) { ... }

• float bytes2float(char bytes[4]) { ... }

• void Recall_DataSet(void) { ... }

функция, которая не делает того, что подразумевает ее имя:

• void Sensor_init(void) { ... }

Заглянув в другие файлы этого приложения, я нашел множество препятствий, мешающих пониманию кода. Также я обнаружил, что организация файлов подразумевает единственный способ тестирования этого кода — непосредственно внутри целевого устройства. Практически каждый бит этого кода знает, что относится к специализированной микропроцессорной архитектуре, используя «расширенные» конструкции языка C[58], привязывающие код к конкретному набору инструментов и микропроцессору. У этого кода нет ни малейшего шанса служить долго, если будет решено перенести продукт на другую аппаратную платформу.

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

Привязка к оборудованию — узкое место

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

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

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

Чистая встраиваемая архитектура — архитектура, поддерживающая тестирование

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

Уровни

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

Рис. 29.1. Три уровня

Раздел между оборудованием и остальной частью системы — объективная реальность, по крайней мере после определения оборудования (рис. 29.2). Именно здесь часто возникают проблемы при попытке пройти тест на профпригодность. Ничто не мешает знаниям об оборудовании инфицировать весь код. Если не проявить осторожность при структурировании кода и не ограничить просачивание сведений об одном модуле в другой, код будет трудно изменить. Я говорю не только о случае, когда изменяется оборудование, но также о ситуации, когда понадобится исправить ошибку или внести изменение по требованию пользователя.

Рис. 29.2. Оборудование должно отделяться от остальной системы

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

Оборудование — это деталь

Линия между программным обеспечением и микропрограммами обычно видна не так четко, как линия, разделяющая код и оборудование (рис. 29.3).

Одна из задач разработчика встраиваемого программного обеспечения — укрепить эту линию. Границу между программным обеспечением и микро-

Рис. 29.3. Линия между программным обеспечением и микропрограммами обычно более размытая, чем линия между кодом и оборудованием

программой (рис. 29.4) называют слоем аппаратных абстракций (Hardware Abstraction Layer; HAL). Это не новая идея: она была реализована в персональных компьютерах еще в эпоху до Windows.

Рис. 29.4. Слой аппаратных абстракций

Слой HAL существует для программного обеспечения над ним, и его API должен приспосабливаться под потребности этого программного обеспечения. Например, микропрограмма может хранить байты и массивы байтов во флеш-памяти, а приложению требуется хранить и читать пары имя/значение с использованием некоторого механизма хранения. Программное обеспечение не должно заботиться о деталях хранения пар имя/значение во флеш-памяти, на вращающемся диске, в облаке или в основной памяти. Слой аппаратных абстракций (HAL) предоставляет услугу и не раскрывает программному обеспечению, как она работает. Реализация поддержки флеш-памяти — это деталь, которая должна быть скрыта от программного обеспечения.

Еще один пример: управление светодиодом привязано к биту GPIO[59]. Микропрограмма может предоставлять доступ к битам GPIO, а слой HAL может иметь функцию Led_TurnOn(5). Это довольно низкоуровневый слой аппаратной абстракции. Давайте посмотрим, как повысить уровень абстракции с точки зрения программного обеспечения/продукта. Какую информацию сообщает светодиод? Допустим, что включение светодиода сообщает о низком заряде аккумуляора. На некотором уровне микропрограмма (или пакет поддержки платформы) может предоставлять функцию Led_TurnOn(5), а слой HAL — функцию Indicate_LowBattery(). Таким способом слой HAL выражает назначение услуги для приложения. Кроме того, уровни могут содержать подуровни. Это больше напоминает повторяющийся фрактальный узор, чем ограниченный набор предопределенных уровней. Назначение ввода/выводов GPIO — это детали, которые должны быть скрыты от программного обеспечения.

Не раскрывайте деталей об оборудовании пользователям HAL

Встраиваемое программное обеспечение с чистой архитектурой допускает возможность тестирования без целевого оборудования. Удачно спроектированный слой аппаратных абстракций (HAL) предоставляет тот шов, или набор, точек подстановки, которые облегчат тестирование без оборудования.

Процессор — это деталь

Когда сборка встраиваемого программного обеспечения производится с применением специализированного набора инструментов, в комплект часто входят заголовочные файлы с <i>поддержкой дополнительных конструкций </i>[60]. Эти компиляторы часто допускают вольное обращение с языком C, добавляя новые ключевые слова для доступа к функциям процессора. Код выглядит как код на C, но он больше не является кодом на C.

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

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

Взгляните на следующий заголовочный файл, созданный для семейства ACME процессоров цифровой обработки сигналов:

Страницы: «« ... 56789101112 »»

Читать бесплатно другие книги:

Во многих организациях на пути внедрения Agile оказывается традиционный менеджмент. Командам тяжело ...
В поисках заколдованной алхимической рукописи Диана Бишоп, историк из Оксфорда и наследница ведьм, в...
Вторая книга из серии «Практический Fake Zen» содержит в себе несложный способ получения желаемых ре...
Толпа бежала к нему с криками, что он Король. Смятение, удивление — он ожидал чего угодно, только не...
Этот известный на Западе бестселлер уже двадцать лет помогает начинающим менеджерам проектов. В нем ...
Если вы постоянно устаете, раздражаетесь, у вас проблемы с кожей или волосами, вы ни с того ни с сег...