基类应该有属性吗?

时间:2010-08-09 19:17:29

标签: c++ oop

在另一个社区,我有人建议你应该“永远”在基类中拥有属性。相反,它应该只有纯虚函数,让派生类负责为所有这些方法提供定义。

我对此的看法是“这不是一个经验法则”。我给出了以下简单的例子:

class Shape
{
    protected:
        double Area;
        double Perimeter ;
    public:
        Shape():Area(0.0), Perimeter(0.0){}
        double getArea(){return Area;}
        double getPerimeter (){return Perimeter;}
        virtual void setArea () = 0 ;
        virtual void setPerimeter () = 0 ;
};

class Rectangle : public Shape
{
    private :
        double length ;
        double breadth ;
    public:
        Rectangle ():length(0.0),breadth(0.0){}
        Rectangle (int _length, int _breadth):length(_length),breadth(_breadth){}
        void setArea ()
        {
            Area = length * breadth ;
        }
        void setPerimeter ()
        {
            Perimeter = 2 * (length + breadth) ;
        }
} ; 

通过上面给出的示例,我觉得任何形状都有以下属性'Area''Perimeter',因此如果我们不在课堂上提供这些属性, Shape类不适合代表'真实世界形状'

请告诉我你对此的看法。

修改

我要说的第一件事确实是我的代码是一个天真的代码并且没有多大意义w.r.t.该设计。我之前想过,我会添加一句话,说这只是一个例子,这篇文章的重点是要知道'基类应该从不有属性'。但后来我认为我也会得到一些好的设计建议,而且我确实有很多:)

提出问题,以下所有帖子,我明白这不是一个经验法则,它是设计选择(这正是我想强调的)。话虽如此,我也承认,如果这些属性可以计算(或导出)为Area = Length * Breadth,那么在基类中不需要属性。 非常感谢您的所有答案(我希望我可以选择接受多个答案)。

10 个答案:

答案 0 :(得分:12)

答案 1 :(得分:9)

在基类中包含变量是可以接受的。但是,很多语言都不允许这样做,所以基类只能有虚函数。这可能就是你听说过的原因。

关于这个问题有很多不同的观点,但在C ++中,我认为基类存储数据是相当普遍的。

答案 2 :(得分:6)

在基类中没有数据需要在派生类中不必要地复制相同的代码。防止代码重复是构建面向对象设计的概念之一。

答案 3 :(得分:2)

一般来说,每当有人告诉你永远不要做某事时,聪明的钱说他要么是绿色的,要么过于简单化了。

您作为程序员的目的是找到解决问题的最简单方法(这通常与最简单的方法不同!)。如果你必须采取巨大的结构性的痛苦来满足“最佳实践”的设计原则,那么在这种特殊情况下,它很可能不是最佳实践。此外,性能方面的考虑可能会让CS教授在实践中嗤之以鼻,尽管这些应该得到很好的记录并明智地应用。

至于具体建议......软件设计主要关注接口。您的类被其他代码使用的方式应该尽可能干净,并且应该设计为可以更改实现细节而无需更改客户端代码。

读两遍这句话让它沉入其中,这非常非常重要。这意味着,除其他外,任何使用您的代码的人都不必关心您的基类是否具有属性。公共数据成员通常是一个坏主意,因为如果将来的需求要求它们不能与计算交换,但如果你的类有一个成员函数来设置/获取值,那么它对于客户端代码是否只是设置/没有任何区别获取数据成员或做更复杂的事情。

因此,如果您的用例需要基类中的数据成员,请继续。唯一非常重要的是使界面足够灵活,以处理使这些成员成为愚蠢想法的未来添加。此外,如果发生这种情况,您可以将原始基类移动到继承树中的某个级别,并保留旧叶类的公共代码。例如,

class base {
public:
  base(int x);
  virtual ~base();

  inline int x() const { return x_; }

  // virtual interface declared here

private:
  int x_;
};

class derived : public base { ... };

可能会成为

class base {
public:
  virtual ~base();
  virtual int x() const = 0;

  // ...
};

class base_with_static_x : public base {
public:
  base_with_static_x(int);
  virtual ~base_with_static_x();

  inline int x() const { return x_; }

  // ...

private:
  int x_;
};

class derived : public base_with_static_x { ... };

一遍又一遍地编写相同的代码真的很少。

答案 4 :(得分:1)

我认为重新定位来自风格问题。将一种协议(接口)类作为基础来发布类的方法被认为是更清晰的。应尽可能地从类客户端隐藏实现。如果还将数据存储在基类中,则实现类不能自由选择存储数据的最佳方法。这样,基类限制了实现的可能性。也许一个实现想要计算一个已经存储的值,因此不需要空间,但是没有办法让interitor释放(非动态)分配的内存。

所以作为简历我会说这取决于你是否只是“编码”一件事,或者你是否想要创建一个需要更清洁方法的10年维护的大型库。

答案 5 :(得分:1)

班级的用户可以用你从未想过的方式扩展它。例如,想象一个没有顶部的矩形(只有其他3个边)。我称之为一个形状,即使谈论它的区域是没有意义的,甚至它的周长也可能是不明确的。为什么我必须包含代码来设置没有意义的属性?更重要的是,对于期望这些属性意味着什么的代码会发生什么?

