5

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);
  }
}
9
  • stackoverflow.com/editing-help Commented Dec 3, 2014 at 22:02
  • I don't think inheritance is a good idea for trying to manage these objects. Look up Strategy Pattern and Decorator Pattern, perhaps one of those might be useful for you. Commented Dec 3, 2014 at 22:04
  • You could try an interface Commented Dec 3, 2014 at 22:06
  • 1
    sorry but your design is flawed - Traitof Type PlayerCharacter doesnt make much sense, it should be more like a Field in PlayerCharacter, also : your nomenclature is confusing, i'd recommend changing PlayerCharacter to Character<Player> and possibly Character<NonPlayer> or something similar, that'll clear things up a bit. Also : Traits dont need to be "applied" to a Character, 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. Commented Dec 3, 2014 at 22:08
  • 1
    nomenclature is THE most important thing in software development - bad software has confusing names and derails your train of thought and good software doesnt. The first stage is planning, conceptualizing a COMPLETELY clear design - if that is done you will have MUCH less problems with many things, solutions to problems will simply leap to the eye - which isnt the case if you yourself need to re-iterate everything and remind yourself of your inital goals everytime you asess a new / old problem Commented Dec 3, 2014 at 22:16

1 Answer 1

3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

This has mostly convinced me that generics aren't the proper solution for this problem. :) (I also realized that I could just rely on method overloading, with children of Trait having more precise apply methods. It's a little tricky, but seems to be working)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.