【C#設計】SOLID原則をUnity公式サンプルで学ぼう~L:リスコフの置換原則~

はじめに

講座トップに戻る

この講座ではプログラミングの設計を勉強する際に避けては通れない「SOLID原則」について学ぶことができます。

SOLID原則は有名ですので名前を知っている人も多いかもしれませんが、全然知らない人でも大丈夫です!

初心者でも分かりやすいようにできるだけ丁寧に解説します。

「Unityの使い方は分かってきたけど、コードが綺麗に書く方法が分からない」

「設計を勉強するといいらしいけど、どうやって勉強したらいいか分からない」

こんな悩みを持っているあなたは、この記事で解決できるかもしれません!

この記事ではUnity公式のサンプルをもとに解説していきます。

公式サンプルのダウンロード方法はこちらの記事を参考にしてください。

SOLID原則とは?

OLID原則とはオブジェクト指向において、拡張性・保守性が高く保つために守る原則のことです。

これは、ソフトウェアエンジニアのRobert C. Martinが提唱した多くの設計原則を5つにまとめたものです。

それぞれの頭文字をとってSOLID原則と呼ばれています。

  • S … Single Responsibility Principle: 単一責任の原則
  • O … Open-Closed Principle: 開放閉鎖の原則
  • L … Liskov Substitution Principle: リスコフの置換原則
  • I … Interface Segregation Principle: インターフェイス分離の原則
  • D … Dependency Inversion Principle: 依存性逆転の原則

何も考えずにプログラムを書いていると、段々と拡張性・保守性が下がってきます。

  • 機能の追加、変更に時間がかかる
  • コードに再利用性がない
  • 膨大な機能を持ったクラスが出来上がる(こういうクラスを神クラスと言ったりします笑)
  • 依存関係が複雑でクラス同士の関係性がハッキリしていない

SOLID原則を学ぶことで、上記のような悩みを解決できる可能性があります。

今回はSOLID原則のL「リスコフの置換原則」を解説していきます。

SOLID原則などの設計に関する学習をしていると「モジュール」という言葉が良く出てきます。

モジュールというと分かりにくいかもですが、C#の場合クラスに置き換えて考えてください。

リスコフの置換原則とは?

これは「親クラスと子クラスは置換可能でなければならない」という原則です。

つまり、使う側は親クラス(基底クラス)の事だけを理解しておけば正常に動かすことができる状態が良いという事です。

もしも置換可能でない場合、子クラスができるたびに使う側は子クラス独自の仕様を把握する必要があります。

使う側が覚えることを減らし使いやすくすることが目的です。

ここでいう置換可能とういのは意図した通りに動くという事ではありません。

プログラムがエラー無く動くという意味です。

犬クラスを使っていたところを猫クラスにすると挙動はもちろん変わります。

あくまで子クラスの詳しい仕様は知らなくても使える状態にするためには、こうあるべきだよねという事です。

Unity公式サンプルを見てみよう

Assets/3 LiskovSubstitution/Scriptsというフォルダの中に8つのスクリプトがあります。

これらのスクリプトはそれぞれ以下のような内容になっています。

  • Unused/Vehicle.cs・・・改善前の基底クラス
  • IMovable.cs・・・移動可能インターフェース
  • ITurnable.cs・・・左右旋回可能インターフェース
  • RailVehicle.cs・・・レールに沿って移動する乗り物の基底クラス
  • RoadVehicle.cs・・・前後、左右に移動できる乗り物の基底クラス
  • Car.cs・・・車クラス
  • Train.cs・・・電車クラス
  • Navigator.cs・・・乗り物クラスを操作するクラス

乗り物を例にしたサンプルコードになっています。

まずは良くない例から見ていきましょう。

改善前のコード

・乗り物基底クラス

public class Vehicle
{
    public float speed = 100;
    public string name;
    public Vector3 direction;

    public void GoForward()
    {

    }

    public void Reverse()
    {

    }

    public void TurnRight()
    {

    }

