Мобільний банк для iOS: додаємо блокову архітектуру Cocoa MVC

Якщо ви пишете програму мобільного банку для iOS, які у вас пріоритети? Думаю, їх два:

  1. Надійність;
  2. Швидкість внесення змін.
Ситуація така, що потрібно вміти вносити зміни (і, зокрема, викочувати нові банківські продукти) справді швидко. Але при цьому не скочуватися в индусокод і копіпаст (див. пункт 1). Все це при тому, що програма дійсно величезна по функціоналу, принаймні у задумом (банки хочуть набагато більше, ніж вміють). Відповідно, у багатьох випадках це проекти на десятки людино-років. Ті, хто брав участь у таких проектах, вже напевно зрозуміли, що завдання нетривіальне, і шкільні знання тут не допоможуть.

Що ж робити?

Відразу зрозуміло, що це завдання по архітектурі. Перш ніж писати код, я побачив просту і красиву блокову архітектуру фронтенда нашої ДБО. Не бачив раніше подібного в мобільних додатках, але в підсумку зробив аналогічне рішення, цілком ефективне і масштабоване, в рамках iOS SDK, без додавання масивних фреймворків. Хочу поділитися основними моментами.

Так, про всі ці рамки SDK, про эппловские косяки особливості шаблону MVC і про быдлокод в офіційній документації знають всі, хто намагався писати і підтримувати серйозні програми (або хоча б читав Хабр — не буду повторюватися. Тут також не буде основ програмування, файлів з прикладами і фотографій з котиками.


Перша гуглокартинка за запитом «advanced architecture»

Суть вирішення
Операції складаються з атомів.
Операція — це інтерфейс для вчинення конкретної дії в АБС. Наприклад, переказ між своїми рахунками.

Атом — це стабільна «капсула» з MVC, якийсь цеглинка, з якого можна будувати будинки будь-якого розміру і форми. Кожен цеглинка має свою сутність у UI: напис, поле для вводу, картинка. Кожен значущий UI-блок інкапсулюється в такий MVC-цеглинка. Наприклад,
UISegmentedControl
інкапсулюється в
SegmentedAtom
.

Ці MVC-цеглинки пишуться один раз. Далі кожна операція будується з цих цеглинок. Покласти цеглинка — це один рядок. Одна строчка! Далі отримати значення — це знову одна строчка. Все інше вирішується успадкування та поліморфізм у підкласах Atom. Завдання — максимально спростити код самих операцій, оскільки саме вони можуть сильно змінюватися. Цеглинки принципово не змінюються (або навіть взагалі не змінюються).

Атом може бути і більш складним елементом. Він може инкапсулировать якусь логіку і дочірні
ViewController
'и. Наприклад, атом для вибору рахунку з списку (причому по фільтру з контексту). Головне, щоб вся складність залишилася всередині атома. Зовні він залишається таким же простим у використанні.


Мою концепцію вже використовують у будівництві

Для прикладу, операція відправлення платіжного доручення «по швидкій формі» (з коду приховав всі моменти, пов'язані з безпекою, та так, права на код належать ТОВ «Цифрові Технології Майбутнього»):

class PayPPQuickOperation : Operation, AtomValueSubscriber {

private let m_dataSource = PayPPDataSource()

private var m_srcTitle, m_destBicInfo: TextAtom!
private var m_destName, m_destInn, m_destKpp, m_destAccount, m_destBic, m_destDesc, m_amount: TextInputAtom!
private var m_button: ButtonAtom!

override init() {
super.init()
create()
initUI()
}

func create() {
m_srcAccount = AccountPickerAtom(title: "Списати з рахунку")
m_destName = TextInputAtom(caption: "ПІБ або повне найменування одержувача")
m_destInn = TextInputAtom(caption: "ІПН одержувача", type: .DigitsOnly)
m_destKpp = TextInputAtom(caption: "КПП одержувача", type: .DigitsOnly)
m_destAccount = TextInputAtom(caption: "Рахунок отримувача", type: .DigitsOnly)
m_destBic = TextInputAtom(caption: "БИК", type: .DigitsOnly)
m_destBicInfo = TextAtom(caption: "Введіть БИК, банк буде визначено автоматично")
m_destDesc = TextInputAtom(caption: "Призначення платежу")
m_amount = TextInputAtom(caption: "Сума, ₽", type: .Number)
m_button = ButtonAtom(caption: "Перевести")

// Тут можна швидко переставляти місцями операції:
self.m_atoms = [
m_srcAccount,
m_destName,
m_destInn,
m_destKpp,
m_destAccount,
m_destBic,
m_destBicInfo,
m_destDesc,
m_amount,
m_button,
]

m_destInn!.optional = true
m_destKpp!.optional = true

m_button.controlDelegate = self // це означає, що тап прийде в onButtonTap нижче
m_destBic.subscribeToChanges(self)
}

func initUI() {
m_destName.wideInput = true
m_destAccount.wideInput = true
m_destDesc.wideInput = true
m_destBicInfo.fontSize = COMMENT_FONT_SIZE
m_destName.capitalizeSentences = true
m_destDesc.capitalizeSentences = true
}

func onAtomValueChanged(sender: OperationAtom!, commit: Bool) {
if sender == m_destBic && commit == true {
m_dataSource.queryBicInfo(sender.stringValue,
success: { (bicInfo: BicInfoReply?) in
self.m_destBicInfo.caption = bicInfo?.data.name
},
failure: { (error: NSError?) in
// обробка помилки, в даному випадку не страшно
self.m_destBicInfo.caption = ""
})
}
}

func onButtonTap(sender: AnyObject?) {
// якщо декілька кнопок, то для кожного sender буде свою дію

var hasError = false
for atom in m_atoms {
if atom.needsAttention {
atom.errorView = true
hasError = true
}
}

if !hasError {
var params: [String : AnyObject] = [
"operation" : "pp",
"з" : m_srcAccount.account,
"name" : m_destName.stringValue,
"kpp" : m_destKpp.stringValue,
"inn" : m_destInn.stringValue,
...
"amount" : NSNumber(double: m_amount.doubleValue),
]
self.showSignVC(params)
}
}
}

І це весь код операції!!! Більше нічого не потрібно, за умови, що у вас в наявності є всі потрібні атоми. В даному випадку вони у мене вже були. Якщо ні, пишемо новий і малюємо в сториборде. Звичайно ж, можна успадковуватися від існуючих.

onAtomValueChanged()
— це імплементація протоколу
AtomValueSubscriber
. Ми підписалися на зміни текстового поля «БИК» і там робимо запит, який повертає ім'я банку за БІК. Значення
commit == true
для текстового поля приходить з евента
UIControlEventEditingDidEnd
.

Остання строчка
showSignVC()
показати
ViewController
для операції підписання, яка представляє з себе просто ще одну операцію, яка складається з тих же самих простих атомів (формується з елементів матриці безпеки, які приходять з сервера).

Я не наводжу код класу
Operation
, тому що ви можете знайти більш вдале рішення. Я вирішив (в цілях економії часу розробки) згодовувати
m_atoms
табличному
ViewController
'у. Всі «цеглинки» намальовані в IB осередками таблиць, инстанцируются за cell id. Таблиця дає автоматичний перерахунок висоти та інші зручності. Але виходить, що зараз розміщення атомів у мене можливо тільки у таблиці. Для айфона добре, для айпад може бути не дуже. Поки що лише поодинокі банки в російському аппсторе грамотно використовують простір на планшетах, решта тупо копіюють UI айфонів і тільки додають ліву менюшку. Але в ідеалі так, треба переробити на
UICollectionView
.

Залежно
Як відомо, при введенні яких-небудь форм, в залежності від значень одних полів, інші поля можуть видозмінюватися або ховатися, як ми бачили на прикладі БИК вище. Але там одне поле, а в повній платіжці їх в рази більше, тому потрібен простий механізм, який потребує мінімум рухів для динамічного показу/приховування полів. Як це легко зробити? На допомогу знову приходять атоми :) а також
NSPredicate
.

Варіант 1:

func createDependencies() {
// m_ppType - вибір типу платіжки, щось на зразок випадаючого списку (EnumAtom)
m_ppType.atomId = "type"
m_ppType.subscribeToChanges(self)
let taxAndCustoms = "($type == 1) || ($type == 2)"
...
// а це поля введення:
m_106.dependency = taxAndCustoms
m_107.dependency = taxAndCustoms
m_108.dependency = taxAndCustoms
...

Це приклад для платіжок. Аналогічна система працює для електронних платежів на користь різних провайдерів, де в залежності від певного вибору (наприклад, платити за лічильниками або довільну суму), користувач заповнює різний набір полів.

Ось тут хлопці створили для цих цілей «якийсь фреймворк», але мені вистачило кількох рядків.

Варіант 2, якщо вам не вистачає мови предикатів, або ви його не знаєте, то вважаємо залежності функцією, наприклад, isDestInnCorrect():

m_destInn.subscribeToChanges(self)
m_destInnError.dependency = "isDestInnCorrect == NO"

Думаю, може, буде більш красиво, якщо перейменувати властивість:

m_destInnError.showIf("isDestInnCorrect == NO")

Текстовий атом
m_destInnError
, у випадку некоректного введення, розповідає користувачеві, як правильно заповнювати ІПН по 107 наказом.

На жаль, компілятор за вас не перевірить, що дана операція імплементує метод
isDestInnCorrect()
, хоча це напевно можна зробити макросами.

І власне установка видимості в базовому класі
Operation
(соррі за Objective C):

- (void)recalcDependencies {
for (OperationAtom *atom in m_atoms) {
if (atom.dependency) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:atom.dependency];
BOOL shown = YES;
NSDictionary<NSString*, id> *vars = [self allFieldsValues];
@try {
shown = [predicate evaluateWithObject:self substitutionVariables:vars];
}
@catch (NSException *exception) {
// тут налагоджувальна інфа
}
atom.shown = shown;
}
}
}

