👋

名著「Javaによる関数型プログラミング」のStrategyパターンをTypeScriptで表現してみた。

2024/07/03に公開

初めに

Strategyパターンは英語で"戦略"を意味する言葉になります。
事前に"戦略"を定義して状況に応じて既存のコードを変更することなく
"戦略"を切り替えることができるデザインパターンになります。
※Strategyパターンの基本は こちらの記事 をご参照ください。

通常のオブジェクト指向デザインパターンではインターフェースを定義して
継承先の具象クラスで戦略を定義するのが一般的ですが、
関数型プログラムを利用するとわざわざインターフェースや
具象クラスを定義せずにすっきり書くことができます。

本項は 名著「Javaによる関数型プログラミング」で紹介されている
StrategyパターンをTypeScript歴数日の筆者が勉強をかねて
TypeScriptに移行してみるという趣旨になります。
※少し本書籍の内容も振り返ります。

おことわり

筆者は元々Java人間です。
TypeScript歴が浅いのでもっと良い書き方があると思いますが
暖かい目で見ていただければと思います。

TypeScriptで書いてみる

type Predicate<T> = (arg: T) => boolean;
function or<T>(...predicates: Predicate<T>[]): Predicate<T> {
    return (arg: T) => predicates.some(predicate => predicate(arg));
}
function and<T>(...predicates: Predicate<T>[]): Predicate<T> {
    return (arg: T) => predicates.every(predicate => predicate(arg));
}

enum AssetType {
    BOND, STOCK
}
class Asset {
    constructor(private assetType: AssetType, private price: number) {}
    get getAssetType(): AssetType {
        return this.assetType;
    }
    get getPrice(): number {
        return this.price;
    }
}

function totalAssetValues(assets: Asset[], strategy: Predicate<Asset>): number {
    return assets.filter(strategy).map(asset => asset.getPrice).reduce((total, num) => total + num, 0);
}

const byBond: Predicate<Asset> = (asset: Asset) => asset.getAssetType === AssetType.BOND;
const byStock: Predicate<Asset> = (asset: Asset) => asset.getAssetType === AssetType.STOCK;
const byBondOrStock = or(byBond, byStock);

const assets: Asset[] = [
    new Asset(AssetType.BOND, 1000),
    new Asset(AssetType.BOND, 2000),
    new Asset(AssetType.STOCK, 3000),
    new Asset(AssetType.STOCK, 4000)
];
console.log("ByBond: " + totalAssetValues(assets, byBond));
console.log("ByStock: " + totalAssetValues(assets, byStock));
console.log("ByBondOrStock: " + totalAssetValues(assets, byBondOrStock));
console.log("All: " + totalAssetValues(assets, () => true));

要素を細かくみていきます。

function totalAssetValues(assets: Asset[], strategy: Predicate<Asset>): number {
    return assets.filter(strategy).map(asset => asset.getPrice).reduce((total, num) => total + num, 0);
}

引数で渡された金額にある条件をもとにフィルタして合計する至ってシンプルなコードです。
この「何か」の部分をオブジェクト指向のデザインパターンだとインターフェースと
具象クラスを用いて実装されますがこのように高階関数を用いると非常にシンプルに書けます。

なおTypeScriptにはJavaのPredicate型は存在しません。
引数の型注釈に直接アロー式を用いることもできますが、
やや煩雑になりそうなのと、型は後ほど別の場所でも利用できそうなので、
型エイリアスを用いて型を作ってあげると便利そうです。

type Predicate<T> = (arg: T) => boolean;
const byBond: Predicate<Asset> = (asset: Asset) => asset.getAssetType === AssetType.BOND;
const byStock: Predicate<Asset> = (asset: Asset) => asset.getAssetType === AssetType.STOCK;
console.log("ByBond:" + totalAssetValues(assets, byBond));
console.log("ByStock:" + totalAssetValues(assets, byStock));

totalAssetValues関数に条件を渡して合計します。

const byBondOrStock = or(byBond, byStock);
function or<T>(...predicates: Predicate<T>[]) :Predicate<T> {
    return (arg: T) => predicates.some(predicate => predicate(arg));
}

ここでは or条件 を定義しています。
JavaのPredicate型にはデフォルトでor、and関数が実装されていますが、
Typescriptにはないので自作しています。
some関数はリストの 何れか が合致したらtrueを返します。

なお以下のようにand条件を自作することも可能です。

function and<T>(...predicates: Predicate<T>[]) :Predicate<T> {
    return (arg: T) => predicates.every(predicate => predicate(arg));
}

every関数はリストの 全て が合致したらtrueを返します。

おわりに

TypeScriptは関数合成に関してはこれからの言語なのかなって思いましたが、
型エイリアスのおかげでそこまで苦にならないのがいいですね。
もっと簡単な実装方法であったり、ライブラリがあれば教えて頂けると助かります。

Discussion