I have a PlayerCharacter class. PlayerCharacter can be extended (for example, VampirePlayerCharacter vs WerewolfPlayerCharacter)
I have a Trait class. Trait can be extended (for example, Generation or Gnosis).
PlayerCharacter has a method, #withTrait(Trait), which adds the Trait to a collection.
PlayerCharacter has a method, #applyAllTraits() which loops through the collection and applies each of them to the character.
A VampirePlayerCharacter should be able to be given any Trait that could apply to a PlayerCharacter, as well as any Trait that could only apply to a VampirePlayerCharacter.
So I added a generic type, making Trait<PC extends PlayerCharacter>
Thus, there can be BasicTrait<PlayerCharacter> and Generation<VampirePlayerCharacter>
My conundrum:
If PlayerCharacter's collection of traits is
Collection<Trait<PlayerCharacter>>, thenVampirePlayerCharactercan't add aTrait<VampirePlayerCharacter>to the collection.If
PlayerCharacter's collection of traits isCollection<Trait<? extends PlayerCharacter>>, thenVampirePlayerCharactercan add aTrait<VampirePlayerCharacter>to the collection. However,PlayerCharactercan no longer loop through the traits, because their type is indeterminate (it could beTrait<PlayerCharacter>or aTrait<VampirePlayerCharacter>or aTrait<WerewolfPlayerCharacter>or...)If
PlayerCharacter's collection of traits isCollection<Trait<? super PlayerCharacter>>, thenVampirePlayerCharactercan't add aTrait<VampirePlayerCharacter>, becauseVampirePlayerCharacterisn't a supertype ofPlayerCharacter
I'm about a hair's-breadth from saying that more specialized traits just have to use a cast in their apply method and if you set things up inappropriately, they'll explode- but I'm certain that this is not a novel problem, and I just can't wrap my head around the solution.
class PlayerCharacter {
private int str;
List<Trait<?>> traits = new ArrayList<>();
PlayerCharacter withStrength(int str) {
this.str = str;
return this;
}
PlayerCharacter withTrait(Trait trait) {
this.traits.add(trait);
return this;
}
void applyTraits() {
traits.forEach((Trait<?> t) -> t.apply(this));
}
}
class VampirePlayerCharacter extends PlayerCharacter {
private int fangLength;
VampirePlayerCharacter withFangLength(int fangLength) {
this.fangLength = fangLength;
return this;
}
}
abstract class Trait<PC extends PlayerChracter> {
void apply(PC pc);
}
class StrengthTrait extends Trait<PlayerCharacter> {
private int str;
StrengthTrait(int str) {
this.str = str;
}
void apply(PlayerCharacter pc) {
pc.withStrength(str);
}
}
class FangLengthTrait extends Trait<VampirePlayerCharacter> {
private int fangLength;
FangLengthTrait(int fangLength) {
this.fangLength = fangLength;
}
void apply(VampirePlayerCharacter pc) {
pc.withFangLength(fangLength);
}
}
Traitof TypePlayerCharacterdoesnt make much sense, it should be more like aFieldinPlayerCharacter, also : your nomenclature is confusing, i'd recommend changingPlayerCharactertoCharacter<Player>and possiblyCharacter<NonPlayer>or something similar, that'll clear things up a bit. Also :Traits dont need to be "applied" to aCharacter, they should only be added, changed or removed. It really helps later on if you clear your design up and make everything logical, consistent and easy to read, even if you're the one and only developer.