Библиотека RxJs - отличный набор инструментов, которые могут помочь вам вывести реактивность вашего приложения на новый уровень. Одна из различных особенностей Observables заключается в том, что они возвращают серию значений. Вы можете просто подключиться к потоку данных и получать различные значения в течение определенного периода времени. Отличным примером этого является прослушивание событий мыши или потока данных, поступающих из веб-сокета. С другой стороны, иногда возникают ситуации, когда вы хотите получить только одно значение из потока и завершить его. В этих случаях многозначная наблюдаемая кажется излишней инженерией. Прекрасным примером из реальной жизни может быть получение данных с помощью HTTP-вызова. Библиотеку на основе RxJs обычно используют для HTTP-запросов, таких как HttpClient from the Angular или axios-observable, потому что обычно данные из запроса возвращаются через определенный период времени. Давайте посмотрим на пример кода:

class HttpClient {
    get<T>(url: string): Observable<T> {...}
}

Мы знаем, что observable вернет только одно значение и завершится, так как это характер HTTP-запросов. Итак, почему тип возвращаемого значения - это Observable, который по определению предлагает серию значений. Это противоречит интуиции и может ввести читателя в замешательство и вызвать вопросы типа «Действительно ли это возвращает одно значение?» 🤔.

В этой статье я выскажу свои мысли о том, как сделать код RxJs более читабельным.

соглашение

В одном из последних проектов, над которым мне посчастливилось работать, у нас было соглашение о присвоении имен всем функциям и методам, возвращающим наблюдаемый объект, с префиксом on. Программист мгновенно узнал, какой тип наблюдаемой функции вернул, просто взглянув на ее имя. В некоторых случаях, когда мы хотели получить одно наблюдаемое значение, мы называли метод once. Это сработало для нас очень хорошо, и я настоятельно рекомендую его другим командам, которые сталкиваются с аналогичными проблемами.

class UserStore {
    // return stream of Users which may change in the future
    onUsers(): Observable<Array<User>> {...}
    // returns an Array of Users once and completes
    onceUsers(): Observable<Array<User>> {...}
}

К сожалению, с помощью этого соглашения невозможно было принудительно установить безопасность типов в коде. Функции по-прежнему возвращали наблюдаемое, и было невозможно предсказать, вернет ли этот наблюдаемый объект одно значение или несколько из них.

Одинокий

Решением этой проблемы может стать концепция Single. Официальная документация ReactiveX как нельзя лучше описывает Single:

Single - это что-то вроде Observable, но вместо того, чтобы испускать серию значений - от нуля до бесконечного числа - он всегда либо выдает одно значение, либо уведомление об ошибке.

Давайте посмотрим на предыдущий пример, но на этот раз давайте использовать Single в качестве возвращаемого типа.

class HttpClient {
    get<T>(url: string): Single<T> {...}
}

Небольшое изменение кода заметно увеличивает его читабельность.

Для работы кода недостаточно.

- Роберт С. Мартин

Реализация Rx в JavaScript не предоставляет нам наблюдаемого типа, который соответствует описанию выше. Есть пара операторов RxJs, которые могут делать то, что нам нужно, например Взять, первый и одиночный. Последний даже имеет то же название, что и предлагаемый тип Single. Этот одиночный оператор возвращает наблюдаемое, которое испускает только одно значение, мы можем взглянуть на пример кода ниже:

const source$ = of(1, 2, 3);
//emit one item that matches predicate
source$
    .pipe(
        single()
    )
    .subscribe(console.log);
//output: 1

Как мы видим, оператор single преобразует наблюдаемое в одно, которое возвращает первое значение. Вы можете сказать, что наша работа сделана, так как это решает нашу проблему, не так ли? Ну что насчет этого:

function requestHttpCall(): Observable<Response> {
return httpCall$     // basic http call 
           .pipe(
               single()
           );
}

К сожалению, оператор single не меняет наблюдаемый тип. Мы по-прежнему получаем реальное наблюдаемое со всеми его различными функциями. Это решение не обеспечивает удобочитаемость, которую дает специальный возвращаемый тип. Ему не хватает смысловой связности.

Команда RxJs распознает проблему и рассматривает идею внедрения Single класса в библиотеку (вы можете прочитать об этом в этом выпуске GitHub). Тем не менее, на сегодняшний день никаких изменений не внесено.

Теперь, когда мы увидели предлагаемые решения, предоставляемые библиотекой RxJs, давайте перейдем к кодированию и попробуем реализовать наш собственный класс Single.

Реализация

Обобщая требования, мы знаем, что Single - это особый тип Observable, который позволяет только один раз вызвать один из методов: next, error, complete . После вызова любого из них Single должен завершить и завершить подписку. Давайте посмотрим на самую упрощенную реализацию:

class Single<T> extends Observable<T> {

   static from<T>(source$: Observable<T>): Single<T> {
      const single = new Single<T>();
      single.source = source$.pipe(take(1));
      return single;
   }

   protected constructor(subscribe?) {
      super(subscribe);
   }

}

Реализация действительно не так уж и сложна. Мы создаем Single с помощью статического метода создания from, который в основном назначает оператор take(1) наблюдаемому источнику. Единственное, что вам нужно не забыть создать экземпляр Single, вызвав метод Single.from.

Теперь мы можем внести изменения в UserService:

class UserService {
   // ... rest of 
    onUsers(): Observable<Array<Users>> {
        return this.repository.selectUsers();
    }
    onceUsers(): Single<Array<Users>> {
        return Single.from(this.onUsers());
    }
}

Метод onceUsers возвращает наш новый тип Single, который сообщает программисту, что наблюдаемый объект будет содержать только одно значение. Класс UserService написан с условием «on & once» в дополнение к типу Single, что упрощает понимание.

Кто-то может возразить, что метод onceUsers можно записать следующим образом:

class UserService {
    // ..
    onceUsers(): Observable<Array<Users>> {
        return this.onUsers().pipe(take(1));
    }
}

К сожалению, реализуя его таким образом, вы теряете информацию о том, что наблюдаемый предоставляет только одно значение. Когда вы решите использовать UserService на другом уровне приложения, типы возвращаемых значений останутся Observable. В предыдущей реализации тип Single остается в потоке, а компилятор TypeScript предоставит программисту дополнительную информацию.

Выводы

Потоки данных действительно полезны, но вы не всегда хотите подписываться на множество ценностей. Иногда нужно просто проверить реальное значение и все. В этой ситуации тип Single может действительно улучшить читаемость вашего кода и помочь вам отловить нежелательные ошибки. Сочетание этого с условием «on & once» выводит ваш реактивный код на более высокий стандарт чистого кода.