“Sets” of a particular enum type but with generics

2020-05-17 02:25发布

问题:

Let's say I have an abstract class

public abstract class Trainer<T extends Animal>{}

I have specific trainers like :

public DogTrainer extends Trainer<Dog>{}
public HorseTrainer extends Trainer<Horse>{}

Each of these 'trainers' has a set of fixed tricks they can train the animal to do, which I'd like to use Enums for. So I have an interface:

public interface TrainingActions<T extends Animal>{}

and in each of the trainers, I have an Enum that implements this interface. So:

public DogTrainer extends Trainer<Dog>{
  public enum Trainables implements TrainingActions<Dog>{
    BARK, BITE, ROLLOVER, FETCH;
  }
}

public HorseTrainer extends Trainer<Horse>{
  public enum Trainables implements TrainingActions<Horse>{
    JUMP, TROT, REARUP;
  }
}

Now in the each of the Trainer classes, I'd like a method say 'trainingComplete' that takes one of the Enums as an input and saves it to a set. So

public DogTrainer extends Trainer<Dog>{
  public enum Trainables implements TrainingActions<Dog>{
    BARK, BITE, ROLLOVER, FETCH;
  }
  public Set<Trainables> completed = new HashSet<Trainables>();
  public void trainingComplete(Trainables t){completed.add(t);}
}

However, instead of defining the 'completed' set in each of the specific Trainers and the 'trainingComplete' method in each of the trainers, I'd like something in the parent 'Trainers' class that can be Enum-type enforced... So it's a weird combination of Enums and generics.

Is this possible?

回答1:

To specify a bound to an interface and that it be an enum, you need a generic intersection, which looks like:

class MyClass<T extends Enum<T> & SomeInterface> {}

Note that when intersecting a class and an interface(s), the class must appear before the interface(s).

In this case, the generics Kung Fu you want is one level more complicated, because the interface of the TrainingActions enum must itself refer back to the animal type.

class Trainer<A extends Animal, T extends Enum<T> & TrainingActions<A>> {}

A complete working example, based on your posted code, that compiles is:

public class Animal {}

public interface TrainingActions<T extends Animal> {}

/**
 * A trainer that can teach an animal a suitable set of tricks
 * @param <A> The type of Animal
 * @param <T> The enum of TrainingActions that can be taught to the specified Animal
 */
public abstract class Trainer<A extends Animal, T extends Enum<T> & TrainingActions<A>> {
    private Set<T> completed = new HashSet<T>();
    public void trainingComplete(T t) {
        completed.add(t);
    }
}

public class Dog extends Animal {};

public class DogTrainer extends Trainer<Dog, DogTrainer.Trainables> {
    public enum Trainables implements TrainingActions<Dog> {
        BARK, BITE, ROLLOVER, FETCH;
    }
}

But I would go one step further and define several TrainingActions enums in the class of the particular Animal to which they apply:

public class Dog extends Animal {
    public enum BasicTrainables implements TrainingActions<Dog> {
        SIT, COME, STAY, HEEL;
    }
    public enum IntermediateTrainables implements TrainingActions<Dog> {
        BARK, BITE, ROLLOVER, FETCH;
    }
    public enum AdvacedTrainables implements TrainingActions<Dog> {
        SNIFF_DRUGS, FIND_PERSON, ATTACK, GUARD;
    }
};

public class PuppyTrainer extends Trainer<Dog, Dog.BasicTrainables> {}

public class ObedienceTrainer extends Trainer<Dog, Dog.IntermediateTrainables> {}

public class PoliceTrainer extends Trainer<Dog, Dog.AdvacedTrainables> {}


回答2:

Despite design analysis, I think the most your-situation (closest to your already created design) answer is:

  • your DogTrainer.Trainables is TrainingActions<Dog>
  • your HorseTrainer.Trainables is TrainingActions<Horse>

Generally, your Trainables is already a TrainingActions<T extends Animal>

so you can just provide Set<TrainingActions<T>> completed, because you already have your animal information in T:

abstract class Trainer<T extends Animal> {
    Set<TrainingActions<T>> completed = new HashSet<TrainingActions<T>>();

    public void add(TrainingActions<T> t ) {
        completed.add( t );
    }
}

And you have exactly what you want.

Usage:

DogTrainer tr = new DogTrainer();
tr.add( DogTrainer.Trainables.BITE );

If you are sure you want to enforce an Enum for your interface:

public <E extends Enum<?> & TrainingActions<T>> void add( E t ) {
   completed.add( t );
}


回答3:

I will post that for further consideration, even there is already accepted answer

If you read this question, consider using more loose coupled elements, with aggregation etc. instead of highly specific and complicated type hierarchy and generics.

With just plain aggregation you can achieve at least the same type & logical safety, but loose coupled design. Like I mentioned in comments:

Propably better will be more collections, agregations etc. to ensure type safety and elasticyty together (different trainers with different abilities to different animals in any combinations, including mixing, not locking a person to be a super-specific training master for some super specific abilities of specific animal and nothing more).

Of course I mean a "John than can teach horse to trot and a dog to bark", not "John that can teach horse or dog to trot or bark"



回答4:

Yes you can move your Trainables , Set<Trainables> completed and trainingComplete(Trainables t) to the class Trainer.

This way you no need to write these code each DogTrainer, HorseTrainer .. and so on...

 abstract class Trainer<T extends Animal>{
     public enum Trainables implements TrainingActions<Animal>{
            BARK, BITE, ROLLOVER, FETCH;
     }
      public Set<Trainables> completed = new HashSet<Trainables>();
      public void trainingComplete(Trainables t){completed.add(t);}
}

Otherwise make trainingComplete(Trainables t) abstract and you need to implement in each Trainer implementaion clasees.

abstract class Trainer<T extends Animal>{
     public enum Trainables implements TrainingActions<Animal>{
            BARK, BITE, ROLLOVER, FETCH;
     }
      public Set<Trainables> completed = new HashSet<Trainables>();
      abstract public void trainingComplete(Trainables t);
}

Now your DogTrainer and HorseTrainer are implementing trainingComplete(Trainables t)

 public class DogTrainer extends Trainer<Dog>{      
     public void trainingComplete(Trainables t){
          completed.add(t);
     }
 }

 public class HorseTrainer extends Trainer<Horse>{     
     public void trainingComplete(Trainables t){
           completed.add(t);
     }
 }