Программная архитектура Назначение архитектуры программного обеспечения Под архитектурой программного обеспечения понимают набор внутренних структур ПО, которые состоят из компонентов, их связей и возможных взаимодействий между компонентами, также доступных извне свойств этих компонентов. Под компонентом понимают достаточно произвольный структурный элемент программного обеспечения, который можно выделить, определив интерфейс взаимодействия между этим компонентом и всем, что его окружает. Архитектурный стиль может определяться архитектурным контуром, промежуточным программным обеспечением, рекомендованным набором шаблонов или методом архитектурного описания системы. Архитектурный стиль определяет основные правила выделения компонентов и организации взаимодействия между ними в рамках системы или подсистемы в целом. Выбор архитектуры задаёт способ реализации требований на высоком уровне абстракции. Различные архитектурные стили подходят для решения различных задач в плане обеспечения нефункциональных требований, а именно: различных уровней производительности, удобства использования, переносимости и удобства сопровождения. Значительно меньшим является влияние архитектуры на функциональность, при этом заданную функциональность можно реализовать с использованием совершенно разных архитектур. 1) Pipes and Filters: предназначен для организации обработки потоков данных в том случае, когда процесс обработки распадается на несколько шагов. Эти шаги могут выполняться отдельными обработчиками, возможно, реализованными различными разработчиками или даже организациями. Решение: каждая отдельная задача по обработке данных разбивается на несколько мелких шагов, при этом выходные данные одного шага являются входными для других. Каждый шаг реализуется специальным компонентом, а именно фильтром. Фильтр потребляет и выдаёт данные инкрементально (небольшими порциями), передача данных между фильтрами осуществляется по каналам (pipes). Структура: основными ролями компонентов являются фильтр и канал, иногда выделяют специальные виды фильтров - источник данных (DataSource) и потребитель данных (DataSink). Каждый поток данных состоит из чередующихся фильтров и каналов, начинается источником и заканчивается их потребителем. +------------------------------+ +-------------+ | Filter1 | | DataSink | +------------------------------+ +-------------+ +------------------------------+ +-------------+ |+processData()DataElement2 | +------+------+ +------------------------------+ | / ^ v v \ +-------------+ +------------++----------------+ | Pipe | |DataSource ||Filter2 | +-------------+ +------------++----------------+ +-------------+ +------------++----------------+ |+readData() | |*getData() ||+processData() | |+writeData() | |DataElement1|+----------------+ +-------------+ +------------+ \ ^ \ | \ +-----------------+ \ | Filter3 | >+-----------------+ +-----------------+ |+writeData() | +-----------------+ Filter1 вызывает getData(), Filter2 вызывает processData(), Filter3 вызывает processData(). 2) Динамика: возможны три различных сценария работы одного фильтра: push model (фильтр сам передаёт данные следующему компоненту, а получает их как результат передачи предыдущего), pull model (фильтр требует данные у предыдущего компонента, следующий сам должен требовать данные у него), push/pull model. Часто реализуется только один вариант передачи данных для всех фильтров в рамках системы, кроме того, канал может буферизировать данные и синхронизировать взаимодействующие с ним фильтры. +-----------+ +---------------+ +-----------------+ |_:Filter1_ | |_:Filter2_ | |_:Filter3_ | +-----------+ +---------------+ +-----------------+ : : : ++ sendData() ++ : |+---------------->|+-+ : || |++|ProcessData() : || |++< sendData() : || |+-------------------->++ || || || ++ ++ ++ : : : ------------------------------------------------------------- +-----------+ +----------------+ +-----------------+ |_:Filter1_ | |_:Filter2_ | |_:Filter3_ | +-----------+ +----------------+ +-----------------+ : : getData() : : getData() ++<-------------------++ ++<----------------|| || || |+--+processData() || ++ |++ | || : |++<+ || : ++ || : : ++ : : : +-------------++----------++-----------++-------------++--------------+ |_:DataSource_||_:Filter1_||_:Pipe_ ||_:Filter2_ ||_:DataSink_ | +-------------++----------++-----------++-------------++--------------+ : : : : : : : : readD() ++ : : readD() ++--+processD++<----------+| : ++<----------+++ | || || : ++ |||<+ || || : : |++ writeD()|| || : : |+---------->|++ |+--+processD() : ++<----------++--+processD||| |++ | : || |++ | |++ |||<+ : ++ |||<+ ++ |++ writeD() : : |++ writeD() : |+------------->++ : |+---------->++ readD() || || : || |++<---------|| ++ : ++ +|| || : : : :++ ++ : : : : : : Примеры: система утилит UNIX, дополненная возможностями оболочки по организации каналов между процессами. Большинство утулит играют роль фильтров.при обработке текстовых данных, а каналы строятся с помощью сочетания стандартного ввода одной программы со стандартным выводом другой. Архитектура компилятора как последовательности фильтров, обрабатывающих входящую программу: синтаксический анализатор, семантический анализатор, набор оптимизаторов, генератор результирующего кода. 3) Многослойная система (layers). Назначение: реализация крупных систем, которые имеют большое количество разноплановых элементов, использующих друг друга. Некоторые аспекты работы таких систем могут включать в себя много операций, выполняемых различными компонентами на различных уровнях. Решение: выделяетя определённый набор уровней, каждый из которых решает за решение своих собственных подзадач; для этого он использует интерфейс, который предоставляется предыдущим уровням, в свою очередь предоставляя определённый интерфейс для следующего уровня. +------------+ +-------------+ / v / v +--------+ +----------++--------------+ +-----------++--------------+ |Client | |LayerN ||LayerN-1 | |Layer2 ||Layer1 | +--------+>+----------++--------------+ ...+-----------++--------------+ +--------+ +----------++--------------+ +-----------++--------------+ +--------+ +----------++--------------+ +-----------++--------------+ ^ / ^ / +------------+ +-------------+ +-----------++-----------++------------+ |_:Layer1_ ||_:Layer2_ ||_:Client_ | +-----------++-----------++------------+ : : : ---->++ReceivePack : : ++----------->++ : : || : ---->++ReceivePack |++ : ++------------>|| : : |++ReserveMess : : ++------------>++ : : ++ : : : +-----------++-----------++------------+ \\ |_:Client_ ||_:Layer2_ ||_:Layer1_ | \\ +-----------++-----------++------------+ \\ : sendMess() : : sendData() \\ ++---------->++sendPacket()++----------------->> || |+----------->|| // || || ++ // || || sendPacket(): sendData() // || |+----------->++-------------<< || || || \\ || || ++ \\ || ++ : \\ ++ : : \\ : : : \\ Часто в виде многих уровней реализуются коммуникационные системы. Две такие системы могут взаимодействовать через самый нижний уровень, при этом пара симметричных сценариев по подъёму-спуску обращений выполняется в рамках одного общего сценария на разных машинах. Обращение клиента к верхнему уровню инициирует цепочку обращений от верхнего уровня до самого нижнего. События на нижнем уровне (получение сообщения по сети. нажатие кнопки мыши) инициирует цепочку обращений снизу вверх, вплоть до события на верхнем уровне, которое является видимым клиентом. При этом бывают ситуации, когда обращение клиента к верхнему уровню не доходит до самого низа (один из уровней кэширует ответы на запросы и соответственно выдаёт результат без необходимости обращения к нижним уровням.