こんにちは、GxPの露木です!
この記事はグロースエクスパートナーズ Advent Calendar 2022の20日目の記事です。
有志メンバーが集まって社内プロダクトを成長させるGrowthUs!という活動にて、
チーム内で「これは勉強していないつけが回ってきているぞ…」とProblemが出されたものがありました。
それは、RxJS…。
Tryとして、有識者からご教授頂くRxJS勉強会が行われました!
その時に分かったことと、ミックスジュースを作りながらチュートリアルを進めるRxJS-fruitsについて書いていこうと思います。
RxJSとは
RxJS (Reactive Extensions for JavaScript) は、非同期またはコールバックベースのコード (RxJS Docs) の作成を容易にする observables を使用したリアクティブプログラミング用のライブラリ。
RxJS は Observable 型の実装を提供する。
※Angularドキュメント より引用
リアクティブプログラミングは、データストリームと変化の伝播に関係する宣言型プログラミングパラダイムである。
※Reactive programming - wikipedia より引用
宣言型でプログラミングが記述でき、SQLライクに書けるので可読性が高く品質にばらつきにくい。
その一方、命令型のパラダイムに宣言型を持ってくるため命令型に慣れているエンジニアの初動学習コストが高く、
命令型本来の書き方よりも柔軟性が低いという面がある。
RxJSの各クラス、関数について
Observableクラス
データを生成し、データを通知する。
Observerクラス
Observableのデータを処理する。
Observableの通知の各タイプに合う3つの関数(next, error, complete)を持つ。
subscribe関数
Observableのデータをsubscribe(購読)することによってObserverは受け取ったデータを処理する。
RxJSを理解するための前提知識
RxJS概要で出てきた用語についての説明をまとめます。
命令的/宣言的
命令的
結果を「どうやって」取りに行くか、まで記述する。
ex) Java、JavaScript、C++
宣言的
結果を取りに行くのに、「どうやって」の指定は特にしない。
ex) SQL、CSS
Observerパターン
プログラム内のクラスインスタンスで起きたイベント(事象)を、他のクラスインスタンスへ通知する処理で使われる、デザインパターンの一種。
Observerパターン - wikipediaより引用
Observerパターンの各クラスについて
Subject
イベントを通知する側のインタフェース。
Observer(イベントを通知される側)のインタフェースの登録・削除・通知のメソッド書式の体裁を提供する。
Observer
イベントを通知される側のインタフェース。通知を受け取って処理をする。
ConcreteObserver
Observerの具象クラス。
※具象クラス:具象メソッド(実際に動作するプログラムが書かれたメソッド)が集まったクラス
ConcreteSubject
Subjectの具象クラス。
ミックスジュースを作る(RxJS-fruits)
概要を理解して、いよいよチュートリアルのミックスジュース作りに入っていきます!
スクリーンショットのように左側にコードの記述箇所、右側にベルトコンベアでフルーツを運んでミックスジュースを作る実行結果が表されています。
Level 1(Exercise: subscribe)
空のObservableをsubscribe(購読)して、Observableの中身を監視する。
Observableはsubscribe-functionで購読された場合のみアクティブとなりデータをストリーミングする。(=ベルトコンベアーが動く)
// ベルトコンベアーを動かす
const conveyorBelt = EMPTY;
conveyorBelt.subscribe();
Level 2(Exercise: subscribe-next)
ここではfruitsがObservable。
fruitsをsubscribe(購読)して、監視。
別クラスで定義されていると仮定したtoConveyorBelt関数で各フルーツをベルトコンベアーに乗せる。
// from関数で配列からobservableを作成
const fruits = from([
"apple",
"banana",
"cherry"]);
fruits.subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Banana
・Cherry
Level 3(Exercise: distinct)
pipe()で異なるRxJS演算子を実行することが出来る。
重複したフルーツがあり1つしかレシピに使わない場合は、同じ処理が2回流れないdistinct()
を用いる。
const fruits = from([
"apple",
"apple",
"banana",
"apple"]);
fruits.pipe(
distinct()
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Banana
Level 4(Exercise: take)
同じ種類のフルーツ4つの中でも2つしか使いたくない場合、最初の値から指定したカウント値までを取得するtake()
を用いる。
const fruits = from([
"banana",
"banana",
"banana",
"banana"]);
fruits.pipe(
take(2)
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Banana
・Banana
Level 5(Exercise: filter)
古いフルーツが紛れており新鮮なフルーツしか使いたくない場合、指定した条件に当てはまるデータを取得するfilter()
を用いる。
ここでは"old-"に当てはまらないフルーツを選択してベルトコンベアーに流している。
const fruits = from([
"apple",
"apple",
"old-apple",
"apple",
"old-apple",
"banana",
"old-banana",
"old-banana",
"banana",
"banana"]);
fruits.pipe(
filter(p => !p.startsWith("old-"))
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Apple
・Apple
・Banana
・Banana
・Banana
Level 6(Exercise: map)
汚れたフルーツがあり拭き取って使いたい場合は、Observable(fruits)が出力する各フルーツにプロジェクト関数を適用して、結果の値をObservableとして出力するmap()
を用いる。
ここではプロジェクト関数としてreplace()を用いて、"dirty-"を""に置き換えている。
const fruits = from([
"dirty-apple",
"apple",
"dirty-banana",
"banana"]);
fruits.pipe(
map(p => p.replace("dirty-",""))
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Apple
・Banana
・Banana
Level 7(Exercise: filter-map-take)
おさらい問題。
汚れたフルーツがあり拭き取って使いたい場合は、Observable(fruits)が出力する各フルーツにプロジェクト関数を適用して、結果の値をObservableとして出力するmap()
を用いる。
ここではプロジェクト関数としてreplace()を用いて、"dirty-"を""に置き換えている。
指定した条件に当てはまるデータを取得するfilter()
で、"old-"に当てはまらないフルーツを取得する。
最初の値から指定したカウント値までを取得するtake()
で最初の2つ(banana,apple)を取得する。
const fruits = from([
"old-banana",
"apple",
"dirty-banana",
"banana"]);
fruits.pipe(
map(p => p.replace("dirty-","")),
filter(p => !p.startsWith("old-")),
take(2)
).subscribe(fruit => toConveyorBelt(fruit));
以下のようにtake()
の代わりに、同じ処理が2回流れないdistinct()
を用いても同じ結果が得られる。
const fruits = from([
"old-banana",
"apple",
"dirty-banana",
"banana"]);
fruits.pipe(
map(p => p.replace("dirty-","")),
filter(p => !p.startsWith("old-")),
distinct()
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Banana
Level 8(Exercise: distinctUntilChanged)
フルーツに重複があれば飛ばして交互にフルーツが来て欲しい場合、最後にPushしたデータと重複していないか確認して取得するdistinctUntilChanged()
を用いる。
const fruits = from([
"banana",
"apple",
"apple",
"banana",
"banana"]);
fruits.pipe(
distinctUntilChanged()
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Banana
・Apple
・Banana
Level 9(Exercise: skip)
最初の2つのフルーツは不要な場合、不要なデータをスキップすることが出来るskip()
を用いる。
今回の場合は2つスキップする。
const fruits = from([
"apple",
"apple",
"banana",
"apple"]);
fruits.pipe(
skip(2)
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Banana
・Apple
Level 10(Exercise: skip-take-map)
おさらい問題。
汚れたフルーツがあり拭き取って使いたい場合は、Observable(fruits)が出力する各フルーツにプロジェクト関数を適用して結果の値をObservableとして出力するmap()
を用いる。
ここではプロジェクト関数としてreplace()を用いて、"dirty-"を""に置き換えている。
不要なデータをスキップすることが出来るskip()
を用いてapple2つをスキップする。
最初の値から指定したカウント値までを取得するtake()
を用いてbananaのみ取得する。
const fruits = from([
"dirty-apple",
"apple",
"dirty-banana",
"dirty-banana",
"apple"]);
fruits.pipe(
map(p => p.replace("dirty-", "")),
skip(2),
take(1)
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Banana
Level 11(Exercise: merge)
異なるObservableを1つのObservableにまとめることが出来るmerge()
を用いて、applesとbananasをマージする。
指定した条件に当てはまるデータを取得するfilter()
で、"old-"に当てはまらないフルーツを取得する。
const apples = from([
"apple",
"apple",
"old-apple",
"apple",
"old-apple"]);
const bananas = from([
"banana",
"old-banana",
"old-banana",
"banana",
"banana"]);
merge(apples,bananas).pipe(
filter(p => !p.startsWith("old-"))
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Apple
・Apple
・Banana
・Banana
・Banana
Level 12(Exercise: takeLast)
最後の値から3番目までのフルーツを取得したい場合、最後の値から指定したカウント値までを取得するtakeLast()
を用いる。
take()
との使い分けは、例えばデータが100あった場合に1,2のデータを取りたい時はtake()
、99,100のデータを取りたい時はtakeLast()
を用いる。(可読性を高くする。)
const fruits = from([
"apple",
"apple",
"banana",
"apple",
"banana"]);
fruits.pipe(
takeLast(3)
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Banana
・Apple
・Banana
Level 13(Exercise: skipLast)
最後の2つのフルーツが不要な場合、最後の値から不要なデータをスキップすることが出来るskipLast()
を用いる。
const fruits = from([
"apple",
"apple",
"banana",
"apple",
"banana"]);
fruits.pipe(
skipLast(2)
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Apple
・Banana
Level 14(Exercise: skipLast, skip & merge)
おさらい問題。
汚れたフルーツがあり拭き取って使いたい場合は、Observable(fruits)が出力する各フルーツにプロジェクト関数を適用して結果の値をObservableとして出力するmap()
を用いる。
ここではプロジェクト関数としてreplace()を用いて、"dirty-"を""に置き換えている。
skip()
とskipLast()
で不要なデータ(古いフルーツ)を飛ばす。
異なるObservableを1つのObservableにまとめることが出来るmerge()
を用いて、freshApplesとfreshBananasをマージする。
const apples = from([
"apple",
"dirty-apple",
"apple",
"old-apple",
"old-apple"]);
const bananas = from([
"old-banana",
"old-banana",
"dirty-banana",
"dirty-banana",
"dirty-banana"]);
const freshApples = apples.pipe(
map(p => p.replace("dirty-", "")),
skipLast(2)
);
const freshBananas = bananas.pipe(
map(p => p.replace("dirty-", "")),
skip(2)
);
merge(freshApples,freshBananas).pipe(
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Apple
・Apple
・Banana
・Banana
・Banana
Level 15(Exercise: zip & concatMap)
交互にフルーツを取得したい場合、2つObservableを接続し交互にpipe()にデータを渡すzip()
、
渡された値の順番で処理するconcatMap()
を用いる。
const apples = from([
"apple",
"apple"]);
const bananas = from([
"banana",
"banana"]);
zip(apples,bananas).pipe(
concatMap(p => p)
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Banana
・Apple
・Banana
Level 16(Exercise: repeat)
フルーツの個数が足りない場合、指定した数値分、値を取得するrepeat()
を用いる。
const fruits = from(["apple"]);
fruits.pipe(
repeat(3)
).subscribe(fruit => toConveyorBelt(fruit));
recipe
・Apple
・Apple
・Apple
チュートリアル終了!最後に
RxJSとは何ぞやというところから始まった勉強会でしたが、まず概要を理解するまでの前提知識が不足していたことに気が付きました。(命令的/宣言的、Observerパターン等)
概要をいきなり理解しようとする前に、これからは出てくる用語で分からないものがあればひとつひとつかみ砕くことを意識して理解できるようになりたいです。
前提知識・チュートリアルを経て、チームで「分からないことが分からない」状態から何となくでも分かる状態になりました!!
参考資料URL