Сервис
Определение
Примерами сервисов являются локальное хранилище и сетевой слой. Обертка над ними, которая выполняет вход и сохраняет токен авторизации в хранилище после процедуры входа, тоже является сервисом. Роутер — тоже сервис, поскольку обладает императивным интерфейсом.
Сервис может предоставлять доступ к своему состоянию синхронно, работать в режиме запрос-ответ, а также предоставлять подписки на обновления данных. В ходе выполнения задач сервис может обращаться к другим сервисам, которые доступны ему в виде зависимостей. Совокупное состояние всех сервисов формирует состояние приложения.
Графическое представление
Каждый сервис должен быть разделен на интерфейс и реализацию. В контексте iOS и языка Swift интерфейс обычно представлен протоколом, а реализация — классом. Жесткие связи между сервисами в терминах UML описываются диаграммой классов (иначе диаграммой сервисов), соответственно, описываемый сервис на диаграмме представлен классом, а все его зависимости — интерфейсами.
Контекстные связи между сервисами формируются в операциях, и этот процесс описывается диаграммой сообщений. Сформированные контекстные связи также можно отобразить на диаграмме сервисов.
Сервисы хранят состояние приложения, поэтому ветвления в бизнес-логике сервиса описываются диаграммой состояний.
Стартовый сервис
В некотором смысле все приложение целиком можно считать сервисом, представленным в 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
Контекстные связи вью-модели формируются в операции и тестируются через отслеживание обращений операции к ключевым сервисам, одним из которых является роутер. Mock-объект роутера воздействует на переданные ему операцией вью-модели, что позволяет в целях независимого тестирования бизнес-логики полностью отвязать ее от UI-логики.