将继承重构为在 C++ 中保持多态功能的组合

Posted

技术标签:

【中文标题】将继承重构为在 C++ 中保持多态功能的组合【英文标题】:Refactor inheritance into composition keeping polymorphic capabilities in C++ 【发布时间】:2014-12-24 08:04:23 【问题描述】:

我将来可能会遇到问题,我希望今天做好充分的准备。该问题涉及 C++ 上下文中的继承、多态性和组合。我们如何才能将“继承代码重用”重构为组合,并且仍然能够保持多态方法?。

我在这里寻找的是有关此问题的更多“动手”指导。我提供了一个非常简化的示例来向您展示,我相信您将能够阅读过去并将其改进为我需要的答案。

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

后来就这样扩展了……

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:
    //....

并像这样应用:

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;

如您所见,一切都非常简单:我可以用它定义一个小型购物目录(想到的第一个示例),并以存储在某处的各种语言呈现给用户。假设语言存储在数据库中,因此“fetch_all_titles_for”看起来像这样:

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

好吧,假设这是一个非常有限的快速启动,因为明天我想在文章中添加另一个“多语言文本属性”,这样我就可以进行描述了。我不需要类别中的描述,所以我不能把它放在 Multilingual_entity 基类中......也许后天我会添加一个“text_review”,一切都会更加破碎,所以我们进入组合马车:

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

好的,很好...现在有了这些转发方法(我猜是可以容忍的),但我完全打破了“fetch_all_titles_for(Multilingual_entity& m)”的任何方法,因为不再有 Multilingual_entity。我熟悉“优先组合胜于继承”的经验法则,但在示例开始时,有一个基类可以提供有关在何处查找语言数据的信息是有意义的。

问题来了……我必须权衡取舍还是我在这里遗漏了什么?有没有类似界面的解决方案可以帮助我解决这个问题?我想到了类似的东西:

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 entitiesTITLE, 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);

但似乎有很多障碍。关于如何能够组合许多多语言属性的对象并使其具有可扩展性的任何想法?

谢谢。

【问题讨论】:

“因为不再有 Multilingual_entity”。为什么当然有多语言实体,每篇购物文章都有其中几个。 是的,对...我的意思是一个基类,它可以以某种方式描述具有 Multilingual_entity 属性或更多属性的所有事物的集体行为。 Multilingual_entity 中没有虚方法,因此它不能成为任何有用的多态基类。它只是一个具有固定的、封闭的扩展行为的实体。您可以向每个实体添加指向其所有者(文章或类别)的指针。然后,您将需要文章和类别的通用基类。将多态行为放在那里。 问题在于 multlingual_entity 实际上描述了多语言文本。因此,您对文章的继承有些扭曲,因为文章具有多语言文本,但也具有多语言度量单位和其他多语言内容。 Christophe & n.m. : 绝对正确。我想我可能会保留组合方法(可扩展、可扩展),并可能在基类中使用类似接口的处理:什么都不做,只声明需要做什么,让派生类处理加载数据的障碍。不过,我真的很喜欢下面 6502 的回答。模板化的方法可以让我从很多耦合中解脱出来……我会接受他的回答——它很清楚并且正在努力——并需要一些时间来思考它如何影响未来的代码。非常感谢。 【参考方案1】:

一个简单的解决方案是将您的 MultilingualEntity 实例保留为该类的公共成员:

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

这样您可以直接访问方法,而无需创建额外的名称和编写转发器。

如果你是一个 const 偏执狂,你也可以让它们更难变异

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_;
        ...
;

对于合成的每个元素只需要一个额外的行。

要编写处理具有多语言实体部分的对象的通用函数,您可以例如使用基于方法指针的访问器:

// 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;

然后通过访问器使用它:

std::set<int> result = searchItems("this", items, &ShoppingItem::title);

【讨论】:

这里的常量偏执,所以我会使用公共访问器和私有属性。如果我找不到更“自动化”的解决方案,我愿意为每个部分支付额外的费用(我真的很想避免浏览代码并在这里和那里添加一些内容,例如“哦,我忘了加载所有描述这个对象在这里!!!”)但是我无法理解的一件事是模板......为什么模板中有两个声明的参数和调用中的三个?有什么我想念的吗?谢谢!。 @TheMarlboroMan:对不起,你是对的。我没有费心尝试编译就写了它,并且有很多错误。我还将访问器更改为运行时参数...语法更好,更灵活(但是,它会在运行时支付一些费用)。 赞成并接受:我真的很喜欢这种方法,我认为我从模板中学到了一些有趣的东西。我会花时间思考未来将如何实施。谢谢!。 只是一点点更新,我会接受你的回答...我尝试编写消费者代码,但它只是让我讨厌精益实现它(可变参数构造函数,或者可能在构造函数中调用静态方法或派生类知道要创建多少语言实体...)。我会尽可能多地信任我的编译器和模板:)。

以上是关于将继承重构为在 C++ 中保持多态功能的组合的主要内容,如果未能解决你的问题,请参考以下文章

C++:可以一次添加多个对象的多态容器

《重构》学习拆分逻辑与多态使用

javascript代码的小小重构

重构手法之处理概括关系

一次简单易懂的多态重构实践,让你理解条件逻辑

设计模式学习笔记面向对象设计原则设计模式编程范式重构的关系