●はじめに
前:【Unity】公式プロジェクトから、デザインパターンを学んでみよう(Part 1:単一責任の原則)
今回は、SOLID原則のO部分である、開放・閉鎖の原則(Open-Closed)について学んでいきます。
●開放・閉鎖の原則とは?
簡単に言うと、変更には閉鎖的で、拡張には開放的にしようっていう原則です。
元々あるコードを変更せずに、クラスや関数を新規作成して拡張する感じです。
これでもあんまりしっくりこないと思うので、実際にコードを見てみましょう。
●プロジェクト内容
- AreaCalculator
- Circle
- Rectangle
- Shape
○AreaCalculator
- 図形の面積の計算を行うクラス
○Cicle
- Shapeクラスの子クラス。
- 円の情報が入ってる。
○Rectangle
- Shapeクラスの子クラス。
- 矩形の情報が入ってる。
○Shape
- 図形に関する抽象クラス。(抽象クラスについて、下で解説してます)
●コード内容
○AreaCalculator
まずは、AreaCalculatorから見てみましょう
クリックして展開
using UnityEngine;
namespace DesignPatterns.OCP
{
public class AreaCalculator
{
// old implemenation: not using open-closed principle
//public float GetRectangleArea(Rectangle rectangle)
//{
// return rectangle.Width * rectangle.Height;
//}
//public float GetCircleArea(Circle circle)
//{
// return circle.Radius * circle.Radius * Mathf.PI;
//}
// new implementation: use open-closed principle
// let each Shape contain the logic for calculating the area
public float GetArea(Shape shape)
{
return shape.CalculateArea();
}
}
}
☆ダメな例
上部のコメントを翻訳してみます。
古い実装: 開閉原理を使用していない
コメントアウトされた部分は、開放・閉鎖の原則に反しているっぽいですね
改めて見てみましょう。
public float GetRectangleArea(Rectangle rectangle)
{
return rectangle.Width * rectangle.Height;
}
public float GetCircleArea(Circle circle)
{
return circle.Radius * circle.Radius * Mathf.PI;
}
これがダメな理由は…
- 図形が増えたら、物凄いコード量になっちゃう
- その図形のコードとこのAreaCalculator をわざわざ行き来しないといけない
からです。
今は円と矩形だけなので問題ないですが、この世に図形は結構存在します。→図形一覧(Wiki)
こんなたくさんの図形の計算を、このAreaCalculatorの中にまとめるのは大変です。
また、引数にCicleやRectangleなどを指定して、プロパティを呼び出して計算していますが、プロパティの定義をしたり、面積の計算式を記述したりでクラスから別のクラスへの移動がめんどくさいので、そのクラス内に計算する関数を置いちゃった方がいいっていう感じです。
☆良い例
次に、下部のコメントとコードを見てみます。
新しい実装: 開閉原理を使用
各形状に面積を計算するためのロジックを含めます
先ほど記述した通り、それぞれのShapeを継承したクラス内に、面積を求める関数を追加しようって感じです。
public float GetArea(Shape shape)
{
return shape.CalculateArea();
}
こんな感じにすれば、関数呼び出し時に引数shapeにShapeクラスを継承した各図形を指定するだけで、その図形の面積が求められます。
また、計算本体は↓で解説します。
○Shape
クリックして展開
namespace DesignPatterns.OCP
{
public abstract class Shape
{
public abstract float CalculateArea();
}
}
(抽象化についての説明です。既知の方は飛ばしてください)
-
クラスと関数に
abstract
っていうのがついてます。これは、クラスや関数を抽象化するための命令です。
抽象化は、抽象クラスを子クラスに継承させたときに、必ず実装しないといけない関数を指定できます。 -
また、抽象化されたクラスを抽象クラス、抽象化されたメソッドを抽象メソッドといいます。
- 抽象クラスとは、抽象メソッド(関数)が1つ以上あるクラスです。
-
抽象メソッドとは、抽象クラス内に定義されてるだけで、何も処理をしないメソッドです。
抽象メソッドの中身は、それぞれの継承先のクラスに記述できます。
情報量が多いですが…そこまで難しくないです!!
一言でいうと、「関数(メソッド)にabstractがついてたら抽象メソッドだから、継承先で中身書かないといけない」ってだけです。
(継承についても説明しちゃうと…少し長くなっちゃうので、各自調べて下さいませ…!)
…ということで、このクラスの抽象メソッドは…
public abstract float CalculateArea();
↑こいつです。また、抽象メソッドがShapeクラスにあるということは、Shapeクラスは抽象クラスということになります。
Shapeクラスを継承しているCircleやRectangleクラスは、このCalculateArea()
を必ず定義しないといけません。
(実際の定義は↓で解説します。)
○Cicle
クリックして展開
using UnityEngine;
namespace DesignPatterns.OCP
{
public class Circle : Shape
{
public float Radius { get; set; }
public override float CalculateArea()
{
return Radius * Radius * Mathf.PI;
}
}
}
抽象クラスであるShapeを継承してるので、必ず定義をしないといけない抽象メソッドがあります。
それは…
public override float CalculateArea()
↑こいつです。 でも、ちょっとだけ抽象クラスの時と違うような…?
さっきのabstract
が…override
になってる…!!
抽象クラスで定義した抽象メソッドは、継承先ではoverrideをつけて定義します。
また、抽象メソッドとは異なり、実際に中身を記述できます。
今回は、Circleクラスなので、面積は、半径×半径×πで求まります。
public override float CalculateArea()
{
return Radius * Radius * Mathf.PI;
}
こんな感じで、同じ関数をそのクラスごとに別々で指定できます。
次のRectangleクラスは、この計算方法とは違う処理をしています。
○Rectangle
クリックして展開
namespace DesignPatterns.OCP
{
public class Rectangle : Shape
{
public float Width { get; set; }
public float Height { get; set; }
public override float CalculateArea()
{
return Width * Height;
}
}
}
ほとんどCircleクラスと同じ感じですが、プロパティやCalculateArea()内の計算方法が異なります。
矩形(四角形)なので、面積は幅×高さで求まります。
public override float CalculateArea()
{
return Width * Height;
}
●さいごに
元からあるコードを変更するのではなく、新しくクラスやメソッドを作成して拡張していくことが大切ってのが理解できたら幸いです!
また、抽象クラスも便利なので、積極的に使ってよりよいコードを作っていきましょう!!!!