SOLID原則の一つであるリスコフの置換原則と、それに関係の深いScalaにおける共変反変について理解が浅かったのでまとめてみました。まとめてみたものの、いまだにわかりづらいのでまたアップデートします。
リスコフの置換原則
リスコフの置換原則 (LSP) とは、全てのクラスやサブクラスを確実に使用できるような継承階層を作成するための決まり。これに従っていないと、派生クラスを実装する際に、基底クラスやインターフェースの変更が生じる可能性がある。
SがTの派生型であるとすれば、T型のオブジェクトをS型のオブジェクトと置き換えたとしても、プログラムは動作し続けるはずである。
置換原則に従うためのルール
- 事前条件を派生型で強化することはできない。
- これはメソッドの引数に関係している。
- メソッドの引数は反変である必要がある。
- 事後条件を派生型で緩和することはできない
- これはメソッドの返り値に関係している。
- メソッドの返り値は共変である必要がある。
- 基底型の不変条件は、派生型でも維持されなければならない
Scalaにおける変位指定アノテーション
共変 (covariant)
A extends B
の時のみ
val G[B] = G[A]
という代入が許される性質を表す。
class G[+A]
のように型パラメータの前に +
をつけることで共変になる。型パラメータを指定する必要のある派生型を、基底型のインスタンスのように扱うことができる点で、ポリモーフィズムとも関連が深い。
たとえば、Scala では List が List[+A]
で定義されている。この性質のおかげで、以下のような代入が可能になる。
scala> :paste // Entering paste mode (ctrl-D to finish) abstract class Animal class Cat extends Animal class Dog extends Animal // Exiting paste mode, now interpreting. defined class Animal defined class Cat defined class Dog scala> val cats = List(new Cat()) cats: List[Cat] = List(Cat@66ee9a20) scala> val dogs = List(new Dog()) dogs: List[Dog] = List(Dog@4934cf32) scala> cats ++ dogs res1: List[Animal] = List(Cat@66ee9a20, Dog@4934cf32) scala> val animals: List[Animal] = cats animals: List[Animal] = List(Cat@66ee9a20)
反変 (contravariant)
A extends B
の時のみ
val G[A] = G[B]
という代入が許される性質を表す。
class G[-A]
のように型パラメータの前に -
をつけることで反変になる。
たとえば、Scala では Function1 が Function1[-T1, +R]
で定義されている。この性質のおかげで、以下のような代入が可能になる。
scala> val x1: String => AnyRef = (x: AnyRef) => x x1: String => AnyRef = $Lambda$6473/1201460973@326f3807
ここで、String
は AnyRef
を継承している。AnyRef
を扱える関数は当然その派生型である String
も扱うことができるということ。(しかし現実的に関数の引数以外で反変を意識することがあるのだろうかと思っている。)
参考
- Adaptive Code ~ C#実践開発手法 第2版 p.273~
- Scalaスケーラブルプログラミング 第4版 p.364~
- 変位指定 | Scala Documentation
- 型パラメータと変位指定 · Scala研修テキスト
- 共変・反変・上限境界・下限境界の関係性まとめ - Qiita