C#の設計の基本【SOLID原則】まとめ

年末年始に #ゆーじ勉強週間 として勉強していたのですが、相当勉強になることがわかったので、「週間」と言わずに定期的に勉強することにしました。

今日は設計、特にSOLID原則について。

 

参加しているSlackコミュニティで設計についての議論が盛り上がっており、いろいろ勉強したくなったので。

こちらのスライドを参考に、自分なりに勉強になった部分をまとめてみました。

 

SOLID原則

オブジェクト指向プログラミングの有名な設計原則。

単一責任の原則1つのクラスは単一の責任だけ持たせる
開放閉鎖の原則拡張に対しては開放的に、変更に対しては閉鎖的にする
リスコフの置換原則派生クラスは基底クラスに置き換えても機能するようにする
インターフェイス分離の原則不要なメソッドも実装しなければならない状態は避ける
依存性逆転の原則下位モジュールではなく抽象に依存するようにする

単一責任の原則

これはよく聞くやつ。
何か仕様変更があった際に、他のクラスに影響しないようにする。

 

クラス名を工夫するのがポイント。
(例えば「PlayerController」ではなく「PlayerMover」「PlayerAnimator」にすれば、クラス内に「Playerを動かす処理」「Playerのアニメーション制御処理」以外に書けなくなる。)

考えすぎると「1クラス1メソッド」みたいになってしまうが、複数の人が同じファイルをいじらないようにするのが主な目的の一つ。

 

「サウンド担当がPlayerManager触って、アニメーション担当がPlayerManager触って、サーバサイド担当がPlayerManager触って・・・」とかの状態にならないように、「そのクラスを使う目的が1つであるべき」であるとする原則。

 

開放閉鎖の原則

機能の追加(拡張)は簡単にできないといけない。
ただし、その時に既存のコードに修正があってはいけない。

 

つまり、基底クラスやインターフェイスを使って「抽象」に対して処理を行うようにする。

 

【ダメな例】

void OnCollisionEnter(Collision other){
    string tag = other.gameObject.tag;
    if(tag == "Player") other.GetComponent().Damaged();
    else if(tag == "Enemy") other.GetComponent().ApplyDamage();
    else if(tag == "Boss") other.GetComponent().ApplyDamage();
}

これだと他に対象のタグが追加されたりした時にOnCollisionEnter内にも処理を追加しなければいけない。

 

【良い例】

void OnCollisionEnter(Collision other){
    other.GetComponent()?.ApplyDamage();
}

呼び出し元ではインターフェイス(抽象)に対して「ダメージを与える」という命令を出すだけ。
PlayerかEnemyかは知らないし、処理内容も知らない。

 

実際のダメージ処理はインターフェイスを実装した側に記述する。

 

これによって、他に対象のタグが追加されたりした場合でもOnCollisionEnter内の処理は全く変わらず、対象のインターフェイスを実装するだけで良い。
(元のコードには全く手を加えないまま、新たに追加するだけで変更が完結する)

 

リスコフの置換原則

派生クラスでは基底クラスでのルールをしっかりと守ること。
アクセスレベルを書き換えたり、条件判定を強化したりするのは良くない。

この原則を守らないと型安全性が壊れ、かつ「この派生クラスのときはこの処理」みたいな条件分岐ができてしまったりする。

 

インターフェイス分離の原則

インターフェイスは汎用的なものを作るよりも、細かく分けて複数継承させる。(せっかく多重継承できるようになっているんだし)

「不要な処理なのに実装しなければいけない」という状況をなくす。

interface IChara {
    void Move(Vector3 vec);
    void Attack(Vector3 dir);
}

上記のようなインターフェイスがあった時に、
「Playerで使う場合はMoveとAttack両方実装するけど、Enemyで使う場合はMoveしか実装しない」
みたいなことが起こってしまう可能性がある。

 

依存性逆転の原則

上位モジュールが下位モジュールに依存してはいけない。
(上位モジュールの処理が下位モジュールの種類や状態によって変わってはいけない。)

先ほど「開放閉鎖の原則」での例と同じ。

上位モジュールも下位モジュールも「抽象」に対して依存するようにする。

「Player→Enemy」を「Player→interface←Enemy」にする。(依存性の逆転)

コメント