駆け出しエンジニアはPMFの夢を見るか?

駆け出しエンジニアの記録

【備忘録】(読書メモ)ちょうぜつソフトウェア設計入門

gihyo.jp

これまでも、たくさんの人が「開放閉鎖原則」をサンプルコード付きで説明してくれていて、原則の言わんとしていることは理解できたが、実際どうすれば原則を守ったコードが書けるのか分からなかった。5章のオブジェクト指向原則でSOLIDについて説明があり、この本のおかげで「開放閉鎖原則」を満たすコードを書くにはどういう指針で考えれば良いか分かった気がする。「変化するであろう仕様と変化しないであろう本質を抽出して、設計・実装する」が「開放閉鎖原則」を満たすためのポイントになりそう。

自分でもFizzBuzzを書いてみて、雰囲気は掴めたと思う。あとは実際の業務で意識して「開放閉鎖原則」を満たせるコードを意識的に書けるようにしていきたい。

FizzBuzzをTSで素振り

// 開放閉鎖原則(拡張に対してオープンであり、変更に対してクローズド)をFizzBazzを参考に理解する
// ルール1: 入力された数字をあるルールに従って変換表示する
// ルール2: 3の倍数の場合は、Fizzと表示する
// ルール3: 5の倍数の場合は、Buzzと表示する
// ルール4: 3の倍数かつ5の倍数の場合は、FizzBuzzと表示する
// ルール5: これらの条件に当てはまらない場合は数字をそのまま表示する

// 変わらないと期待できる本質は以下
// 1. 整数を入力すると文字列を返す
// 2. 任意の変換ルールを複数定義できる
// 3. 変換ルールはなんらかの判定条件を満たした時に適用される
// 4. 変換結果は前のルールの結果を受けて累積したものになる

// 🚀: ルールが追加された場合はIReplaceRuleを実装して、NumberConverterのルールに追加すれば良い(= 拡張に対してオープン)
// 🚀: 判定条件を満たした時の出力が変わる時は、それぞれのルールの実装クラスを修正すれば良い(= 変更に対してクローズド)

interface IReplaceRule {
    match(carry: string, n: number): boolean;
    apply(carry: string, n: number): string;
}

class NumberConverter {
    private rules: IReplaceRule[]
    constructor(rules: IReplaceRule[]) {
        this.rules = rules;
    }

    convert = (n: number): string => {
        let carry = "";
        for (const rule of this.rules) {
            if (rule.match(carry, n)) {
                carry = rule.apply(carry, n);
            }
        }
        return carry;
    }
}

class CyclicNumberRule implements IReplaceRule {
    private base: number;
    private replacement: string;

    constructor(base: number, replacement: string) {
        this.base = base;
        this.replacement = replacement;
    }

    match(carry: string, n: number): boolean {
        return (n % this.base == 0);
    }
    apply(carry: string, n: number): string {
        return carry + this.replacement;
    }
}

// NOTE: 何も表示しない仕様に変わる可能性があるためルールとして実装する
class PassThroughRule implements IReplaceRule {
    match(carry: string, n: number): boolean {
        return carry === "";
    }
    apply(carry: string, n: number): string {
        return n.toString();
    }
}

const fizzBuzz = new NumberConverter([
    new CyclicNumberRule(3, "Fizz"),
    new CyclicNumberRule(5, "Buzz"),
    new PassThroughRule()
]);

console.log(fizzBuzz.convert(1));
console.log(fizzBuzz.convert(3));
console.log(fizzBuzz.convert(5));
console.log(fizzBuzz.convert(15));

読んでいて印象に残ったところ

単体テストの文脈でいうモックの使われ方とは、使われ方を検証するための疑似オブジェクトのことです。モックオブジェクトは、ダミーメソッドの呼び出しが行われたかと、そのパラメータが何であったかをチェックできます。(p.133)

返り値の型がvoidのメソッドのユニットテストを書くときに、ダミーメソッドの引数が期待値通りかやダミーメソッドの呼び出し回数が期待値通りか検証するのもやり方の一つという気づきがあった。

実態の多様性を隠蔽するのが、オブジェクト指向プログラミングの核心部分です。その醍醐味を使うとき、次の課題として常に生成の分離が控えています。(p. 226

OSSソースコードを見たときに、interfaceの実装クラスが大量にあって追いにくいなと思った時があった。が、この追いにくさは隠蔽と抽象化が上手くできている可能性があって、今後どのように隠蔽と抽象化をおこなっているのかという視点でコードを読むという観点が自分に芽生えたと思う。