类的设计

102 阅读3分钟

要集中精力设计定义了API 80%功能的20%的类.

使用继承

避免深度继承树。很深的继承层次结构增加了设计的复杂性,这总是会使设计难以理解,软件也更容易走向失败。给继承深度加一个绝对的数量限制很明显是主观的,但是任何多于两层或三层的继承层次结构已经变得太复杂了(McConnel1,2004)

里氏替换原则

相对于“is-a”,LSP是一个更具限制性的定义。

让我们用一个经典的例了一椭圆形状类型对此加以说明:


class Ellipse {
    public :

    public Ellipse();

    Ellipse(float major, float minor);

    void SetMajorRadius(float major):

    void SetMinorRadius(float minor);

    float GetMajorRadius() const:

    float GetMinorRadius() const;

    private:
    float mMajor
    float mMinor:
};

然后,你可以决定添加一个圆形类。从数学的角度看,圆是椭圆的一种更具体形态,它的两个轴是相等的。因此,将Circle类声明为E]1ipse类的子类。比如,

class Circle : public Ellipse
public:
    Circle();

    explicit Circle(float r):
    void SetRadius(float r);
    float GetRadius( ) const;
};
SetRadius()的实现可以将下层圆的长轴和短轴设置为同一个值来保证圆的属性。

    void Circle::SetRadius(float r)
    SetMajorRadius(r)
    SetMinorRadius(r);
}
float Circle::GetRadius() const
{
    return GetMajorRadius():
}

这造成了很多问题。最明显的问题是Circle类继承并暴露了Ellipse类的SetMajorRadius()和SetMinorRadius()方法。用户可以利用这些方法修改一个半径却不修改另一个,这就破坏了圆的自身一致性。可以通过重写这两个方法来处理这一问题,即在每个方法中同时设置长轴和短轴。但这又造成 创建了非正交的API: 改变一个属性对改变另一个届性有副作用。最后,你破坏了Liskov替换原则,因为你无法在不破坏行为的情况下用Circle来替换E1ipse。

所以,不应该使用公有继承将圆形建模为一种椭圆形,那应该如何表示呢?基于E11ipse类的功能,有两种主要的方式可以正确地构建circle类:私有继承和组合。

class Circle {

public:
    Circle();
    explicit Circle(float r);
    void SetRadius(float r);
    float GetRadius( ) const;
    
private:
    E1lipse mEllipse;
};

SetRadius()和GetRadius()两个方法的定义看起来可能是这样的:

void Circle::SetRadius(float r) 
{
    mEllipse.SetMajorRadius(r);
    mEllipse.SetMinorRadius(r);
}
float Circle::GetRadius() const
{
    return mEllipse.GetMajorRadius();
}

在这个例子中,Ellipse的接口没有暴露在Circle的接口中。通过创建一个私有的Ellipse实例,Circle仍然是构建在Elipse的功能之上。

里氏替换原则指出,在不修改任何行为的情况下用派生类替换基类,这应该总是可行的。

组合优先于继承。

开放-封闭原则

开放-封闭原则( Open/Closed Principle,OCP)是由BertrandMeyer引l入的,该原则指出,类的目标应该是为扩展而开放,为修改而关闭的(Meyer,1997 )。创建可以长期使用的稳定接口。

OCP背后的重要理念是,一旦一个类创建完成并向用户发布,唯一可以修改的就只有编程错误了,新特性的添加或功能的修改应该通过创建新类的方式实现。

OCP可以被认为是一种启发式的指导原则,而非必须遵守的原则。

API应该为不兼容的接口变化而关闭,为功能扩展而开放。

参考:

  1. C++ API设计