    public void TurnLeft()
    {

    }
}

・乗り物操作用クラス

public class Navigator
{
    public void Move(Vehicle vehicle)
    {
        vehicle.GoForward();
        vehicle.TurnLeft();
        vehicle.TurnRight();
    }
}

現状の悪い点

乗り物の基底クラスVechicleに前後・左右の移動関数が定義されています。

前後にしか進まない電車クラスを作る際にVehicleクラスを継承した場合を考えてみましょう。

電車は左右に移動できないにも関わらず、左右移動の関数を呼び出すことができてしまいます。

この状態では使う側は子クラス(電車クラス)の左右に移動できないという機能を把握しておく必要があり、原則に反している状態となります。

改善後のコード

・前後移動用インターフェース

public interface IMovable
{
    public float MoveSpeed { get; set; }
    public float Acceleration { get; set; }

    public void GoForward();
    public void Reverse();
}

・回転可能インターフェース

public interface ITurnable
{
    public void TurnRight();
    public void TurnLeft();
}

・前後、左右に移動できる乗り物の基底クラス

public class RoadVehicle : IMovable, ITurnable
{
    public string Name;

    private float moveSpeed = 100f;
    private float acceleration = 5f;

    public float TurnSpeed = 5f;

    public float MoveSpeed { get => moveSpeed; set => moveSpeed = value; }
    public float Acceleration { get => acceleration; set => acceleration = value; }

    public virtual void GoForward()
    {

    }

    public virtual void Reverse()
    {

    }

    public virtual void TurnLeft()
    {

    }

    public virtual void TurnRight()
    {

    }
}

・前後移動のみする乗り物の基底クラス

public class RailVehicle : IMovable
{
    public string Name;

    private float moveSpeed = 100f;
    private float acceleration = 5f;

    public float TurnSpeed = 5f;

    public float MoveSpeed { get => moveSpeed; set => moveSpeed = value; }
    public float Acceleration { get => acceleration; set => acceleration = value; }


    // implement these differently than RoadVehicles
    public virtual void GoForward()
    {

    }

    public virtual void Reverse()
    {

    }
}

・車クラス(RoadVehicleクラスを継承)

public class Car : RoadVehicle
{

    //車にのみ必要な処理を書く
}

・電車クラス(RailVehicleクラスを継承)

public class Train : RailVehicle
{
    //電車にのみ必要な処理を書く
}

・乗り物操作用クラス

public class Navigator
{
    public void MoveRoadVehicle(RoadVehicle roadVehicle)
    {
        roadVehicle.GoForward();
        roadVehicle.TurnLeft();
        roadVehicle.TurnRight();
    }

    public void MoveRailVehicle(RailVehicle railVehicle)
    {
        railVehicle.GoForward();
        railVehicle.GoForward();
        railVehicle.Reverse();
    }
}

改善後の良い点

改善後では前後移動用、左右移動用それぞれのインターフェースを作成しました。

こちらを使って、前後移動だけするクラス(RailVehicle)と前後左右に移動する(RoadVehicle)基底クラス作成しています。

車クラスと電車クラスはそれぞれに合ったクラスを継承することで、使う側は基底クラスのみの仕様を把握していれば移動させることができます。

他の乗り物が追加されても、使う側はその乗り物の詳細を知る必要がありません。

これは「クラスを置換可能な状態にする」というリスコフの置換原則に則しています。

まとめ

以上が「リスコフの置換原則」でした。

クラスを置換可能な状態にすると、使う側が把握しないといけな仕様が減るため複数人で作る際にも有効です。

また置換できる状態であれば、テスト用のクラスを作って差し替えることもできるのでデバッグの際にも便利です。

👇その他の原則を学ぶ

S:単一責任の原則

O:開放閉鎖の原則

L:リスコフの置換原則←今ここ

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

D:依存性逆転の原則

👇あなたにお勧めの書籍

デザインパターンが分かりやすくまとめられていて、設計を学ぶ際におすすめです!

コメント