You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-08-10 21:51:42 +02:00
shared resources chapter translated
This commit is contained in:
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
154
docs/API.en.html
154
docs/API.en.html
File diff suppressed because one or more lines are too long
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
@@ -6610,7 +6610,7 @@ api.<span class="hljs-title function_">subscribe</span>(
|
||||
</ul>
|
||||
<p><strong>NB</strong>: следует различать Backend-Driven UI и технологию серверного рендеринга (SSR). Второе подразумевает, что одно и то же состояние UI (как правило, в виде HTML-разметки или аналогичного декларативного описания) может быть сгенерировано как сервером, так и клиентом. В SSR, однако, подразумевается, что клиент может интерпретировать ответ сервера и извлекать из него семантические данные.</p>
|
||||
<p>С точки зрения предложения SDK сторонним разработчикам, Backend-Driven UI фактически вынуждает разрабатывать гибридный код (поскольку практика предоставления партнёрам возможности встроиться в функции серверного рендеринга на стороне провайдера API выглядит весьма далёкой от жизнеспособности идеей), и, таким образом, будет страдать от той же проблемы непрозрачности — разработчик не может со своей стороны определить, в каком сейчас состоянии находится визуальный компонент. Как ни странно, этот недостаток одновременно является и достоинством, поскольку вендор API сохраняет возможность на своей стороне производить любые манипуляции с контентом без нарушения обратной совместимости (мы обсудим этот вопрос в главе «<a href="#api-product-lineup">Линейка сервисов API</a>»). Таких клиентских API в мире существует довольно много (в первую очередь, разнообразные виджеты, рекламные API и т.д.), но их сложно назвать полноценными SDK.</p><div class="page-break"></div><h3><a href="#sdk-shared-resources" class="anchor" id="sdk-shared-resources">Глава 47. Разделяемые ресурсы и асинхронные блокировки</a><a href="#chapter-47" class="secondary-anchor" id="chapter-47"> </a></h3>
|
||||
<p>Другой важный паттерн, который мы должны рассмотреть — это доступ к общим ресурсам. Предположим, что в нашем учебном приложении открытие экрана предложения стало требовать выполнения дополнительного запроса к серверу и, таким образом, стало асинхронным. Модифицируем код <code>OfferPanel</code>:</p>
|
||||
<p>Другой важный паттерн, который мы должны рассмотреть — это доступ к общим ресурсам. Предположим, что в нашем учебном приложении открытие экрана предложения стало требовать выполнения дополнительного запроса к серверу и, таким образом, стало асинхронным. Модифицируем код <code>OfferPanelComponent</code>:</p>
|
||||
<pre><code class="language-typescript"><span class="hljs-keyword">class</span> <span class="hljs-title class_">OfferPanelComponent</span> {
|
||||
…
|
||||
show (offer) {
|
||||
@@ -6640,12 +6640,12 @@ api.<span class="hljs-title function_">subscribe</span>(
|
||||
<span class="hljs-title function_">constructor</span> () {
|
||||
…
|
||||
<span class="hljs-variable language_">this</span>.<span class="hljs-property">offerPanel</span>.<span class="hljs-property">events</span>.<span class="hljs-title function_">on</span>(
|
||||
<span class="hljs-string">'beginFullDataLoad'</span>, <span class="hljs-function">() =></span> {
|
||||
<span class="hljs-string">'beginDataLoad'</span>, <span class="hljs-function">() =></span> {
|
||||
<em><span class="hljs-variable language_">this</span>.<span class="hljs-property">isDataLoading</span> = <span class="hljs-literal">true</span>;</em>
|
||||
}
|
||||
);
|
||||
<span class="hljs-variable language_">this</span>.<span class="hljs-property">offerPanel</span>.<span class="hljs-property">events</span>.<span class="hljs-title function_">on</span>(
|
||||
<span class="hljs-string">'endFullDataLoad'</span>, <span class="hljs-function">() =></span> {
|
||||
<span class="hljs-string">'endDataLoad'</span>, <span class="hljs-function">() =></span> {
|
||||
<em><span class="hljs-variable language_">this</span>.<span class="hljs-property">isDataLoading</span> = <span class="hljs-literal">false</span>;</em>
|
||||
}
|
||||
);
|
||||
@@ -6661,11 +6661,15 @@ api.<span class="hljs-title function_">subscribe</span>(
|
||||
</code></pre>
|
||||
<p>Но этот код очень плох по множеству причин:</p>
|
||||
<ul>
|
||||
<li>непонятно, как его модифицировать, если у нас появятся разные виды загрузок данных, причём некоторые из них будут требовать блокировки прокрутки, а некоторые — нет;</li>
|
||||
<li>этот код просто плохо читается: совершенно непонятно, почему события анимации на одном компоненте влияют на прокрутку в другом компоненте;</li>
|
||||
<li>если при выполнении анимации произойдёт какая-то ошибка, событие <code>endAnimation</code> не произойдёт и прокрутка будет заблокирована навсегда.</li>
|
||||
<li>непонятно, как его модифицировать, если у нас появятся разные виды загрузок данных, причём некоторые из них будут требовать блокировки интерфейса, а некоторые — нет;</li>
|
||||
<li>этот код просто плохо читается: совершенно непонятно, почему события загрузки данных на одном компоненте влияют на пользовательскую функциональность в другом компоненте;</li>
|
||||
<li>если при загрузке произойдёт какая-то ошибка, событие <code>endDataLoad</code> не произойдёт, и интерфейс останется заблокированным навсегда.</li>
|
||||
</ul>
|
||||
<p>Если вы внимательно читали предыдущие главы, решение этих двух проблем должен быть очевидным. Необходимо абстрагироваться от самого факта загрузки данных и переформулировать проблемы в высокоуровневых терминах. У нас есть разделяемый ресурс — место на экране. Мы можем показывать в один момент времени только одно предложение. Следовательно, если какому-то актору требуется длящийся доступ к панели, он должен этот доступ явно получить. Отсюда следует, что:</p>
|
||||
<ul>
|
||||
<li>флаг такого доступа должен именоваться явно (например, <code>offerFullViewLocked</code>, а не <code>isDataLoading</code>);</li>
|
||||
<li>флаг контролироваться <code>Composer</code>-ом, но никак не самой панелью предложения (ещё и потому, что подготовка данных для показа — также ответственность <code>Composer</code>-а).</li>
|
||||
</ul>
|
||||
<p>Если вы внимательно читали предыдущие главы, решение этих двух проблем должен быть очевидным. Необходимо абстрагироваться от самого факта анимации и переформулировать проблемы в высокоуровневых терминах. У нас есть разделяемый ресурс — место на экране. Мы можем показывать в один момент времени только одно предложение. Следовательно, если какому-то актору требуется длящийся доступ к панели, он должен этот доступ явно получить. Отсюда следует, что, во-первых, флаг такого доступа должен именоваться явно (например, <code>offerFullViewLocked</code>, а не <code>isDataLoading</code>) и контролироваться <code>Composer</code>-ом, но никак не самой панелью предложения (ещё и потому, что подготовка данных для показа — также ответственность <code>Composer</code>-а).</p>
|
||||
<pre><code class="language-typescript"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SearchBoxComposer</span> {
|
||||
<span class="hljs-title function_">constructor</span> () {
|
||||
…
|
||||
@@ -6718,7 +6722,7 @@ api.<span class="hljs-title function_">subscribe</span>(
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: вторым параметром в <code>acquireLock</code> мы передали максимальное время жизни блокировки — 10 секунд. Если в течение этого времени блокировка не снята (например, в случае, если ), она будет отменена автоматически.</p>
|
||||
<p><strong>NB</strong>: вторым параметром в <code>acquireLock</code> мы передали максимальное время жизни блокировки — 10 секунд. Если в течение этого времени блокировка не снята (например, в случае, если мы забыли обработать какое-то исключение или выставить таймаут на запрос к серверу), она будет отменена автоматически, и интерфейс будет разблокирован.</p>
|
||||
<p>В таком подходе мы можем реализовать не только блокировки, но и различные сценарии, которые позволяют нам более гибко ими управлять. Добавим в функцию захвата ресурса дополнительные данные о целях захвата:</p>
|
||||
<pre><code class="language-typescript">lock = <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">acquireLock</span>(
|
||||
<span class="hljs-string">'offerFullView'</span>, <span class="hljs-string">'10s'</span>, {
|
||||
@@ -6730,7 +6734,7 @@ api.<span class="hljs-title function_">subscribe</span>(
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
<p>Тогда текущий владелец ресурса (или диспетчер блокировок) может, в зависимости от ситуации, отдавать владение ресурсом или, наоборот, запрещать перехват. Скажем, если открытие панели инициировано программистом через вызов API компонента (а не пользователем через выбор предложения в списке), оно может иметь более высокий приоритет и быть разрешено:</p>
|
||||
<p>Тогда текущий владелец ресурса (или диспетчер блокировок, если мы реализуем такой объект) может, в зависимости от ситуации, отдавать владение ресурсом или, наоборот, запрещать перехват. Скажем, если открытие панели инициировано программистом через вызов API компонента (а не пользователем через выбор предложения в списке), оно может иметь более высокий приоритет и быть разрешено:</p>
|
||||
<pre><code class="language-typescript">lock.<span class="hljs-property">events</span>.<span class="hljs-title function_">on</span>(<span class="hljs-string">'tryAcquire'</span>, <span class="hljs-function">(<span class="hljs-params">actor</span>) =></span> {
|
||||
<span class="hljs-keyword">if</span> (sender.<span class="hljs-property">reason</span> == <span class="hljs-string">'apiSelectOffer'</span>) {
|
||||
lock.<span class="hljs-title function_">release</span>();
|
||||
@@ -6742,7 +6746,7 @@ api.<span class="hljs-title function_">subscribe</span>(
|
||||
</code></pre>
|
||||
<p>Дополнительно мы можем ввести и обработку потери контроля ресурса — например, отменить загрузку данных, которые больше не нужны.</p>
|
||||
<pre><code class="language-typescript">lock.<span class="hljs-property">events</span>.<span class="hljs-title function_">on</span>(<span class="hljs-string">'lost'</span>, <span class="hljs-function">() =></span> {
|
||||
<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">cancelFullDataLoad</span>();
|
||||
<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">cancelFullOfferDataLoad</span>();
|
||||
})
|
||||
</code></pre>
|
||||
<p>Паттерн контроля разделяемых ресурсов также хорошо сочетается с паттерном «модель»: акторы могут захватывать доступ на чтение и/или изменение свойств или групп свойств модели.</p>
|
||||
@@ -6750,9 +6754,9 @@ api.<span class="hljs-title function_">subscribe</span>(
|
||||
<ul>
|
||||
<li>открыть панель предложения;</li>
|
||||
<li>вместо настоящих данных отобразить спиннер или какую-то другую индикацию загрузки;</li>
|
||||
<li>асинхронно обновить отображение при получении ответа от сервера.
|
||||
Однако в постановке проблемы это ничего не меняет: нам всё ещё нужно разработать политику разрешения конфликтов для случая, если какой-то актор пытается открыть панель предложения, пока загрузка данных ещё не закончена, для чего нам вновь нужны разделяемые ресурсы и их захват.</li>
|
||||
<li>асинхронно обновить отображение при получении ответа от сервера.</li>
|
||||
</ul>
|
||||
<p>Однако в постановке проблемы это ничего не меняет: нам всё ещё нужно разработать политику разрешения конфликтов для случая, если какой-то актор пытается открыть панель предложения, пока загрузка данных ещё не закончена, для чего нам вновь нужны разделяемые ресурсы и их захват.</p>
|
||||
<p>Отметим, что в современном фронтенде (к нашему большому сожалению) подобные упражнения с захватом контроля на время загрузки данных или анимации компонентов практически не производятся (считается, что такие асинхронные операции происходят быстро, и коллизии доступа не представляют собой проблемы). Однако, если асинхронные операции выполняются долго (происходят длительные или многоступенчатые загрузки данных, сложные анимации), пренебрежение организацией доступа может быть очень серьёзной UX-проблемой.</p><div class="page-break"></div><h3><a href="#chapter-48" class="anchor" id="chapter-48">Глава 48. Вычисляемые свойства</a></h3><div class="page-break"></div><h3><a href="#chapter-49" class="anchor" id="chapter-49">Глава 49. В заключение</a></h3><div class="page-break"></div><h2><a href="#section-7" class="anchor" id="section-7">Раздел VI. API как продукт</a></h2><h3><a href="#api-product" class="anchor" id="api-product">Глава 50. Продукт API</a><a href="#chapter-50" class="secondary-anchor" id="chapter-50"> </a></h3>
|
||||
<p>Когда мы говорим об API как о продукте, необходимо чётко зафиксировать два важных тезиса.</p>
|
||||
<ol>
|
||||
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
@@ -131,7 +131,7 @@
|
||||
<li><a href="API.en.html#sdk-decomposing">Chapter 44. Decomposing UI Components</a></li>
|
||||
<li><a href="API.en.html#sdk-mv-frameworks">Chapter 45. The MV* Frameworks</a></li>
|
||||
<li><a href="API.en.html#sdk-backend-driven">Chapter 46. The Backend-Driven UI</a></li>
|
||||
<li><a href="API.en.html#chapter-47">Chapter 47. Shared Resources and Asynchronous Locks</a></li>
|
||||
<li><a href="API.en.html#sdk-shared-resources">Chapter 47. Shared Resources and Asynchronous Locks</a></li>
|
||||
<li><a href="API.en.html#chapter-48">Chapter 48. Computed Properties</a></li>
|
||||
<li><a href="API.en.html#chapter-49">Chapter 49. Conclusion</a></li>
|
||||
</ul>
|
||||
|
@@ -1 +1,172 @@
|
||||
### Shared Resources and Asynchronous Locks
|
||||
### [Shared Resources and Asynchronous Locks][sdk-shared-resources]
|
||||
|
||||
Another important pattern we need to discuss is accessing shared resources. Imagine that in our study application, opening an offer panel required making an additional request to the server and thus became asynchronous. Let's modify the `OfferPanelComponent` code:
|
||||
|
||||
```typescript
|
||||
class OfferPanelComponent {
|
||||
…
|
||||
show (offer) {
|
||||
let fullData = await api
|
||||
.getFullOfferData(offer);
|
||||
…
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A question arises: what should happen if the user or the developers tries to select another `offerId` while the server response for the previous one hasn't been received yet? Obviously, we must choose which of the two openings needs to be suppressed. Let's say we decided to block the interface during the data load and by doing so, prohibit selecting another offer. To implement this functionality, we need to notify parent components about data requests being initiated or fulfilled:
|
||||
|
||||
```typescript
|
||||
class OfferPanelComponent {
|
||||
…
|
||||
show () {
|
||||
/* <em> */this.events.emit('beginDataLoad');/* </em> */
|
||||
let fullData = await api
|
||||
.getFullOfferData(offer);
|
||||
/* <em> */this.events.emit('endDataLoad');/* </em> */
|
||||
…
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// `Composer` listens to the panel events
|
||||
// and sets the value of the
|
||||
// corresponding flag
|
||||
class SearchBoxComposer {
|
||||
…
|
||||
constructor () {
|
||||
…
|
||||
this.offerPanel.events.on(
|
||||
'beginDataLoad', () => {
|
||||
/* <em> */this.isDataLoading = true;/* </em> */
|
||||
}
|
||||
);
|
||||
this.offerPanel.events.on(
|
||||
'endDataLoad', () => {
|
||||
/* <em> */this.isDataLoading = false;/* </em> */
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
selectOffer (offer) {
|
||||
if (this.isDataLoading) {
|
||||
return;
|
||||
}
|
||||
…
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, this code is flawed for several reasons:
|
||||
* There is no obvious way to modify it if different types of data loads occur, and some of them require blocking the UI while others do not.
|
||||
* It is poorly readable because it is rather difficult to comprehend why data load events on one component affect the user-facing functionality of the other component.
|
||||
* If an exception is thrown while loading the data, the `endDataLoad` event will never happen and the interface will remain blocked indefinitely.
|
||||
|
||||
If you have read the previous chapters thoroughly, the solution to these problems should be obvious. We need to abstract from the fact of loading data and reformulate the issue in high-level terms. We have a shared resource: the space on the screen. Only one offer can be displayed at a time. This means that every actor needing lasting access to the panel must explicitly obtain it. This entails two conclusions:
|
||||
* The access flag must have an explicit name, such as `offerFullViewLocked`m and not `isDataLoading`.
|
||||
* `Composer` must control this flag, not the offer panel itself (additionally, because preparing data for displaying in the panel is `Composer`'s responsibility).
|
||||
|
||||
```typescript
|
||||
class SearchBoxComposer {
|
||||
constructor () {
|
||||
…
|
||||
this.offerFullViewLocked = false;
|
||||
}
|
||||
…
|
||||
selectOffer (offer) {
|
||||
if (this.offerFullViewLocked) {
|
||||
return;
|
||||
}
|
||||
this.offerFullViewLocked = true;
|
||||
let fullData = await api
|
||||
.getFullOfferData(offer);
|
||||
this.events.emit(
|
||||
'offerFullViewChange',
|
||||
this.generateOfferFullView(fullData)
|
||||
);
|
||||
this.offerFullViewLocked = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This approach improves readability but doesn't help with parallel access and error recovery. To address these issues, we must take the next step: not just create a flag but introduce a procedure for *capturing* it (quite classically, similar to managing exclusive access to shared resources in system programming):
|
||||
|
||||
```typescript
|
||||
class SearchBoxComposer {
|
||||
…
|
||||
selectOffer (offer) {
|
||||
let lock;
|
||||
try {
|
||||
// Trying to catpture the
|
||||
// `offerFullView` resource
|
||||
lock = this.acquireLock(
|
||||
'offerFullView', '10s'
|
||||
);
|
||||
let fullData = await api
|
||||
.getFullOfferData(offer);
|
||||
this.events.emit(
|
||||
'offerFullViewChange',
|
||||
this.generateOfferFullView(fullData)
|
||||
);
|
||||
lock.release();
|
||||
} catch (e) {
|
||||
// If we were unable to get access
|
||||
return;
|
||||
} finally {
|
||||
// Don't forget to free the resource
|
||||
// in the case of an exception
|
||||
if (lock) {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: the second argument to the `acquireLock` function is the lock's lifespan (10 seconds, in our case). This implies that the lock will be automatically released after this timeout has passed (which is useful in case we have forgotten to catch an exception or set a timeout for a data load request), thus unblocking the UI.
|
||||
|
||||
With this approach, we can implement not only locks, but also various scenarios to flexibly manage them. Let's add data regarding the acquirer in the lock function:
|
||||
|
||||
```typescript
|
||||
lock = this.acquireLock(
|
||||
'offerFullView', '10s', {
|
||||
// Who is trying to acquire a lock
|
||||
// and for what reason
|
||||
reason: 'userSelectOffer',
|
||||
offer
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Then the current lock holder (or a lock dispatcher, if we implement it) could either relinquish control over the resource or prevent interception depending on the situation. For example, if opening the panel is initiated by the developer by calling an API method (rather than a user selecting another offer from the list), we could prioritize it and grant it the right to seize control:
|
||||
|
||||
```typescript
|
||||
lock.events.on('tryAcquire', (actor) => {
|
||||
if (sender.reason == 'apiSelectOffer') {
|
||||
lock.release();
|
||||
} else {
|
||||
// Otherwise, prevent interception
|
||||
// of the lock
|
||||
return false;
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Additionally, we might add a handler to react to losing control — for example, to cancel the request for data if it is no longer needed:
|
||||
|
||||
```typescript
|
||||
lock.events.on('lost', () => {
|
||||
this.cancelFullOfferDataLoad();
|
||||
})
|
||||
```
|
||||
|
||||
The shared resource access control partner aligns well with the “model” pattern: actors can acquire read and/or write locks for specific data fields (or groups of fields) of the model.
|
||||
|
||||
**NB**: we could have addressed the data load problem differently:
|
||||
* Open the offer panel
|
||||
* Display a spinner or a placeholder instead of the real data
|
||||
* Asynchronously update the view once the data is loaded.
|
||||
|
||||
However, this doesn't change the problem definition: we still need a conflict resolution policy if another actor attempts to open the panel while the data is still loading, and for this we need shared resources and mechanisms for gaining exclusive access to them.
|
||||
|
||||
Regrettably, in modern frontend development, such techniques involving taking control over UI elements while data is loading or animations are being performed are rarely used. Such asynchronous operations are considered fast enough to not be concerned about access collisions. However, if latencies are high (e.g., complex or multi-staged requests or animations occur), neglecting access management could be a major UX problem.
|
@@ -1,6 +1,6 @@
|
||||
### [Разделяемые ресурсы и асинхронные блокировки][sdk-shared-resources]
|
||||
|
||||
Другой важный паттерн, который мы должны рассмотреть — это доступ к общим ресурсам. Предположим, что в нашем учебном приложении открытие экрана предложения стало требовать выполнения дополнительного запроса к серверу и, таким образом, стало асинхронным. Модифицируем код `OfferPanel`:
|
||||
Другой важный паттерн, который мы должны рассмотреть — это доступ к общим ресурсам. Предположим, что в нашем учебном приложении открытие экрана предложения стало требовать выполнения дополнительного запроса к серверу и, таким образом, стало асинхронным. Модифицируем код `OfferPanelComponent`:
|
||||
|
||||
```typescript
|
||||
class OfferPanelComponent {
|
||||
@@ -37,12 +37,12 @@ class SearchBoxComposer {
|
||||
constructor () {
|
||||
…
|
||||
this.offerPanel.events.on(
|
||||
'beginFullDataLoad', () => {
|
||||
'beginDataLoad', () => {
|
||||
/* <em> */this.isDataLoading = true;/* </em> */
|
||||
}
|
||||
);
|
||||
this.offerPanel.events.on(
|
||||
'endFullDataLoad', () => {
|
||||
'endDataLoad', () => {
|
||||
/* <em> */this.isDataLoading = false;/* </em> */
|
||||
}
|
||||
);
|
||||
@@ -58,11 +58,13 @@ class SearchBoxComposer {
|
||||
```
|
||||
|
||||
Но этот код очень плох по множеству причин:
|
||||
* непонятно, как его модифицировать, если у нас появятся разные виды загрузок данных, причём некоторые из них будут требовать блокировки прокрутки, а некоторые — нет;
|
||||
* этот код просто плохо читается: совершенно непонятно, почему события анимации на одном компоненте влияют на прокрутку в другом компоненте;
|
||||
* если при выполнении анимации произойдёт какая-то ошибка, событие `endAnimation` не произойдёт и прокрутка будет заблокирована навсегда.
|
||||
* непонятно, как его модифицировать, если у нас появятся разные виды загрузок данных, причём некоторые из них будут требовать блокировки интерфейса, а некоторые — нет;
|
||||
* этот код просто плохо читается: совершенно непонятно, почему события загрузки данных на одном компоненте влияют на пользовательскую функциональность в другом компоненте;
|
||||
* если при загрузке произойдёт какая-то ошибка, событие `endDataLoad` не произойдёт, и интерфейс останется заблокированным навсегда.
|
||||
|
||||
Если вы внимательно читали предыдущие главы, решение этих двух проблем должен быть очевидным. Необходимо абстрагироваться от самого факта анимации и переформулировать проблемы в высокоуровневых терминах. У нас есть разделяемый ресурс — место на экране. Мы можем показывать в один момент времени только одно предложение. Следовательно, если какому-то актору требуется длящийся доступ к панели, он должен этот доступ явно получить. Отсюда следует, что, во-первых, флаг такого доступа должен именоваться явно (например, `offerFullViewLocked`, а не `isDataLoading`) и контролироваться `Composer`-ом, но никак не самой панелью предложения (ещё и потому, что подготовка данных для показа — также ответственность `Composer`-а).
|
||||
Если вы внимательно читали предыдущие главы, решение этих двух проблем должен быть очевидным. Необходимо абстрагироваться от самого факта загрузки данных и переформулировать проблемы в высокоуровневых терминах. У нас есть разделяемый ресурс — место на экране. Мы можем показывать в один момент времени только одно предложение. Следовательно, если какому-то актору требуется длящийся доступ к панели, он должен этот доступ явно получить. Отсюда следует, что:
|
||||
* флаг такого доступа должен именоваться явно (например, `offerFullViewLocked`, а не `isDataLoading`);
|
||||
* флаг контролироваться `Composer`-ом, но никак не самой панелью предложения (ещё и потому, что подготовка данных для показа — также ответственность `Composer`-а).
|
||||
|
||||
```typescript
|
||||
class SearchBoxComposer {
|
||||
@@ -121,7 +123,7 @@ class SearchBoxComposer {
|
||||
}
|
||||
```
|
||||
|
||||
**NB**: вторым параметром в `acquireLock` мы передали максимальное время жизни блокировки — 10 секунд. Если в течение этого времени блокировка не снята (например, в случае, если ), она будет отменена автоматически.
|
||||
**NB**: вторым параметром в `acquireLock` мы передали максимальное время жизни блокировки — 10 секунд. Если в течение этого времени блокировка не снята (например, в случае, если мы забыли обработать какое-то исключение или выставить таймаут на запрос к серверу), она будет отменена автоматически, и интерфейс будет разблокирован.
|
||||
|
||||
В таком подходе мы можем реализовать не только блокировки, но и различные сценарии, которые позволяют нам более гибко ими управлять. Добавим в функцию захвата ресурса дополнительные данные о целях захвата:
|
||||
|
||||
@@ -137,7 +139,7 @@ lock = this.acquireLock(
|
||||
);
|
||||
```
|
||||
|
||||
Тогда текущий владелец ресурса (или диспетчер блокировок) может, в зависимости от ситуации, отдавать владение ресурсом или, наоборот, запрещать перехват. Скажем, если открытие панели инициировано программистом через вызов API компонента (а не пользователем через выбор предложения в списке), оно может иметь более высокий приоритет и быть разрешено:
|
||||
Тогда текущий владелец ресурса (или диспетчер блокировок, если мы реализуем такой объект) может, в зависимости от ситуации, отдавать владение ресурсом или, наоборот, запрещать перехват. Скажем, если открытие панели инициировано программистом через вызов API компонента (а не пользователем через выбор предложения в списке), оно может иметь более высокий приоритет и быть разрешено:
|
||||
|
||||
```typescript
|
||||
lock.events.on('tryAcquire', (actor) => {
|
||||
@@ -154,7 +156,7 @@ lock.events.on('tryAcquire', (actor) => {
|
||||
|
||||
```typescript
|
||||
lock.events.on('lost', () => {
|
||||
this.cancelFullDataLoad();
|
||||
this.cancelFullOfferDataLoad();
|
||||
})
|
||||
```
|
||||
|
||||
@@ -164,6 +166,7 @@ lock.events.on('lost', () => {
|
||||
* открыть панель предложения;
|
||||
* вместо настоящих данных отобразить спиннер или какую-то другую индикацию загрузки;
|
||||
* асинхронно обновить отображение при получении ответа от сервера.
|
||||
|
||||
Однако в постановке проблемы это ничего не меняет: нам всё ещё нужно разработать политику разрешения конфликтов для случая, если какой-то актор пытается открыть панель предложения, пока загрузка данных ещё не закончена, для чего нам вновь нужны разделяемые ресурсы и их захват.
|
||||
|
||||
Отметим, что в современном фронтенде (к нашему большому сожалению) подобные упражнения с захватом контроля на время загрузки данных или анимации компонентов практически не производятся (считается, что такие асинхронные операции происходят быстро, и коллизии доступа не представляют собой проблемы). Однако, если асинхронные операции выполняются долго (происходят длительные или многоступенчатые загрузки данных, сложные анимации), пренебрежение организацией доступа может быть очень серьёзной UX-проблемой.
|
Reference in New Issue
Block a user