Search code examples
c++inheritanceinterfaceobject-composition

Refactor inheritance into composition keeping polymorphic capabilities in C++


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.


Solution

  • 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);