You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-07-12 22:50:21 +02:00
UI continued
This commit is contained in:
@ -19,4 +19,6 @@
|
||||
|
||||
5. Для всех `GET`-запросов указывайте политику кэширования (иначе всегда есть шанс, что клиент придумает её за вас).
|
||||
|
||||
6. Всегда оставляйте себе возможность обратно-совместимого расширения API и, в частности, всегда возвращайте JSON-объект в ответах эндпойтов — потому что приписать новые поля к объекту вы можете, а к массивам и примитивам — нет.
|
||||
|
||||
В заключение хотелось бы сказать следующее: HTTP API — это способ организовать ваше API так, чтобы полагаться на понимание семантики операций как разнообразным программным обеспечением, от клиентских фреймворков до серверных гейтвеев, так и разработчиком, который читает спецификацию. В этом смысле HTTP предоставляет пожалуй что наиболее широкий (и в плане глубины, и в плане распространённости) по сравнению с другими технологиями словарь для описания самых разнообразных ситуаций, возникающих во время работы клиент-серверных приложений. Разумеется, эта технология не лишена своих недостатков, но для разработчика публичного API она является выбором по умолчанию — на сегодняшний день скорее надо обосновывать отказ от HTTP API чем выбор в его пользу. Для непубличных API (особенно при наличии самописных инструментов кодогенерации) этот выбор не столь очевиден, но и здесь HTTP API обладает рядом преимуществ — большое количество готовых инструментов и широкое распространение знаний о технологии (пусти и фрагментарных) среди разработчиков.
|
@ -1,12 +1,12 @@
|
||||
### UI-библиотеки
|
||||
### UI-компоненты
|
||||
|
||||
Введение в состав SDK UI компонентов обогащает и так не самую простую конструкцию из клиент-серверного API и клиентской библиотеки дополнительным измерением: теперь с вашим API взаимодействуют одновременно и разработчики (которые написали код приложения), и пользователи (которые непосредственно тыкают пальцами в экран). Хотя это изменение на первый взгляд может показаться не очень значительным, с точки зрения дизайна API добавление конечного пользователя — огромная проблема, которая требует на порядок более глубокой и качественной проработки дизайна программных интерфейсов по сравнению с «чистым» клиент-серверным API. Попробуем объяснить, почему так происходит, на конкретном примере.
|
||||
Введение в состав SDK UI-компонентов обогащает и так не самую простую конструкцию из клиент-серверного API и клиентской библиотеки дополнительным измерением: теперь с вашим API взаимодействуют одновременно и разработчики (которые написали код приложения), и пользователи (которые непосредственно тыкают пальцами в экран). Хотя это изменение на первый взгляд может показаться не очень значительным, с точки зрения дизайна API добавление конечного пользователя — огромная проблема, которая требует на порядок более глубокой и качественной проработки дизайна программных интерфейсов по сравнению с «чистым» клиент-серверным API. Попробуем объяснить, почему так происходит, на конкретном примере.
|
||||
|
||||
Пусть мы решили поставлять в составе нашего кофейного API так же и клиентский SDK, который предоставляет готовые компоненты для разработчиков приложений. Достаточно простая функциональность: пользователь вводит поисковый запрос и видит результаты в виде списка либо в виде точек на карте.
|
||||
Пусть мы решили поставлять в составе нашего кофейного API также и клиентский SDK, который предоставляет готовые компоненты для разработчиков приложений. Достаточно простая функциональность: пользователь вводит поисковый запрос и видит результаты в виде списка.
|
||||
|
||||
(здесь будет картинка)
|
||||
|
||||
Пользователь может выбрать какой-либо из объектов, и тогда появится панель действий с выбранным предложением.
|
||||
Пользователь может выбрать какой-либо из объектов, и тогда откроется экран просмотра предложения с панелью доступных действий.
|
||||
|
||||
(здесь будет картинка)
|
||||
|
||||
@ -14,16 +14,16 @@
|
||||
|
||||
#### Проблемы
|
||||
|
||||
С одной стороны нам может показаться, что наш `SearchBox.search` и чистый клиент-серверный `search` — в сущности одно и то же. Увы, это не так; перечислим проблемы, с которыми мы никогда не сталкивались при разработке API без визуальных компонент.
|
||||
С одной стороны нам может показаться, что наш UI — это просто надстройка над клиент-серверным `search`, визуализирующая результаты поиска. Увы, это не так; перечислим проблемы, с которыми мы никогда не сталкивались при разработке API без визуальных компонент.
|
||||
|
||||
##### Захват разделяемого ресурса
|
||||
|
||||
Предположим, что мы хотим разрешить разработчику подставить в наш `SearchBox` свой поисковый запрос. Допустим, потому что где-то в приложении размещён рекламный баннер «найти лунго рядом со мной», по нажатию на который происходит переход к нашему компоненту с введённым запросом «лунго». Этот переход мы выполним, допустим, разработав метод `SeachBox.search`.
|
||||
Предположим, что мы хотим разрешить разработчику подставить в наш `SearchBox` свой поисковый запрос — например, чтобы дать возможность разместить в приложении баннер «найти лунго рядом со мной», по нажатию на который происходит переход к нашему компоненту с введённым запросом «лунго». Программно, разработчику потребуется показать соответствующий экран и вызвать метод `SeachBox.search`.
|
||||
|
||||
Теперь у нас два метода `search`, которые принимают одни и те же параметры и выдают один и тот же результат. Но *ведут себя* эти методы совершенно по-разному:
|
||||
Два наших метода `search` («чистый» клиент-серверный и компонентный `SearchBox.search`) принимают одни и те же параметры и выдают один и тот же результат. Но *ведут себя* эти методы совершенно по-разному:
|
||||
* если вызвать несколько раз `SearchBox.search`, не дожидаясь ответа сервера, то все запросы, кроме последнего во времени, должны быть проигнорированы; даже если ответы пришли вразнобой, только тот из них, который соответствует новейшему запросу, должен быть показан в UI;
|
||||
* дополнительная задача — каким должен быть результат операции `SearchBox.search`, если она была прервана выполнением следующего запроса? Если неуспех, то в чём состоит ошибка вызывающего? Если успех, то почему результат не был отражён в UI?
|
||||
* что порождает другую проблему: а если в момент вызова `SearchBox.search` уже исполнялся какой-то запрос, инициированный пользователем — *что должно произойти*? Какой из вызовов приоритетнее — выполненный разработчиком или выполненный самим пользователем.
|
||||
* дополнительная задача — что должен вернуть вызов метода `SearchBox.search`, если он был прерван выполнением другого запроса? Если неуспех, то в чём состоит ошибка вызывающего? Если успех, то почему результат не был отражён в UI?
|
||||
* что порождает другую проблему: а если в момент вызова `SearchBox.search` уже исполнялся какой-то запрос, инициированный пользователем — *что должно произойти*? Какой из вызовов приоритетнее — выполненный разработчиком или выполненный самим пользователем?
|
||||
|
||||
В реализации клиент-серверного API такой проблемы у нас нет — каждый актор, вызывающий функцию поиска, получит свой ответ независимо. Но с UI-компонентами этот подход не работает, поскольку все они, в конечном итоге, разделяют один общий ресурс — экран приложения и внимание пользователя на нём.
|
||||
|
||||
@ -31,15 +31,15 @@
|
||||
|
||||
##### Сильная связность бизнес-логики и отображения
|
||||
|
||||
Посмотрим теперь на панель действий с предложениями. Допустим, мы размещаем на ней две кнопки — «заказать» и «показать на карте». Эти две кнопки выглядят одинаково и реагируют на действия пользователя одинаково — но при этом осуществляют абсолютно не имеющие ничего общего друг с другом действия.
|
||||
Посмотрим теперь на панель действий с предложениями. Допустим, мы размещаем на ней три кнопки — «заказать», «показать на карте» и «отменить». Эти кнопки выглядят одинаково и реагируют на действия пользователя одинаково — но при этом осуществляют абсолютно не имеющие ничего общего друг с другом действия.
|
||||
|
||||
Допустим, мы предоставили программисту возможность модифицировать панель действий, для чего предоставим в составе SDK класс `Button`. Достаточно быстро мы выясним, что этой функциональностью будут пользоваться в двух основных диаметрально противоположных сценариях:
|
||||
Допустим, мы предоставили программисту возможность добавить свои кнопки действий на панель, для чего предоставим в составе SDK класс `Button`. Достаточно быстро мы выясним, что этой функциональностью будут пользоваться в двух основных диаметрально противоположных сценариях:
|
||||
* для размещения на панели дополнительных кнопок, ну скажем, «позвонить в кафе», *выполненных в том же дизайне, что и стандартные*;
|
||||
* для изменения дизайна стандартных кнопок в соответствии с фирменным стилем заказчика, *сохраняя ту же самую функциональность в неизменном виде*.
|
||||
|
||||
Более того, возможен и третий сценарий: разработчики заходят сделать кнопку «позвонить», которая будет и выглядеть иначе, и выполнять другие действия, но при этом будет наследовать UX кнопки — т.е. нажиматься при клике, располагаться в линию с другими кнопками и так далее.
|
||||
Более того, возможен и третий сценарий: разработчики заходят сделать кнопку «позвонить», которая будет и выглядеть иначе, и программно выполнять другие действия, но при этом будет *наследовать UX* кнопки — т.е. нажиматься при клике, располагаться в ряд с другими кнопками и так далее.
|
||||
|
||||
С точки зрения разработчика SDK это означает, что функциональная кнопка должна позволять независимо переопределять и её внешний вид, и реакцию на действия пользователя.
|
||||
С точки зрения разработчика SDK это означает, что класс `Button` должен позволять независимо переопределять и внешний вид кнопки, и реакцию на действия пользователя, и элементы UX.
|
||||
|
||||
##### Двойная иерархия подчинения сущностей
|
||||
|
||||
@ -53,6 +53,4 @@
|
||||
|
||||
Возникает вопрос: если выбрано предложение сетевой кофейни, какая иконка должна быть на кнопке подтверждения заказа — та, что унаследована из данных предложения (логотип кофейни) или та, что унаследована от «рода занятий» самой кнопки? Элемент управления «создать заказ», таким образом, встроен в две иерархии сущностей (по визуальному отображению и по данным) и в равной степени наследует обоим.
|
||||
|
||||
#### И решения?…
|
||||
|
||||
С решением вышеуказанных проблем, увы, всё обстоит очень сложно. Мы рассмотрим некоторые подходы по разделению областей ответственности составляющих в следующих главах; но очень важно уяснить одну важную мысль: полный декаплинг, то есть разработка функционального SDK+UI, дающего разработчику свободу в переопределении и внешнего вида, и реакции на действия, и UX — невероятно дорогая в разработке задача, которая в лучшем случае утроит вашу иерархию абстракций. Универсальный совет здесь ровно один: *три раза подумайте прежде чем предоставлять возможность программной настройки UI-компонентов*. Хотя цена ошибки дизайна программных интерфейсов, как правило, не очень высока для UI-библиотек, плохо структурированный, нечитабельный и глючный SDK вряд ли может рассматриваться как сильное клиентское преимущество вашего API.
|
||||
С решением вышеуказанных проблем, увы, всё обстоит очень сложно. В следующих главах мы рассмотрим паттерны проектирования, позволяющие в том числе разделить области ответственности составляющих компонента; но очень важно уяснить одну важную мысль: полное разделение, то есть разработка функционального SDK+UI, дающего разработчику свободу в переопределении и внешнего вида, и реакции на действия, и UX — невероятно дорогая в разработке задача, которая в лучшем случае утроит вашу иерархию абстракций. Универсальный совет здесь ровно один: *три раза подумайте прежде чем предоставлять возможность программной настройки UI-компонентов*. Хотя цена ошибки дизайна программных интерфейсов для UI-библиотек, как правило, не очень высока (вряд ли клиент потребует рефанд из-за неработающей анимации нажатия кнопки), плохо структурированный, нечитабельный и глючный SDK вряд ли может рассматриваться как сильное клиентское преимущество вашего API.
|
||||
|
@ -1 +1,201 @@
|
||||
### Изоляция уровней абстракции в UI-библиотеках
|
||||
### Декомпозиция UI-компонентов. MV*-подходы
|
||||
|
||||
Сложность решения проблем, описанных в предыдущей главе, заключается прежде всего в том, что почти любой объект вложен в две (а то и три) различные иерархии сущностей. Если мы возьмём объект «кнопка создания заказа», то она:
|
||||
* вложена в иерархию визуальных сущностей: экран → `SearchBox` → компонент «просмотр предложения» → компонент «панель действий»;
|
||||
* привязана к некоторой сущности внутри иерархии данных (результату поиска);
|
||||
* как программный объект является наследником каких-то базовых классов, отвечающих за UX (системного объекта типа «кнопка», который является реализацией системного интерфейса «интерактивный элемент» и так далее).
|
||||
|
||||
Как мы помним из главы «Разделение уровней абстракции», объединение в одном объекте двух-трёх-четырёх разноплановых контекстов — чрезвычайно деструктивное решение, которое приводит к «перепрыгиваниям» по областям ответственности и, как следствие, переусложнённому коду, в рамках которого разработчику придётся манипулировать множеством самых разных сущностей из самых отдалённых фрагментов документации. Увы, с визуальными компонентами у нас просто нет другого выбора: наша кнопка действительно является таким многомерным контекстом, если мы хотим дать возможность гибко настраивать её внешний вид, UX и бизнес-логику. Таков путь.
|
||||
|
||||
Можно легко продемонстрировать, как пересечение нескольких предметных областей в одном объекте быстро приводит к крайне запутанной и неочевидной логике. Например, представим себе следующую функциональность: если в данных предложения есть поле `checkoutButtonIconUrl`, то иконка будет взята из этого поля — вполне разумное соглашение, если мы хотим позволить выставлять на кнопке иконку сети кофеен, в которой делается заказ. Но тогда разработчик сможет её кастомизировать и показывать не сеть кофеен, а какую-то свою фирменную иконку для действия «заказ», подменив в данных поле `checkoutButtonIconUrl` для каждого результата поиска:
|
||||
|
||||
```
|
||||
const searchBox = new SearchBox({
|
||||
// Предположим, что мы разрешили
|
||||
// переопределять поисковую функцию
|
||||
searchApi: function (params) {
|
||||
const res = await api.search(params);
|
||||
res.forEach((item) {
|
||||
item.checkoutButtonIconUrl =
|
||||
<URL нужной иконки>;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Формально* этот подход корректен и никаких рекомендаций не нарушает. Но с точки зрения связности кода, его читабельности — это полная катастрофа, поскольку следующий разработчик, которого попросят заменить иконку *кнопке*, очень вряд ли пойдёт читать код *функции поиска предложений*.
|
||||
|
||||
Выход из этого логического тупика мы подробно разбирали в главах раздела «Обратная совместимость», посвящённых сильной и слабой связности. Единственный способ «перебросить мостик» между всеми контекстами, собранными в одном UI-компоненте — это декомпозировать компонент на несколько сущностей, каждая из которых отвечает за определённый уровень абстракции, и организовать взаимодействие между ними на принципах слабой связности.
|
||||
|
||||
Идея декомпозиции визуальных объектов, разумеется, придумана не нами и формализована во множестве методологий — в первую очередь, MV*-фреймворках (Model—View—Controller, Model—View—Presenter и т.д.) Все они, в конечно счёте, предлагают реализовать следующий подход:
|
||||
|
||||
1. Выделить сущность *модель*, отвечающую только за данные, поверх которых построен компонент.
|
||||
2. Потребовать, чтобы внешний вид и поведение компонента полностью определялись его моделью.
|
||||
3. Построить механизм изменений внешнего вида компонента через изменение модели.
|
||||
|
||||
Разница между MV*-подходами заключается в разрешённых направлениях взаимодействия между составляющими.
|
||||
|
||||
(схемы MVC, MVP, MVVP)
|
||||
|
||||
MV*-подход выглядит вполне простым и понятным в случае статических объектов, но, увы, становится гораздо менее удобным в случае интерактивных, поскольку оставляет открытым вопрос о том, где и как должно храниться *состояние* компонента (например, «происходит ли сейчас анимация нажатия кнопки»). Фактически, это тоже часть данных, на которых строится компонент, но редко часть модели (в том числе потому, что это явное нарушение принципа изоляции уровней абстракции). Для компонентов, обладающих сложным (и асинхронно изменяемым) внутренним состоянием мы должны пойти ещё дальше, и декомпозировать также данные, поверх которых он строится. UI-компоненты, как правило, оперируют:
|
||||
|
||||
* данными, полученными из внешних источников, и связанные с бизнес-логикой (в случае нашей кнопки — это предложение, которое по нажатию конвертируется в заказ);
|
||||
* данными, унаследованными по различным иерархиям (например, из визуальной иерархии кнопка получает положение своего контекста на экране);
|
||||
* данными, которые описывают её собственное внутреннее состояние (например, состояние анимации нажатия кнопки).
|
||||
|
||||
Если мы позволяем кастомизировать всё вышеперечисленное, кнопка превращается в очень сложный объект:
|
||||
|
||||
```
|
||||
class Button extends SystemButton {
|
||||
// Модель данных
|
||||
public model: IButtonModel;
|
||||
// Состояние кнопки
|
||||
public state: IButtonState;
|
||||
// Отображение кнопки
|
||||
public view: IButtonView;
|
||||
}
|
||||
// Каждый из суб-компонентов
|
||||
// должен в свою очередь
|
||||
// предоставлять функциональность:
|
||||
interface IButtonModel {
|
||||
// доступа к данным
|
||||
data: IDataAccessor,
|
||||
// генерации событий изменения
|
||||
// данных
|
||||
events: IEventEmitter,
|
||||
// вышестоящий контекст
|
||||
parentContext: IContext
|
||||
}
|
||||
```
|
||||
|
||||
Напомним, префикс `I` мы используем, чтобы индицировать *интерфейсы* (абстрактные классы). В данном случае подразумевается, что кнопка должна уметь работать с любой моделью, состоянием и/или отображением, лишь бы они выполняли требуемый программный контракт. Именно благодаря этому требованию мы можем говорить о полной замене одного из компонентов кнопки альтернативной реализацией.
|
||||
|
||||
**NB**: чтобы замена компонента работала, должен существовать способ её осуществить — например, через функции-билдеры.
|
||||
|
||||
```
|
||||
class Button {
|
||||
constructor() {
|
||||
this.view = this.buildView();
|
||||
…
|
||||
}
|
||||
}
|
||||
// Разработчик может переопрелить
|
||||
// метод построения отображения
|
||||
class MyButton {
|
||||
buildView() {
|
||||
return new MyCustomView();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Чтобы вся эта система заработала, мы должны добавить к ней события: каждый из компонентов генерирует определённые события и обрабатывает события других компонентов. Например, реакцию на нажатие мы можем описать так:
|
||||
|
||||
```
|
||||
button.events.on(
|
||||
// Получаем событие
|
||||
// нажатия кнопки
|
||||
'press',
|
||||
(event) => {
|
||||
// Пытаемся заблокировать
|
||||
// кнопку, чтобы не допустить
|
||||
// отправки двух запросов
|
||||
// на создание заказа
|
||||
button.state.lock().then(
|
||||
(lock) => {
|
||||
// В случае успеха
|
||||
// создаём заказ
|
||||
const order = await api
|
||||
.createOrder(
|
||||
button.data.offer
|
||||
);
|
||||
lock.release();
|
||||
this.events.emit(
|
||||
'orderCreated',
|
||||
order
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
// Если блокировку
|
||||
// не удалось получить,
|
||||
// как-то обработать
|
||||
// ошибку
|
||||
…
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
В свою очередь, сущность view должен опираться на состояние кнопки и события её изменения:
|
||||
|
||||
```
|
||||
class ButtonView {
|
||||
public parent: Button;
|
||||
|
||||
protected initialize () {
|
||||
this.parent.state.events.on(
|
||||
'lock',
|
||||
() => {
|
||||
// Вносит изменения во
|
||||
// внешний вид кнопки
|
||||
…
|
||||
}
|
||||
);
|
||||
// аналогично для 'unlock'
|
||||
…
|
||||
// Объект view должен
|
||||
// генерировать событие 'press',
|
||||
// только если он не заблокирован
|
||||
this.element.onTouch(
|
||||
(event) => {
|
||||
if (!this.parent.state.locked) {
|
||||
this.events.emit('press');
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Важно, что при этом каждая составляющая должна оперировать сущностями своей области ответственности, как показано в примере выше: объект View получает от системы событие о том, что над ним произошло касание пользователя, но превращает его в событие 'press' только если UI не заблокирован, таким образом выступая «фильтром», который переформулирует низкоуровневые системные события так, чтобы они стали *ближе* к бизнес-логике.
|
||||
|
||||
Заменив получение и генерацию событий в некоторых парах компонентов на вызовы методов родительского контекста, мы можем редуцировать систему до MV*- или, скажем, Redux-подобного фреймворка. В рамках MVC мы написали бы следующий код:
|
||||
|
||||
```
|
||||
class ButtonView {
|
||||
protected model;
|
||||
protected controller;
|
||||
|
||||
protected initialize () {
|
||||
this.element.onTouch(
|
||||
function (event) {
|
||||
this.controller.onPress();
|
||||
}
|
||||
)
|
||||
|
||||
this.model.onChange(
|
||||
function () {
|
||||
// Перерисовывает кнопку
|
||||
// в соответствие с моделью
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OrderButtonController () {
|
||||
protected writeableModel;
|
||||
|
||||
public onPress () {
|
||||
this.writeableModel.set('locked', true);
|
||||
const order = await api
|
||||
.createOrder(
|
||||
this.model.offer
|
||||
);
|
||||
this.writeableModel.set('order', order);
|
||||
this.writeableModel.set('locked', false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Как можно заметить, получившийся код полностью эквивалентен предыдущему варианту с точностью до замены событий на вызовы функций. Это неслучайно — смысл MV*-фреймворков как раз заключается в том, чтобы интерфейсно *запретить* определённые направления распространения событий (скажем, только контроллер может изменять состояние модели), тем самым снизив общую сложность взаимодействия.
|
71
src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/06.md
Normal file
71
src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/06.md
Normal file
@ -0,0 +1,71 @@
|
||||
### Вычисляемые свойства
|
||||
|
||||
Наличие множественных линий наследования усложняет кастомизация компонентов, поскольку подразумевает, что он может наследовать важные свойства по любой из вертикалей (иконка кнопки может быть задана и в визуальных настройках компонента, и в данных, и в родительском классе) и, более того, любое поддерево иерархии может быть вообще полностью заменено (например, вместо использования системной кнопки мы можем отрисовать её напрямую как графический примитив). При этом корректность взаимодействия с нашей кастомизированной кнопкой по всем остальным иерархиям должна сохраняться:
|
||||
* при изменении дерева визуальных объектов (например, если мы сделаем свою отдельную всегда видимую кнопку «заказать» для каждого элемента в списке) ничего не должно измениться ни в работе с данными, ни в UX кнопки;
|
||||
* если разработчик подменит источник данных (например, реализовав свой алгоритм поиска), кнопка должна продолжать работать как в смысле UX (реагировать на действия пользователя) так и в плане бизнес-логики (создавать заказ);
|
||||
* если будет выбрана альтернативная реализация самого рендеринга кнопки, с точки зрения обработки данных, бизнес-логики и UX ничего измениться не должно.
|
||||
|
||||
Вернёмся к проблеме, которую мы обрисовали в главе «UI-компоненты». Пусть у нас имеется кнопка, которая получает одно и то же свойство `iconUrl` по двум вертикалям — из данных и из настроек отображения, причём в обоих вертикалях оно может наследоваться от какого-то из родителей:
|
||||
|
||||
```
|
||||
const button = new Button({
|
||||
model: {
|
||||
iconUrl: <URL#1>
|
||||
}
|
||||
);
|
||||
button.view.options.iconUrl = <URL#2>;
|
||||
```
|
||||
|
||||
При этом мы можем легко представить себе, что по обеим иерархиям свойство `iconUrl` было получено от кого-то из родителей — например, данные могут быть сгруппированы по бренду, и иконка будет задана для всей группы. Также возможно, что мы разрешим переопределять иконку вообще всем кнопкам через задание свойств по умолчанию для всего SDK.
|
||||
|
||||
В этой ситуации у нас возникает вопрос: каким образом задавать приоритеты, какой из возможных вариантов опции будет выбран.
|
||||
|
||||
Современные графические SDK в зависимости от выбранного подхода делятся на две категории: построенные по образу и подобию CSS и все остальные.
|
||||
|
||||
#### Приоритеты наследования
|
||||
|
||||
Простой подход «в лоб» к этому вопросу — либо зафиксировать приоритеты в точности (скажем, заданное в опциях отображения значение всегда важнее заданного в модели, и они оба всегда важнее любого унаследованного свойства), либо попросту запретить наследование и заставить разработчика копировать все нужные ему свойства. То есть в нашем примере сам разработчик должен написать что-то типа:
|
||||
|
||||
```
|
||||
const button = new Button(…);
|
||||
if (button.model.iconUrl) {
|
||||
button.view.iconUrl = button.model.iconUrl;
|
||||
} else if (
|
||||
button.model.parentCategory.iconUrl
|
||||
) {
|
||||
button.view.iconUrl =
|
||||
button.model.parentCategory.iconUrl;
|
||||
}
|
||||
```
|
||||
|
||||
(В достаточно сложном API оба этих подхода приведут к одному результату. Если приоритеты фиксированы, то это рано или поздно приведёт к необходимости написать код, подобный вышеприведённому, так как разработчик не сможет добиться нужного результата иначе.)
|
||||
|
||||
Достоинства простого решения очевидны — разработчик сам имплементирует ту логику, которая ему нужна. Недостатки тоже очевидны — во-первых, это лишний код; во-вторых, разработчик быстро запутается в том, какие правила он реализовал и почему.
|
||||
|
||||
Альтернативный подход — это предоставить возможность задавать правила, каким образом для конкретной кнопки определяется её иконка, либо напрямую в виде CSS, либо предоставив какие-то похожие механизмы типа:
|
||||
|
||||
```
|
||||
api.options.addRule(
|
||||
// Читать примерно так: кнопки
|
||||
// типа `checkout` значение `iconUrl`
|
||||
// берут из поля `iconUrl` своей модели
|
||||
'button[@type=checkout].iconUrl',
|
||||
'model.iconUrl'
|
||||
)
|
||||
```
|
||||
|
||||
Думаем, излишне уточнять, что разработка своей CSS-подобной системы — огромное количество работы, и к тому же изобретение велосипеда. Использование настоящего CSS, если оно возможно — более разумный подход, однако и он зачастую совершенно избыточен, и только весьма ограниченный набор возможностей системы будет реально использоваться разработчиками.
|
||||
|
||||
#### Вычисленные значения
|
||||
|
||||
Очень важно не забыть предоставить разработчику не только способы задать приоритеты параметров, но и возможность узнать, какой же из вариантов значения был применён. Для этого мы должны разделить заданные и вычисленные значения:
|
||||
|
||||
```
|
||||
// Задаём значение в процентах
|
||||
button.view.width = '100%';
|
||||
// Получаем реально применённое
|
||||
// значение в пикселях
|
||||
button.view.computedStyle.width;
|
||||
```
|
||||
|
||||
Не менее важна и возможность получать оповещения об изменении вычисленных значений.
|
@ -1,50 +1,7 @@
|
||||
Раздел I
|
||||
* описание конечных советов — оставить только кодстайл
|
||||
Раздел II
|
||||
* API-first подход
|
||||
* выбор поддерживаемых стандартов
|
||||
* существующие стандарты (описания) взаимодействия через API
|
||||
* JSON / OpenAPI
|
||||
* XML-RPC / WSDL
|
||||
* JSON-RPC
|
||||
* GraphQL
|
||||
* GRPC
|
||||
* клиентские библиотеки
|
||||
* хелперы, обратная совместимость, работа с умолчаниями
|
||||
* синхронное и асинхронное взаимодействие
|
||||
* push- и poll-модели
|
||||
* сильная и слабая консистентность
|
||||
* машиночитаемое API
|
||||
* observability
|
||||
* перебор списков, курсоры
|
||||
* объёмы передаваемых данных
|
||||
* сжатие
|
||||
* кэширование
|
||||
* ошибки
|
||||
* разрешимость
|
||||
* сведение к умолчанию
|
||||
* ошибка для пользователя vs ошибка для разработчика
|
||||
* деградация
|
||||
* мониторинг состояния
|
||||
Раздел IV
|
||||
* о терминологии
|
||||
* введение в HTTP
|
||||
* REST, определение и реальность
|
||||
* плюсы и минусы разработки HTTP API
|
||||
* проблема статус-кодов
|
||||
* трактовка методов
|
||||
* CRUD
|
||||
* кодстайл
|
||||
* domain/path/query/body
|
||||
Раздел V
|
||||
* SDK как клиент поверх другой платформы
|
||||
* политика перезапросов
|
||||
* консистентность
|
||||
* типы данных
|
||||
* кодстайл
|
||||
* паттерн «кодогенерация»
|
||||
* UI lib: постановка проблемы
|
||||
* общий UX vs платформенный UX
|
||||
* реюз кода vs реюз поведения
|
||||
* трехстороннее взаимодействие
|
||||
* асинхронность, разделяемые ресурсы
|
||||
Паттерны SDK
|
||||
* MV* (пример?)
|
||||
* разделяемые ресурсы, локи
|
||||
* наследование опций и вычисляемые значения
|
||||
* переопределение интерфейсов
|
||||
* динамическое связывание (пример с viewCustomBuilder)
|
||||
* селекторы
|
Reference in New Issue
Block a user