4

I might run into a problem in the future and I will like to be well prepared for it today. The problem deals with inheritance, polymorphism and composition in a C++ context. How can we refactor "inheritance code reuse" into composition and still be able to keep a polymorphic approach?.

What I am looking for here is for a more "hands on" guidance on this matter. I have come with a very simplified example to show you and I trust that you will be able to read past it and refine it into the answer I need.

class Multilingual_entity {
public:    
    enum class t_languages {LAN_ENGLISH, LAN_RUSSIAN, LAN_CHINESE};

private:    
    std::map<t_languages, std::string> texts;

public:
    std::string set_text(t_language t, const std::string s) {texts[t]=s;}
    void get_text(t_language t) const {return texts.at(t);}
}

That is later extended like this...

class Category_shopping_article:public Multilingual_entity {
private:
    unsigned int pk_identifier;

public:
    unsigned int get_pk() const {return pk_identifier;}
    //....
}

class Shopping_article:public Multilingual_entity {
private:   
    unsigned int category_identifier;
    float price;

public:
    //....
}

And applied like this:

void fetch_all_titles_for(Multilingual_entity& m);

Category_shopping_article get_category(unsigned int pk) {
    Category_shopping_article result=get_me_category_by_pk(pk);
    fetch_all_titles_for(result);
    return result;
}

std::vector<Shopping_article> get_articles_by_category(const Category_shopping_article& cat) {
    std::vector<Shopping_article> result=get_me_articles_by_category_id(cat.get_pk());
    for(Shopping_article& a : result) fetch_all_titles_for(a);
    return result;
}

As you can see, all very easy: I can define a small shopping catalogue (first example that came to mind) with this and have it presented to the user in various languages stored somewhere. Say the languages are stored in a database so the "fetch_all_titles_for" would look like this:

void fetch_all_titles_for(Multilingual_entity& m) {
    Database_table T=m.get_database_language_table();   //I know, this is not implemented.
    Database_criteria C=m.get_database_language_criterie(); //Nor is this.

    std::map<Multilingual_entity::t_languages, const::std::string> texts=Database_object::get_me_the_info_i_need(T, C);
    for(const std::pair<Multilingual_entity::t_languages, const::std::string>& p : texts) m.set_texts(p.first, p.second);
}

Well, let's say that this is a very limiting jumpstart because tomorrow I will want to add another "multilingual text property" to the article so I can have a description. I don't need a description in the category so I can't put it in the Multilingual_entity base class... Maybe the day after tomorrow I will add a "text_review" and everything will be even more broken so we get into the composition wagon:

class Category_shopping_article: {
private:
    unsigned int pk_identifier;
    Multilingual_entity titles;

public:
    unsigned int get_pk() const {return pk_identifier;}

    std::string set_title(t_language t, const std::string s) {titles.set_text(t, s);}
    void get_title(t_language t) const {return titles.get_text(t);}
}


class Shopping_article: {
private:    
    unsigned int category_identifier;
    float price;

    Multilingual_entity titles;
    Multilingual_entity descriptions;

public:     
    std::string set_title(t_language t, const std::string s) {titles.set_text(t, s);}
    void get_title(t_language t) const {return titles.get_text(t);}

    std::string set_description(t_language t, const std::string s) {descriptions.set_text(t, s);}
    void get_description(t_language t) const {return descriptions.get_text(t);}
}

Ok, fine... Now there are these forwarding methods (tolerable, I guess) but I completely broke any approach to "fetch_all_titles_for(Multilingual_entity& m)" since there is no Multilingual_entity anymore. I am acquainted with the "prefer composition over inheritance" rule of thumb but at the beginning of the example it made sense to have a base class that could provide information about where to look into for language data.

Here's the question...Do I have to leverage tradeoffs or I am missing something here?. Is there an interface-like solution that would help me with this?. I thought of something like:

class Multilingual_consumer {
private:
    std::vector<Multilingual_entity> entities;

public:     
    Multilingual_entity& add_entity() {
        entities.push_back(Multilingual_entity);
        return entities.back();
    }
    Multilingual_entity& get_entity(unsigned int i) {return entities.at(i);}
};

class Category_shopping_article:public Multilingual_consumer {
private:
    unsigned int pk_identifier;
    enum entities{TITLE, DESCRIPTION};

public:
    unsigned int get_pk() const {return pk_identifier;}

    Category_shopping_article() {
        add_entity();
        add_entity();   //Ugly... I know to come with something better than this but I could store references to these entities.
    }

