design patterns how to avoid instanceOf when using List

Viewed 147

Imagine you have a menu with dishes each dish should be available in multiple languages (French , English , Arabic , ...). The Dish class contains a list with Language type objects.

class Dish { 
     List<Language> languages
     void addLanguage(Language lg){...}
}
class Language { getDescription(){}}

class French extends Language{}

class Menu {List<Dish> dishes }

How do I avoid using instance of when wanting a description of a specific language for that dish?

Should I define for each language a get method in the dish class:getFrench(), getArabic(),..?

Or should I keep it in list and check for instance of French by looping the list and then call getDescription() on this list object<language> ?

Or is there a way for a more polymorphic approach?

3 Answers

I don't think it's a good idea to create a separate class for each language. After all, all these classes will have the exact same methods.

I'd use a single Language class, and inside the Dish class I'll keep a Map<Locale,Description> (I'd rename the Language class to something less confusing such as Description, since it doesn't represent a language, it represents a description in some language).

Now you can have a

Description getDescription(Locale locale)

method in your Dish class, which will return the description of the dish in the language of the passed Locale.

You should prefer using standard JDK classes such as java.util.Locale instead of custom classes when possible.

After considering the comments, and agreeing to some of them, I suggest removing the Map to the Description class.

class Dish 
{ 
      Description description = new Description ();

      void addDescription(Locale locale, String text)
      {
          description.addText(locale,text);
      }

      String getDescription(Locale locale) 
      {
          return description.getText(locale);
      }
 }

class Description 
{
    Map<Locale,String> descriptions = new HashMap<>();

    public void addText(Locale locale,String text) 
    {
        descriptions.put(locale,text);
    }

    public void getText(Locale locale) 
    {
        return descriptions.get(locale);
    }
}

You should also note that searching the language specific description in a Map is more efficient than searching for it in a List (O(1) lookup time in a HashMap vs. O(n) lookup time in a List).

I think a Language doesn't have a menu Description. I would design it in this way.

 class Dish { 
       List<Description> descriptions;
       void addDescription(Description description){...}

       Description getDescription(Language language){
           // iterate over descriptions and select the appropriate
           // or use a Map<Language, Description>
           Optional<Description> applicableDescription =
                          descriptions.stream()
                          .filter(d -> d.getLanguage().isApplicable(language)).findFirst();
           return applicableDescription.orElseGet(this::getDefaultDescription);
       }

       private Description getDefaultDescription(){
           return descriptions.isEmpty() ? null : descriptions.get(0); 
       }
 }


 class Description {
     Language language;
     String shortDescription;
     String title;
     // and so on... 

     public boolean isApplicable(Language language){
         return this.language.equals(language);
         // This logic can be more sophisticated. E.g. If an Italian
         // wants to get a menu description, but no italian translation is
         // available, it might be useful to choose spanish. But then it
         // would be better to return a similarity, like 0.1 to 1.0, instead of true/false.
     }
 }

 class Language {
      String name; // like en, de, it, fr, etc. Maybe an enum is better or just using
                   // java.util.Locale
 }

To propose a different approach about multi language solution, I would suggest to take a look at ResourceBundle. It allows to get values based on the locale.

For instance, let's define to file :

The first for english :

data_en.properties
hello=Hello World

And for the french version :

data_fr.properties
hello=Bonjour le monde

Now, let's load the resources and print the value :

//Locale.setDefault(Locale.FRENCH);
System.out.println(Locale.getDefault());
ResourceBundle rb = ResourceBundle.getBundle("data");
System.out.println(rb.getString("hello"));

It will print :

en_US
Hello World

And if I un-comment the first line to emulate a French locale :

fr
Bonjour le monde

This can be used to get different translation. Note that I never used it for data, only for GUI (or fixed values) but based on this question, this should be quite possible to adapt the solution to use a database.

How to load resource bundle messages from DB in Java?

The advantage ? you don't have to load every translations in memory, only the one for the Locale you are running this, so only the one you really need.

Related