関数で依存性の逆転(DIP)を行う
目次
依存性の逆転とは
依存性の逆転とはオブジェクト指向プログラミングの原則であるSOLIDの原則の1つです。
原則の内容としては、「抽象化した部分が実装の詳細に依存してはならない。実装の詳細が抽象化した部分に依存すべきである。」というものです。
関数を例にかみ砕くと、A関数という詳細を実装してある関数を呼び出すB関数がA関数に依存するべきではないことになります。
そもそも依存とはというところからコードを使って説明していきます。
依存とは
依存とはある関数(クラス)を直接呼び出すことで発生する関係のことです。
以下のコードを例にすると、Reducer
関数はSumCalculator
関数を直接呼び出しているため、Reducer
関数がSumCalculator
関数に依存していることになります。
const SumCalculator = (a: number, b: number): number => {
return a + b;
}
const Reducer = (list: number[]): number => {
return list.reduce(SumCalculator);
}
const vals = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = Reducer(vals);
console.log(result); // 55
依存することのデメリットとして、2つ考えられます。
現在のReducer
関数は足し算しか出来ません。
そのため、引き算、かけ算も行って欲しいという要望に対してReducer
関数を修正しなければいけません。
もし、Reducer
関数が既にテスト済みであった場合、Reducer
関数のテストを再度実行しなければ動作を保障出来ません。
これが1つの関数単位であれば負担を感じにくいですが、ソフトウェア単位でこれが発生した時の負担は小さくない可能性があります。
また、Reducer
関数を呼び出そうとした場合に、SumCalculator
関数も必ず呼び出されます。
しかし、Reducer
関数を使用する場合に必ずしもSumCalculator
関数が必要な訳ではありません。
そのため、Reducer
関数の再利用性が下がってしまいます。
では実際にReducer
関数がSumCalculator
関数に依存している関係を逆転させます。
関数で依存性の逆転を実装
逆転させる際に必要なのは抽象という概念になります。
オブジェクト指向プログラミングでは抽象クラスやインターフェースを使用しますが、関数には存在しないので、今回はtypescriptの型エイリアスを抽象として使用します。
// 計算をするという抽象となる型エイリアス
type Calculator = (a: number, b: number) => number;
// 計算をする抽象に依存する足し算をするという詳細となる関数
const SumCalculator: Calculator = (a, b) => {
return a + b;
}
const Reducer = (list: number[], calculator: Calculator): number => {
return list.reduce(calculator);
}
const vals = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = Reducer(vals, SumCalculator);
console.log(result); // 55
以上のコードでは、Reducer
関数はSumCalculator
関数ではなく、Calculator
という型エイリアスを持つ関数を引数として使用しています。
そのため、Reducer
関数はSumCalculator
関数に依存していません。
一方でSumCalculator
関数はCalculator
という型エイリアスを直接使用しており、Calculator
に依存しています。
これが依存性の逆転です。
依存性逆転の原則を今回のコードに当てはめると、「配列を畳み込むという抽象的なReducer
関数が足し算をするという畳み込みの実装の詳細であるSumCaluculator
関数に依存してはならない。実装の詳細であるSumCalculator
関数が畳み込みの実装を抽象化した型エイリアスであるCalculator
に依存すべきである。」ということになります。
依存性が逆転することで以下のコードのように、足し算以外を行いたいという要望に関しても、Calculator
の存在によって、既存のコードを修正することなく実装可能です。
const SubCalculator: Calculator = (a, b) => {
return a - b;
}
const ProdCalculator: Calculator = (a, b) => {
return a === 0 ? b : a * b;
}
const DivCalculator: Calculator = (a, b) => {
if (a === 0) return 1;
if (b === 0) return a;
return a / b;
}
const sub = Reducer(vals, SubCalculator);
const prod = Reducer(vals, ProdCalculator);
const div = Reducer(vals, DivCalculator);
console.log(sub); // -55
console.log(prod); // 3628800
console.log(div); // 2.7557319223985894e-7
もしReducer
関数が数値以外の配列も畳み込みたい場合には、Calculator
をより抽象化したTwoToOne
を実装することで対応可能です。
type TwoToOne<T> = (a: T, b: T) => T;
const Reducer = <T>(list: T[], callback: TwoToOne<T>): T => {
return list.reduce(callback);
};
const SumCalculator: TwoToOne<number> = (a, b) => {
return a + b;
};
const Joiner: TwoToOne<string> = (a, b) => {
return a + " " + b;
}
const vals = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const vals2 = ["hello", "world", "!"];
const result = Reducer<number>(vals, SumCalculator);
const joinResult = Reducer<string>(vals2, Joiner);
console.log(result); // 55
console.log(joinResult); // hello world !
まとめ
今回はオブジェクト指向プログラミングの文脈で語られるSOLIDの原則の1つである依存性逆転の原則をクラスではなく関数で実装してみました。
さらにこのコードは機能の変更があった場合に、既存のコードを修正することなく、新たなコードの追加のみで対応可能な状態となっています。
そのため、このコードが依存性逆転の原則だけでなく、SOLIDの原則の開放閉鎖の原則にも則ったコードになっていることも示しています。
Discussion