前:https://qiita.com/kiku09020/items/bfcf01511dc385c797ac
●はじめに
今回は、公式プロジェクトのコードを見ながら、引き続きSOLID原則について個人的にまとめてみます。
●リスコフの置換原則(LiskovSubstitution)とは
親クラスができることは子クラスにもできるようにしろってことです。
クソ適当な説明なので、実際に見てもらった方が理解できます。多分
●プロジェクト内容
- Unused/Vehicle
- Car
- IMovable
- ITurnable
- Navigator
- RailVehicle
- RoadVehicle
- Train
●コード内容
○ Unused/Vehicle
未使用の「乗り物」クラスです。
乗り物にもいろんな乗り物がありますが、全ての乗り物の動作の関数とかを記載してしまっている感じです。
例えば、前進、後退、右左折、という各操作の関数があるとしたら、右左折できない電車などの乗り物は、不要な関数がある状態なので、リスコフの置換原則に反した設計になります。
クリックしてコードを展開
using UnityEngine;
namespace DesignPatterns.LSP
{
public class Vehicle
{
public float speed = 100;
public string name;
public Vector3 direction;
// Don't include these in the base class; use composition instead of inheritance.
// We put the functionality instead into the ITurnable and IMovable interfaces.
// The RailVehicle and RoadVehicle classes can then implement only what they need.
//public void GoForward(){}
//public void Reverse(){}
//public void TurnRight(){}
//public void TurnLeft(){}
}
}
コメントを翻訳してみます
これらを基本クラスに含めないでください。継承の代わりに構成を使用します。
代わりに、ITurnable および IMovable インターフェイスに機能を配置します。
RailVehicle クラスと RoadVehicle クラスは、必要なものだけを実装できます。
「インターフェース使って、RailとRoadに必要な関数だけ取り入れろ」って感じのこと言ってます。
これは下で説明します。
○ IMovable
移動可能な乗り物につけるインターフェースです。
乗り物だったら、ほとんど移動できるので、RailVehicleとRoadVehicleの両方についてます。
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.LSP
{
public interface IMovable
{
public float MoveSpeed { get; set; }
public float Acceleration { get; set; }
public void GoForward();
public void Reverse();
}
}
プロパティは、移動に関する、移動速度や加速度などが用意されています。
また、乗り物の「移動可能」というのは、「前進・後退ができる」ということなので、前進と後退それぞれの関数が用意されています。 (多分左右にも移動できる乗り物がありますが…そこらへんは割愛で。)
○ ITurnable
右左折可能な乗り物につけるインターフェースです。
線路を通るRailVehicleは右左折できないので、RoadVehicleのみにつけます
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.LSP
{
public interface ITurnable
{
public void TurnRight();
public void TurnLeft();
}
}
右折、左折する関数が定義されています。それだけです。
○ RoadVehicle
道路を通る乗り物のクラスです。
道路を通るので、前進、後退、右左折ができます。
クリックしてコードを展開
using UnityEngine;
namespace DesignPatterns.LSP
{
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 RoadVehicle : IMovable, ITurnable
こんな感じで移動と右左折のインターフェースを適用してます。
インターフェースは、抽象クラスの上位互換的なやつだと思っていいと思います。
複数のクラスを継承できちゃうような感じです。
気難しいことは↓で解説されてるのでどうぞ
インターフェース - C# によるプログラミング入門
○ RailVehicle
線路を通る乗り物のクラスです。
線路を通るので、前進と後退のみしかできません。
右左折関数を除いているので、リスコフの置換原則に則った設計ができています。
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.LSP
{
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()
{
}
}
}
public class RailVehicle : IMovable
今回は、移動だけできればいいのでIMovableのみ適用してます。
○ Car
RoadVehicleクラスの子クラスのひとつです。中身は空っぽなので省略
○ Train
RailVehicleのクラスの子クラスのひとつです。Car同様に省略
○ Navigator
運転手のクラスだと思います。
乗る乗り物によって、関数の内容を変更しています。
クリックしてコードを展開
namespace DesignPatterns.LSP
{
public class Navigator
{
// Imagine a turn-based game where you move the Vehicle along a prescribed path: forward, left, right
// This example violates Liskov Substitution Principle. We cannot substitute the subtype Train
// for the type Vehicle here. A train cannot functionally turn left/right:
//public void Move(Vehicle vehicle)
//{
// vehicle.GoForward();
// vehicle.TurnLeft();
// vehicle.TurnRight();
//}
// Instead, we can use these methods that do follow Liskov Substitution principle.
// Here, you can use a Car or a Truck wherever we use the type
// RoadVehicle. You can use a Train where we encounter RailVehicle.
// Enforce inheritance hierarchy based on software design, not on real-world analogies.
public void MoveRoadVehicle(RoadVehicle roadVehicle)
{
roadVehicle.GoForward();
roadVehicle.TurnLeft();
roadVehicle.TurnRight();
}
public void MoveRailVehicle(RailVehicle railVehicle)
{
railVehicle.GoForward();
railVehicle.GoForward();
railVehicle.Reverse();
}
}
}
コメントの翻訳
悪例
所定の経路 (前方、左、右) に沿ってビークルを移動するターン制のゲームを想像してください。
この例はリスコフの置換原則に違反しています。サブタイプ Train を代用することはできません
タイプ Vehicle の場合はこちら。列車は機能的に左右に曲がることができません:
public void Move(Vehicle vehicle)
{
vehicle.GoForward();
vehicle.TurnLeft();
vehicle.TurnRight();
}
列車は曲がれないのに、TurnLeftとTurnRightがあったらダメでしょ❗😡😡😡って言ってます。
適例
代わりに、リスコフの置換原理に従うこれらのメソッドを使用できます。
ここでは、タイプを使用する場所で Car または Truck を使用できます
RoadVehicle. RailVehicle に遭遇した場所で Train を使用できます。
(現実世界の類推ではなく、ソフトウェア設計に基づいて継承階層を適用します。)
public void MoveRoadVehicle(RoadVehicle roadVehicle)
{
roadVehicle.GoForward();
roadVehicle.TurnLeft();
roadVehicle.TurnRight();
}
public void MoveRailVehicle(RailVehicle railVehicle)
{
railVehicle.GoForward();
railVehicle.GoForward();
railVehicle.Reverse();
}
ガバ翻訳ですが…「道路を移動する乗り物」と「線路を移動する乗り物」で分けて、別々の処理にしろよ❗✊😁って言ってます。
●さいごに
公式のドキュメントからの引用ですが、今回の設計を図式化するとこんな感じです。
矢印は、そのクラスが参照しているクラスへの方向を示しています。
こういうのをクラス図っていうらしいです。すごい見やすい…。少しめんどくさそうですが、いつか作ってみます。
インターフェース、使えたらすごい便利だなーと思いました。もっと早く知りたかった!!!