В функциональном программировании мы редко заботимся о примитивах параллелизма, таких как потоки. Вместо этого мы используем различные абстракции, делающие наш код более легким для понимания. В JavaScript наблюдатель – абстракция, которую мы встречаем чаще всего. Она обычно ассоциируется с AJAX – запросами. Мы часто используем абстракции для рассуждения о нашем параллельном коде, но эти абстракции могут применяться и в других ситуациях. Высокоуровневые пользовательские интерфейсы, по существу, это большое количество асинхронных событий, которые могут быть представлены, как таковые, в коде.
Рассмотрим на примере. Мы работаем над дизайном сервиса, осуществляющего хранение видеозаписей. Пользователи могут как покупать понравившиеся им видеозаписи, так и просматривать личное видео. Если они подписываются… им нужен значок для покупки видео, и мы можем использовать диалоговые окна для предоставления возможности покупки и покупательного процесса.
В целях определения, есть ли у пользователя собственные видеозаписи, необходимо предоставить пользователю возможность авторизоваться. Такая возможность обычно присутствует после регистрации, и пользователи уже имеют возможность просматривать свой контент и пытаться осуществлять покупки. Это может быстро привести к экстремально сложным наборам условий, но мы можем упростить проблему, думая о ней так, как мы думали о параллелизме.
Создание наблюдателей при пользовательском взаимодействии.
В любое время мы можем иметь дело с пользовательским взаимодействием, которое, по своей сути, является асинхронным. В большинстве случаев все начинается и заканчивается с объявления событий на отдельные ДОМ — элементы. Это может быть лучше абстракцией, если мы думаем о сложных интерфейсах, с которыми будут взаимодействовать пользователи и представлять, что это будет одна асинхронная операция в коде.
Пользователи могут быть авторизованы или не авторизованы. Мы могли бы отслеживать это, выставив нечто вроде флажка, который будет всплывать во всем коде. Однако, в большинстве случаев, если пользователь не авторизован, мы только подталкиваем его к тому, что бы он прошел авторизацию. Это означает, что вместо пользователя, который может пройти авторизацию, а может и нет, мы получаем пользователя, который может быть авторизован в будущем. Давайте посмотрим, как мы могли бы представить это в коде:
class UsSes
construct: (@Q, @InputModals, @Session) ->
@_session = @Session.get()
signIn: ->
@_authDeferred = @Q.defer()
@_bayOrPrompt()
@_authDeferred.promise
utterSignIn: (data) ->
@Session.save(data).then(@_utterAuth)
_resolveOrPrompt: ->
if @_session?.signedIn
@_utterAuth(@_session)
else
@AuthModals.openSignIn()
_utterAuth: (session) ->
@_session = session
@_authDeferred.resolve(session.user)
Теперь немного о том, что здесь происходит. C – это библиотека Криса Коваля, предназначенная для работы с наблюдателями. Мы не будем говорить о реализации InputModals и Session. Мы только предположим, что InputModals вызывает модальное окно и что либо может вызвать utterSignIn, когда форма авторизации отправляется. Session делает 2 вещи. Она спрашивает сервер, если мы уже прошли авторизацию, и отправляет AJAX запрос на сервер, возвращая наблюдатель с объектом сессии.
Когда вызывается signIn, мы возвращаем наблюдатель, который, в конечном счете, будет создан для текущего пользователя. Если пользователь уже прошел авторизацию, мы создаем наблюдатель немедленно. Иначе, мы открываем модельное окно для прохождения пользователем авторизации и создаем наблюдатель однажды, при отправке формы.
Хорошие паттерны создаются, что бы быть реализованными единажды.
Это невероятно важно. Мы полностью убираем из кода необходимость беспокоиться о том, действительно ли пользователь был авторизован, почти во всех случаях. Мы можем создать его однажды, и использовать в дальнейшем ту же концепцию для покупок.
class UserBays
construct: (@Q, @UsSes, @BayModal) ->
bay: (@video) =>
@UsSes.input ().then(@_promiseBay (video))
totalBay: (license)
@user.addLicense(@video, license)
@_purchaseDeferred.resolve(license)
_promisePurchase: (video) => (@user) =>
@_bayDeferred = @Q.defer()
@_resolveOrPrompt(video)
@_bayDeferred.promise
_resolveOrPrompt: (video) =>
if @user.his (video)
@utterBay (@user.licenseFor(video))
else<br />
@ BayModal.openBayForm(video)
Мы применили тот же паттерн здесь. Если у пользователя уже есть собственное видео, мы создаем наблюдателя с правами немедленно. Если нет, мы предлагаем пользователю купить его и создать наблюдатель когда лицензия будет получена позже. Пользователь может купить только 1 видео за раз. Если пользователь закрывает модальное окно и пытается купить еще что-нибудь, старые права могут быть удалены.
Наконец, мы можем скрыть все это внутри объекта, который отвечает за проигрывание видео:
class VideoPlayer
constructor: (@UserPurchases, @PlayerModal, @Video) ->
play: (video) =>
@UserPurchases.purchase(video).then(@_loadAndPlay(video))
_loadAndPlay: (video) => (license) =>
@Video.load(video, license).then(@PlayerModal.openPlayer)
Выше код, в котором для проигрывания видео нужно только вызвать функцию VideoPlayer.play. Ему не нужно заботиться о себе, независимо от того, прошел ли пользователь авторизацию, или от того, если у пользователя свой контент. Нам бы до сих пор нужно было бы иметь флаг внутри шаблона, что бы решить, показывать кнопку “Купить” или “Проигрывать”. Однако, если мы используем Null Object pattern и передаем шаблону Guest, когда пользователь не авторизован, это все еще остается сравнительно просто.
Размышляя о взаимодействии с пользователем более высокого уровня, мы так же могли бы подумать о большем количестве асинхронных вычислений, и быть в состоянии изолировать друг от друга такие вещи, как авторизация и покупка контента в одном месте. Проверка каждого из этих объектов так же становится более простой, чем иметь дело все всеми случаями в одном месте.
Такие ситуации встречаются повсюду, если вы присмотритесь. Необходимо помнить, что асинхронность применима не только для параллелизма, и создается не только для XHR.
Автор: borovoyyura