答案 6 :(得分:1)

您需要将接口与实现分开。是的,一个形状有区域和边界,是的​​,你应该提供一种方法来查询这些值,但就是它。你不应该告诉形状如何来做这件事。

请注意,在您的示例中,您已经破坏了不变量;具有一定长度和宽度的矩形可以使其区域从其脚下公开变化。 (因为它还没有重新计算)一团糟!仅定义属性的接口:

class shape
{
    public:
        ~shape(){}

        virtual double area(void) const = 0;
        virtual double perimeter(void) const = 0;
};

让形状以自己的方式返回信息:

class square : public shape
{
public:
    square(double pSide) : mSide
    {}

    double area(void) const
    {
        return mSide * mSide;
    }

    double perimeter(void) const
    {
        return 4 * mSide;
    }

private:
    double mSide;
};

但是,假设这些事情确实很难计算,并且您希望在特定于形状的属性更改后缓存这些属性。您可以引入一个 mixin ,它只是一个帮助基类。这个特别使用CRTP:

template <typename Shape>
class shape_attribute_cache : public shape
{
public:
    // provide shape interface
    double area(void) const
    {
        return cached_area();
    }

    double perimeter(void) const
    {
        return cached_perimeter();
    }

protected:
    shape_attribute_cache() : mArea(0), mPerim(0), mChanged(false) {} 
    ~shape_attribute_cache(){} // not to be used publically 

    double cached_area(void) const { update_area(); return mArea; }
    void cached_area(double pArea) { mArea = pArea; }

    double cached_perimeter(void) const { update_perimeter(); return mPerim; }
    void cached_perimeter(double pPerim) { mPerim = pPerim; }

    void changed(void) { mAreaChanged = true; mPerimChanged = true; }

private:
    void update_area(void)
    {
        if (!mAreaChanged) return;

        // delegate to derived shapes update method
        static_cast<Shape*>(this)->update_area();
        mAreaChanged = false;
    }

   void update_perimeter(void)
    {
        if (!mPerimChanged) return;

        // delegate to derived shapes update method
        static_cast<Shape*>(this)->update_perimeter();
        mPerimChanged = false;
    }

    double mArea;
    double mPerim;
    bool mAreaChanged;
    bool mPerimChanged;
};

// use helper implementation
class expensive_shape : public shape_attribute_cache<expensive_shape>
{
public:
    // ...

    void some_attribute(double pX)
    {
        mX = pX;
        changed(); // flag cache is bad, no longer callers responsibility
    }

private:
    // ...

    void update_area(void)
    {
        double newArea = /* complicated formula */;
        cached_area(newArea);
    }

    void update_perimeter(void)
    {
        double newPerim = /* complicated formula */;
        cached_perimeter(newPerim);
    }
}; 

形状仍然纯粹是一个界面,但是你提供了一个辅助形状界面,可以使用,但你不会强迫其他形状的实现。

在这种情况下,这可能只是一个糟糕的例子。你应该做你需要的东西以获得一个干净的设计,如果这意味着将变量引入基类,那么这样做。 (标准库流执行此操作,然后派生类更改虚函数而不更改“实际”函数;我在我的一些代码中执行此操作。)

答案 7 :(得分:1)

在基类中包含数据很好。为了数据隐藏和OO设计的利益,它几乎不应该是公开的或受到保护的(这只是一种说法“任何人都可以弄乱我的数据”的奇特方式)。例如,我将你的样本重做如下:

class Shape
{
    private:
        double Area;
        double Perimeter ;
    public:
        Shape(double the_area, double the_perimeter): Area(the_area), Perimeter(the_perimeter){}
        double getArea() const {return Area;}
        double getPerimeter () const {return Perimeter;}
};

class Rectangle : public Shape
{
    private :
        double length ;
        double breadth ;
    public:
        Rectangle (int _length, int _breadth):Shape(length * breadth, 2 * (length + breadth)), length(_length),breadth(_breadth){}
} ; 

答案 8 :(得分:0)

回答你的问题......是的,在你的基类中使用适用于所有子类的属性。例如,如果您知道形状(POLYGON!)总是具有长度和宽度,请将它们用作基类的属性。

我不明白你为什么要为你创建的形状(POLYGON)的每个实现重新创建这些...

......我想如果你可以调用圆周的外围,你可以使用形状类。

答案 9 :(得分:0)

您的其他社区中的人似乎混淆了与基类或抽象类的接口。接口是他们描述的纯虚函数列表,但是,这与接口不同。

你可以说接口定义了一个类应该能够做什么(即它必须具有哪些可公开访问的函数),而“基类”是它的部分实现,通常在它的子类具有的情况下某个功能的相同实现或需要存储相同的信息。

举个例子。 Shape类可以是一个接口,只定义获取所有Shapes(=实现Shape的类)必须具有的区域和周长的方法。

之后,您需要定义两个形状:Rectangle和Square。 Square只有一个属性,宽度,也是它的高度。矩形具有宽度和高度。现在,您可以将Rectangle实现为具有Height和Width的单独类,但是您将拥有两个具有Width - Square和Rectangle的类。而不是那样,你可以继承Square,添加Height,你有一个Rectangle。

可能不是最好的例子,但是呃。