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

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

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



В такой модели появляется сильная зависимость контроллов друг от друга, что влечет за собой сразу целый ряд проблем. Основная из которых – невозможно использовать в другом приложении, например, контролл листинга без корзины, а переиспользование является главной нашей целью. В этой ситуации модель контролл – сервис подойдет больше. Пусть ViewModel и дальше занимается отображением всей актуальной информации, однако вопрос взаимодействия с другими контроллами унесем на уровень сервисов. Сами сервисы – singleton’ы, их интерфейсы добавляются в DI контейнер. Таким образом, например, при добавлении товара в корзину, достаточно будет один раз проверить есть ли в DI контейнере сервис корзины, и если да – отображать кнопку добавления в корзину, ну и осуществлять сами подписки на изменения в корзине. Тоже самое и с сервисом корзины. Товары можно добавлять откуда угодно, например с сайта, а в приложении только отображать корзину. Корзина в таком случае не зависит от наличия листинга. И сам контролл теперь можно переиспользовать во всех других приложениях внутри компании.

Перейдем конкретно к коду. Допустим, существует API с методами:

  • поиск товаров внутри категории (api/catalogue/search)
  • получение основной информации о товаре по его SKU (api/catalogue/materials)
  • получение информации о цене товара по его SKU (api/calatogue/price)
  • получение информации о остатках товара по SKU (api/catalogue/remains)
  • добавление товара в корзину (api/cart/add)
  • удаление товара из корзины (api/cart/remove)

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

Пред созданием контролла листинга, хотелось бы немного затронуть тему самого обращения к API методам. При создании клиентского приложения, надо всегда учитывать, то что не все запросы могут доходить до сервера (в какой-то момент времени интернет может просто пропасть). По этому, необходимо реализовать слой обращения к данным, которые учитывая эту ситуацию, смогут сначала выстоиться в очередь запросов, и, при появлении интернета осуществить запрос еще раз. В такой ситуации, чтоб не тормозить работу клиента с приложением, можно, в зависимости от необходимой точности данных – отображать данные из кэша. Данная тема заслуживает отдельного цикла статей. Как мы решаем данную ситуацию в Mobile Dimension, если будет такой интерес – мы опишем в отдельном цикле статей.

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

Слой бизнес логики не должен знать о том, каким образом приходят данные, а учитывая то, что все сервисы как раз таки находятся на уровне бизнес логики – они будут обращаться к «виртуальному» слою данных, который по запросу может вернуть результат (успешный или не успешный). Надо учитывать, что запрос может обрабатываться неопределенное количество времени, однако, как писала моя коллега в своей статье про интерфейсы, интерфейс должен моментально реагировать на любое нажатие. Поэтому, любой запрос на уровне данных (DAL) имеет события, на которые можно подписаться – это событие успешного запроса с результатом, и неуспешного запроса с ошибками. В общем случае — это билет (tiket) в котором есть сам запрос, метод обращения к API, ну и сами события. Таким образом сервис отправляет запрос, подписывается на эти события и, в зависимости от пришедшего результата, вызывает собственные события, на которые подписывается какой угодно контролл. В самом контролле, по нажатию инициализируется начало работы по запросу в сервисе (BLL), в этот момент показывается индикатор загрузки и происходит подписка на событие результата.

Например, код вызова запроса добавления товара в корзину, на уровне сервиса будет выглядеть следующим образом:

public event Action<string, string> OnProductAddedSuccessfully;
public event Action<string> OnProductAddedFailure;
 
