庞大的、代码成百上千行的类通常难以理解,这种类的可维护性、可测试性、可复用性都很差。
作者建议一般类的代码不要超过50行。我认为很难做到。如果以50行为目标,那要创建很多小类。小类的确更容易使用、理解和测试,但要注意这些相互关联的类应该创建在一起,不要放到不同的文件里面,否则会使工程的文件数量急剧增多。
一个类通常只有一个单一且明确的职责。遵循此原则的类通常非常清晰和容易理解。
单一职责是比代码行数更好的标准,没有违背此原则,不管代码有多少行都没问题。
类对于拓展应该是开放的,对于修改应该是封闭的。
由于用户需求变化等原因,类应该是可拓展的,但如果对已经过测试的功能进行大改那么后果是难以预料的。最好能在不修改现有代码的基础上进行拓展。
对于面向对象对象来说,这种方式是继承。通过继承,可以在不修改现有功能的基础上增加新功能。
当要因为拓展一个类而继承该类时,应该遵循:子类可以替换父类。即任何父类可以出现的地方,子类一定要可以出现。
具体来说应该遵循:
这是编写抽象类的原则。即:接口应高度内聚、高度抽象。也就是说接口不应该包含实现类实现后无意义的成员函数。比如一个关于鸟的抽象类,不应该包含关于飞的成员函数,因为企鹅也是鸟但不能飞,企鹅类继承鸟的抽象类后实现飞的成员函数是没有意义的。
如果A类的定义里面包含B类的成员变量,B类的定义里包含A类的成员变量,这就是环依赖,也是紧耦合的不良表现,编译无法通过。
处理办法有两种,一是使用指针:
- #ifndef ACCOUNT_H
- #define ACCOUNT_H
-
- class Account
- {
- public:
- Account();
- void setOwner(class Customer *newOwner);
-
- private:
- Customer * owner;
- };
-
- #endif // ACCOUNT_H
- #ifndef CUSTOMER_H
- #define CUSTOMER_H
-
- class Customer
- {
- public:
- Customer();
- void setCustomerAccount(class Account *newCustomerAccount);
-
- private:
- Account * customerAccount;
- };
-
- #endif // CUSTOMER_H
- #include "account.h"
- #include "customer.h"
-
- int main(int argc, char *argv[])
- {
- Account * account = new Account;
- Customer * customer = new Customer;
- account->setOwner(customer);
- customer->setCustomerAccount(account);
- }
抽象不应该依赖细节,细节应该依赖抽象。
上面处理环依赖的第二种方式就是:使A类继承一个接口A0,A类可以依赖B类,B类依赖接口A0而不是直接依赖A类。
- #ifndef OWNER_H
- #define OWNER_H
-
- #include "string"
- #include "memory"
-
- class OWner
- {
- public:
- virtual ~OWner() = default;
- virtual std::string getName() = 0;
- };
-
- using OWnerPtr = std::shared_ptr
; -
- #endif // OWNER_H
- #ifndef CUSTOMER_H
- #define CUSTOMER_H
-
- #include "owner.h"
- #include "account.h"
-
- class Customer : public OWner
- {
- public:
- Customer();
- void setCustomerAccount(AccountPtr newCustomerAccount);
- std::string getName();
-
- private:
- AccountPtr customerAccount;
- };
-
- #endif // CUSTOMER_H
- #ifndef ACCOUNT_H
- #define ACCOUNT_H
-
- #include "owner.h"
-
- class Account
- {
- public:
- Account();
- void setOwner(OWnerPtr newOwner);
-
- private:
- OWnerPtr owner;
- };
-
- using AccountPtr = std::shared_ptr
; -
- #endif // ACCOUNT_H
- #include "account.h"
- #include "customer.h"
-
- int main(int argc, char *argv[])
- {
- Account * account = new Account;
- Customer * customer = new Customer;
- account->setOwner(std::shared_ptr
(customer)); - customer->setCustomerAccount(std::shared_ptr
(account)); - }
一个对象应当对其他对象尽可能少的了解,不和陌生人说话。
即不希望类之间建立直接的联系(两个类不直接通信)。如果真的有需要建立联系,也希望能通过它的友元类来转达。
这可能有一个副作用,即有大量类的存在只是为了用作在类之间传达信息,增加了系统的复杂度。
- class A
- {
- public:
- A() = default;
- void info()const
- {
- qDebug()<<"我是A,你呢?";
- }
-
- friend class C;
- };
-
- class B
- {
- public:
- B() = default;
- void info()const
- {
- qDebug()<<"我是B";
- }
- friend class C;
- };
-
- class C
- {
- public:
- C() = default;
- void introduce(const A & a,const B & b)const
- {
- a.info();
- b.info();
- }
- };
-
- int main(int argc, char *argv[])
- {
- A a;
- B b;
- C c;
- c.introduce(a,b);
- }
“贫血”类是指类里面只包含私有成员和成员的 getter、setter 函数。这种数据类型完全不必封装成类,把数据包装成结构体用起来更方便。
尽量少地获取其他对象的状态。
- class doSomeThing
- {
- public:
- void start()
- {
- if(a.isRuning()
- {
- a.doSome();
- }
- if(b.isRuning()
- {
- b.doSome();
- }
- if(c.isRuning()
- {
- c.doSome();
- }
- }
- private:
- A a;
- B b;
- C c;
- };
根据只说不问原则,上面的代码可以改成:
- class doSomeThing
- {
- public:
- void start()
- {
- a.doSome();
- b.doSome();
- c.doSome();
- }
- private:
- A a;
- B b;
- C c;
- };
不去获取成员变量的状态,把是否 Runing 等判断放到对象自身的函数中处理。这样做是为了增强类的内聚性。
一般的工具类会大量使用甚至全部使用静态成员。
大量使用静态成员的缺点:
大量使用静态成员是一种面向过程的编程方法,如果需要大量的工具方法,可以把它们声明在一个头文件里面但建议不要封装成类。
静态成员常量和创建对象实例的静态成员函数(工厂方法)不在此列。