java – Get specific subclass fields from parent class with many subclasses?

Being able to refactor your Recipe-classes makes a lot easier for you.

Just use OOP

If you can, you should introduce an abstract method in the superclass with concrete implementations in subclasses:

public abstract class Recipe{
    public abstract String getIngredientMeasurementFromRecipe(String ingredient);
}
public class DessertRecipe extends Recipe{
    @Override
    public String getIngredientMeasurementFromRecipe(String ingredient){
        return switch(ingredient){
            case "flour" -> getFlour();
            case "sugar" -> getSugar();
            //...
            default -> throw new IlegalArgumentException();
        }
    }
}

This code also makes use of Switch expressions which were introduced in Java 14. If you don’t want to or can’t use Switch Expression, you could usw an if-else if-else block (like in AppleRecipe) instead.

If you have subclasses of subclasses (as mentioned in the comments), you can let it handle parts of the logic in one and other parts in the other class. For example, you could have an AppleDessetRecipe that is a DessertRecipe that also contains apples:

public class AppleRecipe extends DessertRecipe{
    @Override
    public String getIngredientMeasurementFromRecipe(String ingredient){
        if(ingredient.equals("apple")){//or use a switch expression
            return getApple();
        }else{
            return super.getIngredientMeasurementFromRecipe(ingredient);//call DessertRecipe#getIngredientMeasurementFromRecipe
        }
    }
}

The abstract method in the superclass describes that the method should be present in subclasses. These subclasses then implement the method.

See this for more information about abstract classes.

Restructure your model

You could also just store all ingredients in your parent class using a Map (if applicable):

public abstract class Recipe{
    protected final Map<String, String> ingredients=new HashMap<>();
    public String getIngredientMeasurementFromRecipe(String ingredient){
        return ingredients.get(ingredient);
    }
}
public class DessertRecipe extends Recipe{
    public DessertRecipe(String yourFlour){
        ingredients.put("flour",yourFlour);
    }
}

You could make use of Java’s more recent language features.

Instanceof patternmatching

One simple improvement to your code is instanceof Patternmatching:

private String getIngredientMeasurementFromRecipe(Recipe recipe, String ingredient)
  {
    if (recipe instanceof DessertRecipe desertRecipe){
      if(ingredient.equals("flour")) {
          return dessertRecipe.getFlour();
      } else if (ingredient.equals("sugar")){
          return dessertRecipe.getSugar();
      }
    } else if (recipe instanceof DinnerRecipe dinnerRecipe){
      if(ingredient.equals("chicken")) {
          return dinnerRecipe.getChicken();
      } else if (ingredient.equals("salt")){
          return dinnerRecipe.getSalt();
      }    
    } else if (recipe instanceof BeverageRecipe beverageRecipe){
      if (ingredient.equals("sugar")) {
          return beverageRecipe.getSugar();
      } else if (ingredient.equals("water")){
          return bevarageRecipe.getWater();
      } 
    }
}

Instead of casting after the instanceof check, you can get the casted instance directly with the check.

Switch Patternmatching

In future Java versions, you’ll be able to use Switch patternmatching as well:

private String getIngredientMeasurementFromRecipe(Recipe recipe, String ingredient)
  {
    return switch(recipe){
        case DessertRecipe desertRecipe
          when ingredient.equals("flour") -> desertRecipe.getFlour();
        case DessertRecipe desertRecipe
          when ingredient.equals("sugar") -> desertRecipe.getSugar();
        case DinnerRecipe dinnerRecipe 
          when ingredient.equals("chicken") -> dinnerRecipe.getChicken();
        case DinnerRecipe dinnerRecipe 
          when ingredient.equals("salt") -> dinnerRecipe.getSalt();
        case BeverageRecipe beverageRecipe
          when ingredient.equals("sugar") -> beverageRecipe.getSugar();
        case BeverageRecipe beverageRecipe
          when ingredient.equals("water") -> beverageRecipe.getWater();
        default -> throw new IlegalArgumentException();
    }
  }

Instanceof Patternmatching with flattened if

You could also use the instanceof patternmatching approach but not nest the if-statements:

private String getIngredientMeasurementFromRecipe(Recipe recipe, String ingredient)
  {
    if (recipe instanceof DessertRecipe desertRecipe && ingredient.equals("flour")) {
        return dessertRecipe.getFlour();
    } else if (recipe instanceof DessertRecipe desertRecipe && ingredient.equals("sugar")){
        return dessertRecipe.getSugar();
    } else if (recipe instanceof DinnerRecipe dinnerRecipe && ingredient.equals("chicken")) {
        return dinnerRecipe.getChicken();
    } else if (recipe instanceof DinnerRecipe dinnerRecipe && ingredient.equals("salt")){
        return dinnerRecipe.getSalt();   
    } else if (recipe instanceof BeverageRecipe beverageRecipe && ingredient.equals("sugar")) {
        return beverageRecipe.getSugar();
    } else if (recipe instanceof BeverageRecipe beverageRecipe && ingredient.equals("water")){
        return bevarageRecipe.getWater(); 
    }
}

This approach is conceptually similar to switch-patternmatching.

If you have multiple methods like that, you could also use the Visitor pattern.

The Visitor pattern allows you to specify “Visitors” for different tasks to execute on each implementation.

This is (most likely) a bad idea to use but I’m listing it for completeness. Reflection allows you to execute methods with their name.

Don’t use this unless you know what you are doing

private String getIngredientMeasurementFromRecipe(Recipe recipe, String ingredient) throws Exception{
    String getterName = "get"+Character.toUpperCase(ingredient.get(0))+recipe.substring(1);
    Class<? extends Recipe> cl = recipe.getClass();
    Method m = cl.getMethod("get");
    return (String)m.invoke(recipe);
}

This obtains the getter name from the recipe, looks up the getter and calls it.

You tried to compare Strings with someString = "text". = does an assignment (changes the value) while == checks whether the value is the same.

However, you should not use == for comparing Strings. Use equals() instead.

You also mixed up ingredient and ingredients and didn’t put any semicolons but that’s probably due to you writing it in the online editor.

Enums

Aside from that, you might want to use enums instead of Strings containing ingredients:

enum Ingredient {
    FLOUR, SUGAR, CHICKEN, SALT, WATER
}

//Usage:
Ingredient ingredient = Ingredient.FLOUR;
//...
if(ingredient == Ingredient.FLOUR){
    //...
}else if(ingredient == Ingredient.SUGAR){
    //...
}
//or
switch(ingredient){
    case FLOUR:
        //...
        break;
    case SUGAR:
        //...
        break;
    //...
}
//switch expressions are possible as well

This is more performant and protects you from typos.

Leave a Comment