№1 в разработке мобильных решений для бизнеса
109,95
рейтинг
24 февраля 2015 в 17:19

Разработка → Архитектурный дизайн мобильных приложений: часть 2 tutorial

Чтобы направить всю энергию системы в необходимом направлении, нужно эту систему ограничить правилами.


Привет, Хабр! Продолжаем серию статей об архитектурном дизайне мобильных приложений. Под катом поговорим о проектировании слоёв UI. Добро пожаловать!

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

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

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

Дальнейшая эволюция уже не преследовала целей обеспечить выживания, а источником новых задач стало сознание, породив такие векторы развития, как «культура» и «наука». Фактически, образование и воспитание напрямую не являются факторами, обеспечивающими выживание индивида, без них вполне можно обойтись, живя в лесу.

Образование и воспитание не являются естественной целью, поставленной природой, их породил сам человек.

Дальнейшее развитие вида обеспечивается целями, которые этот вид сам себе придумывает.

И каждая из этих целей — это ограничение.

Нельзя громко разговаривать в библиотеке. Нельзя чавкать за столом. Нужно много тренироваться: писать, рисовать, печатать, играть на гитаре — без труда никак, и это — насилие над собой.

Чтобы развиваться, человек сам ставит себя в рамки, которые сам же и придумывает.

Хотите, чтобы ваша программная система была на пике эволюции?
Хотите, чтобы ваш продукт динамично развивался?
— задайте проекту жёсткие рамки и векторы расширения.

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

P.s.
И да, любое правило — будь то трёхслойный архитектурный дизайн или культура держать столовые приборы — придумано людьми, а потому не является объективным в последней инстанции.

Ничто не истинно.
Всё дозволено.


О чём речь?


Видовое разделение

В предшествующей статье, посвящённой преимущественно архитектурному дизайну сервисного и транспортного уровней мобильных приложений (далее — МП), была дана подсказка о том, какими предпосылками следует руководствоваться в ходе проектирования уровня UI.

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

N.B. Прошу заметить, что идеология построения пользовательского интерфейса в качественных продуктах строго завязана на платформу приложения, и добиваться какой-то «кросс-платформенности» при выборе решений — занятие, прямо скажем, глупое.


Нельзя просто так взять — и приравнять системы, изначально спроектированные быть разными, пусть даже Navigation Drawer может выполнять те же функции, что и UITabBar.

Текущая статья преимущественно посвящена решениям, применимым к платформе iOS.

Строим UI


Продолжаем рассказывать замечательные истории

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

Предположим, что дизайн вашего приложения рисовал не умалишённый кретин, а более-менее адекватный художник, знакомый с понятием «UX», и способный удобно и органично вписать все элементы управления в цепочки экранов на мобильном устройстве.



На примере какого-нибудь банковского приложения, интерфейс делится на типичные истории, вроде: «Приветствие», состоящее из нескольких слайдов, представляющих приложение; «Авторизация» с полями логинов-паролей-email’ов и прочих; наконец, основного экрана, состоящего из нескольких логических частей: «Главная» (про ваши деньги), «Платежи и переводы», «Карта» с банкоматами и отделениями и так далее.

Каждая история включает в себя несколько экранов (или «страничек»), позволяющих пользователю совершать те или иные действия.
«Платежи и переводы»: зайти на список платежей; зайти в раздел «Мобильная связь»; выбрать оператора; ввести номер телефона и сумму пополнения, выбрать источник денег — вашу кредитную карту; оплатить.

На сервисном уровне присутствуют все необходимые инструменты, способные обеспечить UI необходимыми данными и рычагами воздействия на back-end.

Для разделения на сущности я предпочитаю использовать не классический MVC, а его модификацию MVP, позволяющую создавать более абстрактные контроллеры, и разбросать код, связанный с моделью данных, по классам, непосредственно являющимся «потребителями» этих данных.

Для каждой пользовательской истории можно выделить три ключевых вида сущностей. Это View, ViewController и Helper.

ViewController — классы, выступающие контроллерами отдельных логических единиц интерфейса; как правило, логической единицей выступает страничка приложения, но при более сложной вёрстке бывает полезно выделить на странице дочерние контроллеры для разгрузки материнского класса.

