Reputation: 2957
I have a class
public class MyCompleteObject {
private MyEnumA enumA;
private MyObjectB objectB;
}
EDIT (Making it more clear): enumA and objectB are interdependent. MyObjectB has 3 subclasses. And each value of MyEnumA goes with object of only one of the subclass of MyObjectB
Since MyObjectB has 3 subclasses, so objectB can be of 3 types. MyEnumA has a lot of values (~30, since I need only their text values, couldn't think of any other option than enum, correct me if enum should not have these many values).
So, there can be 30x3 = 90 combinations of MyEnumA and MyObjectB but not all of those 90 combinations are valid. That's where I am stuck.
What would be the best way to address this? Couple of options which I can think of are:
1) Whenever an instance of MyCompleteObject gets created, I should check if enumA and objectB are consistent with each other and throw an exception if they are not consistent (Not feeling very comfortable with this approach).
2) Make a lot of subclasses with different combinations of enumA and objectB. Again, doesn't seem a promising solution as a lot of subclasses will be created depending on different combinations enumA and objectB.
Update 1)
Third approach which I can think of after reading various answers is:
3) First make extensible rick enum types as @scottb's answer tells. Then I think I can make 3 different constructors in MyCompleteObject class as: EnumA, EnumB and EnumC are my enums categorized as per their validity with 3 sublclasses of MyObjectB (@scottb's answer). MyObjectBFirst, MyObjectBSecond and MyObjectBThird are the three subclasses of MyObjectB.
public MyCompleteObject(EnumA enumA, MyObjectBFirst objectB){//}
public MyCompleteObject(EnumB enumB, MyObjectBSecond objectB){//}
public MyCompleteObject(EnumC enumC, MyObjectBThird objectB){//}
This can ensure compile time checking. However, 3 different constructors are there. I also looked into builder pattern but couldn't fit it here. It was oriented towards adding up optional arguments, but here I have conditional arguments and all are required.
Thanks!
Upvotes: 4
Views: 121
Reputation: 18772
Since you mentioned you wanted compile time check solution. Here is one way I could think of.
I am proposing that you inject a subclass of MyOBjectB into MyEnumA definitions. Lets assume that MyEnumA can be Animal in real life, and MyObjectB is a type of Animal in real life. So, certain animals will be of certain type and any other combination will be invalid.
public class Test {
enum Animal {
PIGEON(new Flyers()), EAGLE(new Flyers()), //flyers
SNAKE(new Crawlers()), CROCODILE(new Crawlers()), //crawlers
COW(new Walkers()), DOG(new Walkers()); //walkers
private AnimalCharacteristics characteristics;
private Animal(AnimalCharacteristics characteristics) {
this.characteristics = characteristics;
}
public AnimalCharacteristics getCharacteristics() {
return characteristics;
}
}
interface AnimalCharacteristics {
void setWeight(double kgs);
};
public static class Flyers implements AnimalCharacteristics {
@Override
public void setWeight(double kgs) {
// do something
}
}
public static class Crawlers implements AnimalCharacteristics {
@Override
public void setWeight(double kgs) {
// do something
}
}
public static class Walkers implements AnimalCharacteristics {
@Override
public void setWeight(double kgs) {
// do something
}
}
public static void main(String[] args) {
System.out.println(Animal.PIGEON.getCharacteristics() instanceof Flyers); //true
System.out.println(Animal.PIGEON.getCharacteristics() instanceof Crawlers); //false
//Make updates
Animal.PIGEON.getCharacteristics().setWeight(0.75);
Animal.COW.getCharacteristics().setWeight(240.00);
}
}
UPDATE Updated code below as the OP had left a comment saying a new instance is preferable instead of re-using instances of AnimalCharacteristics
public class Test {
enum Animal {
PIGEON(Flyers.class), EAGLE(Flyers.class), //flyers
SNAKE(Crawlers.class), CROCODILE(Crawlers.class), //crawlers
COW(Walkers.class), DOG(Walkers.class); //walkers
private Class<? extends AnimalCharacteristics> characteristicsClass;
private Animal(Class<? extends AnimalCharacteristics> characteristicsClass) {
this.characteristicsClass = characteristicsClass;
}
public AnimalCharacteristics getCharacteristics() {
try {
System.out.println(" ~~~ Creating new instance of: " +
characteristicsClass.getCanonicalName());
return characteristicsClass.newInstance();
} catch (Exception e) {
System.out.println(" ~~~ Exception while creating instance: "
+ e.getMessage());
return null;
}
}
}
interface AnimalCharacteristics {
AnimalCharacteristics setWeight(double kgs);
};
public static class Flyers implements AnimalCharacteristics {
@Override
public AnimalCharacteristics setWeight(double kgs) {
return this;
}
}
public static class Crawlers implements AnimalCharacteristics {
@Override
public AnimalCharacteristics setWeight(double kgs) {
return this;
}
}
public static class Walkers implements AnimalCharacteristics {
@Override
public AnimalCharacteristics setWeight(double kgs) {
return this;
}
}
public static void main(String[] args) {
AnimalCharacteristics pigeon = Animal.PIGEON.getCharacteristics();
System.out.println("Is pigeon a flyer => "
+ (pigeon instanceof Flyers)); //true
System.out.println("Is pigeon a crawler => "
+ (pigeon instanceof Crawlers)); //false
//Make updates
pigeon.setWeight(0.75);
AnimalCharacteristics cow = Animal.COW.getCharacteristics().setWeight(240.00);
System.out.println("Cow is of type:" + cow.getClass().getCanonicalName());
}
}
The above code if run will produce following output:
~~~ Creating new instance of: Test.Flyers
Is pigeon a flyer => true
Is pigeon a crawler => false
~~~ Creating new instance of: Test.Walkers
Cow is of type:Test.Walkers
Upvotes: 1
Reputation: 155
Make the EnumA value within your class immutable (no setter), and force the user to provide it in the constructor (or through a factory). This forces a constructed class to always use the same value of EnumA. You can then enforce class types on ObjectB in the constructor or in its setter.
public class MyCompleteObject {
private MyEnumA enumA;
private MyObjectB objectB;
public MyCompleteObject(MyEnumA enumA) { this.enumA = enumA; }
public void setObjectB(MyObjectB objectB) {
if(objectB.getClass() == enumA.getValidClassName()) {
this.objectB = objectB;
} else {
throw new InvallidArgumentException();
}
}
}
If you do decide to go with sub classes, I would suggest a base interface, with three abstract classes implementing it - one for each possible type of ObjectB. You can then extend the specific abstract class for each EnumA value that represents the given ObjectB.
public class MyObjectB {};
//Three sub classes of MyObjectB follows
public class MyObjectBType1 extends MyObjectB {};
public class MyObjectBType2 extends MyObjectB {};
public class MyObjectBType3 extends MyObjectB {};
// enum with constructor parameter that tells
// which enum value is compatible with which subclass of MyObjectB
public enum MyEnum {
A (MyObjectBType1.class),
B (MyObjectBType1.class),
C (MyObjectBType2.class),
D (MyObjectBType3.class),
E (MyObjectBType3.class),
;
private Class<? extends MyObjectB> validClassName;
MyEnum(Class<? extends MyObjectB> cls) {
this.validClassName = cls;
}
public Class<? extends MyObjectB> getValidClassName() {
return validClassName;
}
}
Upvotes: 1
Reputation: 10084
This sounds like a good use case for an extensible rich enum type:
public enum EnumA implements MyEnumType {
COMMON_TO_A_1,
:
:
COMMON_TO_A_N;
@Override public void commonMethod1() { ... }
:
}
public enum EnumB implements MyEnumType {
COMMON_TO_B_1,
:
:
COMMON_TO_B_N;
@Override public void commonMethod1() { ... }
:
}
public interface MyEnumType {
void commonMethod1();
:
:
int commonMethodN(String myParam);
}
By using MyEnumType as the type name, you will be able to pass in any of your enum groups and perform common type-safe operations on them. The enum facility in Java is robust and typesafe and is usually superior to roll-your-own enum classes. I recommend using the enum facility when possible.
The downside is that this is not an inheritance pattern, and there is no superset of common enums. Sometimes, this can be emulated in code without too much trouble depending on your needs. For example, you can define another enum that provides the class literal of the "base enum" so that you can always refer to those constants in all the operations you perform, as well as any enum groups you have passed using the interface type.
Another downside is that when you pass an enum constant by the interface type, it loses its identity as a member of Java's enum facility. By this I mean that you won't be able to use things like EnumMap and EnumSet with your interface-typed constants. There are workarounds for this limitation, but they may not always be clean to implement.
Upvotes: 1