文章目录
一、 设计模式(Design Pattern,DP)
1.1、产生背景
1. “设计模式”这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中。 2. 直到 1990 年,软件工程界才开始研讨设计模式的话题。 3. 1995年,“四人组”(Gang of Four,GoF)合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,在书籍中收录了 23 个设计模式,这是设 计模式领域里程碑的事件,导致了软件设计模式的突破。 4. 直到今天,狭义的设计模式还是该书中所介绍的23种经典设计模式。1.2、设计模式的概念
软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、 代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它 是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。其目的是为了提 高代码的可重用性、代码的可读性和代码的可靠性。
二、设计模式六大设计原则
2.1、开闭原则:Open Closed Principle,OCP
2.1.1、 开闭原则的定义
开闭原则由勃兰特·梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向对象软件构造》(Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification),这就是开闭原则的经典定义。
简单点说就是是:一个软件实体应 该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。那么什么是软件实体呢?
这里的软件实体包括以下几个部分:
1. 项目中划分出的模块
2. 类与接口
3. 方法
一个软件产品在她的生命周期内一般都会发生变化,开闭原则视为软件实体的未来事件而制定的对现行开发设计进 行约束的一个原则。
举个例子:以书店图书销售为例:
public interface iBook {
String getName();
int getPrice();
String getAuthor();
}
-----------------------
public class Novel implements iBook{
private String name;
private int price;
private String author;
public Novel(String name, int price, String author) {
this.name = name;
this.price = price;
this.author = author;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getPrice() {
return this.price;
}
@Override
public String getAuthor() {
return this.author;
}
}
-----------------------------------
import java.util.ArrayList;
public class BookStore {
private static ArrayList<iBook>BookList = new ArrayList<>();
static {
BookList.add(new Novel("红楼梦",9900,"曹雪芹"));
BookList.add(new Novel("海贼王1",4900,"尾田荣一郎"));
BookList.add(new Novel("原则",8900,"瑞达里奥"));
BookList.add(new Novel("西游记",5900,"吴承恩"));
}
public static void main(String[] args) {
System.out.println("-----------买书的记录如下:----------");
for (iBook book:BookList) {
System.out.println("书籍名称:"+book.getName()+"\t\t作者:"+book.getAuthor()+"\t\t价格:"+book.getPrice()/100.0+"元");
}
}
--------------------------------------------
}
运行结果:
项目上线,书籍能正常销售了。但是现在双十一要到了,书店决定要来一波促销活动,70元以上的书籍9折销售, 70元以下的书籍8折销售。对该项目来说,这就是一个变化,我们该怎么应该如下变化呢?
有3中解决方法:
(1)修改接口 在IBook接口里新增getOffPrice()方法,专门用于进行打折,所有的实现类都实现该方法。但这样修改的后果就 是,实现类NovelBook要修改,书店类BookStore中的main方法也要修改,同时,IBook作为接口应该是稳定且可 靠的,不应该经常发生变化,因此,该方案被否定。
(2)修改实现类 修改NovelBook类中的方法,直接在getPrice()方法中实现打折处理,这个方法可以是可以,但如果采购书籍的人 员要看价格怎么办,由于该方法已经进行了打折处理,因此采购人员看到的也是打折后的价格,会因信息不对称出 现决策失误的情况。因此,该方案也不是一个最优的方案。
(3)通过扩展实现变化 增加一个子类OffNovelBook,覆写getPrice方法,高层次的模块(也就是BookStore中static静态块中)通过 OffNovelBook类产生新的对象,完成业务变化对系统的最小开发。这样修改也少,风险也小。
public class OffNovel extends Novel{
public OffNovel(String name, int price, String author) {
super(name, price, author);
}
@Override
public int getPrice() {
int sellPrice = super.getPrice();
int offPrice = 0;
if(sellPrice >= 7000){
offPrice = sellPrice * 90/100;
}else{
offPrice = sellPrice * 80/100;
}
return offPrice;
}
-----------------------------
import java.util.ArrayList;
public class BookStore {
private static ArrayList<iBook>BookList = new ArrayList<>();
static {
BookList.add(new OffNovel("红楼梦",9900,"曹雪芹"));
BookList.add(new OffNovel("海贼王1",4900,"尾田荣一郎"));
BookList.add(new OffNovel("原则",8900,"瑞达里奥"));
BookList.add(new OffNovel("西游记",5900,"吴承恩"));
}
public static void main(String[] args) {
System.out.println("-----------买书的记录如下:----------");
for (iBook book:BookList) {
System.out.println("书籍名称:"+book.getName()+"\t\t作者:"+book.getAuthor()+"\t\t价格:"+book.getPrice()/100.0+"元");
}
}
}
运行结果:
2.1.2、开闭原则的作用
开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。
具体来说,其作用如下:
- 对软件测试的影响
软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运 行。 - 可以提高代码的可复用性
粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。 - 可以提高软件的可维护性
遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
2.2、单一职责原则:Single responsibility principle,SRP
2.2.1、背景
这是一个备受争议的原则,跟人吵架的时候这个是屡试不爽的一个梗。
为什么会备受争议呢?怎么就能吵起来呢?主要就是对职责如何定义,什么是类的职责,以及怎么划分类的职责。
举个栗子:老师对学生有很多的工作要做:例如了解个人信息、每天的学习情况、记录考勤;回答学生问题,帮助解决bug,重难点串讲;行业经验分享等。
如果将这些工作交给一位老师负责显然不合理,正确的做法是:班主任负责日常工作,技术辅导老师负责技术辅导;企业师傅负责行业经验分享等。
2.2.2、定义概念
单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则,由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规 定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
1. 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
2. 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码 的浪费。
public interface IPhone{
//拨通电话
public void dial(String phoneNumber);
//通话
public void chat(Object o);
//通话完毕,挂断电话
public void hangup();
}
以上是符合单一职责原则的吗?说白了是一个接口只负责一件事情吗?是只有一个原因引起变化么? 好像不是哦!
其实他负责了两个内容:
1、协议管理,2、数据传送。
dial()和hangup()两个方法实现的是协议管理;chat()方法负责的是数据的传送。那么协议的改变可能引起接口或者 实现类的变化;同样数据传送(电话不仅可以打电话,还能上网)的变化也可能会引起接口或实现类的变化。两个 原因都能引起变化,而两个职责直接是互不影响的,所以可以考虑拆分为两个接口。
public interface IPhone{
}
public interface IConnectionManager extends IPhone{
//拨通电话
public void dial(String phoneNumber);
//通话完毕,挂断电话
public void hangup();
}
public interface IDataTransfer extends IPhone{
//通话
public void chat(IConnectionManager con);
}
2.2.3、 单一职责原则的优点
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点:
1. 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
2. 提高类的可读性。复杂性降低,自然其可读性会提高。
3. 提高系统的可维护性。可读性提高,那自然更容易维护了。 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其 他功能的影响。
单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模 块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
PS:单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会 变得很粗,不利于重用。
但是原则是死的,人是活的。所以有些时候我们可以为了效率,牺牲一定的原则性。
2.3、里氏替换原则 Liskov Substitution Principle,LSP
背景:这是一个爱恨纠葛的父子关系的故事。该原则可以理解为:子类可以替换父类。 父子类是在我们学习继承这个知识点的时候学习到的概念。
我们先来回忆一下继承的优缺点:
优点:
1. 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
2. 提高代码的重用性;
3. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留了自己独特的个性;其实很多开源框架的扩展都 是通过继承父类实现的。
4.提供产品或者项目的开放性。
缺点:
1. 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性;
2. 降低了代码的灵活性。子类必须拥有父类的属性和方法,让子类中多了约束
3. 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能 会造成非常糟糕的结果,要重构大量的代码。
java中使用extends关键字来实现继承,采用的是单一继承的规则,C++则采用了多重继承的规则,即一个子类可 以继承多个父类。从整体上上看,利大于弊,怎么才能更大的发挥“利”的作用呢? 解决方案就是引入里氏替换原则。什么是里氏替换原则呢?
2.3.1、里氏替换原则定义
- 如果每一个类型S的对象o1,都有一个类型T的对象o2,在以T定义的所有程序P中将所有的对象o2都替 换为o1,而程序P的行为没有发生变化,那么S是T的子类。(解读:其实意思就是说:在一个程序中,如果可以将一个类T的对象全部替换为另一个类S的对 象,而程序的行为没有发生变化,那么S是T的子类。)
- 所有引用基类的地方必须能透明地使用其子类对象。(解读:通俗地讲,就是任何一个使用父类的地方,你都可 以把它替换成它的子类,而不会发生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过 来就不行了,有子类出现的地方,父类未必可以替换。)
定义中包含了4层含义:
1. 子类必须完全实现父类的方法。
2. 子类中可以增加自己特有的方法。
3. 当子类覆盖或实现父类的方法时,方法的输入参数(方法的形参)要比父类方法的输入参数更宽松。
4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
2.2.3、里氏替换原则的作用
里氏替换原则的主要作用如下。
- 里氏替换原则是实现开闭原则的重要方式之一。
- 它克服了继承中重写父类造成的可复用性变差的缺点。
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来 的继承关系,重新设计它们之间的关系。
总结
这里对本文进行总结:
以上就是今天要讲的内容,本文仅仅简单介绍了三种设计模式原则。