Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java generic fields

Tags:

java

generics

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);
  }
}
like image 285
Drew Stevens Avatar asked Oct 31 '22 13:10

Drew Stevens


1 Answers

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 Traits to construct objects you might think about using a builder or factory so you don't need to use generics.

like image 92
Radiodef Avatar answered Nov 08 '22 05:11

Radiodef