DanLevy.net

パイプラインの達人:ステートの受け渡し

やあ、Closure、久しぶりだ。

Hero image for パイプラインの達人:ステートの受け渡し

パイプラインの達人: 状態の受け渡し

関数型パイプラインで状態を渡す際に、困ったことはありませんか?

コードの構造(あるいは構造の欠如)は、状態をどれだけ簡単に受け渡せるかに直結します。

この記事では、パイプラインを通して状態を渡す実用的な手法を探ります。途中でコードの整理と可読性も向上させます。

今回の対象となる「実例」スニペットは次です。userIdproducts 配列を受け取り、4 つの関数を順に実行する Promise チェーンを返すチェックアウト関数です。

const checkout = (userId: number, products: number[]) => {
return getProductsSubtotal(userId, products)
.then(subTotal => applyTaxes(userId, subTotal))
.then(total => purchaseProducts(userId, total))
.then(result => sendReceipt(userId, result));
};

ちょっと待ってください。このコード、JS のパイプラインとしては実はかなり良い出来です!

それでも、いくつかの微妙な問題があり、組み合わせるとより大きな課題になる可能性があります。

1 つ目の問題は、(論理的に関連する)各関数に対して userId を何度も渡していることです。これに加えて、開発者や TypeScript が見落としがちな別の問題があります。数値引数の順序を入れ替えると、静かにバグが発生します。(applyTaxespurchaseProducts を参照してください。最初に来るべきは userIdamount

このコードをどう改善するか検討する前に、まずは長所と短所を整理しましょう。

長所と短所

長所

####欠点

解決策、パート 1:モジュール化する!

この手法は、関連する関数を単一のモジュール(例:CartHelpers)にまとめることにある。特定のパターンを強制するものではない。ファクトリ関数クラス、クロージャ、ミックスイン等を検討し、プロジェクトやチームに適した形を選べ。

CartHelpers ファクトリ

userId を一度だけ渡し、すべてのメソッドが単一引数になる CartHelpers モジュールの例。

const CartHelpers = (userId: number) => {
return {
getProductsSubtotal: products => getProductsSubtotal(userId, products),
applyTaxes: subTotal => applyTaxes(userId, subTotal),
purchaseProducts: total => purchaseProducts(userId, total),
sendReceipt: invoice => sendReceipt(userId, invoice)
};
};

CartHelpers クラス

クラス派ならこちらでも簡単に適用できる:

class CartHelpers {
constructor(userId) {
this.userId = userId;
}
getProductsSubtotal = products => getProductsSubtotal(this.userId, products);
applyTaxes = subTotal => applyTaxes(this.userId, subTotal);
purchaseProducts = total => purchaseProducts(this.userId, total);
sendReceipt = invoice => sendReceipt(this.userId, invoice);
}

すぐに得られるメリット:

関連関数をひとまとめにすることで、公開インターフェース(例:checkout()CartHelpers の公開メソッド)の表面積を削減できる機会が生まれる。

表面積が小さいほど認知負荷が減り、テストしやすく保守性も向上する。
意図と焦点を持ってデザインシステムを構築しよう。 ✨

Checkout と CartHelpers の使い方

checkout() 関数が現在どのようになるか見てみましょう:

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
return Promise.resolve(products)
.then(products => cart.getProductsSubtotal(products))
.then(subTotal => cart.applyTaxes(subTotal))
.then(total => cart.purchaseProducts(total))
.then(result => cart.sendReceipt(result));
};
さらに改善した Checkout

もっと改善できるか? もちろんです! 引数を全く繰り返す必要はありません。

関数の引数が前の関数の出力で供給される場合、コードはさらにシンプルにできます。

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
// 🌈 関数はレゴのように積み上げられ、普通の「人間の言葉」みたいに読めます! 💅
return Promise.resolve(products)
.then(cart.getProductsSubtotal)
.then(cart.applyTaxes)
.then(cart.purchaseProducts)
.then(cart.sendReceipt);
};

パラメータを単一(オブジェクト)引数にまとめるのが不自然に感じる場合は、 関数を分割する または より適切なスコープのモジュールにまとめることを検討してください。

どこから始める?

関連する関数を見つけて、ひとまとめにする。(例:CartHelpers

論理的なモジュールを見つける際の課題のひとつは、まずコードが関連しているかどうかを特定することです。

関数が関連しているかどうかの判断基準

ひとつの便利な手法:関数のパラメータに繰り返しがないか探す。何らかの関係性や共通の責務が隠れていないか?

単一の「正解」なるモジュール設計は存在しませんが、整理のために 2〜3 種類の構成案を挙げてみると良いでしょう。概要を描き、ファンタジーコードを書き、「これで楽しいか?」 と自問してください。

cart.sendReceipt() が決済系メソッドと合わないと感じるかもしれません。その場合は customerNotifications.sendReceipt() に移す方が、顧客向けメッセージングとして自然です。CartHelper の重要度が十分高ければ、内部で必要な サービス(例: customerNotifications)を呼び出す コントローラ として機能させても構いません。

どのように自分の取り組みが有効か判断するか?

アドホックな引数を削除しても可読性が損なわれなければ、おめでとうございます!!! 明確で長く使えるスコープを持つモジュールができ上がっているはずです。

それでは、機能はどこに追加すべきか、という問いが出てきます。

私の経験上、機能追加を検討する際に評価すべき主な戦略は 2 つあります。

  1. 既存メソッドを拡張/リファクタリングする。
    (新しいコードが既存コードに十分近い場合。)
  2. チェーン内の適切な位置に新しい(5 番目の)関数を作成する。
    (新しいコードが既存関数と無関係であると判断した場合。)

この二分法に従うことで、どこに新機能を置くべきかが判断しやすくなります(例: cart.applyDiscounts()cart.applyTaxes()rewards.getBalance())。

結論

複雑なパイプラインを通して状態を渡すのは手間がかかりますが、リファクタリングを少しずつ実践すれば、認知負荷を減らしつつ、より読みやすいコードを書けるようになります。

質問やコメント、懸念があれば遠慮なく @justsml まで、または email へお問い合わせください。

次回予告

次回は状態の外部化と、モジュール内での機能拡張について掘り下げます!

関連記事