Архитектура приложений Surf
October 2, 2019 · View on GitHub
Архитектура приложений Surf
Архитектура
Архитектуру, которую мы используем основана на принципах CleanArchitecture.
Основной ее особенностью является строгое разделение на слои(каждый со своей областью ответственности) и направление зависимостей между слоями - слои верхних уровней не должны зависеть от слоев нижних уровней.

Стрелками показаны направления зависимостей
-
Entities - бизнес обьекты приложения
-
UseCases - юзкейсы приложения (например, авторизация, с сохранением сессии; получение профиля пользователя; изменение региона/вылюты)
-
InterfaceAdapters
- Gateways - обертки над сервером, базой данных и тд, которые конвертируют данные из низкоуровневого источника в данные для удобного использования в UseCase (в основном модели Entity)
- Presenter - Управляет отображением, конвертирует данные полученные от слоя UseCase в структуру, подходящую для UI. И наоборот: конвертирует данные и события от UI в подходящие для слоя UseCase. UI условно можно разделить на 2 части: ввод и вывод. Соответственно презентер также отвечает за обработку ввода с UI и конфигурирования UI чтобы он выводил правильные данные.
-
Frameworks and Drivers
- UI - отвечает за взаимодействие с пользователем
- DB, Web, Framework… - Api сервера, базы данных, сервисы и тд.
Уровень абстракции данных и команд с помощью которых взаимодействуют слои возрастает с приближением к центру.
На самом деле это только общая структура приложения и по разным направлениям диаграммы может быть различное количество слоев, количество слоев зависит от сложности системы - чем сложнее система, тем на большее количество подсистем ее нужно разделить для уменьшения сложности не забывая при этом про соблюдение иерархичности (уровней абстракции). UseCase и Gateways объединены в один слой, это сделано потому что иначе для некоторых приложений сущность из UseCase просто пробрасывала бы вызовы к сущности из Gateways.
Эта архитектура может быть в некоторых случаях упрощена так что UseCase и Interface Adapter объединяются, в некоторых приложениях они называются Репозитории.
Также очень важным правилом в нашей архитектуре является деление каждого слоя на небольшие практически независимые модули со строго определенными ответственностями (деление на экраны, модули с внутренней логикой).
Для более глубокого понимания следует ознакомиться со следующей хорошей статьей.
Структура и принадлежность классов слоям
Структура , используемая в проектах является гибридной, с одной стороны она позволяет разделить классы по некоторым слоям, с другой по фичам.
Основные слои проекта (названия могут отличаться для разных проектов):
Слой app
Содержит ключевые классы инфраструктуры приложения. В большинстве случаев классы из этого пакета не изменяются во время разработки приложения.
Слой domain
Соответствует слою entity и содержит бизнес объекты приложения (например, Product, Profile, Order...)
Слой interactor
Типовые для Interactor сущности
Содержит модули с внутренней логикой приложения. Каждый модуль имеет единственную ответственность, например: сервис получения геолокации, хранилище продуктов, хранилище акций, сервис авторизации... Соответствует слоям UseCase, Interface Adapters, Web/BD/Framework… Иногда все эти слои вместе мы называем слоем Interactor, поскольку граница между ними может быть размыта (см ниже).
Пример модуля с разделением ответственностей, точно соответствующем архитектуре

- Api, Responses, Requests - классы, определяющие интерфейс для Retrofit
- Dao, Dbo - низкоуровневые классы базы данных, Dbo - DataBase Object
- RemoteRepository - обертка над Api, Responses, Request. Производит конвертацию входных параметров в Requests и преобразование ответов сервера (Responses) в сущности слоя Entity.
- LocalRepository - обертка над Dao, Dbo. Производит конвертацию Entity моделей в Dbo и наоборот. LocalRepostory может быть совмещен с Dao, если у Dao будет интерфейс, соответствующий уровню абстракции слоя InterfaceAdapters.
- Interactor - может содержать следующую логику:
- данные, полученные из RemoteRepository отдает на сохранение в LocalRepository
- в случае ошибки получения данных из RemoteRepository отдает данные из LocalRepository
- может позволять подписываться на изменения какой либо сущности (например с помощью PublishSubject)
- ....
Примеры модулей с совмещенными слоями
В следующем случае логики для UseCase практически нет, и он просто должен пробрасывать вызов в Interface Adapters

В следующих случаях логики немного больше но ее все равно недостаточно для выделения дополнительной сущности:

Если необходимо реализовать простое локальное хранилище (например хранилище настроек приложения) в котором практически логики, то оно может выглядеть так:

Сущность, которая относится к нескольким слоям должна иметь публичный интерфейс, соответствующий уровню абстракции самого высшего из обьединенных слоев.
Взаимодействие между модулями
Хотя и следует стремиться чтобы модули были максимально независимыми, в реальных проектах это не всегда получается. В этом случае следует пользоваться вышеприведенными принципами построения архитектуры и здравым смыслом, чтобы максимально уменьшить связанность кода и снизить к минимуму количество связей и side эффектов. Наиболее предпочтительным способом является взаимодействие через интерактор, поскольку логика связывания различных модулей относится именно к слою UseCase. Еще следует придерживаться такого правила: чем в более низшем слое находится класс, тем меньше должна быть вероятность найти в нем зависимость от класса другого модуля и обратно - от этого класса не должны зависеть другие модули.
Когда следует выделять логику в интерактор
Хорошими индикаторами того, что репозиторий содержит логику слоя UseCase являются
- наличие .doOnNext() и .flatMap() на цепочках Observable
- наличие Subject в самом репозитории
Если методов с этой логикой больше четверти от всего числа методов, пора обзавестись интерактором. Причем интерактор должен оборачивать все публичные методы репозитория, а сам репозиторий должен использоваться только в интеракторе.
Слой ui
Отвечает за логику и отображение ui. Соответствует слоям Presenter, UI/Framework… Иногда эти слои вместе мы называем ui слоем.
Принадлежность классов экрана к слоям:

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

И вы будете абсолютно правы, это нарушение архитектуры, но оно решается очень просто: если у сущности каждого слоя выделить интерфейс и перенести его в более высший слой то проблема будет решена. Например:
- Интерфейс View относится к слою Presenter
- Интерфейс LocalRepository относится к слою UseCase
- ....
Мы же этого не делаем поскольку:
- На разработку и поддержку такого кода нужно больше времени
- Если публичный интерфейс класса такой же как и интерфейс, который он реализует, то переменные типа этого класса и типа интерфейса для той сущности, которая будет использовать эту переменную, будут абсолютно равнозначны.
Хотя публичный интерфейс конкретного класса View содержит методы фреймворка (пр. getResources()) мы все равно не выделяем для нее интерфейс, а проблему использования этих методов в презентере решаем строгим правилом, прописанным в Технической Документации и внутренней дисциплиной.
Однако это не означает что интерфейсы вообще не нужны. Интерфейсы, к примеру, можно использовать чтобы подменять реализацию модуля в Runtime.