Collection<Foo>
を受け取るコンストラクターを持つクラスParty
があります。 2つのサブクラスNpcParty
とPlayerParty
を用意する予定です。すべてのParty
インスタンスには、入力コレクションのサイズに上限があります(6)。ただし、NpcParty
には1
の下限がありますが、PlayerParty
には0
の下限があります(または、 List
は負のサイズにすることはできません)。 NpcParty
は入力コレクションのサイズに関して前提条件を強化するため、これはLSPの違反です。
NpcParty
は不変であることを意図しています。つまり、これが持つFoo
sは、コンストラクターで指定されたものだけです。 PlayerParty
のFoo
sは、実行時に順序または参照/値のいずれかで変更できます。
public class Party {
// collection must not be null
// collection size must not exceed PARTY_LIMIT
public Party(List<Foo> foos) {
Objects.requireNonNull(foos);
if (foos.size() > PARTY_LIMIT) {
throw new IllegalArgumentException("foos exceeds party limit of " + PARTY_LIMIT);
}
}
}
public class NpcParty extends Party {
// additional precondition that there must be at least 1 Foo in the collection
public NpcParty(List<Foo> foos) {
super(foos);
if (foos.size() < 1) {
throw new IllegalArgumentException("foos must contain at least 1 Foo");
}
}
}
public class PlayerParty extends Party {
// no additional preconditions
public PlayerParty(List<Foo> foos) {
super(foos);
}
}
NpcParty
が最小の境界を持つことを許可されるように、この違反をどのように解決できますか?
[〜#〜] edit [〜#〜]:これをテストする方法の例を示します。
すべてのAbstractPartyUnitTests
実装の最小コントラクトをテストするクラスParty
があるとします。
public class AbstractPartyUnitTests {
@Test(expected = NullPointerException.class)
public void testNullConstructor() {
createParty(null);
}
@Test
public void testConstructorWithEmptyList() {
party = createParty(new ArrayList<Foo>());
assertTrue(party != null);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorThatExceedsMaximumSize() {
party = createParty(Stream.generate(Foo::new)
.limit(PARTY_LIMIT + 1)
.collect(Collectors.toList()));
}
protected abstract Party createParty(List<Foo> foos);
private Party party;
}
PlayerParty
およびNpcParty
のサブクラスを使用
public class PlayerPartyUnitTests extends AbstractPartyUnitTests {
@Override
protected Party createParty(List<Foo> foos) {
return new PlayerParty(foos);
}
}
そして
public class NpcPartyUnitTests extends AbstractPartyUnitTests {
@Test
public void testConstructorThatMeetsMinimumSize() {
party = createParty(Stream.generate(Foo::new)
.limit(1)
.collect(Collectors.toList());
assertTrue(party != null);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorThatDoesNotMeetMinimumSize() {
party = createParty(new ArrayList<Foo>());
}
@Override
protected Party createParty(List<Foo> foos) {
return new NpcParty(foos);
}
}
testConstructorWithEmptyList
として実行中のNpcPartyUnitTests
を除いて、すべてのテストケースが合格します。コンストラクタは失敗するため、テストは失敗します。
AbstractPartyUnitTests
のすべてのタイプに実際に適用されるわけではないので、このテストをParty
クラスから削除できます。しかし、Party
がある場所ならどこでも、NpcParty
を1対1で置き換えることができない可能性があります。
上記の例では、実際にはLSPの違反はありません。各NpcParty
オブジェクトは引き続き有効なParty
オブジェクトであり、その代わりに使用できます。 LSPは、クラスAをクラスBで交換することではなく、タイプAのオブジェクトをタイプBのオブジェクトで交換することです。したがって、コンストラクタ、および制約そこにのみチェックインされ、LSPの対象ではありません。
たとえば、Playerに「Size」という追加のプロパティがあるとします。プレーヤーを外部から挿入するテストケースを書き、そのサイズが常に許容範囲内にあるかどうかをテストしたいとします。
void TestSize(Player p)
{
AssertTrue(p.Size>=0);
AssertTrue(p.Size<=PARTY_LIMIT);
// ??? try to write a test for "Size" which does not fail for
// ordinary Player objects, or PlayerParty objects, but fails for NpcPLayer
// -> not possible without explicitly checking the type
}
ご覧のとおり、そのテストメソッドに何を渡しても、テストは失敗しません。ここには、通常のPlayerオブジェクトをNpcPlayerオブジェクトで置き換えることができないものはありません。
ただし、Party
クラスがその入力をどこかに保存し、後で要素の数を変更するメソッドを提供する場合(要素の数がゼロより大きくなければならないという制約を確認する)、より強い派生クラスを持つNpcParty
の交換としてParty
オブジェクトを使用できないため、制約はLSPに違反します。
これを解決するには、より一般的なParty
を実装します。コンストラクターによってコレクションの最小境界と最大境界を挿入し、どこかに格納します。このように、Party
オブジェクトの各ユーザーは、これらの境界がゼロであり、PARTY_LIMIT
-NpcParty
をParty
の有効な交換にします。
記述されているコードは必ずしもリスコフ置換原則に違反しているわけではありません。インスタンス化NpcParty
オブジェクトがfoos
コレクションには常に少なくとも1つのメンバーがあります。
そうではないようで、おそらくアプリケーションのビジネスロジックでNpcParty
オブジェクトのそのプロパティに依存する必要があります。私が正しいのであれば、あなたは原則としてこれがリスコフ代替原則の違反であるとあなたは正しいです。
この違反を修正するには、パーティーサイズの制約に依存する動作をサブクラスに移動します。PlayerParty
は、NpcParty
が行うように、パーティーサイズの制約を定義する必要があります。また、Party
パーティーの規模については一切主張しないでください(または、少なくとも、そのすべてのサブタイプに当てはまる主張のみを行ってください)。