View — это классы, непосредственно представленные на ваших storyboard’ах: наследники UIView, UITableViewCell, UILabel, UIButton и так далее.

Helper — классы-утилиты, выполняющие индивидуальные или делегируемые обязанности. К ним относятся утилиты для форматирования строк, классы, следующие протоколам UITableViewDataSource и UITableViewDelegate, и так далее.

Поговорим о частностях.

ViewController


Учимся у конкурентов

Вообще, iOS-разработчикам следовало бы многому поучиться у своих коллег, изготавливающих ПО под Android.

Последние в некоторой степени унаследовали всю ту чопорность и архитектурность, которые присущи классическим Java-инженерам, а потому архитектурный дизайн действительно классных Android-приложений выглядит гораздо стройнее, чем архитектурный дизайн классных iOS-приложений.

Первое ограничение, которое у себя ввели Android-коллеги — это неспособность передавать сложные объекты между контроллерами (activity) — для этого дополнительно необходимо обеспечить сериализацию этих объектов.

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



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

И, конечно, второе ограничение, которому следовало бы поучиться у Android-разработчиков — у них в SDK в качестве делегата таблицы используется «Adapter» — и это отдельный класс, а не протокол, поэтому слияние таких вещей, как view controller и UITableViewDelegate/UITableViewDataSource — попросту невозможно.

N.B. Открытым текстом. UITableViewController — это прямое нарушение принципов SOLID, и тот инженер в Apple, который придумал этот класс — совершил серьёзную ошибку, из-за которой сейчас тысячи разработчиков по всему миру считают нормальным вешать на view controller по дюжине протоколов, вроде UITableViewDataSource и UITableViewDelegate.


View


Абстрагируем и делим на стили

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

MVC:
RMREntity *entity = [self entityForIndexPath:indexPath];
cell.title = entity.name;
cell.subtitle = entity.shortDescription;

MVP:
RMREntity *entity = [self entityForIndexPath:indexPath];
[cell fillWithEntity:entity];

Второй подход позволяет абстрагироваться от структуры UI, сосредоточив всю логику контроллеров и helper’ов вокруг обрабатываемого типа данных — RMREntity.

Таким образом, за интерфейсом –fillWithEntity: можно спрятать десяток классов-наследников RMRTableViewCell, каждый из которых будет способен по-своему отрисовать entity. При этом, для каждой UITableView будет использовать один и тот же UITableViewDataSource, позволяя значительно сэкономить на написании boilerplate-кода.

Итого.
Первое: не забывайте: view могут наследоваться не только от классов SDK, но и друг от друга.

“Архитектурный дизайн приложения должен кричать о том, что это за приложение. Если вы видите чертежи библиотеки, вы точно понимаете, что это — библиотека: в ней есть читальные залы и стеллажи с книгами!”

У вас в приложении есть тип данных Message? Сделайте под него абстрактную ячейку MessageCell — и наследуйте от неё другие ячейки! Каждый класс вашего кода должен детерминированно выполнять свою функцию, и не допускать логики, которой в нём не должно быть.

Второе. Помните, я упоминал дизайнера, который знаком с «UX»?

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

Помимо цветов, у хорошего дизайнера для каждого проекта всегда будет готова таблица «стилей»:

Заголовок: Helvetica Bold, 36pt.
Подзаголовок: Helvetica Medium, 24pt.
Количество Денег: Helvetica Light, 18pt, Light Blue.
И так далее.

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

К примеру, можно создать абстрактный класс Label, который при любой инициализации будет опрашивать фабричный метод –fontStyle, возвращающий стиль шрифта — и применять этот стиль.

Наследники, в свою очередь, будут просто возвращать необходимый стиль написания. Таким образом, у вас под рукой будут классы: HeaderLabel, SubheaderLabel, MoneyAmountLabel… и, о чудо! Их можно будет просто подставлять в поле «Class» прямо внутри Interface Builder’a применимо к созданной вёрстке.

Helper


Тестируемый дизайн

Уже было сказано о тех вещах, которыми контроллеры не должны заниматься.
Так кто же должен? — ответ: утилиты.

