色んな事を書く

シンプルさを極めたエンジニアになりたい

【読書メモ】オブジェクト設計指向実践ガイド 単一責任のクラスを設計する

目次

データ構造の隠蔽

あるクラス内で複雑なデータ構造を取り扱う場合に、そのデータ構造も正しく隠蔽して DRY に則ったコードを書こうよってお話です。

先ずはサンプルコードをこんな感じで書きます。
(普段は絶対にこんなコード書かないですよ...)

namespace PracticalOOP.Srp
{
    internal class ObsucuringReference
    {
        private int[][] Data { get; set; }

        internal ObsucuringReference(int[][] data)
        {
            Data = data;
        }

        internal int[] Diameters()
        {
            return Array.ConvertAll(
                Data,
                // [0]: リム
                // [1]: タイヤ
                new Converter<int[], int>((d) => d[0] + d[1] * 2));
        }
    }
}

そしてこのクラスのインスタンス化とメソッド呼び出し側のコードです。

var data = new int[][]{
    new int[] { 10, 10 },
    new int[] { 20, 20 },
    new int[] { 30, 30 }
};

var obsucuring = new ObsucuringReference(data);
Array.ForEach(obsucuring.Diameters(), new Action<int>((i) => Console.WriteLine(i)));

このコードで問題なのは、

  1. Diameters() は直径の計算方法と Data のデータ構造という2つの知識を持っている事
  2. Data のデータ構造に依存している箇所があらゆる個所に散見してしまう事

です。

まず 1 つ目に関して。Diameters() が直径の計算方法を知っている分には正しいように思います。しかしデータ構造への依存は見逃せません。

例えば、配列の第二要素にはギアを格納して、第三要素にタイヤを格納するみたいない修正があったとします。この時点で Diameters() は正しく直径の値を求める事が出来ず合わせて修正が必要になりますね。その修正が漏れてしまえばお察しですね....

次に 2 点目ですが、今はまだデータ構造への依存が Diameters() のみにとどまっていますが、それがこの先増えていくも考えられるよねってことです。

その際に上記のデータ構造が変わった瞬間に修正が発生してしまいますし、それが漏れてしまえばお察しな状況再びです。

なので、データ構造に関する知識を一か所にまとめて DRY にしちゃおうってことです。 そのコードがこちらです。

namespace PracticalOOP.Srp
{
    internal class RevealingReferences
    {
        private Wheel[] Wheels { get; set; }

        internal RevealingReferences(int[][] data)
        {
            Wheels = Wheelify(data);
        }

        internal int[] Diameters()
            => Array.ConvertAll(
                Wheels,
                new Converter<Wheel, int>((w) => w.Rim + w.Tire * 2));

        private Wheel[] Wheelify(int[][] data)
            => Array.ConvertAll(
                data,
                new Converter<int[], Wheel>((d => new Wheel(d[0], d[1]))));

        private record Wheel(int Rim, int Tire)
        {
        }
    }
}

Record を使ってデータ構造への依存を一か所にまとめてみました。これでデータ構造に関する知識は一か所にまとまり、DRY なコードになりました。

データ構造への変更があったとしても new Wheel の部分を修正すれば済みますし、Wheel の呼び出し側はプロパティで参照をしているので、内部のデータへの依存はありません。

これで修正しやすいコードになりました。

とはいえ配列でデータを渡すことは現実的にあまりないと思うので良いサンプルにはならなかったかもしれませんね。

単一責任に関して

この章ではメソッドに対しても単一責任の概念を役立てていこうってお話です。

メソッドの責任を一つに絞ることで変更がしやすく、再利用が行いやすいものになります。

具体例として、すでに紹介した RevealingReferences の Diameters メソッドを見てみます

internal int[] Diameters()
    => Array.ConvertAll(
        Wheels,
        new Converter<Wheel, int>((w) => w.Rim + w.Tire * 2));

このメソッドは、直径の計算と wheels の繰り返し処理の 2 つの責任を持っています。このメソッドを分割して単一責任なメソッドとしてみます。

public int Diameter(int rim, int tire)
    => rim + tire * 2;

internal int[] Diameters()
    => Array.ConvertAll(
        Wheels,
        new Converter<Wheel, int>((w) => Diameter(w.Rim, w.Tire)));

直径の計算方法は Diameter メソッドに切り出しました。これにより Diameter の再利用性が高まりましたね。

メソッドを単一責任にすると以下のような嬉しみがあると述べられています。

  • 隠蔽されていた性質を明らかにする
  • コメントする必要がない
  • 再利用を促進する
  • ほかのクラスへの移動が簡単