    void get_title(Multilingual_entity::t_language t) const {return get_entity(TITLE).get_text(t);}
    void get_description(Multilingual_entity::t_language t) const {return get_entity(DESCRIPCION).get_text(t);}
}

But seems like a lot of hurdle. Any ideas on how to be able to compose an object of many multilingual properties and have them being scalable?.

Thanks.

bolov
  • 72,283
  • 15
  • 145
  • 224
The Marlboro Man
  • 971
  • 7
  • 22
  • "since there is no Multilingual_entity anymore". Why of course there *are* multilingual entities, each shopping article **has** several of those. – n. m. could be an AI Dec 24 '14 at 08:30
  • Yeah right... I meant a base class that can somehow describe the collective beaviour of everything with a Multilingual_entity property or more. – The Marlboro Man Dec 24 '14 at 08:32
  • You have no virtual methods in `Multilingual_entity`, so it cannot be a useful polymorphic base class for anything. It's just an entity with a fixed, closed-for-extensions behaviour. You can add to each entity a pointer to its owner (article or category). Then you would need a common base class for articles and categories. Put polymorphic behaviour there. – n. m. could be an AI Dec 24 '14 at 08:48
  • The problem is that multlingual_entity describes in fact a multilingual text. Hence your inheritance with article is somewhat twisted, because an article HAS a multilingual text, but has as well mulilingual units of measures and other multilingual stuff. – Christophe Dec 24 '14 at 09:11
  • Christophe & n.m. : absolutely right. I think I may keep the compositional approach (scalable, extensible) and maybe use an interface-like treatment in the base class: do nothing, just declare what needs to be done and let the derived classes handle the hurdle of loading the data. Still, I really like 6502's answer below. The templated approach may free me from a lot of coupling... I am going to accept his answer - it's clear and worked on - and take some time to think about how it may affect future code. Thanks a lot. – The Marlboro Man Dec 24 '14 at 10:44

1 Answers1

1

A simple solution would be leaving your MultilingualEntity instances as a public member of the class:

class ShoppingItem {
    public:
        MultilingualEntity title;
        MultilingualEntity description;
        MultilingualEntity tech_specs;
    private:
        ...
};

this way you can access the methods directly without having to create extra names and write forwarders.

If you're a const paranoid you can also possibly keep them harder to mutate with

class ShoppingArticle {
    public:
        const MultilingualEntity& title() const { return title_; }
        const MultilingualEntity& description() const { return description_; }
        const MultilingualEntity& tech_specs() const { return tech_specs_; }
    private:
        MultilingualEntity title_;
        MultilingualEntity description_;
        MultilingualEntity tech_specs_;
        ...
};

that only requires one extra line for each element of the composition.

To write generic functions that process objects with multilingual entity parts you could for example use a method pointer based accessor:

// Search elements matching in current language
template<typename T>
std::set<int> searchItems(const std::string& needle,
                          const std::vector<T>& haystack,
                          const MultilingualEntity& (T::*a)() const) {
    std::set<int> result;
    for (int i=0,n=haystack.size(); i<n; i++) {
        if (match(needle, (haystack[i].*a)().get(current_language))) {
            result.insert(i);
        }
    }
    return result;
}

and then use it passing the accessors:

std::set<int> result = searchItems("this", items, &ShoppingItem::title);
6502
  • 112,025
  • 15
  • 165
  • 265
  • Const paranoid here so I would go with the public accessors and private properties. I am willing to pay the extra line for each part if I can't find a more "automated" solution (I really would like to avoid skimming through code and add bits here and there like "Oh, I forgot to load all the descriptions for this object here!!!") but one thing I can't wrap my head around is the template... How come there are two declared parameters in the template and three in the call?. Is there something I am missing?. Thanks!. – The Marlboro Man Dec 24 '14 at 08:59
  • @TheMarlboroMan: sorry, you're right. I wrote it without bothering trying to compile and there were quite a few mistakes. I also changed the accessor to a run-time parameter... syntax is better and it's more flexible (it's going to pay a bit on runtime, however). – 6502 Dec 24 '14 at 09:20
  • Upvoted and accepted: I really like this approach and I think I learned an interesting bit with the template. Will take my time to think how it will be implemented in the future. Thanks!. – The Marlboro Man Dec 24 '14 at 10:45
  • Just a little update, I will go with your answer... I tried coding a consumer and it just made me sick to implement it leanly (variadic constructors, or maybe calling static methods in the constructor or the derived class to know how many language entities to create...).I will trust my compiler and template as much stuff as I can :). – The Marlboro Man Dec 24 '14 at 12:33