一、让类尽可能小
庞大的、代码成百上千行的类通常难以理解,这种类的可维护性、可测试性、可复用性都很差。
作者建议一般类的代码不要超过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<OWner>;
#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<Account>;
#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));
customer->setCustomerAccount(std::shared_ptr<Account>(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 等判断放到对象自身的函数中处理。这样做是为了增强类的内聚性。
十一、慎重使用类的静态成员
一般的工具类会大量使用甚至全部使用静态成员。
大量使用静态成员的缺点:
- 破坏了类的封装性。
- 会使类的内聚性很弱甚至没有。
- 工具类的代码量会十分庞大,这不符合类应该尽量小的原则。
- 工具类的功能十分多样化,这不符合类应该具有单一职责的原则。
大量使用静态成员是一种面向过程的编程方法,如果需要大量的工具方法,可以把它们声明在一个头文件里面但建议不要封装成类。
静态成员常量和创建对象实例的静态成员函数(工厂方法)不在此列。