web-dev-qa-db-ja.com

最小前提条件に基づいてLSP違反を解決する方法

Collection<Foo>を受け取るコンストラクターを持つクラスPartyがあります。 2つのサブクラスNpcPartyPlayerPartyを用意する予定です。すべてのPartyインスタンスには、入力コレクションのサイズに上限があります(6)。ただし、NpcPartyには1の下限がありますが、PlayerPartyには0の下限があります(または、 Listは負のサイズにすることはできません)。 NpcPartyは入力コレクションのサイズに関して前提条件を強化するため、これはLSPの違反です。

NpcPartyは不変であることを意図しています。つまり、これが持つFoosは、コンストラクターで指定されたものだけです。 PlayerPartyFoosは、実行時に順序または参照/値のいずれかで変更できます。

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で置き換えることができない可能性があります。

7
Zymus

上記の例では、実際には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-NpcPartyPartyの有効な交換にします。

8
Doc Brown

記述されているコードは必ずしもリスコフ置換原則に違反しているわけではありません。インスタンス化NpcPartyオブジェクトがfoosコレクションには常に少なくとも1つのメンバーがあります。

そうではないようで、おそらくアプリケーションのビジネスロジックでNpcPartyオブジェクトのそのプロパティに依存する必要があります。私が正しいのであれば、あなたは原則としてこれがリスコフ代替原則の違反であるとあなたは正しいです。

この違反を修正するには、パーティーサイズの制約に依存する動作をサブクラスに移動します。PlayerPartyは、NpcPartyが行うように、パーティーサイズの制約を定義する必要があります。また、Partyパーティーの規模については一切主張しないでください(または、少なくとも、そのすべてのサブタイプに当てはまる主張のみを行ってください)。

1
Evan