例外について見てみる。基本クラスでスローするとされていない例外は、LSP により、派生クラスでもスローしてはいけないことになる。
ここで言う "基本クラスでスローするとされていない例外" とは "基本クラスでスローしない例外" ではない。.NET なら XML コメントの <exception> セクションで宣言されていない例外のことを指している。Java は詳しくないけど Java なら throws で宣言されていない例外。 ( ※ )
これを逆に言えば、基本クラスではスローしなくとも、派生クラスでスローされる可能性があるのなら、基本クラスで宣言しなければならないということになる。でなければ LSP に違反してしまう。
ここで注意しなければいけないのが、例外の抽象度だと思う。
例えば、書籍データをデータストアから取得するための GetBookData メソッドが IBookFinderインターフェイスに定義されているとする。
IBookFinder インターフェイスを利用するアプリケーションは今の所 2 つあるとする。片方はデータストアとして XML ファイルを利用し、BookFinderFromXml クラスが IBookFinder インターフェイスを実装している。もう片方はデータストアとして DB を利用し、BookFinderFromDB クラスが IBookFinder インターフェイスを実装している。
BookFinderFromXml クラスの GetBookData メソッドは、XML ファイルが見つからなかった時に FileNotFoundException をスローする。
一方、BookFinderFromDB クラスの GetBookData メソッドは、DB が見つからなかった時に DBNotFoundException をスローする ( こんな例外 .NET に用意されていないけど )。
では、IBookFinder インターフェイスの GetBookData メソッドにはこれらの例外がスローされる可能性があると宣言するのかというと、それは違う。そんなことしてたら、派生クラスが増えるたびに、IBookFinder インターフェイスに修正をすることになるかもしれない。
ここで抽象度が出てくる。例えば DataStoreNotFoundException といった例外クラスを定義し、IBookFinder インターフェイスの GetBookData メソッドでは、この例外をスローすると宣言するべきである。派生クラスでは、FileNotFoundException や DBNotFoundException の代わりに、この例外、またはこの例外を派生させた例外 ( DataStoreFileNotFoundException とか ) をスローできる。
ちなみに、LSP に準拠させる前の BookFinderFromXml クラスは FileNotFoundException をスローしていたが、FileNotFoundException のこういう使い方は不適切な気がする。LSP とか関係なく、FileNotFoundException ではなく「データストアが存在しない」という意味の例外をスローすべきだと思う。
LSP を適用することで、この点も解消されている所が面白い。
なお、この例で挙げた設計は、僕がこの記事を書くために適当に考えただけの設計であり、実際の業務アプリには適用できない ( または適用すべきでない ) 設計かもしれない。クラス名も適当だし。あくまでも LSP と例外について書きたかっただけなので。
※ .NET と Java では例外の扱い方に大きな差があります。ここでは .NET の <exception> セクションと Java の throws を並べてますが、全く性質の異なるものです。
あの後、実は指定できないこともないということに気づきました。ごめんなさい。
まず、なぜうまく指定できないと書いたかですが、先日の記事であげたケースと物品の例で説明します。
ケース クラスには格納する物品を指定するためのジェネリックパラメータを用意します。
物品 クラスには親となるケースを指定するためのジェネリックパラメータを用意します。
つまり、ケース<TArticle> クラスと 物品<TCase> クラス となるわけです。
この時、ケース クラスのジェネリックパラメータ TArticle の型制約には、物品<TCase> を指定するわけですが、物品<TCase> の型パラメータ TCase を指定しなければなりません。ここで不都合が生じるわけです。TCase に対して ケース<TArticle> と指定することはできますが、これではダメなんです。
なぜ不都合かというと、ジェネリック クラスは、型パラメータが完全一致していない場合、変換不可だからです。例えば、List<object> と List<int> では、object と int の間には継承関係がありますが、そんなの関係なく変換不可です。List<int> 型の変数を List<object> 型にキャストすることはできないのです。
すると、TCase が指定できないことがわかります。例えば、ケース クラスを継承した CDケース クラスでは、TCase は、CDケース にならなければなりません。ケース<CD> ではダメなのです。 ( 物品<ケース<CD> は 物品<ケース> への変換が不可ということです。 )
と、ちょっとややこしい話になってしまいましたが、要はこのままでは型制約の指定は無理だということです。で、先日の記事には無理だよ~と書いたわけです。
では、どうすればいいか、というのが今回の記事の本題です。
答えは、「自身のクラス階層を含め、ミラー階層において関連するクラス ( 階層 ) 全てを型パラメータに持たせる」 となります。
つまり、ケースには TCase と TArticle の2つの型パラメータを持たせ、物品にも TCase と TArticle の2つの型パラメータを持たせるわけです。
こうすることによって、先ほどの問題が解決できます。先ほど指定できなかった TCase には、ケースに新しく追加した型パラメータ TCase を指定すればいいわけですから。
コードにすると以下のようになります。
public abstract class ケース<TCase, TArticle>
where TCase : ケース<TCase, TArticle>
where TArticle : 物品<TCase, TArticle>
{
}
public abstract class 物品<TCase, TArticle>
where TCase : ケース<TCase, TArticle>
where TArticle : 物品<TCase, TArticle>
{
}
これらを継承した CDケース クラスと CD クラスは以下のようになります。
public sealed class CDケース : ケース<CDケース, CD>
{
}
public sealed class CD : 物品<CDケース, CD>
{
}
TCase と TArticle に関連しない型を指定した場合 ( 例えば ケース<CDケース, ペン> ) 、コンパイルエラーとなりますので、変なバグが入り込んだりすることはないかと思います。
これで問題は解決しましたが、一つ気を付けなければならないことがあります。ケース クラスに TCase という型パラメータ、そして 物品 クラスに TArticle という型パラメータがあることは、他人から見ておかしなことをやっているように見えてしまう ( つまり理解し難い ) ということです。こればかりは仕方ありませんので、理由や使い方をしっかりとドキュメントに書き記すようにしましょう。( いや、理由はしっかり書くとややこしくなるからざっくりとの方がいいかもw )
[ 余談 ]
ちなみに、これ以外にも方法はあります。非ジェネリックな基本クラスを用意するという方法です。つまり、「ケース クラス」 とこれを継承した 「ケース<TArticle> クラス」、「物品 クラス」 とこれを継承した 「物品<TCase> クラス」 を用意するわけです。こうすれば、型制約には非ジェネリックな基本クラスを指定することができます。
"物品" は "CD" や "ペン" などの総称です。"ケース" は "物品" をなんでも格納できます。
"物品" は抽象的なものですが、"ケース" は具体的なものです。
さらに、"CD" だけを格納できる "CDケース" と、"ペン" だけを格納できる "ペンケース" があります。
この時、"物品" と "CD" の間に継承関係は成り立つでしょうか?また、"ケース" と "CDケース" の間に継承関係は成り立つでしょうか?
継承関係とは 「is - a」 の関係です。つまり、「"CD" は "物品" である」と言えるなら、"物品" と "CD" の間に継承関係が成り立ちます。「"CDケース" は "ケース" である」 と言えるなら、"ケース" と "CDケース" の間に継承関係が成り立ちます。
この2つはどちらも成り立ちそうな気がします。しかし、実際成り立つのは "物品" と "CD" の継承関係だけです。"ケース" と "CDケース" の継承関係は成り立ちません。
オブジェクト指向設計における原則の一つに「リスコフの置換原則」と言う原則があります。「基本クラスのインスタンスの代わりに派生クラスのインスタンスを使用できなければならない」といったものです。
" ケース" は "CD" 以外の "物品" も格納できる必要がありますが、"CDケース" には "CD" しか格納できません。よって、"CDケース" のインスタンスは "ケース" のインスタンスの代わりにはなれません。これではリスコフの置換原則に違反してしまいます。違反しているということは 「"CDケース" は "ケース" ではない」 ということになります。

