Добавлен: 29.02.2024
Просмотров: 49
Скачиваний: 0
В среде Android разработчиков большой популярностью пользуется так называемая Чистая Архитектура [15], схема которой представлена на рис. 2.2. Следуя этой архитектуре, приложение должно делиться на 3 слоя: данные (data), бизнес-логика (domain), представление (presentation). У каждого из слоев своя зона ответственности. Первый слой отвечает за получение данных из различных источников, таких как сервер или база данных, и последующий перевод данных в структуры, понятные слою бизнес-логики. Слой бизнес-логики отвечает за основную логику приложения, такую как, например, определение
того, истекла ли у пользователя подписка или вычисление суммы платежа, которую пользователь будет должен заплатить за желаемую транзакцию. Наконец, слой представления ответственен за отображение данных.
Рисунок 2.3. Архитектура клиентана примере некоторых классов
Однако, во многих приложениях бизнес-логика хранится на сервере для использования ее на многих клиентах и из соображений безопасности. В разработанном приложении бизнес-логика также хранится на сервере, таким образом, поддержка слоя domain была бы излишней и не оправдывала бы себя, ведь этот слой бы просто передавал объекты от Данных к Представлению. Поэтому архитектура разработанного мобильного приложения состоит из двух слоев: данные и представление (рис. 2.3). Слой данных реализует паттерн Repository: в зависимости от параметров запроса, текущего состояния сети и многих других параметров, Repository сам решает, откуда получать данные – из локального хранилища или сервера, инкапсулируя в себе все взаимодействие с источниками данных. Mapper ответственен за преобразования объектов одного типа в другой: например, из типа MessageEntity, которым оперирует локальное хранилище, в тип Message, о котором знает слой представления. Примененная архитектура делает приложение тестируемым и расширяемым за счет использования принципов единственной ответственности и инверсии зависимости, входящими в список принципов SOLID[16]. Так как классы выполняют ровно одну задачу и не зависят напрямую от реализаций других классов, приложение будет легко покрыть unit-тестами. Архитектура разработанного приложения позволяет ему быть независимым от источников данных и интерфейса, таким образом, изменения, вносимые в приложение при изменении способа хранения данных или их отображения, затронут минимальное количество классов, что положительно сказывается на простоте поддержки разработанного программного продукта.
Фреймворк Android содержит 4 основных компонента, используемых при разработке приложений [17]. Один из них, Activity, используется для работы с пользовательским интерфейсом. У этого компонента достаточно сложный жизненный цикл [18], включающий в себя последовательный вызов более 15 методов, таких как «onStart», «onStop»,
«onRequestPermissionResult», «onDestroy» и другие. Во многих случаях, от разработчика требуется переопределение как минимум нескольких из этих методов. Более того, некоторые действия, такие, как открытие диалога, получение разрешений на действия в системе, старт фонового действия, доступны только из контекста Activity. Это часто приводит к увеличению количества строк в классе, наследующемся от Activity. Другой проблемой компонента является то, что он по умолчанию не сохраняет свое состояние при изменении конфигурации, например, повороте экрана.
Рисунок 2.4. Сравнение MVPи MVVM.
Для решения вышеперечисленных проблем можно использовать такой паттерн, как MVP [19] (рис. 2.4). Этот паттерн предполагает, что слой представления разделяется на две части: View и Presenter. Model в MVP – часть, не связанная с отображением, в нашей архитектуре это слой данных. View ответственна за работу с системой, описанную выше. Presenter же ответственен за логику, которая не имеет отношения к системе, например, сортировку записей перед их показом, получение записей от Model, а также за хранениесостояния экрана. Таким образом, связанность кода уменьшается, ведьView ничего не знает про Model, Model про View, между собой их связывает Presenter.
Однако, к недостаткам приведенного выше паттерна можно отнести то, что после многократного вызова Presenter’ом методов View может быть сложно отследить итоговое состояние системы, а также восстановить его после изменения конфигурации. Это делает связь двух перечисленных классов менее надежной, а Presenter менее тестируемым. Поэтому в последнее время паттерн MVVM [19](рис. 2.4) пользуется все большей популярностью. Этот паттерн отличается от MVP введением новой сущности ViewState, которая представляет из себя буквально «состояние отображения» и используется для передачи данных из Presenter во View.Presenter не имеет права вызывать методы View, вместо этого, View сама подписывается на поток ViewState и обрабатывает его так, как захочет.Для восстановления состояния отображения после смены конфигурации,Presenter’у достаточно передать в поток изменений последний сохраненный ViewState.
Архитектура MVVM в мобильном приложении была реализована самостоятельно с помощью системных компонентов и библиотеки для реактивного программирования RxJava [20], так как на этапе разработки ни одна из библиотек, реализующих MVVMнаAndroid, не показалась надежной.
2.3 Архитектура серверной части приложения
Рисунок 2.5. Архитектура сервера на примере некоторых классов
Как говорилось ранее, в задачи серверавходитзаимодействие с клиентом через протоколы HTTPS иWebSocket. В контексте сервераклассы, принимающие запросы от клиентской части приложения, играют рольслоя представления, ответственного за взаимодействие с внешним миром. Такие классыименуются в фреймворкеSpring как Controllers. Эти классы хранят логику обмена информацией с клиентом, работы с FileStorage, предоставляющим доступ к файлам, а также Repository, который является оберткой для упрощенной работы с базой данных (рис. 2.5). Перед отправкой данных на клиент, сервер должен преобразовать их в понятную клиенту форму, поэтому, для преобразования объектов одного типа в объекты другого, типа используются Mappers.
Глава 3. Детали реализации приложения «клиент-сервер»
При разработке приложения был использован фреймворкRxJava [20], реализующий принципы реактивного программирования [21], в основе которого лежит паттерн Observer [22]. Реактивное программирование позволяет оперировать потоками данных, используя многочисленные операторы обработки, фильтрации и изменения данных. Также, RxJava из коробки поддерживает переключение потоков выполнения кода по мере прохождения данных по потоку, что может быть использовано для работы с сервером и базой данных.Реактивное программирование используется на всех слоях мобильного приложения. Например, в виде потоков представлены: сообщения, получаемые через WebSocket от сервера; записи, получаемые из базы данных; действия с элементами пользовательского интерфейса; изменения состояния интернет-соединения; изменения ViewState в паттерне MVVM.
Для хранения данных на устройстве была выбрана СУБД SQLite [23]. Она является компактной, полностью покрыта тестами и встроена в фреймворкAndroid, поэтому для подключения СУБД не нужно будет добавлять в зависимости приложения дополнительную библиотеку. Однако, работа с SQLite напрямую требует написания большого количества однотипного кода, поэтому в качестве обертки над SQLiteбыла использована легковеснаябиблиотека StorIO [24].
StorIOпозволяет описывать таблицы базы данных в виде data классов, доступных в языке Kotlin, с помощью аннотаций. Таким образом, в одном классе можно описать создаваемую таблицу в базе данных, а также тип, объектами которого другие классы будут оперировать для работы с базой данных. Например, на схеме 3.1. приведено описание таблицы сообщений в виде класса MessageEntity.
@StorIOSQLiteType(table = TABLE_NAME)
dataclassMessageEntity @StorIOSQLiteCreatorconstructor(
@StorIOSQLiteColumn(name = COLUMN_UUID, key = true) valuuid: String,
@StorIOSQLiteColumn(name = COLUMN_TYPE) val type: Int,
@StorIOSQLiteColumn(name = COLUMN_TIMESTAMP) val timestamp: Long,
@StorIOSQLiteColumn(name = COLUMN_SENDER_UUID) valsenderUuid: String,
@StorIOSQLiteColumn(name = COLUMN_RECIPIENT_UUID) valrecipientUuid: String,
@StorIOSQLiteColumn(name = COLUMN_SENDER_FULL_NAME) valsenderFullName: String?,
@StorIOSQLiteColumn(name = COLUMN_RECIPIENT_FULL_NAME) valrecipientFullName: String?,
@StorIOSQLiteColumn(name =COLUMN_TEXT) val text: String?,
@StorIOSQLiteColumn(name = COLUMN_IMAGE_RELATIVE_URL) valimageRelativeUrl: String?,
@StorIOSQLiteColumn(name = COLUMN_CALL_STATUS) valcallStatus: Int?,
@StorIOSQLiteColumn(name = COLUMN_CALL_UUID) valcallUuid: String?
)
Схема 3.1. КлассMessageEntity
private fun getConversationMessageQuery(
fromTimestamp: Long,
userUuid: String,
recipientUuid: String
) = Query.builder()
.table(MessageEntityContract.TABLE_NAME)
.where(
"$COLUMN_TIMESTAMP > ? " +
"AND ($COLUMN_SENDER_UUID = ? OR $COLUMN_RECIPIENT_UUID = ?) " +
"AND ($COLUMN_SENDER_UUID = ? OR $COLUMN_RECIPIENT_UUID = ?)"
)
.whereArgs(fromTimestamp, userUuid, userUuid, recipientUuid, recipientUuid)
.build()
Схема 3.2. Метод getConversationMessageQuery
Одной из удобных функций библиотеки StorIO, ради которых ее и стоит использовать, является возможность создания запросов к базе данных с помощью паттерна Builder [25]. Например, можно описать запрос на получения всех сообщений в чате, отправленных после определенного момента (схема 3.2).
Всего в локальной базе данных мобильного приложения хранятся 3 не связанные между собой таблицы: сообщения, показатели здоровья, медицинские события. Таблица сообщений используется для хранения всей истории переписки, две другие таблицы – для обеспечения подхода Offline-first [10] при работе пациента с медицинской картой. Для каждой таблицы в приложении создается отдельный класс хранилища, наследуемый от BaseLocalStore, содержащего в себе удобные методы для работы с StorIO. Один из таких методов принимает на вход запрос к базе данных, а возвращает список объектов, полученных по запросу, в виде потока RxJava (cхема 3.3).
fun getAllByQuery(query: Query): Single<List<T>> =
storIOSQLite.get()
.listOfObjects(objectClass)
.withQuery(query)
.prepare()
.asRxSingle()
Схема 3.3. Метод getAllByQuery
Для отправки REST-запросов использовалась библиотека Retrofit [26]позволяющая описывать запросы к серверу в виде интерфейсов, реализация которых создается с помощью кодогенерации. Для примера, рассмотрим интерфейсAccountRestApi для работы с аккаунтом, в котором описаны методы получения текущего аккаунта с сервера и обновления аккаунта на сервере (cхема 3.4).
interfaceAccountRestApi {
@GET("/account")
fungetAccount(): Single<UserModelWrapper>
@Multipart
@POST("/account")
funupdateAccount(
@Part("userRequest")userRequest: UserModelWrapper,
@Partimage: MultipartBody.Part?
): Single<UserModelWrapper>
}
Схема 3.4. Интерфейс AccountRestApi
Как видно на схеме 3.4, Retrofit позволяет отправлять запросы на сервер буквально в несколько строк. Для создания запросов используются специальные аннотации, применяемые к методам и их параметрам. Возможность использования в качестве возвращаемого значения методов потоков данных RxJava и автоматическоепреобразование Kotlin объектов в формат JSON и обратно делают использование этой библиотеки еще более удобным.
Однако, Retrofit не поддерживает протокол WebSocket, поэтому дополнительно была использована библиотека Scarlet [27], позволяющая описывать взаимодействие с сервером в похожем стиле с использованием протокола WebSocket. На схеме 3.5. представлен интерфейс, ответственный за получение от сервера и отправку на сервер сообщений.
interfaceChatSocketApi {
@Receive
funobserveEvents(): Flowable<WebSocketEvent>
@Send
funsendMessage(message: MessageRequestWrapper)
}
Схема 3.5. Интерфейс ChatSocketApi
Для хранения данных на сервере была выбрана СУБД MySQL [28]. Она широко распространена и поддерживается фреймворкомSpring.Всего в базе данных на сервере хранится 9 таблиц (рис. 3.1).
За хранение данных аккаунтов пользователей отвечают таблицы «patients» и «doctors». Было принято решение разделить пользователей на два типа: пациент и врач, так как пользовательские сценарии для пациентов и врачей кардинально отличаются, и в будущем не предполагается создание регистрации аккаунтов, имеющих права на действия для обоих типов.
Пациенты в приложении могут инициировать общение со врачами через чат, для чего в базе данных сервера были созданы две таблицы: «conversations» и «messages». Таблица «conversations» связанас «patients», «doctors» и «messages» связями one-to-many.То есть, у любого пользователя может быть сколь угодно большое число переписок, состоящих из сколь угодно большого числа сообщений.
Двумя другими таблицами в базе данных являются «body_parameters» и «medical_events». Они представляют собой записи пациента о показателях здоровья и медицинских событиях, и связаны с таблицей «patients» связями one-to-many. Таким образом, пациент может добавлять в приложение неограниченное число записей. Важной функцией приложения является возможность предоставления пациентом врачу доступа к медицинским записям:таблица «medical_accesses» хранит для пары пациент-врач доступные для предоставленные для просмотра типы записей.