• 《C++代码简洁之道》学习笔记:类的设计原则


    一、让类尽可能小

    庞大的、代码成百上千行的类通常难以理解,这种类的可维护性、可测试性、可复用性都很差。

    作者建议一般类的代码不要超过50行。我认为很难做到。如果以50行为目标,那要创建很多小类。小类的确更容易使用、理解和测试,但要注意这些相互关联的类应该创建在一起,不要放到不同的文件里面,否则会使工程的文件数量急剧增多。

    二、单一职责

    一个类通常只有一个单一且明确的职责。遵循此原则的类通常非常清晰和容易理解。

    单一职责是比代码行数更好的标准,没有违背此原则,不管代码有多少行都没问题。

    三、开闭原则

    类对于拓展应该是开放的,对于修改应该是封闭的。

    由于用户需求变化等原因,类应该是可拓展的,但如果对已经过测试的功能进行大改那么后果是难以预料的。最好能在不修改现有代码的基础上进行拓展。

    对于面向对象对象来说,这种方式是继承。通过继承,可以在不修改现有功能的基础上增加新功能。

    四、里氏替换原则

    当要因为拓展一个类而继承该类时,应该遵循:子类可以替换父类。即任何父类可以出现的地方,子类一定要可以出现。

    具体来说应该遵循:

    • 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
    • 子类可以增加自己特有的方法。
    • 子类实现父类的抽象方法时,方法的输入参数要比父类方法的输入参数更宽松。
    • 子类实现父类的抽象方法时,方法的返回值要比父类的返回值更严格。

    五、接口隔离原则

    这是编写抽象类的原则。即:接口应高度内聚、高度抽象。也就是说接口不应该包含实现类实现后无意义的成员函数。比如一个关于鸟的抽象类,不应该包含关于飞的成员函数,因为企鹅也是鸟但不能飞,企鹅类继承鸟的抽象类后实现飞的成员函数是没有意义的。

    六、避免环依赖

    如果A类的定义里面包含B类的成员变量,B类的定义里包含A类的成员变量,这就是环依赖,也是紧耦合的不良表现,编译无法通过。

    处理办法有两种,一是使用指针:

    1. #ifndef ACCOUNT_H
    2. #define ACCOUNT_H
    3. class Account
    4. {
    5. public:
    6. Account();
    7. void setOwner(class Customer *newOwner);
    8. private:
    9. Customer * owner;
    10. };
    11. #endif // ACCOUNT_H
    1. #ifndef CUSTOMER_H
    2. #define CUSTOMER_H
    3. class Customer
    4. {
    5. public:
    6. Customer();
    7. void setCustomerAccount(class Account *newCustomerAccount);
    8. private:
    9. Account * customerAccount;
    10. };
    11. #endif // CUSTOMER_H
    1. #include "account.h"
    2. #include "customer.h"
    3. int main(int argc, char *argv[])
    4. {
    5. Account * account = new Account;
    6. Customer * customer = new Customer;
    7. account->setOwner(customer);
    8. customer->setCustomerAccount(account);
    9. }

    七、依赖倒置原则

    抽象不应该依赖细节,细节应该依赖抽象。

    上面处理环依赖的第二种方式就是:使A类继承一个接口A0,A类可以依赖B类,B类依赖接口A0而不是直接依赖A类。

    1. #ifndef OWNER_H
    2. #define OWNER_H
    3. #include "string"
    4. #include "memory"
    5. class OWner
    6. {
    7. public:
    8. virtual ~OWner() = default;
    9. virtual std::string getName() = 0;
    10. };
    11. using OWnerPtr = std::shared_ptr;
    12. #endif // OWNER_H
    1. #ifndef CUSTOMER_H
    2. #define CUSTOMER_H
    3. #include "owner.h"
    4. #include "account.h"
    5. class Customer : public OWner
    6. {
    7. public:
    8. Customer();
    9. void setCustomerAccount(AccountPtr newCustomerAccount);
    10. std::string getName();
    11. private:
    12. AccountPtr customerAccount;
    13. };
    14. #endif // CUSTOMER_H
    1. #ifndef ACCOUNT_H
    2. #define ACCOUNT_H
    3. #include "owner.h"
    4. class Account
    5. {
    6. public:
    7. Account();
    8. void setOwner(OWnerPtr newOwner);
    9. private:
    10. OWnerPtr owner;
    11. };
    12. using AccountPtr = std::shared_ptr;
    13. #endif // ACCOUNT_H
    1. #include "account.h"
    2. #include "customer.h"
    3. int main(int argc, char *argv[])
    4. {
    5. Account * account = new Account;
    6. Customer * customer = new Customer;
    7. account->setOwner(std::shared_ptr(customer));
    8. customer->setCustomerAccount(std::shared_ptr(account));
    9. }

    八、最少知识原则

    一个对象应当对其他对象尽可能少的了解,不和陌生人说话。

    即不希望类之间建立直接的联系(两个类不直接通信)。如果真的有需要建立联系,也希望能通过它的友元类来转达。

    这可能有一个副作用,即有大量类的存在只是为了用作在类之间传达信息,增加了系统的复杂度。

    1. class A
    2. {
    3. public:
    4. A() = default;
    5. void info()const
    6. {
    7. qDebug()<<"我是A,你呢?";
    8. }
    9. friend class C;
    10. };
    11. class B
    12. {
    13. public:
    14. B() = default;
    15. void info()const
    16. {
    17. qDebug()<<"我是B";
    18. }
    19. friend class C;
    20. };
    21. class C
    22. {
    23. public:
    24. C() = default;
    25. void introduce(const A & a,const B & b)const
    26. {
    27. a.info();
    28. b.info();
    29. }
    30. };
    31. int main(int argc, char *argv[])
    32. {
    33. A a;
    34. B b;
    35. C c;
    36. c.introduce(a,b);
    37. }

    九、避免“贫血”类

    “贫血”类是指类里面只包含私有成员和成员的 getter、setter 函数。这种数据类型完全不必封装成类,把数据包装成结构体用起来更方便。

    十、只说不问原则

    尽量少地获取其他对象的状态。

    1. class doSomeThing
    2. {
    3. public:
    4. void start()
    5. {
    6. if(a.isRuning()
    7. {
    8. a.doSome();
    9. }
    10. if(b.isRuning()
    11. {
    12. b.doSome();
    13. }
    14. if(c.isRuning()
    15. {
    16. c.doSome();
    17. }
    18. }
    19. private:
    20. A a;
    21. B b;
    22. C c;
    23. };

    根据只说不问原则,上面的代码可以改成:

    1. class doSomeThing
    2. {
    3. public:
    4. void start()
    5. {
    6. a.doSome();
    7. b.doSome();
    8. c.doSome();
    9. }
    10. private:
    11. A a;
    12. B b;
    13. C c;
    14. };

    不去获取成员变量的状态,把是否 Runing 等判断放到对象自身的函数中处理。这样做是为了增强类的内聚性。

    十一、慎重使用类的静态成员

    一般的工具类会大量使用甚至全部使用静态成员。

    大量使用静态成员的缺点:

    1. 破坏了类的封装性。
    2. 会使类的内聚性很弱甚至没有。
    3. 工具类的代码量会十分庞大,这不符合类应该尽量小的原则。
    4. 工具类的功能十分多样化,这不符合类应该具有单一职责的原则。

    大量使用静态成员是一种面向过程的编程方法,如果需要大量的工具方法,可以把它们声明在一个头文件里面但建议不要封装成类。

    静态成员常量和创建对象实例的静态成员函数(工厂方法)不在此列。

  • 相关阅读:
    java8新特性(中)-StreamAPI
    JVM第一话 -- JVM入门详解以及运行时数据区分析
    链表高阶面试题及二叉树的遍历【前、中、后、层】
    RabbitMQ之延迟队列
    JavaWeb(一):MySql基础
    torch 神经网络模型构建
    这5款鲜为人知的电脑软件值得你去尝试
    Python eval 函数动态地计算数学表达式
    【ArcGIS Pro二次开发】(64):多分式标注
    【vue】0到1的常规vue3项目起步
  • 原文地址:https://blog.csdn.net/kenfan1647/article/details/127461472