public void StartAddingProduct(string sku)
{
    var newProduct = new BasketProduct() { Sku = sku, State = Enums.RequestState.InProgress };
    //сохраняем локально новый экземпляр возможного продукта в корзине
    _products.Add(newProduct);
    //обращаемся к хранилищу корзины, получаем билет, в котором выполняется запрос...
    var tiket = _basketRepository.AddToBasket(sku);
    tiket.OnSuccess += (response) =>
    {
        //результат содержит поле, которое указывает на то удалось ли добавить товар в корзину
        if(response.Data.Succseeded != null && response.Data.Succseeded.Value)
        {
            //каждый товар в корзине должен содержать свой собственный идентификатор, который тоже приходит в ответе (positionId)
            newProduct.PositionId = response.Data.PositionId;
            newProduct.State = Enums.RequestState.Succseeded;
            //оповещаем подписчиков (контроллы), что товар добавлен успешно
            OnProductAddedSuccessfully?.Invoke(newProduct.Sku, newProduct.PositionId);
        }
        else
        {
            //оповещаем подписчиков, что товар не удалось добавить
            OnProductAddedFailure?.Invoke(sku);
            newProduct.State = Enums.RequestState.Failed;
        }
    };
    //дополнительный запрос на то, чтоб конкретизировать цену (необходимо при подсчете общей стоимости корзины)
    var priceTicket = _catalogRepository.GetPriceTicket(sku);
    priceTicket.OnSuccess += (response) =>
    {
        if(response.Data != null){
            //обновляем данные о цене
            newProduct.Price = response.Data.Price;
        }
    };
}

Сервисный метод добавления товара в корзину

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

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

public int? TotalCount
{
    get
    {
        return _basketService.TotalCount;
    }
}
 
 
public int? TotalPrice
{
    get
    {
        return _basketService.TotalPrice;
    }
}
void _basketService_OnProductAddedSuccessfully(string sku, string positionId)
{
    var product = Products.ToList().FirstOrDefault(x => x.Sku == sku);
    product.CountInBasket++;
    product.IsAddingInProfress = false;
    product.PositionIds.Add(positionId);
    //Рейз происходит не в сеттере поля, а именно в тот момент, когда в корзине действительно что-то поменялось
    RaizePropertyChanged(nameof(TotalCount), nameof(TotalPrice));
}

Сервисный метод добавления товара в корзину

Cами поля из ViewModel, отображаемые интерфейсом выглядят не так, как обычно, кода стало меньше а само обновление полей стало управляемым. Это является дополнительным бонусом такого подхода. Тут же могут существовать и другие ViewModel, если выносить их взаимодействие на уровень сервисов – код становится понятнее, его проще поддерживать.



Минусом такого подхода является сам момент дебага. Если вдруг, где-то на уровне сервиса или данных возникнет NRE или другие исключения – сама ошибка будет отображаться на уровне ViewModel. Visual Studio не определит на каком именно уровне возникла ошибка, так как по сути ошибка возникает в callback функции. Возможно это можно настроить где-то в самой студии, однако меня обычно спасает call-stack, в котором видно откуда именно пришла ошибка.

image image

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

Пример можно посмотреть на GitHub.

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

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


  1. ANTPro
    21.06.2017 13:54
    +1

    В первый раз пользуетесь системой контроля версий?


    1. sprodan8
      21.06.2017 16:28

      Нет, просто кое-кто забыл апрувнуть PR :)


  1. R2D2_RnD
    22.06.2017 13:50

    Ваши контролы нормально переживают специфику Android Activity/Fragment Lifecycle?
    Тесты при включенной опции «Don't Keep Activities» в настройках разработчика все проходятся или есть нюансы?


    1. MobileDimension
      22.06.2017 17:43

      Спасибо за хороший вопрос! Вообще у Xamarin.Forms интересная история с Activity Lifecycle в Android. Долгое время известным багом платформы было то, что приложение невозможно было поднять в принципе после того, как оно было убито ОС. После долгих обсуждений на форумах (пример), разработчики все-таки добавили этот баг в свою «internal feature tracking system». Но сейчас все хорошо, причем сервисы даже могут помочь контроллам возвращаться в исходное состояние, если это описать в соответствующих событиях. В данном примере это не предусмотрено, основной упор был сделан именно на взаимодействии 2 контроллов через сервисы.