I have a class Party
that has a constructor that takes a Collection<Foo>
. I plan to have two subclasses NpcParty
and PlayerParty
. All Party
instances have an upper limit on the size of the input collection (6). However, an NpcParty
has a lower bound of 1
, while a PlayerParty
has a lower bound of 0
(or rather, there is an implied minimum bound because a List
cannot have a negative size). This is a violation of LSP because the NpcParty
strengthens the preconditions in terms of the size of the input collection.
An NpcParty
is intended to be immutable; meaning that the only Foo
s it will ever have, are those that are specified in the constructor. A PlayerParty
's Foo
s can be changed at runtime, either in order, or reference/value.
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);
}
}
In what ways can I resolve this violation, such that an NpcParty
is allowed to have a minimum bound?
EDIT: I'm going to give an example of how I might test this.
Suppose I had a class AbstractPartyUnitTests
that tested the minimum contract of all Party
implementations.
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;
}
With subclasses for PlayerParty
and NpcParty
public class PlayerPartyUnitTests extends AbstractPartyUnitTests {
@Override
protected Party createParty(List<Foo> foos) {
return new PlayerParty(foos);
}
}
and
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);
}
}
All of the test cases would pass, with the exception of testConstructorWithEmptyList
while running as an NpcPartyUnitTests
. The constructor would fail, and as such, the test would fail.
Now, I could remove that test from the AbstractPartyUnitTests
class, as it does not really apply to all types of Party
; but then anywhere I have a Party
, I may not be able to have a 1:1 replacement of NpcParty
.