1.单一职责原则
单一职责原则(Single Responsibility Principle)简称SRP,单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情。
单一职责原则的好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义
- 可读性提高,复杂性降低
- 可维护性提高
- 变更引起的风险降低
单一职责适用于接口、类,同时也适用于方法,什么意思呢?一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中。
对于单一职责原则,我的建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
2.里氏替换原则
里氏替换原则(Liskov Substitution Principle,LSP),它要求所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或者异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
规则:
- 子类必须完全实现父类的方法。如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大
- 覆写或实现父类的方法时输出结构可以被缩小
3.依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP),主要有三层含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
class ICar
{
public:
virtual void run() = 0;
};
class BMW : public ICar
{
public:
void run()
{
std::cout << "BMW run" << std::endl;
}
};
class Benz : public ICar
{
public:
void run()
{
std::cout << "Benz run" << std::endl;
}
};
class IDriver
{
public:
virtual void driver(ICar *car) = 0;
};
class Driver : public IDriver
{
public:
void driver(ICar *car)
{
car->run();
}
};
在业务场景中,我们贯彻“抽象不应该依赖细节”,也就是我们认为抽象(ICar接口)不依赖BMW和Benz两个实现类(细节),因此在高层次的模块中应用都是抽象。
高层模块调用:
ICar* bmwCar = new BMW;
IDriver *zhangSan = new Driver;
zhangSan->driver(bmwCar);
ICar* benzCar = new Benz;
zhangSan->driver(benzCar);
在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如Driver类不需要做任何修改,业务就可以运行,把“变更”引起的风险扩散降到最低。
依赖的三种写法:
1.构造函数传递依赖对象。在类中通过构造函数声明依赖对象。
class Driver : public IDriver
{
public:
//构造函数注入
Driver(ICar *car):_car(car) {
}
void driver()
{
_car->run();
}
private:
ICar* _car;
};
2.setter方法传递依赖对象
class IDriver
{
public:
virtual void setCar(ICar *car) = 0;
virtual void driver() = 0;
};
class Driver : public IDriver
{
public:
void setCar(ICar *car)
{
_car = car;;
}
void driver()
{
_car->run();
}
private:
ICar* _car;
};
3.成员函数参数中声明依赖对象
class IDriver
{
public:
virtual void driver(ICar *car) = 0;
};
class Driver : public IDriver
{
public:
void driver(ICar *car)
{
car->run();
}
};
4.接口隔离原则
接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
一个定义是:客户端不应该被迫依赖于它不使用的方法。另外一个定义是:一个类对另一个类的依赖应该建立在最小的接口上。两个定义的含义是:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。通俗一点就是,接口尽量细化,同时接口中的方法尽量少。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:①单一职责原则注重的是职责 ,而接口隔离原则注重的是对接口依赖的隔离; ②单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要
约束接口,主要针对抽象和程序整体框架的构建。
ISP的几个使用原则
- 根据接口隔离原则拆分接口时,首先必须满足单一职责原则: 没有哪个设计可以十全十美的考虑到所有的设计原则,有些设计原则之间就可能出现冲突,就如同单一职责原则和接口隔离原则,一个考虑的是接口的职责的单一性,一个考虑的是方法设计的专业性(尽可能的少),必然是会出现冲突。在出现冲突时,尽量以单一职责为主,当然这也要考虑具体的情况。
- 提高高内聚: 提高接口,类,模块的处理能力,减少对外的交互。比如你给杀手提交了一个订单,要求他在一周之内杀一个人,一周后杀手完成了任务,这种不讲条件完成任务的表现就是高内聚。具体来说就是:要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险就越小,也有利于降低成本。
- 定制服务: 单独为一个个体提供优良服务(只提供访问者需要的方法)。
- 接口设计要有限度:接口的设计粒度越小,系统越灵活,但是灵活的同时带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的,所以接口设计一定要注意适度。这个度需要根据经验和尝试判断。
5.迪米特法则
迪米特法则(Law of Demeter,LoD)也成为最少知识原则(least Knowledge Principle,LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你的(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情, 我就知道你提供的这么多public方法骂我就调用这么多,其他的我一概不关心。
使用原则:
- 只和朋友交流。朋友类的定义是:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法内部的类不属于朋友类。
- 朋友间也是有距离的。一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友间的距离,在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private、protected等访问权限。
- 是自己的就是自己的。在实际应用中,经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中
6.开闭原则
软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。那什么又是软件实体呢?软件实体包括以下几个部分:
- 项目或软件产品中按照一定的逻辑规则划分的模块。
- 抽象和类。
- 方法。
一个软件产品只要在生命期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
注意 开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。