Подивимося, які ще будуть перевірки. Можливо, операція не повинна ці займатися, тобто всі перепишеться на варіант

let verifications = Verifications.instance
...
if let shown = predicate.evaluateWithObject(verifications,
substitutionVariables: vars) { ...

One More Thing
А взагалі, що стосується об'єктних патернів в клієнт-серверному взаємодії, не можу не відзначити виняткову зручність при роботі з JSON, яке дає бібліотека JSONModel. Dot notation замість квадратних дужок для отримуваних JSON-об'єктів, і відразу типізація полів, у т. ч. масиви і словники. Як результат, сильне підвищення читаності (і як наслідок надійності) коду для великих об'єктів.

Беремо клас
JSONModel
, успадковуємо від нього
ServerReply
(бо кожен відповідь містить базовий набір полів), від
ServerReply
успадковуємо відповіді сервера на конкретні види запитів. Основний мінус бібліотеки — її немає на Swift (оскільки вона працює на якихось мовних лайфхаках), і з тієї ж причини синтаксис странноват…

Шматок прикладу
@class OperationData;
@protocol OperationOption;


#pragma mark OperationsList

@interface OperationsList : ServerReply
@property (readonly) NSUInteger count;
@property OperationData<Optional> *data;
...
@end


#pragma mark OperationData

@interface OperationData : JSONModel
@property NSArray<OperationOption, Optional> *options;
@end


#pragma mark OperationOption

@interface OperationOption : JSONModel
@property NSString<Optional> *account;
@property NSString<Optional> *currency;
@property BOOL isowner;
@property BOOL disabled;
...


Але все це легко юзается з Swift-коду. Єдине, щоб програма не падало з-за неправильного формату відповіді сервера, всі
NSObject
-поля потрібно позначати повідомлення як <
Optional
> і самому перевіряти на
nil
. Якщо хочемо свою властивість, щоб воно не мешалось
JSONModel
, пишемо їх модифікатор <
Ignore
> або, по ситуації,
(readonly)
.

Висновки
Концепція «атомів» дійсно спрацювала і дозволила створити хорошу масштабовану архітектуру мобільного банку. Код операцій дуже простий для розуміння і модифікації, і його підтримка, при збереженні вихідної ідеї, буде займати O(N) часу. А не експонента, в яку скочуються багато проектів.

Мінус цієї реалізації полягає в тому, що відображення зроблено по-простому через
UITableView
, на смартфонах це працює, а під планшети, у разі «просунутого» дизайну, потрібно переробити View і частково Presenter (відразу загальне рішення для смартфонів і планшетів).

А як ви вписуєте свою архітектуру в iOS SDK на великих проектах?
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.