LoginSignup
19
11

【SOLID】リスコフの置換原則を完全に理解したい

Last updated at Posted at 2022-03-10

SOLIDの原則とは?

SOLIDは

  • 変更に強い
  • 理解しやすい

などのソフトウェアを作ることを目的とした原則です。
次の5つの原則があります。

  • Single Responsibility Principle (単一責任の原則)
  • Open-Closed Principle (オープン・クローズドの原則)
  • Liskov Substitution Principle (リスコフの置換原則)
  • Interface Segregation Principle (インタフェース分離の原則)
  • Dependency Inversion Principle (依存関係逆転の原則)

上記5つの原則の頭文字をとってSOLIDの原則と言います。
今回の記事では Liskov Substitution Principle (リスコフの置換原則) について解説します。

その他の原則に関しては下記参照。

簡単に言うと...

あるクラスを継承するとき、継承元と継承先のクラスの振る舞いを同じにしよう」ということです。
あるいは「あるクラスを複数のクラスに継承するとき、継承先のクラスの振る舞いを同じにしよう」と表現することもできます。

簡単な具体例を出しますと、

  • 継承元クラスAと継承先クラスB1, B2が存在する
  • 以下のようにインスタンスを生成するとき、
    • A hoge = new B1();
    • A hoge = new B2();
  • hogeのメソッドを呼ぶときに同じような振る舞いをする

ということです。

では、同じような振る舞いとは一体何を指しているのでしょうか。
もう少し詳しく見ていきましょう。

リスコフの置換原則の定義

「アジャイルソフトウェア開発の奥義」という書籍では、

S型のオブジェクトo1の各々に、対応するT型のオブジェクトo2が1つ存在し、Tを使って定義されたプログラムPに対してo2の代わりにo1を使ってもPの振る舞いが変わらない場合、SはTの派生型であると言える。

と書かれています。難しくてよくわからないですね。
Wikipediaはもう少し平易に表現していますが、

リスコフの置換原則(りすこふのちかんげんそく、: Liskov substitution principle)は、オブジェクト指向プログラミングにおいて、サブタイプのオブジェクトはスーパータイプのオブジェクトの仕様に従わなければならない、という原則である。

これでもまだ難しいですね。
また、同じくWikipediaにて、リスコフの置換原則を満たすために、下記の項目の守る必要があるとされています。

  1. 事前条件(preconditions)を、派生型で強めることはできない。派生型では同じか弱められる。
  2. 事後条件(postconditions)を、派生型で弱めることはできない。派生型では同じか強められる。
  3. 不変条件(invaritants)は、派生型でも保護されねばならない。派生型でそのまま維持される。
  4. 基底型の例外(exception)から派生した例外を除いては、派生型で独自の例外を投げてはならない。

今回はこのうち1-3の項目それぞれについて、具体例を用いながら解説していきます。

事前条件について

そもそも事前条件とは何でしょうか。
Wikipediaには

事前条件 (precondition) は、メソッド開始時に保証されるべき条件の表明である。

と示されています。
例えば、「除算をするメソッドの中で割る数が0ではない」というのは事前条件に当たります。

それでは、

事前条件(preconditions)を、派生型で強めることはできない。派生型では同じか弱められる。

とはどういうことでしょうか。

下図は、RPGのキャラクターについて表現した図です。
事前条件.png

剣士と魔法使いはプレーヤーを継承しています。
引数に武器を受け取る攻撃という振る舞いを持っています。
プレーヤーは武器を参照しています。
剣と魔法の杖は武器を継承しています。

ここで、

  • 剣士が使える武器は剣のみ
  • 魔法使いが使える武器は魔法の杖のみ

という条件をつけくわえます。
プレーヤーの攻撃メソッドを見ると、引数の武器に対する制限はとくにありません。
つまり、プレーヤーの攻撃メソッドはどんな武器でも扱えるということになります。
プレーヤーを継承したサブクラス (剣士クラスや魔法使いクラス) では、使える武器に制限があります。
使える武器の制限を表現するため、攻撃メソッド内で武器の種類チェックなどを挟む必要があるでしょう。

プレーヤーの攻撃メソッドの事前条件は「すべての武器」であるのに対し、
剣士の攻撃メソッドの事前条件は「剣のみ」、
魔法使いの攻撃メソッドの事前条件は「魔法の杖のみ
となっており、事前条件が狭まっています。
言い換えると、事前条件を強めています。

