Вводная

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

Старт

Начнём с того, что в нашей жизни мы часто встречаем типичные ситуации, в которых есть субъект «Пользователь» и он взаимодействует с объектом «Функция». Примерами могут выступать пользователь и опции в личном кабинете какого-нибудь банковского приложения, абонент и услуги связи мобильного оператора или администратор сети, активирующий опции в веб-панели управления маршрутизатора.
Давайте эти примеры обобщим и немного формализуем. В каждом из них представим, что пользователь нажимает кнопку, действием которой выполняется запрос по некоему API и активируется сервис. Это вполне рабочая история. На этом шаге мы можем выделить сущности «Пользователь», «Действие» и «Сервис».
Сущность «Действие» в некоторых системах не является простой операцией, и она связана с техническими сложностями в реализации. Речь пойдёт не про то, как регистрировать запросы в очередях, использовать ли асинхронное исполнение и т.п. Мы сейчас рассмотрим приближенные к жизни примеры и увидим какие проблемы будут таиться. Понимание проблемы подготовит наше сознание к выработке решения: детализируем «Действия» на несколько слоёв и сущностей, которые используются в нашем паттерне.

Подготовка к эксперименту

Для понимания процесса будем рассматривать простые с определенной долей упрощения кейсы из сферы телекома. Пусть от нас требуют реализовать приложение, который принимает запросы из личного кабинета пользователя на подключение нескольких услуг: «Услуга телефонии», «Услуга роуминга», «Услуга международных вызовов», «Услуга запрета исходящих вызовов».
Чтобы процесс был более понятным и приближенный к боевым условиям, спроектируем простое приложение, которое должно подключаться к оборудованию для мобильной связи и выполняет там запросы. В частности, мы будем работать с HLR. Действие по подключение/отключению сервисов называется умным словом «провижининг», но для нас это сейчас не принципиально, главное мы понимаем о чём дальше будет идти речь :)

Начнём работу, конечно же, с изучения спецификации (API) такой железки. В сети можно найти общедоступные спеки и вспомогательные файлы одного из производителей HLR тут и тут.

У многих из вас доступа к реальному железу нет и скорее всего по объективным причинам не будет, поэтому создадим «мысленно» простой эмулятор, который будет принимать от нас запросы и менять состояние абонентского профиля. У нашего эмулятора HLR, как и у его настоящего собрата, будет использоваться интерфейс по типу TELNET для осуществления провижининга. Запросы — это простые текстовые строки содержащий идентификатор абонента, имя сервиса и некоторое значение. Само значение либо включает или отключает сервис, либо изменяет поведение работы сервиса. Оборудование выполняет валидацию команд и не даёт совершить некорректную последовательность действий, например, активировать услугу переадресации абоненту, которому запрещено совершать голосовые вызовы.

Мыслительная деятельность а-ля дискаверинг

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

HGSDC:MSISDN=78001234567,SUD=TS11-1;
HGSDC:MSISDN=78001234567,SUD=OBO-2;

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

HGSDC:MSISDN=78001234567,SUD=OBO-0;

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

HGSDC:MSISDN=78001234567,SUD=OBR-0;

Пока выглядит всё просто и понятно.
А теперь представим, что абонент подключил услугу запрета исходящих вызовов ровно на 10 минут. Зачем эта услуга нужна ему, нам инженерам особо знать не хочется и не требуется. Основное правило: желание абонента для нас закон. Так выполним же соответствующую команду:

HGSDC:MSISDN=78001234567,SUD=OBO-1;

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

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

Правильный ответ

Мы не должны отправлять никаких команд.

Вопрос: И почему же?
Ответ: У абонента на момент отключения услуги «международных вызовов» всё ещё присутствует услуга «запрета исходящих вызовов» и она по нашим ощущениям имеет «большую силу»1, поэтому если сервис был заблокирован, то он и должен остаться заблокированным. После же удаления запрещающей услуги мы должны выставить OBO-2, т.к. более не будет услуг запрета и международных вызовов, поэтому абоненту разрешим вызовы только внутри домашней страны.

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

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

HGSDC:MSISDN=79001234567,SUD=TS11-1;
HGSDC:MSISDN=79001234567,SUD=OBO-0;

Вопрос: Так-так… почему теперь OBO-0?

Дело в том, что у нашего абонента ранее подключалась услуга, разрешающая международные вызовы, поэтому не логично выполнять команду сначала на OBO-2, а следом отправлять команду на OBO-0. Как видим логика нашего кода ещё должна усложниться. Нам требуется не только подключить корректно сервисы, но сделать это оптимально, не нагружая оборудование излишними командами.

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

Концепция каталога услуг

Как бы нам хотелось бы иметь такую систему, в которой можно было бы:

  • Легко сопровождать, особенно людям далёким от разработки/кодинга

  • Декларативно назначать правила взаимодействия между услугами

  • Уменьшить сложность конечного решения и упростить его вывод на рынок

Все описанные проблемы решаются «каталогом услуг»!

Вопрос: Так, подождите! Мы уже поняли, что существует волшебство, которое решит все наши проблемы, но мы ещё ничего не знаем о каталоге. Что он из себя представляет?

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

CFS (Customer Facing Service) - слой содержит в себе объекты, которые представляют из себя пользовательскую услугу. В качестве пользователя может выступать конечный потребитель, т.е. мы с вами. Также этот объект может быть не последним звеном и входить в объекты на более высоком уровне, например, на продуктовом слое, в котором внутри продукта содержится одна и более пользовательских услуг.
Такую сущность пользователь может "потрогать" руками, например, подключив услугу "Статический IP-адрес" для своего домашнего интернета и взаимодействовать с ней в личном кабинете. Например, можно подключить услугу, отключить услугу или изменить параметр «IP адрес».

