新卒エンジニアが感じたChain of Responsibilityの魅力と実装 – KENTEM TechBlog

こんにちは!KENTEMに入社して5か月目の新卒エンジニア、Y.Kです。

私は現在、Next.jsを用いたWebアプリケーションの開発に携わっています。6月に製品開発部門に配属されてから約3か月が経ちましたが、その中で出会った面白いデザインパターン、CoR(Chain of Responsibility)についてご紹介したいと思います。

CoR(Chain of Responsibility)とは、「責任の連鎖」と訳されるデザインパターンです。その名の通り、処理の責任を複数のオブジェクトや関数に分散させ、順番に処理を渡していく仕組みです。

例えば、二次元データのNaNの処理、正規化処理、ダミー変数処理の流れを考えてみましょう!もしデザインパターンを採用せずに実装すると以下の図のようになるでしょう。

この方法では、上図で水色で囲われている部分がすべて条件分岐となってしまい、可読性や保守性が低下してしまいます。

しかしCoRを採用して実装する場合は以下の図のようになります。

これは各処理で次の処理を呼ぶか、処理を止めてしまうかという実装になります。
このように、各処理を独立した「責任」として扱うことで、コードの見通しが良くなり、再利用性や拡張性も向上します。

私は学生時代にPythonを触った経験はありましたが、コードの可読性や設計を意識して実装することはほとんどありませんでした。

しかし、入社後の研修や業務を通じて設計の重要性を学び、意識的にコードを書くようになったことで、このCoRパターンに出会いました。理解が深まるにつれて、設計やデザインの奥深さに触れ、非常に面白く感じました。

そしてこのパターンを使いこなせるようになったとき、自分自身の成長を実感することができました。

最後に実装してみましょう!今回は先ほど例に挙げた二次元データ処理のTypeSctiptでの実装をご紹介したいと思います!

type Data = { label: string[] | null, value: number[][] | null };
type Processor = (data: Data, next: (v: Data) => Data) => Data;

const handleMissing: Processor = ({ value, label }, next) => {
  if (!(value && label)) throw new Error("Invalid data in handleMissing");
  try {
    const colMeans = value[0].map((_, col) => {
      const validValues = value
        .map(row => row[col])
        .filter(v => !Number.isNaN(v));
      const sum = validValues.reduce((a, b) => a + b, 0);
      return sum / validValues.length;
    });
    const filled = value.map(row =>
      row.map((v, i) => Number.isNaN(v) ? colMeans[i] : v)
    );
    return next({ value: filled, label });
  } catch {
    throw new Error("Error in handleMissing");
  }
};

const handleScaling: Processor = ({ value, label }, next) => {
  if (!(value && label)) throw new Error("Invalid data in scale");
  try {
    const min = value[0].map((_, col) => {
      const colValues = value.map(row => row[col]).filter(v => !Number.isNaN(v));
      return Math.min(...colValues);
    });
    const max = value[0].map((_, col) => {
      const colValues = value.map(row => row[col]).filter(v => !Number.isNaN(v));
      return Math.max(...colValues);
    });
    const scaled = value.map(row =>
      row.map((v, i) => {
        if (Number.isNaN(v)) return 0;
        const range = max[i] - min[i];
        if (range === 0) return 0;
        return (v - min[i]) / range;
      })
    );
    return next({ value: scaled, label });
  } catch {
    throw new Error("Error in scale");
  }
};

const handleEncodeLabels: Processor = ({ value, label }, next) => {
  if (!(value && label)) throw new Error("Invalid data in encodeLabels");
  try {
    const classes = Array.from(new Set(label));
    const encoded = label.map(label => classes.indexOf(label).toString());
    return next({ value, label: encoded });
  } catch {
    throw new Error("Error in encodeLabels");
  }
};

const valueArray = [
  [5.1, 3.5, 1.4, 0.2],
  [4.9, NaN, 1.4, 0.2],
  [7.0, 3.2, 4.7, NaN]
];
const labelArray = ["setosa", "virginica", "versicolor"];

const chainArray = [
  handleMissing,
  handleScaling,
  handleEncodeLabels
];


const dispatch = (value: Data, i: number): Data => {
  if (i >= chainArray.length) {
    return value;
  }
  const response = chainArray[i];
  return response(value, (v) => dispatch(v, i + 1));
}

const preprocessChain = (value: Data): Data | undefined => {
  try {
    return dispatch(value, 0);
  } catch (error) {
    console.error("Error in preprocessChain:", error);
    return undefined;
  }
}
const result = preprocessChain({ value: valueArray, label: labelArray });
console.log(result);

この実装によって、新たな処理を追加したい場合でも簡単に拡張できます。
また、処理が複雑になったとしても、各処理が独立しているため、バグの原因を特定しやすくなることや、テストコードが書きやすいという大きなメリットがあります。

皆さんもぜひ、自分なりの処理を追加して共有していただけるとうれしいです!

refactoring.guru

KENTEMでは、様々な拠点でエンジニアを大募集しています!
建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。
recruit.kentem.jp
career.kentem.jp


元の記事を確認する

関連記事