Чтобы направить всю энергию системы в необходимом направлении, нужно эту систему ограничить правилами.
Привет!
Продолжаем серию статей об архитектурном дизайне мобильных приложений. Под катом поговорим о проектировании слоёв UI.
Добро пожаловать!
Эволюционируя, различные виды живых организмов сталкивались с элементарными задачами, которые необходимо было решить для обеспечения собственного выживания.
Нужно усваивать энергию из внешней среды? — решение: фотосинтез.
Нужно обеспечить разнообразие в генах? — решение: разделение по гендерам.
Чем сложнее организм становился, тем более высокоуровневые задачи ему приходилось решать.
Время шло. Появились виды, у которых приоритет вопросов выживания значительно снизился, и ресурсы отдельного индивида оказались частично незадействованными.
Эволюция приняла новый вектор. Большая часть естественных проблем, будь то пропитание, защита от хищников, защита от непогоды и прочее — была так или иначе решена, и дальше развиваться, вроде как, было некуда, но тут сыграла свою роль фантазия.
Дальнейшая эволюция уже не преследовала целей обеспечить выживания, а источником новых задач стало сознание, породив такие векторы развития, как «культура» и «наука». Фактически, образование и воспитание напрямую не являются факторами, обеспечивающими выживание индивида, без них вполне можно обойтись, живя в лесу.
Образование и воспитание не являются естественной целью, поставленной природой, их породил сам человек.
Дальнейшее развитие вида обеспечивается целями, которые этот вид сам себе придумывает.
И каждая из этих целей — это ограничение.
Нельзя громко разговаривать в библиотеке. Нельзя чавкать за столом. Нужно много тренироваться: писать, рисовать, печатать, играть на гитаре — без труда никак, и это — насилие над собой.
Чтобы развиваться, человек сам ставит себя в рамки, которые сам же и придумывает.
Хотите, чтобы ваша программная система была на пике эволюции?
Хотите, чтобы ваш продукт динамично развивался?
— задайте проекту жёсткие рамки и векторы расширения.
Культура программирования заключается в том, что инженер пишет не так, как ему хочется, а так, чтобы соответствовать правилам.
Ничто не истинно.
Всё дозволено.
О чём речь?
Видовое разделение
В предшествующей статье, посвящённой преимущественно архитектурному дизайну сервисного и транспортного уровней мобильных приложений (далее — МП), была дана подсказка о том, какими предпосылками следует руководствоваться в ходе проектирования уровня 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