Вкратце, делается примерно так:
– создаётся SPM-пакет, в который будут подкачиваться зависимости;
– для созданного SPM-пакета генерируется *.xcodeproj;
– рядышком создаётся iOS-проект;
– оба объединяются в один workspace, линкуются и т.п.
Для использования нужно сходить в папку Dependencies, там кастануть swift package generate-xcodeproj
Потом открыть верхнеуровневый workspace, запустить App и получить 200 во viewDidLoad.
Я думаю, пример в статье стоит рассматривать больше с «клиентской» стороны.
В предполагаемом мобильном приложении на языке Swift не будет никакой бизнес-логики, типа «Провести оплату». Т.е. наверняка будет какой-то вызов API для проведения оплаты, чтобы сервер провёл эту операцию, но реализовывать метод Execute() на стороне клиента не придётся.
На стороне клиента придётся реализовывать ленту с историей проведенных операций оплаты, в которой должны содержаться сущности разного рода: Cash Payment, Card Payment, PayPal Payment и другие. И в данной ситуации важным является именно то, как организовать модель данных, полученных от сервера, чтобы их можно было единообразно отобразить в единой ленте.
И перечисления в данной ситуации играют роль логических ограничений. Например, у сущности PaymentWithEnums физически не может быть воплощения, в котором будет содержаться и срок истечения карты expirationDate, и transactionAuthCode.
REST в чистом виде использовать далеко не всегда удобно.
Например, необходимо зарегистрировать пользователя в системе страховой компании.
Регистрация происходит с мобильного устройства, где вводить кучу полей (имя, фамилия, отчество, телефон, адрес электропочты и пр.) — мягко говоря, не самое лучшее решение.
В качестве оптимизации UX регистрацию можно разбить на два шага. На первом шаге пользователь вводит номер страхового полиса, и дальше на серверной стороне система уже сама может подтянуть имя, фамилию и пр., скомпоновав их в конечную «Учётную запись».
Если у пользователя нет полиса, или системе введенный номер полиса по какой-то причине неизвестен — инициируется второй шаг, где уже нужно вбивать все личные данные.
Для большинства клиентов страховой компании второй шаг никогда не придётся проходить, т.к. каждого из них можно однозначно идентифицировать по номеру полиса. Соответственно, количество полей ввода для регистрации на мобильном устройстве сокращается до одного.
С подобными предпосылками, возникает вопрос: как рассматривать процесс регистрации с точки зрения API?
Необходимо создать задачу/операцию «Зарегистрировать пользователя по номеру полиса»?
Хорошо. А что должно возвращаться в ответ на запрос ниже?
POST /users/register_with_insurance_number_operations
Будет возвращаться объект «Операция» с идентификатором?
И потом мобильное устройство должно отправлять ещё один запрос с этим идентификатором?
Это — не вариант, количество запросов с мобильного устройства нужно минимизировать.
Или в ответ на запрос будет возвращаться сразу объект «Учётная запись»? Это — неконсистентно.
Мы в компании остановились на том, что подход REST в чистом виде использовать для некоторых целей неудобно, и в качестве компромисса можно задействовать запросы «с глаголами». В ответ может прийти учётная запись:
Я бы не стал говорить, что MVPM может быть полезней MVVM.
Я бы сказал, что каждый из этих шаблонов может быть более уместен в тех или иных условиях.
Набор классов iOS SDK физически разделяет пользовательские истории на экраны, предоставляя логическую единицу в виде наследников UIViewController. Учитывая подобный подход, мы получаем возможность применения различных шаблонов (MVC, MVP, MVVM, MVPM) в рамках одного и того же приложения. Один экран = один шаблон в самом абсурдном случае.
Следует понимать, что, с одной стороны, программная логика в рамках экрана может потребовать разделения. Выделения сущностей, посвящённых той или иной ответственности.
С другой стороны, MVPM и MVVM (как и вообще любой шаблон проектирования) предполагают изначальный overhead по коду; MVPM и MVVM физически сложнее MVC и MVP.
Таким образом, там, где это действительно необходимо, может быть применен MVPM.
В большинстве случаев не стоит делать предварительной оптимизации, и городить сущности ради конвенции.
В качестве примера могу привести три граничных случая.
Первый: классический экран авторизации с логином и паролем.
Второй: экран с таблицей, выводящей набор модельных сущностей посредством типичных ячеек.
Третий: экран с таблицей, содержащей набор полей ввода, значения которых могут влиять друг на друга.
Внимание, викторина: какой из шаблонов где следует применять? (-:
Зачем в сигнатуре директива __kindof, когда здесь и так возвращаемый тип (UITableViewCell *), а наследование с полиморфизмом ещё никто не отменял? Привет Барбаре Лисков.
Этот подход не просто имеет право быть — он обязан быть.
Другой вопрос: как это корректно организовать?
Обратите внимание, в конце предыдущей статьи я упоминал архитектурный шаблон VIPER, в рамках которого самый интересный элемент (на мой взгляд) — это непосредственно Router, т.е. сущность (или сущности), ответственная за навигацию по приложению.
Существует несколько наработок по этому поводу (включая предложенный способ конечного автомата). Если получится сделать из них что-то действительно стройное — обязательно напишу по этому поводу статью.
Главное опасение касательно данной задачи заключается в том, что сущность Router может быстро разрастись и покрыться методами, которые без рефакторинга будет невозможно реиспользовать.
В то же время, другая проблема — это over-engineering, вероятность попросту довести абстракцию до такого уровня, на котором её будет сложно понять.
Кстати, введение подобного элемента в дизайн исходного кода никак не конфликтует с Достоинством 1.
И да, небольшой, но интересный момент.
Обратите внимание, как именуются «делегаты» на платформе Android:
Я — за явное именование. Причём в данном случае оно никак не противоречит конвенции Apple.
И последнее.
Недостаток 4: В обоих местах мы захардкодили одну и ту же стрингу с айдишником сегвея — плохо. Либо — что тоже плохо — мы загрязнили свой контроллер какой-то левой константой, которая де факто вообще не про этот контроллер, а про сториборд, в котором сидит его nib.
Небольшой спойлер-наводка: попробуйте использовать утилиты, которые будут генерировать и собирать константы на основании файлов вёрстки — таким образом Вы избавитесь от дублирования средствами автоматизации.
Это действительно удобно, когда необходимо один и тот же объект отрисовывать несколькими способами.
По поводу ненужной сложности наследования позволю себе с Вами не согласиться: Ваш подход, фактически, предполагает то же самое, только «абстрактный» класс RMRTableViewCell заменяется на протокол DataObjectRenderer.
В то же время, мы получаем ситуацию, когда в исходном коде присутствует излишне абстрактная сущность (ячейка), следующая некоему протоколу.
На мой взгляд, это — возможный источник ошибок, ведь велико искушение на эту же ячейку навесить ещё несколько протоколов, чтобы она же отрисовывала и другие объекты.
Т.е. потенциально возрастает вероятность нарушения принципа единственной ответственности и разрастания класса.
Я — не сторонник писать слишком абстрактные вещи там, где можно ввести более строгую типизацию.
iOS SDK — абстрактен, и этого достаточно. А про «кричащий» архитектурный дизайн я написал ещё в статье.
Сужение абстракции в приложении — да, это ограничение, но оно ограждает нас от ошибок.
В то же время, рекомендую вспомнить принцип You Ain't Gonna Need It: действительно ли Вам необходимо объединять принципиально разные представления под эгидой одного протокола? Не будет ли это over-engineering'ом? Не придаёте ли Вы одному протоколу слишком много информационной значимости?
Если я правильно понял вопрос, Вы предлагаете использовать нечто, вроде шаблона «Стратегия»: ячейка будет предоставлять слот для алгоритма собственного заполнения.
Это удобно, когда в приложении для нескольких типов данных используется одно и то же представление.
Например, ячейка с заголовком, подзаголовком и пиктограммой — нечто универсальное.
Один и тот же класс представляет эту ячейку на Interface Builder'e, а необходимый алгоритм присваивается в зависимости от сущности, которую в ячейку нужно вписать.
В этом случае Вам всё равно понадобится десяток классов, каждый из которых будет следовать протоколу этого алгоритма заполнения. Т.е. Вы выигрываете за счёт вёрстки, но сущностей от этого меньше не станет.
И да, я бы не рекомендовал подобный подход. Универсальность — это благо, но только до определённого предела.
Часто бывает так, что вёрстка может разниться в зависимости от типа данных. Где-то мелкую иконку добавить, где-то заголовок сделать жирным, где-то ещё одно поле на экран вывести — море вариантов.
В этом случае создаются классы ячеек вместе с выделенной под них вёрсткой. Один класс — один layout. Таким образом, чисто с точки зрения информатики, мы не нагружаем один и тот же макет ответственностью за отрисовку различных типов данных, т.е. соблюдаем принцип единственной ответственности.
Да, существует правило о том, что композицию зачастую лучше использовать, чем наследование. Но в данном контексте подключается ещё и вопрос вёрстки, который смещает равновесие.
Пример (слегка надуманный, но суть дела иллюстрирует):
Приложение — магазин для яхтсменов.
Экран-1 представляет собой список судоходных канатов.
Каждый канат — определённого типа и длины.
Длина в модельной сущности указана в миллиметрах.
Rope
+type: Enum;
+length: Real;
Когда пользователь выбирает один из заказов — открывается Экран-2 с детальной информацией.
От заказчика поступило указание сделать такое же приложение, но для США.
А в США длина измеряется в дюймах.
Нерадивый программист модернизирует Экран-1 так, что в модельной сущности поле длины перевычисляется так, чтобы на Экран-2 попадало значение в дюймах.
Но вот незадача: Экран-2 позволяет делать заказы, а для этого приложение отсылает соответствующий HTTP-запрос, и в запрос сериализуется эта самая модельная сущность.
В итоге сервер получает заказ на канат с неправильно рассчитанной длиной.
Не хотелось бы смешивать теорию с практикой — может получиться каша.
Так, в этой статье добавились недостающие элементы подхода проектирования — уровни представления.
В любом случае, благодарю за напоминание. С меня статья.
Благо, не так давно наши юристы дали добро на раскрытие некоторых деталей реализации… но это уже совсем другая история.
NSFetchedResultsController представляет паттерн MVC с использованием активной модели.
В данной статье предполагается использование пассивной модели, без оповещений о её изменениях.
До активных моделей я тоже постараюсь добраться в последующих статьях — это неплохая альтернатива, но требует немного иной идеологии.
Предположим, в приложении используется база данных, к которой обращается сервисный уровень с запросами на предмет закешированной информации.
Из этой базы данных вычитывается некая сущность DBEntity. Соответствующий EntityService генерирует на её основании сущность Entity, которая не несёт с собой обвес из логики, доставшийся от NSManagedObject'a.
Затем Entity отправляется наверх, на уровни UI.
При недобросовестном подходе в эту сущность могут быть внесены изменения.
Таким образом, передавая её по цепочке между экранами, мы получаем ситуацию, когда данные «на руках» будут отличаться от тех, что записаны в БД. Более того, информация может не обладать необходимой консистентностью, которую изначально обеспечил EntityService, но которая впоследствии была нарушена недобросовестным разработчиком.
Есть несколько подходов к решению подобных проблем.
Во-первых, делать неизменяемые модельные объекты.
Во-вторых, не передавать их куда попало — об этом и сказано в статье.
Посудите сами, какое искушение: Entity изначально попадает на первый экран, там дорабатывается до необходимого состояния, затем отдаётся на второй экран.
Бывают такие ситуации? По неопытности — конечно, бывают.
Только вот тут же возникает зависимость экрана-2 от экрана-1 — отныне второй экран не может существовать отдельно от первого, ведь он ожидает «доработанные» данные.
Я привожу аналогию с побочными эффектами императивного программирования, потому что обрабатываемая сущность, казалось бы, одна и та же — а результат работы экрана может отличаться в зависимости от того, где эта сущность до этого побывала.
О побочных эффектах
Идея функционального программирования декларирует использование методов, результат работы которых детерминированно задаётся входными параметрами.
В ходе же работы императивной программы результат выполнения того или иного метода может зависеть ещё и от сторонних переменных, на состояние которых влияют внешние условия. Такие, как порядок выполнения предшествующих инструкций исходного кода, к примеру.
Пользовательские сценарии — будут в следующих статьях (-:
Смотри, если ты знаешь о том, что твой API будет меняться, но ты этого не учёл в архитектурном дизайне своего приложения, то… улавливаешь? (-: Ты делаешь что-то неправильно.
На уровне проектирования, чтобы предусмотреть возможные изменения API — хорошо ложатся шаблоны «фабрика» и «стратегия» по отношению ко всем изменяемым элементам (а это, как правило, парсеры и сериализаторы).
Я предполагаю, что API изменяться не будет.
Почему?
Потому что моим API пользуюсь не только я, но и сторонние разработчики.
Мой клиент заинтересован, чтобы на его серверных мощностях строились другие приложения, он не прячет документацию API.
Таким образом, довольно легко будет напедалить приложение, скажем, под другую мобильную платформу, или даже под desktop.
В конечном итоге, я ведь не зря привёл в статье типичный URL для сервиса:
api1.domain.com/1-0/messages
— тут уже заложено версионирование.
Для версии 1-1, если она сильно будет отличаться, то можно будет придумать, как выкрутиться.
Вспоминаем принцип YAGNI, и не делаем предварительной оптимизации.
И да, если у твоего клиента серверный разработчик — дурак — и обратной совместимости в API не закладывает, тогда сам знаешь, куда такого клиента посылать.
Нельзя просто так взять — и перелопатить структуру API, не перелопатив при этом БД, а это стоит дорого.
Стоит дорого = занимает много времени серверного разработчика.
Следовательно, займёт много и твоего времени.
А клиент пусть платит, да.
По поводу проектирования. Тут, я думаю, гораздо проще найти человека, который расскажет.
Про фундаментальные принципы проектирования ПО я вкратце намекнул в статье. Про принципы нормализации баз данных — думаю, стоит глянуть в Википедии.
В целом, здравый смысл — он везде.
Мне кажется, что достаточно просто периодически задаваться вопросом «А зачем я это делаю?», и я серьёзно.
Часто наблюдаю ошибки, что порождены безалаберным отношением к собственному коду.
«А сделаю-ка я тут класс».
Зачем? Почему? Что этот класс даст?
Хорошо спроектированное приложение, как чертёж здания — должно «кричать» о своём предназначении.
У концертного зала будет большой холл с рядами кресел.
У библиотеки — комната со стеллажами.
…
<сарказм>Но нет! Давайте же сделаем DataManager! И он будет обрабатывать данные! </сарказм>
Я думаю, следующая статья от меня так или иначе будет подробнее раскрывать эту архитектуру.
Этот подход используется практически на всех наших новых проектах («новый» — возраст от полугода), поэтому попросту описать один из них будет довольно легко.
Вкратце, делается примерно так:
– создаётся SPM-пакет, в который будут подкачиваться зависимости;
– для созданного SPM-пакета генерируется *.xcodeproj;
– рядышком создаётся iOS-проект;
– оба объединяются в один workspace, линкуются и т.п.
Вот проект «на коленке»:
https://github.com/taflanidi/spm-ios
Для использования нужно сходить в папку Dependencies, там кастануть
swift package generate-xcodeproj
Потом открыть верхнеуровневый workspace, запустить App и получить 200 во viewDidLoad.
Я думаю, пример в статье стоит рассматривать больше с «клиентской» стороны.
В предполагаемом мобильном приложении на языке Swift не будет никакой бизнес-логики, типа «Провести оплату». Т.е. наверняка будет какой-то вызов API для проведения оплаты, чтобы сервер провёл эту операцию, но реализовывать метод
Execute()
на стороне клиента не придётся.На стороне клиента придётся реализовывать ленту с историей проведенных операций оплаты, в которой должны содержаться сущности разного рода:
Cash Payment
,Card Payment
,PayPal Payment
и другие. И в данной ситуации важным является именно то, как организовать модель данных, полученных от сервера, чтобы их можно было единообразно отобразить в единой ленте.И перечисления в данной ситуации играют роль логических ограничений. Например, у сущности
PaymentWithEnums
физически не может быть воплощения, в котором будет содержаться и срок истечения картыexpirationDate
, иtransactionAuthCode
.Например, необходимо зарегистрировать пользователя в системе страховой компании.
Регистрация происходит с мобильного устройства, где вводить кучу полей (имя, фамилия, отчество, телефон, адрес электропочты и пр.) — мягко говоря, не самое лучшее решение.
В качестве оптимизации UX регистрацию можно разбить на два шага. На первом шаге пользователь вводит номер страхового полиса, и дальше на серверной стороне система уже сама может подтянуть имя, фамилию и пр., скомпоновав их в конечную «Учётную запись».
Если у пользователя нет полиса, или системе введенный номер полиса по какой-то причине неизвестен — инициируется второй шаг, где уже нужно вбивать все личные данные.
Для большинства клиентов страховой компании второй шаг никогда не придётся проходить, т.к. каждого из них можно однозначно идентифицировать по номеру полиса. Соответственно, количество полей ввода для регистрации на мобильном устройстве сокращается до одного.
С подобными предпосылками, возникает вопрос: как рассматривать процесс регистрации с точки зрения API?
Необходимо создать задачу/операцию «Зарегистрировать пользователя по номеру полиса»?
Хорошо. А что должно возвращаться в ответ на запрос ниже?
Будет возвращаться объект «Операция» с идентификатором?
И потом мобильное устройство должно отправлять ещё один запрос с этим идентификатором?
Это — не вариант, количество запросов с мобильного устройства нужно минимизировать.
Или в ответ на запрос будет возвращаться сразу объект «Учётная запись»? Это — неконсистентно.
Мы в компании остановились на том, что подход REST в чистом виде использовать для некоторых целей неудобно, и в качестве компромисса можно задействовать запросы «с глаголами». В ответ может прийти учётная запись:
Что вы думаете по этому поводу?
Можно рассматривать логин в качестве операции создания объекта «Сессия»:
Я бы сказал, что каждый из этих шаблонов может быть более уместен в тех или иных условиях.
Набор классов iOS SDK физически разделяет пользовательские истории на экраны, предоставляя логическую единицу в виде наследников UIViewController. Учитывая подобный подход, мы получаем возможность применения различных шаблонов (MVC, MVP, MVVM, MVPM) в рамках одного и того же приложения. Один экран = один шаблон в самом абсурдном случае.
Следует понимать, что, с одной стороны, программная логика в рамках экрана может потребовать разделения. Выделения сущностей, посвящённых той или иной ответственности.
С другой стороны, MVPM и MVVM (как и вообще любой шаблон проектирования) предполагают изначальный overhead по коду; MVPM и MVVM физически сложнее MVC и MVP.
Таким образом, там, где это действительно необходимо, может быть применен MVPM.
В большинстве случаев не стоит делать предварительной оптимизации, и городить сущности ради конвенции.
В качестве примера могу привести три граничных случая.
Первый: классический экран авторизации с логином и паролем.
Второй: экран с таблицей, выводящей набор модельных сущностей посредством типичных ячеек.
Третий: экран с таблицей, содержащей набор полей ввода, значения которых могут влиять друг на друга.
Внимание, викторина: какой из шаблонов где следует применять? (-:
Зачем в сигнатуре директива __kindof, когда здесь и так возвращаемый тип (UITableViewCell *), а наследование с полиморфизмом ещё никто не отменял? Привет Барбаре Лисков.
Другой вопрос: как это корректно организовать?
Обратите внимание, в конце предыдущей статьи я упоминал архитектурный шаблон VIPER, в рамках которого самый интересный элемент (на мой взгляд) — это непосредственно Router, т.е. сущность (или сущности), ответственная за навигацию по приложению.
Существует несколько наработок по этому поводу (включая предложенный способ конечного автомата). Если получится сделать из них что-то действительно стройное — обязательно напишу по этому поводу статью.
Главное опасение касательно данной задачи заключается в том, что сущность Router может быстро разрастись и покрыться методами, которые без рефакторинга будет невозможно реиспользовать.
В то же время, другая проблема — это over-engineering, вероятность попросту довести абстракцию до такого уровня, на котором её будет сложно понять.
Кстати, введение подобного элемента в дизайн исходного кода никак не конфликтует с Достоинством 1.
И да, небольшой, но интересный момент.
Обратите внимание, как именуются «делегаты» на платформе Android:
— они все «говорящие».
А что может Вам сказать название «delegate» — объект типа UITextFieldDelegate?
Если Вы не знакомы с iOS SDK — то ничего оно Вам не скажет.
В противовес, небольшой пример в рамках Вашего кода: Я — за явное именование. Причём в данном случае оно никак не противоречит конвенции Apple.
И последнее.
Небольшой спойлер-наводка: попробуйте использовать утилиты, которые будут генерировать и собирать константы на основании файлов вёрстки — таким образом Вы избавитесь от дублирования средствами автоматизации.
Спасибо за вопрос. Проблема довольно насущная.
Но да, всегда нужно соблюдать равновесие при проектировании.
А про дисциплину — в яблочко. Это, пожалуй, самое важное.
Это действительно удобно, когда необходимо один и тот же объект отрисовывать несколькими способами.
По поводу ненужной сложности наследования позволю себе с Вами не согласиться: Ваш подход, фактически, предполагает то же самое, только «абстрактный» класс RMRTableViewCell заменяется на протокол DataObjectRenderer.
В то же время, мы получаем ситуацию, когда в исходном коде присутствует излишне абстрактная сущность (ячейка), следующая некоему протоколу.
На мой взгляд, это — возможный источник ошибок, ведь велико искушение на эту же ячейку навесить ещё несколько протоколов, чтобы она же отрисовывала и другие объекты.
Т.е. потенциально возрастает вероятность нарушения принципа единственной ответственности и разрастания класса.
Я — не сторонник писать слишком абстрактные вещи там, где можно ввести более строгую типизацию.
iOS SDK — абстрактен, и этого достаточно. А про «кричащий» архитектурный дизайн я написал ещё в статье.
Сужение абстракции в приложении — да, это ограничение, но оно ограждает нас от ошибок.
В то же время, рекомендую вспомнить принцип You Ain't Gonna Need It: действительно ли Вам необходимо объединять принципиально разные представления под эгидой одного протокола? Не будет ли это over-engineering'ом? Не придаёте ли Вы одному протоколу слишком много информационной значимости?
Решать Вам.
Отписал Вам в личные сообщения.
Если я правильно понял вопрос, Вы предлагаете использовать нечто, вроде шаблона «Стратегия»: ячейка будет предоставлять слот для алгоритма собственного заполнения.
Это удобно, когда в приложении для нескольких типов данных используется одно и то же представление.
Например, ячейка с заголовком, подзаголовком и пиктограммой — нечто универсальное.
Один и тот же класс представляет эту ячейку на Interface Builder'e, а необходимый алгоритм присваивается в зависимости от сущности, которую в ячейку нужно вписать.
В этом случае Вам всё равно понадобится десяток классов, каждый из которых будет следовать протоколу этого алгоритма заполнения. Т.е. Вы выигрываете за счёт вёрстки, но сущностей от этого меньше не станет.
И да, я бы не рекомендовал подобный подход. Универсальность — это благо, но только до определённого предела.
Часто бывает так, что вёрстка может разниться в зависимости от типа данных. Где-то мелкую иконку добавить, где-то заголовок сделать жирным, где-то ещё одно поле на экран вывести — море вариантов.
В этом случае создаются классы ячеек вместе с выделенной под них вёрсткой. Один класс — один layout. Таким образом, чисто с точки зрения информатики, мы не нагружаем один и тот же макет ответственностью за отрисовку различных типов данных, т.е. соблюдаем принцип единственной ответственности.
Да, существует правило о том, что композицию зачастую лучше использовать, чем наследование. Но в данном контексте подключается ещё и вопрос вёрстки, который смещает равновесие.
Надеюсь, я правильно понял Ваш вопрос.
Приложение — магазин для яхтсменов.
Экран-1 представляет собой список судоходных канатов.
Каждый канат — определённого типа и длины.
Длина в модельной сущности указана в миллиметрах.
Rope
+type: Enum;
+length: Real;
Когда пользователь выбирает один из заказов — открывается Экран-2 с детальной информацией.
От заказчика поступило указание сделать такое же приложение, но для США.
А в США длина измеряется в дюймах.
Нерадивый программист модернизирует Экран-1 так, что в модельной сущности поле длины перевычисляется так, чтобы на Экран-2 попадало значение в дюймах.
rope.length = rope.length / 25.4f; // TODO: Вынести «магию» в константы.
Но вот незадача: Экран-2 позволяет делать заказы, а для этого приложение отсылает соответствующий HTTP-запрос, и в запрос сериализуется эта самая модельная сущность.
В итоге сервер получает заказ на канат с неправильно рассчитанной длиной.
Так, в этой статье добавились недостающие элементы подхода проектирования — уровни представления.
В любом случае, благодарю за напоминание. С меня статья.
Благо, не так давно наши юристы дали добро на раскрытие некоторых деталей реализации… но это уже совсем другая история.
В данной статье предполагается использование пассивной модели, без оповещений о её изменениях.
До активных моделей я тоже постараюсь добраться в последующих статьях — это неплохая альтернатива, но требует немного иной идеологии.
Из этой базы данных вычитывается некая сущность DBEntity. Соответствующий EntityService генерирует на её основании сущность Entity, которая не несёт с собой обвес из логики, доставшийся от NSManagedObject'a.
Затем Entity отправляется наверх, на уровни UI.
При недобросовестном подходе в эту сущность могут быть внесены изменения.
Таким образом, передавая её по цепочке между экранами, мы получаем ситуацию, когда данные «на руках» будут отличаться от тех, что записаны в БД. Более того, информация может не обладать необходимой консистентностью, которую изначально обеспечил EntityService, но которая впоследствии была нарушена недобросовестным разработчиком.
Есть несколько подходов к решению подобных проблем.
Во-первых, делать неизменяемые модельные объекты.
Во-вторых, не передавать их куда попало — об этом и сказано в статье.
Посудите сами, какое искушение: Entity изначально попадает на первый экран, там дорабатывается до необходимого состояния, затем отдаётся на второй экран.
Бывают такие ситуации? По неопытности — конечно, бывают.
Только вот тут же возникает зависимость экрана-2 от экрана-1 — отныне второй экран не может существовать отдельно от первого, ведь он ожидает «доработанные» данные.
Я привожу аналогию с побочными эффектами императивного программирования, потому что обрабатываемая сущность, казалось бы, одна и та же — а результат работы экрана может отличаться в зависимости от того, где эта сущность до этого побывала.
В ходе же работы императивной программы результат выполнения того или иного метода может зависеть ещё и от сторонних переменных, на состояние которых влияют внешние условия. Такие, как порядок выполнения предшествующих инструкций исходного кода, к примеру.
Пользовательские сценарии — будут в следующих статьях (-:
Смотри, если ты знаешь о том, что твой API будет меняться, но ты этого не учёл в архитектурном дизайне своего приложения, то… улавливаешь? (-: Ты делаешь что-то неправильно.
На уровне проектирования, чтобы предусмотреть возможные изменения API — хорошо ложатся шаблоны «фабрика» и «стратегия» по отношению ко всем изменяемым элементам (а это, как правило, парсеры и сериализаторы).
Я предполагаю, что API изменяться не будет.
Почему?
Потому что моим API пользуюсь не только я, но и сторонние разработчики.
Мой клиент заинтересован, чтобы на его серверных мощностях строились другие приложения, он не прячет документацию API.
Таким образом, довольно легко будет напедалить приложение, скажем, под другую мобильную платформу, или даже под desktop.
В конечном итоге, я ведь не зря привёл в статье типичный URL для сервиса:
api1.domain.com/1-0/messages
— тут уже заложено версионирование.
Для версии 1-1, если она сильно будет отличаться, то можно будет придумать, как выкрутиться.
Вспоминаем принцип YAGNI, и не делаем предварительной оптимизации.
И да, если у твоего клиента серверный разработчик — дурак — и обратной совместимости в API не закладывает, тогда сам знаешь, куда такого клиента посылать.
Нельзя просто так взять — и перелопатить структуру API, не перелопатив при этом БД, а это стоит дорого.
Стоит дорого = занимает много времени серверного разработчика.
Следовательно, займёт много и твоего времени.
А клиент пусть платит, да.
По поводу проектирования. Тут, я думаю, гораздо проще найти человека, который расскажет.
Про фундаментальные принципы проектирования ПО я вкратце намекнул в статье. Про принципы нормализации баз данных — думаю, стоит глянуть в Википедии.
В целом, здравый смысл — он везде.
Мне кажется, что достаточно просто периодически задаваться вопросом «А зачем я это делаю?», и я серьёзно.
Часто наблюдаю ошибки, что порождены безалаберным отношением к собственному коду.
«А сделаю-ка я тут класс».
Зачем? Почему? Что этот класс даст?
Хорошо спроектированное приложение, как чертёж здания — должно «кричать» о своём предназначении.
У концертного зала будет большой холл с рядами кресел.
У библиотеки — комната со стеллажами.
…
<сарказм>Но нет! Давайте же сделаем DataManager! И он будет обрабатывать данные! </сарказм>
Этот подход используется практически на всех наших новых проектах («новый» — возраст от полугода), поэтому попросту описать один из них будет довольно легко.
Очень-очень интересно.