従って上記の例は、

事前条件(preconditions)を、派生型で強めることはできない。派生型では同じか弱められる。

というリスコフの置換原則に反しているということになります。

事後条件について

そもそも事後条件とは何でしょうか。
Wikipediaには

事後条件 (postcondition) は、メソッド正常終了時に保証されるべき条件の表明である。これはメソッド単位で表明される。正常終了とは、例外スロー終了やエラー発生終了ではないことを指す。

と示されています。

例えば、int Add(int x, int y)というメソッドであれば「xとyを足した値を返す」というのは事後条件に当たります。
あるいは、List<T>.Add(T)メソッドであれば、「Listの要素数が1増えること」「Listの末尾に要素が追加されること」などが事後条件に当たります。
それでは

事後条件(postconditions)を、派生型で弱めることはできない。派生型では同じか強められる。

とはどういうことでしょうか。

下図は有名な正方形・長方形問題の図です。
事後条件.png

長方形には

  • 4つの角がすべて等しい (すべて90度)
  • 二組の対辺がそれぞれ等しい

などの特徴があります。
上記の特徴を満たし、かつ、すべての辺の長さが等しい四角形を正方形と定義できます。
つまり、正方形は長方形の一種であるため、
上記例では正方形が長方形を継承しています。

ここで長方形と正方形のSetHeight()についてみてみましょう。

Rectangle.cs
// 長方形のSetHeight
public class Rectangle
{
    // ...略
    public void SetHeight(int height)
    {
        Height = height;
    }
    // ...略
}
Square.cs
// 正方形のSetHeight
public class Square
{
    // ...略
    public void SetHeight(int height)
    {
        Height = height;
        Width = height;
    }
    // ...略
}

長方形のほうでは、高さのみを更新しているのに対して、
正方形のほうでは、高さと幅を更新しています。

ここで、長方形と正方形のSetHeightに関して、事後条件を考えてみましょう。
長方形のSetHeightの事後条件は、

  • Heightが変わること
  • Widthが変わらないこと

です。
一方、正方形のSetHegithの事後条件は、

  • Heightが変わること
  • Widthが変わること

です。
長方形のSetHeightの事後条件である「Widthが変わらないこと」が正方形の場合ではなくなっています。
言い換えると、正方形は長方形を継承し、事後条件を弱めてしまっています。

従って上記の例は、

事後条件(postconditions)を、派生型で弱めることはできない。派生型では同じか強められる。

というリスコフの置換原則に反しているということになります。

不変条件について

そもそも不変条件とは何でしょうか。
Wikipediaには

クラス不変条件 (class invariant) は、クラスが持つ公開された各メソッドの開始時と正常終了時に共通して保証されるべき状態についての条件である。

と示されています。

例えば、自然数であれば「0より大きい整数」が不変条件に当たります。

それでは

不変条件(invaritants)は、派生型でも保護されねばならない。派生型でそのまま維持される。

とはどういうことでしょうか。

下図は未成年と乳幼児についての図です。
不変条件.png

未成年は0歳以上18歳未満です (2022年4月から変わりますね!)。
乳幼児は0歳以上6歳未満です (正確には未就学児らしいです)。

未成年の不変条件は、そのまま「0歳以上18歳未満」であることです。
乳幼児の不変条件も、そのまま「0歳以上6歳未満」であることです。
上記の例の継承では、不変条件が狭まっています。
言い換えると、不変条件がそのまま維持されていません。

従って上記の例は、

不変条件(invaritants)は、派生型でも保護されねばならない。派生型でそのまま維持される。

というリスコフの置換原則に反しているということになります。

まとめ

リスコフの置換原則とは「あるクラスを継承するとき、継承元と継承先のクラスの振る舞いを同じにしよう」ということでした。
これはオープン・クローズドの原則にも通じるところがあります。

リスコフの置換原則を守るためには下記のような原則を守る必要があります。

  1. 事前条件(preconditions)を、派生型で強めることはできない。派生型では同じか弱められる。
  2. 事後条件(postconditions)を、派生型で弱めることはできない。派生型では同じか強められる。
  3. 不変条件(invaritants)は、派生型でも保護されねばならない。派生型でそのまま維持される。
  4. 基底型の例外(exception)から派生した例外を除いては、派生型で独自の例外を投げてはならない。

参考文献

19
11
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
11