一、单一职责原则
1.1 原则解读
原则定义:应该有且仅有一个原因引起类的变更,也可以说成是一个类只负责一件事情。
该原则要求类的职责明确清晰,这样符合该原则的设计有如下好处:
- 由于单个类只负责一件事情,职责清晰明确,类的复杂性降低
- 单个类的复杂性降低,整体可读性提高
- 可读性好,可维护性提高,由于类的职责明确清晰,没有产生不必要的耦合,每个类可以独立变化
- 变更引起的风险降低,因为单一职责每个类负责一个职责,单个职责或接口变化不会引起其它职责类的变化
原则上,我们只要将不同职责打包到不同的类中去,即可满足单一职责原则。然而,满足该原则的难处在于如何划分职责?,如何划分职责只能在具体场景中进行具体划分,不同的需求下,划分可能是不同的。
1.2 例1
假设现在我们需要实现用户管理的功能,包括了用户的信息更改,增加机构,增加角色等等。为了维护用户的诸多信息,我们将这些写到一个接口当中,作为一个用户管理类。我们来看看类图:
这个接口设计的问题在于:用户的属性和用户的行为没有分开。违反了单一职责的原则。我们可以吧用户信息抽出来成为一个业务对象BO,把用户的行为抽取出来成为一个业务逻辑Biz。其中BO对象的职责就是收集和反馈用户的属性信息,而Biz对象的职责是负责用户的行为。此时类图如下所示:
这样把一个接口拆分成两个有什么好处呢?
1.3 例2
单一职责的好处是不言而喻的,概念的定义也十分清晰明确。但是实现单一职责的首要前提是要会划分职责,而划分职责不是一项容易的工作。我们来看一个例子:
这是一个电话类。在电话通话的过程中,应该包含以下几个过程:
- 拨号
- 通话
- 回应
- 挂机
代码如下所示:
class IPhone{
public:
void dial(String phoneNumber) = 0;
void chat(Object o) = 0;
void hangup() = 0;
}
这个类在直观上看非常合理,但是实际上已经违反了单一职责模式,因为该类负责了不止一个职责。它包含了两个职责:
- 协议管理
- 数据传送
协议管理包括的是dial
和hangup
两个方法,而数据传送包含了chat
方法。对于联通或者移动或者电信,在不同的协议下,拨号和挂断的协议都不同,而数据传送方式对于拨号和挂断没有什么关系,因为只要拨通电话号码之后,数据传输只和底层的数据传输协议有关系。
为什么我们说这是两个不同的职责呢?我们是如何划分的呢?首先我们考虑两个问题:
- 这两个职责会引起类的变化
- 这两个职责可以独立变化而不互相影响
既然变化互不影响,也就是说这两组接口是相互独立的,所以我们可以考虑拆分成两个不同的类。现在拆分后的类图如下:
二、里氏替换原则
2.1 原则解读
原则定义:所有引用父类的地方,必须能透明地使用其子类对象
就是子类必须能够完全替代 父类,否则就是不合理的继承关系。
换就话说,就是父类的方法 是子类全部需要的,如果不是全部需要的,你的继承关系就存在问题。例如,父类(Anaimal)里有两个方法 Fly 和 Run. 但是 子类 Dog 只需要Run方法,而不需要Fly这个方法。所以这个父类就有问题。因为这里的子类不能完全替代父类,正在引用Anaimal->Fly
的地方不能使用Dog->Fly
去替代,考虑以下代码
void doSth(Anaimal* A){
...
A->Fly;
...
}
int main(){
Dog *dog = new Dog;
doSth(dog);//错误,dog没有实现Fly
return 0;
}
这时候需要进一步优化,脱离继承关系,变成 飞行类动物和不会飞行的动物两个类,这两个类都继承动物类。并将 原动物类里的 方法 Fly 移动飞行类动物里,Run方法 还留在动物类里,而Dog 继承 不会飞行的动物那个