如何处理与派生类不兼容的基类方法?

Posted

技术标签:

【中文标题】如何处理与派生类不兼容的基类方法?【英文标题】:How to deal with base class methods that are incompatible with the derived class? 【发布时间】:2021-09-26 09:19:33 【问题描述】:

假设您正在制作一个 GUI,并且有一个 DataViewList 类,它是一个显示数据行的小部件(例如 like this)。你有方法AddRow(std::vector<std::string> row)DeleteRow(std::vector<std::string> row)AddColumn(std::string name)DeleteColumn(std::string name)

现在假设您要创建一个显示音乐播放列表的新类。它具有预先确定的列(标题、专辑、年份),您不想添加任何新列或删除现有列。此外,您希望能够在单个方法调用中将 Song 对象添加到列表中,因此您需要一个可以做到这一点的方法。你如何实现这样的类?

最基本的想法是创建一个新类MusicPlaylsit,它公开继承DataViewList,并在构造函数中添加预定的列。然后重载AddRow 方法,使其接受Song 对象作为参数。这种方法有一个很大的问题:有人可能会调用MusicPlaylist::AddColumn 或其他与MusicPlaylist 类的逻辑不兼容的方法。由于 MusicPlaylist 应该只有三个预定义的列,所以不应该有添加或删除列的方法(或访问任何其他不兼容的基类方法,例如基类非重载的 AddRow 方法)。

为了解决这个问题,我可以使用组合而不是继承,并重新实现我可能想要使用的任何方法。在我看来,这是一个坏主意,因为如果我将来想更改某些内容,这比继承更困难,因为我无法覆盖基类方法。

另一个选项是继承为protected。这可以防止使用不兼容的基类方法,并允许在未来继承的情况下进行覆盖。问题是,现在我必须用using 明确声明我想公开使用的每个方法,这似乎违背了面向对象编程的全部观点(能够在深层基类中更改某些内容并且仍然有任何继承的类能够使用它)因为在DataViewList 中新添加的公共方法在MusicPlaylist 或任何从它继承的类中将不可见,直到它们被显式公开。

所以我的问题是:在创建与基类具有“是”关系但仅与其方法部分兼容的新类时,我应该使用哪种模式?

谢谢

【问题讨论】:

继承表示“A履行了B的契约”的关系。如果您的DataViewList 代表可编辑的列,而您的MusicPlaylist 不可编辑,则MusicPlaylist 不履行DataViewList 的合同,不得继承它。 B的合同是什么意思? ***.com/questions/8537018/… 【参考方案1】:

让我们退后一步,再看看你的设计:

class DataViewList 
    using Col = std::string;
    using Row = std::vector<std::string>;
    virtual void addRow(Row) = 0;
    virtual void deleteRow(Row) = 0;
    virtual void addColumn(Col) = 0;
    virtual void deleteColumn(Col) = 0;
    virtual void draw(Context) = 0;   // not in your question, but inferred
;

你想做的是:

class MusicPlaylist : public DataViewList  /* ... */ 

您发现这行不通,因为MusicPlaylist 不履行DataViewList 定义的合同。让我们回顾一下。 DataViewList的职责是什么?

它会画画。 它提供了一个项目列表。 它会修改该项目列表。

这是 3 个职责。这违反了SOLID principles中的2条:

一个类必须只有一个职责(单一职责)。 不得强迫消费者依赖他们不使用的东西(界面隔离)。

这就是您遇到问题的原因:MusicPlaylist 只关心其中的两个职责,即绘制和维护项目列表。它维护字段列表,因此不能继承DataViewList

如何解决?

分工。

// An interface for a data grid. Only provides a read-only view. No drawing.
struct DataList 
    virtual const std::vector<Column>& getColumns() const = 0;
    virtual const std::vector<Row>& getRows() const = 0;
protected:
    ~DataList(); // or make it public virtual to be able to delete a DataList*
;

// The widget that draws data.
class DataViewList : public Widget 
public:
    virtual void setData(const DataList&); //could be a pointer, YMMV
    virtual void draw(Context);
;

注意方法:

DataViewList 不再包含任何数据,而是引用其他包含数据的对象。 由于它只显示数据,它依赖于一个仅包含读取功能的简单界面。

此时你可以简单地做一个:

// Contains music data - does not draw it
class MusicPlaylist : public DataList 
    void addSong(Song);
    void deleteSong(Song);
    const std::vector<Column>& getColumns() const override;
    const std::vector<Row>& getRows() const override;
;

// Contains more complex data with configurable columns - does not draw it
class SomeMoreComplexList : public DataList 
    void addColumn(Col);
    void deleteColumn(Col);
    void addRow(Row);
    void deleteRow(Row);
    const std::vector<Column>& getColumns() const override;
    const std::vector<Row>& getRows() const override;
;

也就是说,您的列表实现了小部件显示它们所需的界面,以及它们所需的任何特定功能。然后,您可以将它们提供给小部件的setData

【讨论】:

【参考方案2】:

不存在“部分是”的关系。要么MusicPlaylist 可以做任何DataViewList 宣传的事情,要么MusicPlaylist 不是DataViewList

DataViewList 发广告可以AddColumn。也许这是一个过于乐观的承诺。大多数类型的数据只有固定的有意义的列集,所以也许AddColumn 应该移动到DataViewList 的单独子类中。说,EditableColumnsDataViewList

另一方面,允许用户向任何类型的数据添加列也许并不是什么大不了的事。也许认为像colourshapedistanceFromParis这样的列对于一首歌没有多大意义,但如果你的用户有不同的想法呢?

【讨论】:

以上是关于如何处理与派生类不兼容的基类方法?的主要内容,如果未能解决你的问题,请参考以下文章

使用不同的兼容类型覆盖属性

为啥我的类不继承其基类中定义的方法?

不是抽象类的基类不是好基类

5继承与派生3-类型兼容规则

类型兼容规则

赋值兼容原则