1

I'm designing a class that holds several types of data. Some of the properties are optional. For example, let's say I have a class that represents a person, and one of the properties is occupation. I have another property, military rank, but this property is can only be retrieved if the person's occupation is military. What's the best way to handle this?

  1. Have getMilitaryRank() throw IllegalStateException if the occupation isn't military
  2. Have getMilitaryRank() return an Optional<Rank>
  3. Return null if the occupation is invalid (I don't like nulls)
  4. Something else?

Idea 1 works because the user can check the occupation prior to calling this method, making sure they don't call it on an invalid person.

Idea 2 seems unnecessary, because you can infer whether the Optional is present by calling getOccupation().

However, let's say my Person class has another optional property, but its presence can't be inferred from the value of another property; for example, a middle name. Obviously it's best for getMiddleName() to return an empty Optional or null if the person has no middle name, as the presence of a middle name can't really be inferred some other way.

Since I should probably be consistent in how I'm handling optional properties, it seems like I could do one of the following:

  1. Use Optional for any property that may be absent, even if it can be inferred from another property.
  2. Add methods like hasMiddleName() to resolve the discrepancies (this seems like a bad idea)
  3. Not be consistent and let the properties whose presence can be determined externally throw an exception, while the ones that are independent be wrapped in an Optional.
  4. Something else?

While nulls could simplify things, it causes a problem if an optional property is an int, which can't be nullable in Java. I could use an Integer, but that seems like a bad idea.

What is the best way to handle these different types of optional properties?

  • bad programmers copy, great programmers steal. Steal from the way how JCF handles properties of collections being modifiable or not: "modification operations in each interface are designated optional — a given implementation may elect not to support all operations. If an unsupported operation is invoked, a collection throws an UnsupportedOperationException. Implementations are responsible for documenting which of the optional operations they support..." – gnat Apr 23 '14 at 22:19
  • Well, that works for methods, but these aren't really methods, they're fields. The issue is, I need these fields to be optional for any given instance of the class, not be optional for any given implementation of the class. – codebreaker Apr 23 '14 at 22:57
  • @gnat I feel that's a terrible solution; it undermines the type system in the same way null does. An interface should only expose the operations it allows; start breaking that rule and you start sliding down a slippery slope of dynamic typing. – Doval Apr 24 '14 at 0:23
  • @codebreaker using fields is a major mistake in your design. Instead, use getter methods if you're dealing with DTO and tell, don't ask approach otherwise – gnat Apr 24 '14 at 6:19
  • @Doval successful use of collections framework makes your statement hard to believe. Approach like this could probably be a wrong solution in a language like Eiffel; for Java it's quite idiomatic – gnat Apr 24 '14 at 6:23
2

There's two solutions, depending on the specifics of your problem. If these optional properties are only combined in a relatively small number of ways, you can use a sum type to distinguish between them. The advantage of this approach is that you can guarantee exhaustiveness - you can statically ensure that wherever a decision must be taken based on the particular kind of person, all of the kinds are handled somehow.

The other solution is to group the optional properties that always occur together into interfaces and then implement the interfaces you can support in each class. You can detect the particular interfaces a person supports by applying instanceof and then casting. This approach won't force you to update all the use sites when you add a new interface, but works better when the number of possible combinations of optional properties is very large. E.g. if there's 7 groups of optional properties, there's 127 ways to combine those groups into classes, and it'd be ridiculous to take a decision based on each possible combination. If you need to reuse the implementations of those interfaces, use composition.

That aside, do be careful about making assumptions about people's names.

  • Having multiple interfaces isn't a bad idea. If I wanted to have a mutable implementation of my Person class, though, this wouldn't work. Also, the name thing is pretty interesting. I'll check that out. The middle name thing was just an example, though, I was trying to come up with something that a person may optionally have exactly one of. – codebreaker Apr 24 '14 at 0:55
  • Why does mutability rule out interfaces? Are you saying you need to change whether an optional property is present or not at runtime? You could use the decorator pattern for that. – Doval Apr 24 '14 at 0:58
  • Well, that is true, but things could get messy if an object has to be decorated multiple times (or un-decorated). If possible I'd like to not get into using the runtime class of an object to determine if properties exist, and just stick with optionals/ nulls. My main question is, should I care about being consistent with throwing exceptions vs. returning some kind of absent value if the presence of some, but not all properties can be inferred? – codebreaker Apr 24 '14 at 1:13
  • @codebreaker You should strive to make invalid states impossible. For an approach using Optional or exceptions I recommend that you get and set entire groups of related properties at once. E.g. If the presence of militaryRank always implies the presence of commandingOfficer, then you should get or set both of those simultaneously, so the type system protects you from the illegal state where you ended up with one but not the other. – Doval Apr 24 '14 at 2:00
1

I like DFord idea of inheritance...by extending the Person class... here are some additional ideas to consider...

public class Person {

  private String middelName;
  private String occupation;

  public String getMiddleName() {
      if (middleName == null) {
         return ""; // or any default value
      } else {
         return middleName;
      }
  }

  public String getOccupation() {
     if (occupation == null) {
         return "Unknown";
     } else {
         return occupation;
     }
  }

  public class Military extends Person {
     String rank;
     public String getMilitaryRank() {
        return rank;
     }
  }

....then in the code somewhere...

if (person instanceof Military) {
   String rank = person.getMilitaryRank();
}

Another idea is to create an enum for Occupation and have it return the default value of the Occupation if nothing is set, instead of "unknown". Sometimes checking for specific values like the text "unknown" can add extra complexity.

 if (occupation == null) {
    return Occupation.UNKNOWN;
 } else {
    return occupation;
 }

... a better option is to initialize all of the variables in the constructor

public class Person {
    public Person() {
       middleName = "";
       occupation = Occupation.UNKNOWN;
    }

    public Occupation getOccupation() {
       return occupation;
    }

    public String getMiddleName() {
       return middleName;
    }
-1

Why not create a sub-class which represents someone in the military and have it inherit from your parent class. That way, you can check the type of class the object is. If its the Military class, you can call getMilitaryRank().

You can then create additional classes for other situations similar to this.

  • 2
    The problem with that is Java doesn't have multiple inheritance, so this would work to some degree, but any time I have objects that need to have two of these properties, it gets messy. Otherwise, yes, this would probably be the best way. – codebreaker Apr 23 '14 at 22:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.