设计模式的六大原则
单一职责原则
单一职责的原则英文名称是Single Responsibility Principle,简称是SRP。
单一职责的定义:有且只有一个原因引起类的变更。意思就是,对于类的变化只能有一个原因影响,不能出现第二个。
举例:一个电话通话功能,假设分为4个过程发生,拨号、通话、回应、挂机,那么我们写一个接口,其类如下所示:
interface IPhone {
// 拨通电话
public void dial(String phoneNumber);
// 通话
public void chat(Object o);
// 通话完毕,挂电话
public void hangUp();
}
IPhone这个接口不是只有一个职责,包含了两个职责:一个是协议管理,一个是数据传输。dial()
和hangUp()
两个方法实现了协议管理,分别是拨号接通和挂机,相当于通信连接,关闭通信信道;chat()
实现的是数据的传送,把我们说的话转化成模拟信号或者数字信号发送出去。
经过上述拆分后,能影响IPhone这个接口的就是两个职责,协议变化会引起这个接口或者实现类的变化,数据传输的变化会引起接口或实现类的变化。这样就不符合SRP原则了。
按照SRP原则将上述事例的接口拆分成两个接口:
interface ConnectionManager {
// 拨通电话
void dial(String phoneNumber);
// 通话完毕,挂电话
void hangUp();
}
interface DataTransfer {
// 通话
void chat(Object o);
}
// 实现了两个接口
class IPhone implements ConnectionManager, DataTransfer {
@Override
public void dial(String phoneNumber) {
}
@Override
public void hangUp() {
}
@Override
public void chat(Object o) {
}
}
一个类实现两个接口,把两个职责融合在一个类中。你会觉得影响IPhone变化的是两个原因,是的。但是我们是面向接口编程,我们对外公布的是接口不是实现类。
对于接口,我们再设计的时候一定要做到单一,但是对于实现类需要根据实际情况考虑。本来一个类可以实现的行为硬拆成两个类,然后使用聚合或者组合的方式耦合在一起,制造了系统的复杂性。组合是一种强耦合的方式,尽量减少。
单一职责使用于接口、类、同时也适用于方法。一个方法的作用是修改用户密码,不要将他放到修改用户信息的方法中。
里氏替换原则
里氏替换原则(Liskov Substitution Principle,简称LSP)
定义:所有引用基类的地方必须能透明的使用其子类的对象。
一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。
比如,假设有两个类,一个是Base类,另一个是Child类,并且Child类是Base的子类。那么一个方法如果可以接受一个基类对象b的话:method1(Base b)那么它必然可以接受一个子类的对象method1(Child c).
里氏替换原则是继承复用的基石。只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正的被复用,而衍生类也才能够在基类的基础上增加新的行为。
但是需要注意的是,反过来的代换是不能成立的,如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。如果一个方法method2接受子类对象为参数的话method2(Child c),那么一般而言不可以有method2(b).
可以使用数学中的图形做为事例,一个等边三角形和一个普通三角形,等边三角形就相当于是一个子类,普通的三角形就相当于是一个基类,等边三角形具备所有三角形的特性,也就是说子类有一切父类的特性,并且在父类的基础上实现了自己的功能,让三条边相等,每个夹角都是60度,但是反过来说,子类的功能不能完全适用于基类。
实际上也是继承和多态的结合使用,使用父类作为参数,传递不同的子类(向下转型)完成不同的业务逻辑,这是对于java语言,对于python语言来说本身是多态的,可以传递的参数只要含有所需的方法就可以。
依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,简称DIP)。
- 高层模块不应该依赖其低层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;抽象就是指抽象类或者接口,细节就是继承抽象类或实现接口的类。
- 细节应该依赖抽象。
抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有细节不脱离契约的范畴,去报约束双方按照既定的契约共同发展。
接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP)表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中的方法分组,然后用多个接口替代它,每个接口服务于一个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好很多。
ISP 的主要观点如下:
1)一个类对另外一个类的依赖性应当是建立在最小的接口上的。
ISP 可以达到不强迫客户(接口的使用方法)依赖于他们不用的方法,接口的实现类应该只呈现为单一职责的角色(遵循 SRP 原则) ISP 还可以降低客户之间的相互影响---当某个客户要求提供新的职责(需要变化)而迫使接口发生改变时,影响到其他客户程序的可能性最小。
2)客户端程序不应该依赖它不需要的接口方法(功能)。
客户端程序就应该依赖于它不需要的接口方法(功能),那依赖于什么?依赖它所需要的接口。客户端需要什么接口就是提供什么接口,把不需要的接口剔除,这就要求对接口进行细化,保证其纯洁性。
过于臃肿的接口设计是对接口的污染。所谓的接口污染就是为接口添加不必要的职责,如果开发人员在接口中增加一个新功能的目的只是减少接口实现类的数目,则此设计将导致接口被不断地“污染”并“变胖”。
“接口隔离”其实就是定制化服务设计的原则。使用接口的多重继承实现对不同的接口的组合,从而对外提供组合功能---达到“按需提供服务”。 接口即要拆,但也不能拆得太细,这就得有个标准,这就是高内聚。接口应该具备一些基本的功能,能独一完成一个基本的任务。
迪米特法则
迪米特法则(Law of Demeter, LoD)是1987年秋天由lan holland在美国东北大学一个叫做迪米特的项目设计提出的,它要求一个对象应该对其他对象有最少的了解,所以迪米特法则又叫做最少知识原则(Least Knowledge Principle, LKP)。
迪米特法则的意义在于降低类之间的耦合。由于每个对象尽量减少对其他对象的了解,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
迪米特法则还有一个英文解释是:talk only to your immediate friends(只和直接的朋友交流)。什么是朋友呢?每个对象都必然会与其他的对象有耦合关系,两个对象之间的耦合就会成为朋友关系。那么什么又是直接的朋友呢?出现在成员变量、方法的输入输出参数中的类就是直接的朋友。迪米特法则要求只和直接的朋友通信。
开闭原则
开闭原则定义如下:
Software entities like classes,modules and functions should be open for extension but closed for modifications.
一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。
开闭原则明确的告诉我们:软件实现应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。那什么是软件实体呢?软件实体包括以下几个部分:
- 项目或软件产品中按照一定的逻辑规则划分的模块
- 抽象和类
- 方法
一个软件产品只要在生命周期内,都会发生变化,即然变化是一个事实,我们就应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
我们举例说明什么是开闭原则,以书店销售书籍为例,其类图如下:
IBook定义了三个属性:名称、价格、作者。小说类NovelBook是一个具体的实现类,是所有小说书籍的总称,BookStore是书店。
书籍接口代码:
interface IBook {
// 书籍名称
String getName();
// 书籍售价
int getPrice();
// 书籍作者
String getAuthor();
}
小说类代码:
class NovelBook implements IBook {
// 书籍名称
private String name;
// 书籍价格
private int price;
// 书籍作者
private String author;
public NovelBook(String name, int price, String author) {
this.name = name;
this.price = price;
this.author = author;
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
@Override
public String getAuthor() {
return author;
}
}
书店售书过程代码
class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
static {
bookList.add(new NovelBook("python", 1200, "iu"));
bookList.add(new NovelBook("java", 3500, "haha"));
bookList.add(new NovelBook("golang", 4500, "liu"));
}
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println("-----书店卖出书籍记录如下:--------");
for (IBook book : bookList) {
System.out.println("书籍名称:" + book.getName() + " 书籍价格:" + book.getPrice() + " 书籍作者:" + book.getAuthor());
}
}
}
如果因为双十一活动,书店开始对书进行打折处理,所有书籍一律八折,那么我们如何实现。
我们有下面三种方法可以解决此问题:
-
修改接口
在IBook接口中,增加一个方法getOffPrice(),专门用于进行打折处理,所有的实现类实现此方法。但是这样的一个修改方式,实现类NovelBook要修改,同时IBook接口应该是稳定且可靠,不应该经常发生改变,否则接口作为契约的作用就失去了。因此,此方案否定。 -
修改实现类
修改NovelBook类的方法,直接在getPrice()方法中实现打折处理。此方法是有问题的,例如我们如果getPrice()方法中只需要读取书籍的打折前的价格呢?这不是有问题吗?当然我们也可以再增加getOffPrice()方法,这也是可以实现其需求,但是这就有二个读取价格的方法,因此,该方案也不是一个最优方案。 -
通过扩展实现变化
我们可以增加一个子类OffNovelBook,覆写getPrice方法。此方法修改少,对现有的代码没有影响,风险少,是个好办法。下面是修改后的类图:
打折类代码:
class OffNovelBook extends NovelBook{
public OffNovelBook(String name,int price,String author){
super(name,price,author);
}
//覆写价格方法,当价格大于40,就打8析,其他价格就打9析
public int getPrice(){
if(super.getPrice() > 40){
return (int) (super.getPrice() * 0.8);
}else{
return (int) (super.getPrice() * 0.9);
}
}
}
现在打折销售开发完成了,我们只是增加了一个OffNovelBook类,我们修改的代码都是高层次的模块,没有修改底层模块,代码改变量少,可以有效的防止风险的扩散。