普通に考えて、「CDケースはケースではない」 なんて、おかしな話です。なぜこんなことになってしまうのでしょうか?
実は、"ケース" の定義に誤りがあります。"ケース" は "物品" をなんでも格納できるのではなく、「なんらかの "物品" 」 を格納できるものであるべきなのです。"物品" をなんでも格納できるのは "ケース" ではなく "物品ケース" というような別のクラスにするべきです。
言い換えると、「"ケース" という名前は適切じゃないから "物品ケース" に変更して、"ケース" という基本クラスを別に用意しよう」 ということです。 ( 物品ケースという名前も不適切だと思う場合は、 "万能ケース" などで読み替えてください。 )
" ケース" は 「なんらかの "物品" 」 を格納できます。この 「なんらかの "物品" 」 というのは派生クラスで定められます。"物品ケース" なら全ての "物品" 、"CDケース" なら "CD" となります。扱う "物品" が定まらないため、"物品" ( 及びその派生クラス ) と "ケース" の間には関連を結びません。"ケース" の派生クラスがそれぞれ関連を持つことになります。

"ケース" は 「なんらかの "物品" 」 を格納できるのだから、コード 1-1 のように 「なんらかの "物品" 」 の格納を補助する機能・仕組みを備えることはできます。
コード 1-1. なんらかの"物品"の格納補助機能を備えた "ケース"
using System;
using System.Collections.Generic;
using System.Text;
public abstract class ケース
{
private readonly List<物品> 格納物品コレクションフィールド;
protected List<物品> 格納物品コレクション
{
get
{
return this.格納物品コレクションフィールド;
}
}
protected ケース()
{
this.格納物品コレクションフィールド = new List<物品>();
}
protected void 物品の格納を補助(物品 対象物品)
{
this.格納物品コレクションフィールド.Add(対象物品);
}
protected 物品 物品の取り出しを補助(int 格納位置)
{
物品 取り出す物品 = this.格納物品コレクションフィールド[格納位置];
this.格納物品コレクションフィールド.Remove(取り出す物品);
return 取り出す物品;
}
}
これを継承した "CDケース" が コード 1-2 になります。
コード 1-2. コード 1-1 の "ケース" を継承した "CDケース"
using System;
using System.Collections.Generic;
using System.Text;
public class CDケース : ケース
{
public CD this[int 格納位置]
{
get
{
return (CD)this.格納物品コレクション[格納位置];
}
}
public CDケース()
: this(null)
{
}
public CDケース(List<CD> 格納CDコレクション)
: base()
{
if (格納CDコレクション != null)
{
foreach (物品 item in 格納CDコレクション)
{
base.格納物品コレクション.Add(item);
}
}
}
public void CDを格納(CD 対象CD)
{
base.物品の格納を補助(対象CD);
}
public CD CDを取り出す(int 格納位置)
{
return (CD)base.物品の取り出しを補助(格納位置);
}
}
また、ジェネリックを利用すれば派生クラスはさらに簡単に作成できます。
コード 2-1. なんらかの"物品"の格納補助機能をジェネリックとして備えた "ケース"
using System;
using System.Collections.Generic;
using System.Text;
public abstract class ケース<T>
where T : 物品
{
private readonly List<T> 格納物品コレクションフィールド;
protected List<T> 格納物品コレクション
{
get
{
return this.格納物品コレクションフィールド;
}
}
public T this[int 格納位置]
{
get
{
return this.格納物品コレクション[格納位置];
}
}
protected ケース()
: this(new List<T>())
{
}
protected ケース(List<T> 格納物品コレクション)
{
this.格納物品コレクションフィールド = 格納物品コレクション;
}
public void 物品を格納(T 対象物品)
{
this.格納物品コレクションフィールド.Add(対象物品);
}
public T 物品を取り出す(int 格納位置)
{
T 取り出す物品 = this.格納物品コレクションフィールド[格納位置];
this.格納物品コレクションフィールド.Remove(取り出す物品);
return 取り出す物品;
}
}
これを継承した "CDケース" が コード 2-2 になります。
コード 2-2. コード 2-1 の "ケース" を継承した "CDケース"
using System;
using System.Collections.Generic;
using System.Text;
public class CDケース : ケース<CD>
{
public CDケース()
: base()
{
}
public CDケース(List<CD> 格納CDコレクション)
: base(格納CDコレクション)
{
}
}
ちなみに、物品側から親ケースを参照できるようにしようとした場合どうなるでしょうか?
" 物品ケース" は "物品" ならなんでも格納できるのだから、"物品" には親物品ケースというプロパティを持たせ、さらに、"CD" は親CDケース、"ペン" は親ペンケースというプロパティを別に持つ必要があります。ただ、これでは "CD" が "物品ケース" に格納されている場合と "CDケース" に格納されている場合があることになります。これでは複雑な設計となってしまうため、"物品" には親ケース プロパティを用意せず、"物品" の各派生クラス ( "CD", "ペン" )がそれぞれの親ケース プロパティを持つだけにした方がいいかもしれません。あるいは、なんでも格納できる "物品ケース" というある意味特別なクラスを元々用意しなければ、このような問題は起きず綺麗な設計になります。
なお、"物品ケース" をジェネリッククラスにした場合、 "物品" も同じようにジェネリックで親ケースを指定できるようにする、ということはできないことに注意します。これは、ジェネリックの型制約の指定がうまくできないためです。
→ 解決方法がありました。詳細は C#と諸々 - ミラー階層とジェネリック を参照してください。

最後になりましたが、このように複数のクラス階層間において、各クラス毎に同じような関連を結ぶモデルを、ミラー階層と呼びます。
[ 参考書籍 ]
オブジェクト開発の神髄