🔄

関数で依存性の逆転(DIP)を行う

2023/09/30に公開

目次

  1. 依存性の逆転とは
  2. 依存とは
  3. 関数で依存性の逆転を実装
  4. まとめ

依存性の逆転とは

依存性の逆転とはオブジェクト指向プログラミングの原則である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