设计模式 01 类的设计原则

发布于 2021-06-09  4 次阅读


一、让类尽可能小

  对单个类我们建议其行数不超过50行,但这是一种不严谨的标准,我们将采用职责的项数来度量一个类的大小。下面将提到一些原则。

二、单一职责原则(SRP)

  单一职责原则(Single Responsibility Principle)规定,每个软件单元,其中包括组件、类和航母是,应该只有一个单一且明确定义的职责。

三、开闭原则(OCP)

  开闭原则(Open Closed Principle)指出软件实体(模块、类、函数等)对扩展应该是开放的,但是对修改应该是封闭的。将需要扩展的代码抽象出来,通过扩展来增加新的功能,而不修改原有的代码。开闭原则具体的实践有两种可能,其中一种是将变化依赖成接口;另一种,将变化抽象成方法,具体交给子类。

四、里氏替换原则(LSP)

  里氏替换原则(Liskov Substitution Principle)的简单表述:子类不能移除父类已经实现的功能,子类处理的问题应该比父类的广,如果遇到需要移除父类部分功能的时候,尝试使用组合的方式将父类作为子类的成员来使用。

4.1 实例

  考虑下面一种情况:我们有一个矩形类Rect,现在我们想要实现正方形类Squ。

class Rect
{
private:
    int x, y;
public:
    // 构造函数
    Rect(const int& p_x = 3, const int& p_y = 5)
    {
        x = p_x;
        y = p_y;
    }

    // 设置高
    void setHeight(int p_y)
    {
        y = p_y;
    }

    // 设置宽
    void setWidth(int p_x)
    {
        x = p_x;
    }

    // 取得面积
    int GetArea()
    {
        return x * y;
    }
};

4.2 不好的实现

  如果我们要使用继承的方式来实现Squ,将是如下方式(但继承是不好的方式)。这里我们需要使用virtual将setWidth()和setHeight()声明为虚函数,之后在Squ中覆写基函数:

class Squ :public Rect
{
public:
    Squ(const int& p_e = 3) :Rect{ p_e, p_e } {};

    virtual void setHeight(int p_e) override
    {
        Rect::setHeight(p_e);
        Rect::setWidth(p_e);
    }

    virtual void setWidth(int p_e) override
    {
        Rect::setHeight(p_e);
        Rect::setWidth(p_e);
    }

    void setEdge(int p_e)
    {
        Rect::setHeight(p_e);
        Rect::setWidth(p_e);
    }
};

  这里使用虚函数是不好的,它试图“移除”基类的方法。而使用常规的继承(不适用virtual),则基类的方法也被继承到子类中,虽然这些方法不会被使用。

4.3 好的实现

class Squ
{
private:
    Rect impl;
public:
    Squ(const int& p_e = 3)
    {
        impl = Rect(p_e, p_e);
    }

    void setEdge(int p_e)
    {
        impl.setHeight(p_e);
        impl.setWidth(p_e);
    }

    int GetArea()
    {
        return impl.GetArea();
    }
};

  这里将父类作为子类的成员来使用,避免了继承带来的一系列问题。这便是里氏替换原则的简单实例。

五、接口隔离原则(ISP)

  接口隔离原则(Interface Segregation Principe)指出接口不应该包含那些与实现类无关的成员函数,或者这些类不能以有意义的方式实现。我们应该将宽接口分解成更小且高内聚的接口。生成的小接口也称为角色接口。

5.1 实例

  考虑下面一个例子,我们有一个Bird类、我们想实现Sparrow类和Penguin类。

class Bird
{
public:
    virtual void eat();

    virtual void run();

    virtual void fly();
};

class Sparrow :public Bird
{
public:
    virtual void eat() override;

    virtual void run() override;

    virtual void fly() override;
};

class Punguin :public Bird
{
public:
    virtual void eat() override;

    virtual void run() override;

    virtual void fly() override;
    // ?
};

  对于上述情况,企鹅是不会飞的,为了应对这种情况,我们需要拆分他们的接口:

5.2 拆分接口后

class Lifeform
{
public:
    virtual void eat() = 0;
    virtual void run() = 0;
};

class Flyable
{
public:
    virtual void fly() = 0;
};

class Sparrow : public Lifeform, public Flyable
{
    // do sth
};

class Penguin : public Lifeform
{
    // do sth
};

六、无循环依赖原则

  考虑下面一个情况,我们需要实现一个购物系统,有Customer和Account两个类:

6.1 实例

// Customer.h
class Customer
{
private:
    Account customerAccount;
};

// Account.h
class Account
{
private:
    Customer owner;
};

  这样是无法通过编译的,但是我们可以使用前置声明的方式使它通过编译。这里前置的是标识符的声明,但是不定义该标识符的完整结构,这些类型有时候又被称为不完整类型。因此,只能声明它们的指针或引用,而不能用于实例化的成员变量,因为编译器对其大小一无所知。

6.2 使用前置声明

// Account.h
#pragma once

class Customer;

class Account
{
private:
    Customer* owner;
public:
    Account() { /* do nothing */ }

    void setOwner(Customer* p_owner)
    {
        owner = p_owner;
    }
};

// Customer.h
#pragma once

class Account;

class Customer
{
private:
    Account* customerAccount;
public:
    Customer() { /* do nothing */ }

    void setAccount(Account* p_Account)
    {
        customerAccount = p_Account;
    }
};

// main.cpp
#include <iostream>
#include "Account.h"
#include "Customer.h"

int main()
{
    Account* acc = new Account{ };
    Customer* cus = new Customer{ };
    acc->setOwner(cus);
    cus->setAccount(acc);

    return 0;
}

  这样虽然解决了编译的问题,但是导入了循环依赖,当Account实例被删除的时候,Customer中的Account仍然存在,并且指向被删除的Account。这将导致不确定状态。下面我们将介绍打破循环依赖的方法。

七、依赖转置原则(DIP)

7.1 Cus-Acc案例的依赖转置

  下面我们可以使用一个接口类来打破类上的循环依赖:

图1
图1:使用接口类

  通过上述的操作,我们阻断了类与类之间的循环依赖,但是仍然存在组件与组件之间的依赖,下面我们对它修改:
图2
图2:稍作修改

  我们稍作修改已经消除了部分依赖,但是却引入的新的依赖。依赖倒置原则(Dependency Inversion Principe):

  1.   高级模块不应该依赖于低级模块,两种都应该依赖于抽象类。
  2.   抽象依赖不应该依赖于细节,细节应该依赖于抽象。即:高层 → 抽象类、接口 ← 底层

  下面我们重新设计:

图3
图3:最终设计