この記事はPONOS Advent Calendar 2022の17日目の記事です。
昨日は、@nisei275さんの「RustでWebAPIを構築し単純なリクエストデータを返してみる」でした。
※この記事内のサンプルソースは、C#、Unityでの実行環境を想定しています
設計とは何か
何をどのようにつくるのか決定する作業
開発フローの中での位置付け
- 仕様決定
- 設計 ←ここ
- コーディング
設計すると何が良いのか
-
全体像が可視化される
- 効率よくコーディングしやすい
- 規模感がわかる
- 今後問題が起きそうな箇所を事前に洗い出せる
設計をするには
・ UML の クラス図 を読み書きできるようになるのが良い
・ 設計原則を覚える必要がある
クラス図とは
UMLとは
統一モデリング言語(Unified Modeling Language)
オブジェクト指向でシステムの仕様を分析、設計、記述するのに使う
MacでVSCodeを使用している場合は、下記のURLを参考に「PlantUML」で クラス図作成が可能 です
https://qiita.com/wariichi/items/e55a728b10b310c822f2
SOLID原則
オブジェクト指向プログラミングにおける5つの原則
-
単一責任の原則(Single Responsibility Principle)
クラスは単一の責任を持つべきだ
class Animal
{
// 名前の取得
public string GetName () { }
// 情報をデータベースにセーブ
public bool Save () { }
}
上記クラスはAnimalのプロパティ管理とデータベース管理の2つの責任を負ってしまっています。
下記のようにクラスを分ける事で、機能追加や変更を行う際の影響範囲を狭める事ができます
class Animal
{
// 名前の取得
public string GetName () { }
}
class AnimalDB
{
// 情報をデータベースにセーブ
public bool Save () { }
}
-
開放/閉鎖の原則(Open-Closed Principle)
モジュールは拡張について開き、修正に対して閉じていなければいけない
→機能の追加は行いやすく。その際に修正が発生しないように
void OnHitBullet (GameObject hit)
{
switch (hit.tag)
{
case "Player":
if (hit.TryGetComponent (out Player src))
{
// 衝突処理
src.HitBullet ();
}
break;
case "Enemy":
if (hit.TryGetComponent (out Enemy src))
{
// 敵にダメージを与える
src.ApplyDamage ();
}
break;
}
}
上記のコードは、弾の衝突時の処置を行う関数ですが、衝突対象が増えるたびにswitch文のcaseを追加していく必要があります。また、追加が漏れた際にもエラーなく動作するので問題に気づきにくいでしょう。
基底クラスやインターフェースを使う実装に変更する事で上記問題を解決できます
void OnHit (GameObject hit)
{
if (hit.TryGetComponent (out IApplicableDamage src))
{
src.ApplyDamage ();
}
}
-
リスコフの置換原則(Liskov Substitution Principle)
派生型はその基底型で置き換え可能でなければならない
例) 基底型:Candy、派生型:BigCandy
public class Candy
{
public virtual string GetPrice ()
{
return "30";
}
}
public class BigCandy : Candy
{
public override string GetPrice ()
{
return "100円";
}
}
public class Print
{
public static void PrintPrice (string price)
{
return "{price}円です"
}
}
var candy = new Candy ();
// 30円です
console.log(Print.PrintPrice (candy.GetPrice ()));
var bigCandy = new BigCandy ();
// 100円円です ←日本語がおかしい
console.log(Print.PrintPrice (bigCandy.GetPrice ()));
上記の場合、GetPrice関数の振る舞いが基底クラスと派生クラスで変わってしまっています
今回は、戻り値を統一する事で基底クラスと派生クラスの振る舞いを統一することができました
public class Candy
{
public virtual string GetPrice ()
{
return "30";
}
}
public class BigCandy : Candy
{
public override string GetPrice ()
{
return "100"; // 基底クラスと戻り値を統一!
}
}
-
依存性逆転の原則(Dependency Inversion Principle)
上位モジュールが下位モジュールに依存してはいけない。どちらも抽象に依存すべきである
上位モジュール: 相手を使う側のクラス
下位モジュール: 使われる側のクラス
void OnHit (GameObject hit)
{
switch (hit.tag)
{
case "Player":
if (hit.TryGetComponent (out Player src))
{
// 衝突処理
src.HitBullet ();
}
break;
case "Enemy":
if (hit.TryGetComponent (out Enemy src))
{
// 敵にダメージを与える
src.ApplyDamage ();
}
break;
}
}
上記のコードは、弾衝突時の処理がタグに依存してしまっています
(タグに変更が加わった際に弾衝突時の処理をいじらなければならない)
インターフェースを実装することで、衝突処理のタグへの依存をなくし、タグがインターフェースに依存する様にしましょう(依存性の逆転)
void OnHit (GameObject hit)
{
if (hit.TryGetComponent (out IApplicableDamage src))
{
src.ApplyDamage ();
}
}
-
インターフェース分離の原則(Interface Segregation Principle)
クライアントが利用しないメソッドへの依存を強制してはいけない
不必要なメソッドがインターフェースに含まれていると、使う側で混乱する
→適切にインターフェースを分解しよう
まとめ
設計をする事によって、全体像が可視化されるメリットはとても大きいです(効率よくコーディングしやすい、規模感がわかる等)
実際に手を動かしながら設計を試し、より良いコーディングを目指していきたいですね
明日は@caramel_cafeさんです。