Самое главное, о чём нужно помнить, и что нужно соблюдать — это простое правило: утилитарные классы не должны нести никакой информации. У них не должно быть публичных свойств, и вся их логика должна быть «сквозной» с минимумом void-методов, как будто мы пишем в функциональном стиле: метод всегда должен возвращать что-то.

За счёт подобного «отсутствия состояния» утилиты достаточно безопасно использовать на разных слоях логики приложения, однако не стоит лениться создавать отдельных помощников для контроллеров, представлений, бизнес-логики и прочего.

Утилитарные классы — или hepler’ы — это первые кандидаты на роль тестируемых сущностей.

Форматирование дат, сортировка массивов, создание изображений из других изображений — это всё алгоритмика, которая легко подпадает под последовательность «arrange, act, assert», и вполне может быть принесена в жертву автоматическим тестам.

Заключение


Итоги

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

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

Тем не менее, я этого сознательно не делал: материал и так намеренно простой, ибо исходный код не должен быть сложным.

Оставайтесь с нами.
Автор: @BepTep
REDMADROBOT
рейтинг 109,95
№1 в разработке мобильных решений для бизнеса

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

  • +1
    > таким образом вы обеспечите своему приложению минимум побочных эффектов, присущих императивному программированию
    С этого момента, пожалуйста, поподробнее.
    • +4
      Предположим, в приложении используется база данных, к которой обращается сервисный уровень с запросами на предмет закешированной информации.

      Из этой базы данных вычитывается некая сущность DBEntity. Соответствующий EntityService генерирует на её основании сущность Entity, которая не несёт с собой обвес из логики, доставшийся от NSManagedObject'a.
      Затем Entity отправляется наверх, на уровни UI.

      При недобросовестном подходе в эту сущность могут быть внесены изменения.
      Таким образом, передавая её по цепочке между экранами, мы получаем ситуацию, когда данные «на руках» будут отличаться от тех, что записаны в БД. Более того, информация может не обладать необходимой консистентностью, которую изначально обеспечил EntityService, но которая впоследствии была нарушена недобросовестным разработчиком.

      Есть несколько подходов к решению подобных проблем.
      Во-первых, делать неизменяемые модельные объекты.
      Во-вторых, не передавать их куда попало — об этом и сказано в статье.

      Посудите сами, какое искушение: Entity изначально попадает на первый экран, там дорабатывается до необходимого состояния, затем отдаётся на второй экран.
      Бывают такие ситуации? По неопытности — конечно, бывают.
      Только вот тут же возникает зависимость экрана-2 от экрана-1 — отныне второй экран не может существовать отдельно от первого, ведь он ожидает «доработанные» данные.

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

      О побочных эффектах
      Идея функционального программирования декларирует использование методов, результат работы которых детерминированно задаётся входными параметрами.
      В ходе же работы императивной программы результат выполнения того или иного метода может зависеть ещё и от сторонних переменных, на состояние которых влияют внешние условия. Такие, как порядок выполнения предшествующих инструкций исходного кода, к примеру.
      • +1
        Второй экран ожидает определённый объект, что значит «доработанные» данные? Можете привести пример этих «недобросовестных» изменений?
        • +2
          Пример (слегка надуманный, но суть дела иллюстрирует):
          Приложение — магазин для яхтсменов.

          Экран-1 представляет собой список судоходных канатов.
          Каждый канат — определённого типа и длины.
          Длина в модельной сущности указана в миллиметрах.

          Rope
          +type: Enum;
          +length: Real;

          Когда пользователь выбирает один из заказов — открывается Экран-2 с детальной информацией.

          От заказчика поступило указание сделать такое же приложение, но для США.
          А в США длина измеряется в дюймах.
          Нерадивый программист модернизирует Экран-1 так, что в модельной сущности поле длины перевычисляется так, чтобы на Экран-2 попадало значение в дюймах.

          rope.length = rope.length / 25.4f; // TODO: Вынести «магию» в константы.

          Но вот незадача: Экран-2 позволяет делать заказы, а для этого приложение отсылает соответствующий HTTP-запрос, и в запрос сериализуется эта самая модельная сущность.
          В итоге сервер получает заказ на канат с неправильно рассчитанной длиной.
          • 0
            Экран-1 и Экран-2 должны сами уметь вычислять в дюймах? Хм, скорее всего, это должен уметь делать сервер.
  • +1
    BepTep, в прошлой статье в комментах вы отписали:
    Я думаю, следующая статья от меня так или иначе будет подробнее раскрывать эту архитектуру.
    Этот подход используется практически на всех наших новых проектах («новый» — возраст от полугода), поэтому попросту описать один из них будет довольно легко.

    Все еще ждем.
    • +1
      Не хотелось бы смешивать теорию с практикой — может получиться каша.
      Так, в этой статье добавились недостающие элементы подхода проектирования — уровни представления.

      В любом случае, благодарю за напоминание. С меня статья.
      Благо, не так давно наши юристы дали добро на раскрытие некоторых деталей реализации… но это уже совсем другая история.
  • +2
    десяток классов-наследников RMRTableViewCell

    Не лучше/проще ли уйти от наследования в сторону протоколов?
    • +1
      Во многом зависит от ситуации.

      Если я правильно понял вопрос, Вы предлагаете использовать нечто, вроде шаблона «Стратегия»: ячейка будет предоставлять слот для алгоритма собственного заполнения.

      Это удобно, когда в приложении для нескольких типов данных используется одно и то же представление.
      Например, ячейка с заголовком, подзаголовком и пиктограммой — нечто универсальное.
      Один и тот же класс представляет эту ячейку на Interface Builder'e, а необходимый алгоритм присваивается в зависимости от сущности, которую в ячейку нужно вписать.

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

      И да, я бы не рекомендовал подобный подход. Универсальность — это благо, но только до определённого предела.

      Часто бывает так, что вёрстка может разниться в зависимости от типа данных. Где-то мелкую иконку добавить, где-то заголовок сделать жирным, где-то ещё одно поле на экран вывести — море вариантов.

      В этом случае создаются классы ячеек вместе с выделенной под них вёрсткой. Один класс — один layout. Таким образом, чисто с точки зрения информатики, мы не нагружаем один и тот же макет ответственностью за отрисовку различных типов данных, т.е. соблюдаем принцип единственной ответственности.

      Да, существует правило о том, что композицию зачастую лучше использовать, чем наследование. Но в данном контексте подключается ещё и вопрос вёрстки, который смещает равновесие.

      Надеюсь, я правильно понял Ваш вопрос.
      • +1
        Надеюсь, я правильно понял Ваш вопрос.

        Не совсем :)
        Я имел ввиду некий протокол
        @­protocol DataObjectRenderer
        - (void)reloadCellWithDataObject:(DataObject *)object;
        @­end
        

        В таком случае нет прямой необходимости создавать базовый класс и ячейки (как и любые контролы) могут максимально абстрагироваться, декларируя только то, что они могут рендерить некий DataObject.
        Кроме того, с таким подходом нас не интересует тип контрола: ячейка таблицы, ячейка коллекции, UIView, etc.

        Наследование же привносит ненужную сложность и увеличивает связанность, имхо.
        • +1
          Ага, я понял.

          Это действительно удобно, когда необходимо один и тот же объект отрисовывать несколькими способами.
          По поводу ненужной сложности наследования позволю себе с Вами не согласиться: Ваш подход, фактически, предполагает то же самое, только «абстрактный» класс RMRTableViewCell заменяется на протокол DataObjectRenderer.

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

          Я — не сторонник писать слишком абстрактные вещи там, где можно ввести более строгую типизацию.
          iOS SDK — абстрактен, и этого достаточно. А про «кричащий» архитектурный дизайн я написал ещё в статье.

          Сужение абстракции в приложении — да, это ограничение, но оно ограждает нас от ошибок.
          В то же время, рекомендую вспомнить принцип You Ain't Gonna Need It: действительно ли Вам необходимо объединять принципиально разные представления под эгидой одного протокола? Не будет ли это over-engineering'ом? Не придаёте ли Вы одному протоколу слишком много информационной значимости?

          Решать Вам.
          • +2
            Мне кажется обе наши точки зрения вполне валидны.
            Дело в том, что я противник наследования в целом (ну кроме как от классов iOS SDK), потому как люди очень часто нарушают другой принцип — принцип подстановки Лисков.
            Касательно нескольких протоколов для одной ячейки — да, может быть такая проблема, но наследование от нее тоже не защищает, имхо.
            Главное дисциплина!
            • +1
              наследование от нее тоже не защищает, имхо.
              Скажем, у меня бы рука не поднялась реализовать протокол AccountRenderer в ячейке MessageCell.

              Но да, всегда нужно соблюдать равновесие при проектировании.

              А про дисциплину — в яблочко. Это, пожалуй, самое важное.
  • +3
    BepTep спасибо за статьи. Приятно видеть, что у вас есть процесс переноса знаний, практик и подходов между мобильными платформами. В большинстве проектов, где я участвовал команды были довольно обособленные, каждая со своим багажом и наработками. Поэтому в чужой монастырь со своим уставом мало кто рисковал ходить.

    Приезжайте к нам в Университет Иннополис, будет интересно обсудить разработку архитектуры и другие практики программной инженерии. Мы вам расскажем как и чему мы учим студентов на курсе Architectures for Software Systems, а вы расскажете про то, что вы используете. Хочется быть ближе к индустрии, давать студентам востребованные знания, которые соответствуют методологиям из реального мира. Кстати, как вы отнесетесь к предложению прочитать гостевую лекцию?
    • +1
      Я положительно отношусь к подобного рода мероприятиям.
      Отписал Вам в личные сообщения.
  • +2
    BepTep Спасибо за ваши посты! Очень интересно! Жду продолжения. Пожалуйста, ответьте на вопрос от начинающего. :) Вопрос относится к этой вашей иллюстрации:

    Правильно ли я понимаю, что подразумевается передача данных в методе prepareForSegue? Дело в том, что мне очень не нравится яблочный паттерн «сегвеев», но может быть, я чего-то не понимаю, а вы с вашим более обширным опытом как раз меня переубедите. :)
    Смотрите сами (сорри только на свифте умею):
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        switch segue.identifier! {
        case "showAccountDetails":
            let vc = segue.destinationViewController as AccountDetailsViewController
            vc.accountId = self.selectedAccount.accountId
        ...
        }
    }
    

    Недостаток 1: один метод на все случаи — с большим свичем внутри
    Недостаток 2: первый VC должен знать не только конкретную имплементацию следующего (класс AccountDetailsViewController), но и уметь правильно его сетапить (accountId) — нарушена та самая обособленность контроллеров
    Недостаток 3: на самом деле, элементарное поведение «перейти в AccountDetails» разбито на две части в совершенно разных местах контролера, так как помимо сниппета внутри prepareForSegue нам нужно было засетить selectedAccount внутри accessoryButtonTapped, и там же, собственно, вызвать performSegueWithIdentifier. Жуть!!! Код был бы гораздо читабельнее, если бы все это в одном месте, а именно accessoryButtonTapped
    Недостаток 4: В обоих местах мы захардкодили одну и ту же стрингу с айдишником сегвея — плохо. Либо — что тоже плохо — мы загрязнили свой контроллер какой-то левой константой, которая де факто вообще не про этот контроллер, а про сториборд, в котором сидит его nib.

    Все это мне кажется чрезмерной платой за
    Достоинство 1: наглядная стрелочка в XCode с формочкой аттрибутов.
    При этом, на самом деле в больших story это вовсе не наглядно, а превращается в стремное спагетти, особенно с суррогатными пустыми контроллерами из RBStoryboardLink. :)

    Как быть?? Я предлагаю как раз между контроллерами и Service Layer вместить еще один слой делегатов, отвечающий за правильную настройку переходов:
    func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
        delegate.didRequestDetailsForAccount(accounts[indexPath.row])
    }
    

    Таким образом, снимаем груз ответственности с первого VC, делая его по-настоящему обособленным: все, что он должен уметь — это показать список аккаунтов и дернуть делегат в нужный момент, мол, юзер хочет видеть details. Как вы думаете, такой подход имеет право быть?
    • +2
      Этот подход не просто имеет право быть — он обязан быть.
      Другой вопрос: как это корректно организовать?

      Обратите внимание, в конце предыдущей статьи я упоминал архитектурный шаблон VIPER, в рамках которого самый интересный элемент (на мой взгляд) — это непосредственно Router, т.е. сущность (или сущности), ответственная за навигацию по приложению.

      Существует несколько наработок по этому поводу (включая предложенный способ конечного автомата). Если получится сделать из них что-то действительно стройное — обязательно напишу по этому поводу статью.

      Главное опасение касательно данной задачи заключается в том, что сущность Router может быстро разрастись и покрыться методами, которые без рефакторинга будет невозможно реиспользовать.
      В то же время, другая проблема — это over-engineering, вероятность попросту довести абстракцию до такого уровня, на котором её будет сложно понять.

      Кстати, введение подобного элемента в дизайн исходного кода никак не конфликтует с Достоинством 1.

      И да, небольшой, но интересный момент.
      Обратите внимание, как именуются «делегаты» на платформе Android:
      OnItemClickListener
      OnScrollListener
      OnTouchListener
      — они все «говорящие».

      А что может Вам сказать название «delegate» — объект типа UITextFieldDelegate?
      Если Вы не знакомы с iOS SDK — то ничего оно Вам не скажет.

      В противовес, небольшой пример в рамках Вашего кода:
      func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
          router.navigateToAccountDetails(accounts[indexPath.row])
      }
      Я — за явное именование. Причём в данном случае оно никак не противоречит конвенции Apple.

      И последнее.
      Недостаток 4: В обоих местах мы захардкодили одну и ту же стрингу с айдишником сегвея — плохо. Либо — что тоже плохо — мы загрязнили свой контроллер какой-то левой константой, которая де факто вообще не про этот контроллер, а про сториборд, в котором сидит его nib.
      Небольшой спойлер-наводка: попробуйте использовать утилиты, которые будут генерировать и собирать константы на основании файлов вёрстки — таким образом Вы избавитесь от дублирования средствами автоматизации.

      Спасибо за вопрос. Проблема довольно насущная.
      • 0
        Спасибо за развернутый ответ!
        Вы навели меня на VIPER, теперь изучаю этот подход. К сожалению, он довольно молодой, все статьи в гугле не старше года-двух, и материалов действительно очень мало, в особенности — конкретных примеров.
        • +1
          Мы недавно проводили изучение архитектуры VIPER и пришли к выводу, что она несколько недоработана. Основная проблема заключается как раз-таки в слое Router. Единственное(?) решение, которое действительно позволит соблюсти все принципы, это отказ от Storyboard (ну или как минимум отказ от Segues). На мой взгляд, это будет шаг назад, т.к. Storyboards и Segues во многих случаях очень удобны и наглядны. У меня есть наработки по вытаскиванию всей логики переходов в отдельный слой (контроллеры все так же ловят сегвеи, но сразу же передают их в Router через Presenter). К сожалению, какого-то адекватного законченного решения пока нет. Так как это наша первая попытка реализации слоистой архитектуры, вся система показалась нам довольно громоздкой и мы не увидели каких-то радикальных преимуществ. Ну и смущает отсутствие каких-то внятных примеров реализации (пара-тройка демо-приложений с одним-двумя экранами не в счет), а также многозначительное молчание со стороны авторов подхода.
          Гораздо более удачной оказалась комбинация различных подходов: MVCP, MVVM, SOA. Как только мы отвязались от жесткого следования структуре VIPER, большинство возникших проблем исчезли сами собой.
  • 0
    Первое ограничение, которое у себя ввели Android-коллеги — это неспособность передавать сложные объекты между контроллерами (activity) — для этого дополнительно необходимо обеспечить сериализацию этих объектов.

    каждый контроллер должен быть максимально обособленным

    Вы здесь связали два несвязанных принципа. То, что нельзя передавать сложные объекты предварительно не реализовав сериализацию, не является посылом к максимальной обособленности контроллеров.
    Данные простых типов (String, Integer и т.д) тоже сериализуются при передаче между Activity. Просто о серилизации простых типов подумали разработчики Android, а о кастомных (сложных) типах должен позаботиться разработчик.

    Правильным посылом к максимальной обособленности контроллеров, как мне кажется, является принцип вызова Activity через Intent.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка