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>>
, then VampirePlayerCharacter
can't add a Trait<VampirePlayerCharacter>
to the collection.
If PlayerCharacter
's collection of traits is Collection<Trait<? extends PlayerCharacter>>
, then VampirePlayerCharacter
can add a Trait<VampirePlayerCharacter>
to the collection. However, PlayerCharacter
can no longer loop through the traits, because their type is indeterminate (it could be Trait<PlayerCharacter>
or a Trait<VampirePlayerCharacter>
or a Trait<WerewolfPlayerCharacter>
or...)
If PlayerCharacter
's collection of traits is Collection<Trait<? super PlayerCharacter>>
, then VampirePlayerCharacter
can't add a Trait<VampirePlayerCharacter>
, because VampirePlayerCharacter
isn't a supertype of PlayerCharacter
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);
}
}
Well the problem is that you need to retain your inheritance as generic type information.
Basically you would have to do something like:
class PlayerCharacter<P extends PlayerCharacter<P>> {
List<Trait<? super P>> myTraits;
}
class VampirePlayer extends PlayerCharacter<VampirePlayer> {...}
abstract class Trait<P extends PlayerCharacter<P>> {
abstract void apply(P player);
}
class FangLengthTrait extends Trait<VampirePlayer> {...}
It begins to get very clunky, though. You can somewhat improve the situation by approaching from composition:
class Attributes {}
class Base extends Attributes {
int strength;
}
class Vampire extends Base {
int fangLength;
}
class Player<A extends Attributes> {
final A attributes;
final List<Trait<? super A>> traits = new ArrayList<>();
Player(A attributes) {
this.attributes = attributes;
}
void applyTraits() {
for(Trait<? super A> t : traits)
t.apply(this);
}
}
interface Trait<A extends Attributes> {
void apply(Player<? extends A> player);
}
class StrengthTrait implements Trait<Base> {
@Override
public void apply(Player<? extends Base> player) {
player.attributes.strength = 1000;
}
}
class FangLengthTrait implements Trait<Vampire> {
@Override
public void apply(Player<? extends Vampire> player) {
player.attributes.fangLength = 100;
}
}
final class Factory {
private Factory() {}
public static Player<Base> newPlayer() {
return new Player<Base>(new Base());
}
public static Player<Vampire> newVampire() {
return new Player<Vampire>(new Vampire());
}
}
I still find it clunky, personally. If you are mainly just using these Trait
s to construct objects you might think about using a builder or factory so you don't need to use generics.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With