Сервис

Определение

👉
Сервис — это объект, который выполняет задачи, определенные в рамках своей зоны ответственности, реагируя на сообщения.
👉
Объект — сущность в цифровом пространстве, способная сохранять свое состояние (информацию) и обеспечивающая набор методов (поведение) для получения, проверки и изменения этого состояния.

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

💡
Отличительной особенностью сервиса является наличие императивного интерфейса и внутреннего состояния.

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

Графическое представление

Каждый сервис должен быть разделен на интерфейс и реализацию. В контексте iOS и языка Swift интерфейс обычно представлен протоколом, а реализация — классом. Жесткие связи между сервисами в терминах UML описываются диаграммой классов (иначе диаграммой сервисов), соответственно, описываемый сервис на диаграмме представлен классом, а все его зависимости — интерфейсами.

Диаграмма классов описывает сервис PhoneViewModel с двумя зависимостями.
Диаграмма классов описывает сервис PhoneViewModel с двумя зависимостями.

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

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

💡
Одной диаграммы состояний для описания внутренней логики сервиса может оказаться недостаточно и возникает потребность в диаграммах деятельности и/или последовательности. Это признак недостаточной декомпозиции сервиса. «Развесистую» логику следует перенести в отдельную операцию.

Стартовый сервис

В некотором смысле все приложение целиком можно считать сервисом, представленным в iOS в виде объекта UIApplicationMain:

#import "AppDelegate.h"
int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(
            argc, argv, nil, NSStringFromClass([AppDelegate class])
        );
    }
}

Действительно, UIApplicationMain – это объект, который реагирует на сообщения, порождаемые операционной системой, что соответствует определению сервиса.

Ключевые и контекстные сервисы

👉
Сервис называется ключевым, если он представлен в единственном экземпляре и его время жизни сравнимо со временем жизни всего приложения.

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

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

Инициализация ключевых сервисов выполняется специальной операцией MakeServicesOp:

class ServiceContainer {
    var storage: IStorageService
    var mapEngine: IMapEngineProvider
    init(
        storage: IStorageService,
        mapEngine: IMapEngineProvider
        ) {
            self.storage = storage
            self.mapEngine = mapEngine
        }
}

protocol IMakeServicesOp {
    func launch(onFinish: @escaping VoidCompletion) -> ServiceContainer
}

final class MakeServicesOp: IMakeServicesOp {

    // MARK: IMakeServicesOp
    func launch(onFinish: @escaping VoidCompletion) -> ServiceContainer {
        let storage = StorageService()
        let mapEngine = MapEngineProvider()
        return ServiceContainer(
            storage: storage,
            mapEngine:  mapEngine
        )
    }
}

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

👉
Сервис называется контекстным, если он может быть представлен в нескольких экземплярах одновременно и время его жизни сопоставимо с временем выполнения сценария, в контексте которого работает сервис.

Примером контекстного сервиса является контроллер карты: MapController. MapController инкапсулирует логику работы с экземпляром карты MapView, который формируется в MapEngineProvider.

В отличие от MapEngineProvider, который всегда существует в единственном экземпляре, экземпляров MapView может быть одновременно несколько (например, на разных вкладках). Соответственно, в этом случае потребуется несколько экземпляров MapController, каждый из которых будет обрабатывать события своего MapView.

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

ViewModel – это самый часто встречающийся пример контекстного сервиса. Вью-модель формируется непосредственно перед показом соответствующего экрана с учетом контекста сценария, поэтому она называется контекстным сервисом.

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

Жесткие и контекстные связи

👉
Жесткие связи передаются в сервис через его конструктор/инициализатор и должны быть определены в момент создания сервиса.

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

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

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

ViewModel

👉
Вью-модель — это контекстный сервис, который выступает связующим звеном между UI-логикой и бизнес-логикой приложения.

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