📝

引数の話

2024/07/01に公開

関数に渡す引数はどんなふうに設計されるべきでしょうか。

引数に関するベストプラクティスを少し調べてみると、引数の命名や引数の順序についての記述を見つけることができました。一方で、引数で渡すべき変数それ自体についての記述はあまり見当たりませんでした。
このトピックについてちょうど最近考えていたところだったので、記事を書くことにしました。

はじめに

この記事ではソフトウェアはデータ関数で構成されていると考えます。
データはしばしば DB やストレージといったハードウェアに束縛されており、ソフトウェアがソフトでありつづけるためには、関数によってデータを柔軟に取り扱うことができる必要があります。

関数の分類

関数には 3 つの役割が存在します。

  1. データの変換
  2. データの定義
  3. 副作用の処理

1. データの変換

データは関数によって、他のデータと組み合わされたり、ビジネスロジックを実行することによって、別のデータへと変換されます。

2. データの定義

ソフトウェアは運用され、徐々に複雑さが増していきます。
複雑さが増すごとに、データのプロパティは増大し、データそのものを理解することは困難になっていきます。
複雑さが増したデータに対して、関数はデータの意味合いを定義する役割を担います。

以下で、データを定義する関数を例示します。

type Human {
    name: string;
    age: number;
    ...
}

// Humanが女性専用車両に乗れるかどうかを定義する関数
const canRideTheWomenOnlyCar = (human: Human): boolean => {
    // 性別による判定
    if (human.sex === 'female') {
        return true;
    }
    // 年齢による制限
    if (human.age < 3) {
        // 3歳未満であれば性別を問わず乗れるというビジネスロジックである場合
        return true;
    }
    return false;
}

3. 副作用の処理

ソフトウェアはビジネスロジックを実行するだけではなく、実行した結果のデータを保存したり、データを別のソフトウェアに通知したりといった副作用を扱います。

ソフトウェアはデータを中心に設計されるべき

上記の分類で明らかになったように、データは関数に先立ちます。
また、データには実体があり、関数に比べ変更が容易ではありません。
この 2 つの特徴から考えて、ソフトウェアの知識はデータを中心に構築されていくべきでしょう。

では、ソフトウェアの知識がデータを中心に構築されるとしたら、
今回の主題である関数の引数はどのように設計されるべきでしょうか?

関数は呼び出されるために存在する

開発者にとって、関数はビジネスロジックの定義となります。
ソフトウェアの内部ではしばしば、同様の処理が実行されます。

ソフトウェアで避けるべき最も重要なことの一つは、ビジネスロジックの定義が分散することです。複数箇所で同様の処理が実装されると、保守性が下がることはもちろん、どれが真実なのかがわからなくなります。

ビジネスロジックの定義である関数が満たすべき必要条件の一つは、似た関数が増えていかないように、呼び出しやすいことであるといえるでしょう。

ドメインオブジェクトを引数で渡す

引数はソフトウェアで共有されている意味のあるデータのまとまりで渡すべきです。
意味のあるまとまりとは、色々な表現があるとおもいますが、例えば Entity であったり、ドメインオブジェクトのようなものです。

つまり、上述の例を参考にすれば、

関数はこのように定義すべきで、

// Humanが女性専用車両に乗れるかどうかを定義する関数
const canRideTheWomenOnlyCar = (human: Human): boolean => {
  // 性別による判定
  if (human.sex === "female") {
    return true;
  }
  // 年齢による制限
  if (human.age < 3) {
    // 3歳未満であれば性別を問わず乗れるというビジネスロジックである場合
    return true;
  }
  return false;
};

このように定義すべきではないということです。

// Humanが女性専用車両に乗れるかどうかを定義する関数
const canRideTheWomenOnlyCar = (
  sex: "male" | "female",
  age: number
): boolean => {
  // 性別による判定
  if (sex === "female") {
    return true;
  }
  // 年齢による制限
  if (age < 3) {
    // 3歳未満であれば性別を問わず乗れるというビジネスロジックである場合
    return true;
  }
  return false;
};

アプリケーションの知識がデータを中心として構築されるのであれば、関数の呼び出しのタイミングでは、データを解釈するのではなく、データを解釈する責務までを関数に担わせるべきです。

よく処理のなかで不要なプロパティを排して、必要なプロパティだけを引数として受け取るような実装を見かけます。この実装は、関数の実装者にとって複雑性を排しているのであって、呼び出す側からしたら複雑性が増しています。
呼び出す側の複雑性が増すと、引数や処理がマイナーチェンジされた似通った関数が多数出現し保守性が下がる原因となりえます。

さいごに

コードは実装にかけるコストよりも、後で人間が読む回数の方が圧倒的に多いです。
関数設計の際に、どれだけ自明な引数を用意できるかを意識すれば、保守性が高いコードに一歩近づけるかなとおもいました。

ひさびさに記事を書いたのですが、今後は定期的に更新をしていく予定なので頑張ります。

CHILLNN Tech Blog

Discussion