用C++设计ABC(抽象基类)的良好实践

Good practice to design a ABC(Abstract Base Class) in C++

本文关键字:基类 设计 C++ ABC 抽象      更新时间:2023-10-16

在java中,我们可以定义不同的接口,然后我们可以为一个具体的类实现多个接口。

// Simulate Java Interface in C++
/*
interface IOne {
    void   MethodOne(int i);
    .... more functions
}
interface ITwo {
    double MethodTwo();
    ... more functions
}
class ABC implements IOne, ITwo {
    // implement MethodOne and MethodTwo
}
*/

在C++中,一般来说,我们应该避免使用多重继承,尽管多重继承在某些情况下确实有其优势。

class ABC {
public:
    virtual void   MethodOne(int /*i*/) = 0 {}
    virtual double MethodTwo() = 0 {}
    virtual ~ABC() = 0 {}
protected:
    ABC() {} // ONLY ABC or subclass can access it
};

问题1>根据ABC的设计,我是否应该改进其他方面,使其成为一个不错的ABC?

问题2>一个好的ABC不应该包含成员变量,而是应该将变量保存在子类中,这是真的吗?

问题3>正如我在评论中指出的,如果ABC必须包含太多纯函数,该怎么办?有更好的方法吗?

  1. 除非必要,否则不要为纯虚拟方法提供实现
  2. 不要让你的析构函数纯粹是虚拟的
  3. 不要让你的构造函数受到保护。不能创建抽象类的实例
  4. 最好将构造函数和析构函数的实现隐藏在源文件中,以免污染其他对象文件
  5. 使您的界面不可复制

如果这是一个接口,最好不要有任何变量。否则,它将是一个抽象基类,而不是一个接口。

太多的纯函数是可以的,除非你可以用不太纯的函数。

在C++中,一般来说,我们应该避免使用多重继承

与任何其他语言功能一样,您应该在适当的地方使用多重继承。接口通常被认为是对多重继承的适当使用(例如,请参阅COM)。

ABC的构造函数不需要保护——因为它是抽象的,所以不能直接构造它。

ABC析构函数不应该声明为纯虚拟的(当然,它应该声明为虚拟的)。如果派生类不需要用户声明的构造函数,则不应要求它们实现该构造函数。

接口不应该有任何状态,因此不应该有任意成员变量,因为接口只定义了如何使用某个东西,而不是如何实现它。

ABC不应具有过多的成员函数;它应该具有所需的确切数字。如果太多,显然应该删除那些没有使用或不需要的,或者将接口重构为几个更具体的接口。

根据ABC的设计,我是否应该改进其他方面,使其成为一个像样的ABC?

您有几个语法错误。出于某种原因,不允许将纯虚拟函数的定义放在类定义中;在任何情况下,你几乎肯定不想在ABC中定义它们。因此,声明通常是:

virtual void MethodOne(int /*i*/) = 0;   // ";" not "{}" - just a declaration

尽管析构函数应该是虚拟的(或者,在某些情况下,是非虚拟的和受保护的,但使其成为虚拟的是最安全的),但使它成为纯析构函数并没有任何意义。

virtual ~ABC() {}  // no "= 0"

不需要受保护的构造函数——事实上,它是抽象的,除了作为基类之外,已经阻止了实例化。

一个好的ABC不应该包含成员变量,而是应该将变量保存在子类中,这是真的吗?

通常,是的。这在接口和实现之间提供了一个清晰的分离。

正如我在评论中指出的,如果ABC必须包含太多纯函数,该怎么办?有更好的方法吗?

接口应该像它需要的那样复杂,而不是更多。如果有些功能是不必要的,那么只有"太多"的功能;在这种情况下,把他们赶走。如果界面看起来太复杂,它可能会尝试做不止一件事;在这种情况下,您应该能够将其分解为更小的接口,每个接口都有一个目的。

第一:为什么我们要避免C++中的多重继承?我从没见过一个没有广泛使用的较大的应用程序。继承自多个接口是使用它的一个很好的例子。

请注意,Java的interface已损坏—只要你想使用通过契约编程,您只能使用抽象类,并且它们不允许多重继承。然而,在C++中,这很容易:

class One : boost::noncopyable
{
    virtual void doFunctionOne( int i ) = 0;
public:
    virtual ~One() {}
    void functionOne( int i )
    {
        //  assert pre-conditions...
        doFunctionOne( i );
        //  assert post-conditions...
    }
};
class Two : boost::noncopyable
{
    virtual double doFunctionTwo() = 0;
public:
    virtual ~Two() {}
    double functionTwo()
    {
        //  assert pre-conditions...
        double results = doFunctionTwo();
        //  assert post-conditions...
        return results;
    }
};
class ImplementsOneAndTwo : public One, public Two
{
    virtual void doFunctionOne( int i );
    virtual double doFunctionTwo();
public:
};

或者,你可以有一个复合界面:

class OneAndTwo : public One, public Two
{
};
class ImplementsOneAndTwo : public OneAndTwo
{
    virtual void doFunctionOne( int i );
    virtual double doFunctionTwo();
public:
};

并从中继承,这是最有意义的。

这或多或少是一个标准的成语;在不能可以想象为接口中的任何前置或后置条件(通常调用反转),虚拟函数可以是公共的,但一般来说,它们将是私有的,因此您可以强制执行岗位条件。

最后,请注意,在很多情况下(尤其是当类表示一个值),您将直接实现它,而不需要界面与Java不同,您不需要单独的接口来维护在与类不同的文件中的实现定义—这是C++默认情况下的工作方式(使用类头中的定义,但源文件中的实现代码)。