RFS (Resource Facing Service) - слой лежит ниже уровня CFS и представляет из себя услугу абстрактного оборудования. В дальнейшем такую услуги будем именовать словом «сервис» и нам будет понятно, что говорим сейчас про объекты типа RFS. Каждый RFS сопоставляется с некой атомарной сущностью/флагом/фичей/капабилити конечного оборудования.

Чаще всего пользовательская услуга состоит из нескольких сервисов. Например, мы могли бы смоделировать CFS «Услуга интернета» и связать с несколькими RFS сервисами: RFS для активации GPRS сервиса на HLR (2G/3G интернет), RFS для активации регистрации в LTE (4G интернет), RFS для ограничения по-умолчанию интернета в домашней сети, RFS для выбора стандартной скорости, RFS для выбора точки доступа APN.

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

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

Картинка, конечно, не сможет передать всю логику модели каталога, но сейчас разберём, что здесь происходит:
У нас смоделированы 3 RFS. Название RFS мы дали осмысленные, ровно также как в программировании переменные называют понятными именами для удобства работы с ними. Длинное название «RBARRING_OUTGOING_CALLS» означает, что этот RFS должен на некотором оборудовании управлять сервисом запрета (барринг) и касается он только исходящих вызовов. Т.е. это намекает, что в будущем мы могли бы создать RFS связанный с выставление ограничения на входящие вызовы, если такую возможность поддерживает конечное оборудование.

Конкретно этот RFS имеет ещё определенный параметр (настройка/свойство/характеристика/переменная, называйте как угодно), которая может принимать одно из текстовых значений «HOME», «NO» или «ALL». По смыслу нам станет понятно, что когда RFS примет значение настройки как «NO», то ограничения никакого не должно быть в профиле абонента на оборудовании. В случае с «ALL» мы должны заблокировать все исходящие вызовы абоненту.

Далее мы видим, что у всех RFS есть несколько возможных действий с ними «Activate» и «Modification». Стоит заметить, что мы не можем модифицировать сервис, если он ранее не был подключён. Т.е. при подключении первой услугой CMNCALLS абоненту ничего не должно произойти, т.к. связь модификации не работает над тем, чего ещё нет. Если же после добавления этой услуги добавят CVOICECALLS, которая активирует 2 RFS, то произойдёт следующее:
а) Нужно выполнить активацию «RTELESERVICE_VOICE»
б) Нужно выполнить активацию «RBARRING_OUTGOING_CALLS», но значение для параметра взять из услуги CMNCALLS, т.к. она более приоритетная, чем CVOICECALLS.
Т.к. CMNCALLS уже была ранее подключена, и она более приоритетная, и чтобы состояние услуг соответствовало состоянию активированных сервисов на оборудовании, то требуется чтобы параметр внутри RFS имел значение именно от этой услуги.

К сожалению, эта картинка не показывает, как именно приоритет CFS влияет на выбор значения для параметра RFS, поэтому для наглядности перерисуем для RBARRING_OUTGOING_CALLS в другом виде. Здесь достаточно всё очевидно и пояснений, думаю, не потребуется.

Алгоритм поиска изменений

Мы уже ранее немного смогли сформулировать правила, как должен каталог поступить при подключении услуги. Так вот вся задача каталога сводится к тому, что он должен понять, какое действие нужно выполнить или не выполнять с RFS при подключении/отключении CFS.

Каталог выполняет следующие этапы:

  1. Формирует список CFS, которые были подключены абоненту до выполнения запроса

  2. Формирует список CFS, которые останутся подключенными абоненту после выполнения запроса с подключением/отключением услуг

  3. Для каждого из списка получит список активированных RFS с выставленными значениями параметров

  4. Найдёт отличие между списками RFS и сформирует дельту изменений, которую требуется выполнить

Давайте попробуем формализовать некоторые правила поиска таких отличий в таблицу:

CFS, подключенные ДО

CFS, подключенные ПОСЛЕ

Действие с RFS

CVOICECALLS, CMNCALLS

CMNCALLS

Деактивация RTELESERVICE_VOICE. Деактивация RBARRING_OUTGOING_CALLS

CMNCALLS

CMNCALLS, CBLOCKCALLS

Нет действий

CMNCALLS, CBLOCKCALLS

CMNCALLS, CBLOCKCALLS, CVOICECALLS

Активация RTELESERVICE_VOICE. Активация RBARRING_OUTGOING_CALLS с значением «ALL»

CMNCALLS, CBLOCKCALLS, CVOICECALLS

CMNCALLS, CVOICECALLS

Модификация RBARRING_OUTGOING_CALLS с «ALL» на «NO»

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

  • Нельзя деактивировать RFS, если ранее он не был активирован

  • Нельзя модифицировать RFS, если ранее он не был активирован

  • Нельзя повторно активировать RFS, если он бы ранее активирован

  • Модификация RFS с параметром возможна, если подключается более приоритетная CFS и значение параметра в RFS будет отличаться от текущего

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

Вместо заключения

Статья была больше нацелена на получение теоретических знаний. Т.к. каждое решение с использованием паттерна каталога будет индивидуальным и реализует конкретные потребности вашего бизнеса. Тем, кто впервые сталкивается с ситуациями, описанными в кейсах, будет полезно знать, какие варианты решений существует. А если Вы и так всё уже знали, то это просто замечательно! Значит Вы сможете реализовать сложный проект и при этом не допуская ошибок и просчётов, которые могут поджидать в самых непредсказуемых местах…

Ссылки:

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

Комментарии (0)