设计模式的背景
经过这段时间的学习,了解到了“设计模式”这个词,“设计模式”这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中。
直到1990年,软件工程界才开始研讨设计模式的话题。
1995年,“四人组”(Gang of Four,GoF)合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,在书籍中收录了23个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。
直到今天,狭义的设计模式还是该书中所介绍的23种经典设计模式。
设计模式的概念
软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。其目的是为了提高代码的可重用性、代码的可读性和代码的可靠性。
开闭原则由勃兰特·梅耶(BertrandMeyer)提出,他在1988年的著作《面向对象软件构造》(ObjectOrientedSoftwareConstruction)中提出:软件实体应当对扩展开放,对修改关闭(Softwareentitiesshouldbeopenforextension,butclosedformodification),这就是开闭原则的经典定义。简单点说就是是:一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
这里的软件实体包括以下几个部分:
案例
//书籍接口
public interface IBook {
String getName();
int getPrice();
String getAuthor();
}
package com.wln.principle.ocp;
//小说类
public 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 this.name;
}
@Override
public int getPrice() {
return this.price;
}
@Override
public String getAuthor() {
return this.author;
}
}
package com.wln.principle.ocp;
//书店类
import java.util.ArrayList;
public class BookStroe {
private static ArrayList<IBook> bookList=new ArrayList<>();
static {
bookList.add(new OffNovelBook("红楼梦", 9900, "曹雪芹"));
bookList.add(new OffNovelBook("侠客行", 8900, "金庸"));
bookList.add(new OffNovelBook("原则", 6900, "瑞达利欧"));
bookList.add(new OffNovelBook("海贼王1", 4900, "尾田荣一郎"));
}
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+"元");
}
}
}
运行结果:
通过扩展的方式来实现新的功能,这就是开闭原则。
开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下。
1.对软件测试的影响
软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
2.可以提高代码的可复用性
粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
3.可以提高软件的可维护性
遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
这是一个备受争议的原则,跟人吵架的时候这个是屡试不爽的一个梗。
为什么会备受争议呢?怎么就能吵起来呢?主要就是对职责如何定义,什么是类的职责,以及怎么划分类的职责。
举个栗子:一个项目的开发,有很多的事情要做,需求、设计、开发、测试、上线等。
如果将这些工作交给一个人负责显然不合理,正确的做法是一个人只负责一个职责。
单一职责原则(SingleResponsibility Principle,SRP)又称单一功能原则,由罗伯特·C.马丁(RobertC.Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
1.一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
2.当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
案例
package com.wln.principle.scp;
public interface IPhone {
//拨通电话
void didal(String phoneNumber);
//通话
void chat(Object obj);
//通话完毕,挂断电话
void hangup();
}
上述不符合,他负责了两个内容:1、协议管理,2、数据传送。
publicinterfaceIPhone{
}
publicinterfaceIConnectionManagerextendsIPhone{
//拨通电话
public void dial(StringphoneNumber);//通话完毕,挂断电话
public void hangup();
}
public interface IDataTransfer extends IPhone{
//通话
publicvoidchat(IConnectionManagercon);
}
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。
单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
PS:单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。
但是原则是死的,人是活的。所以有些时候我们可以为了效率,牺牲一定的原则性。
这是一个爱恨纠葛的父子关系的故事。该原则可以理解为:子类可以替换父类。
父子类实在我们学习继承这个知识点的时候学习到的概念。我们先来回忆一下继承的优缺点:
优点:
1.代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
2.提高代码的重用性;
3.提高代码的可扩展性,子类可形似于父类,但异于父类,保留了自己独特的个性;其实很多开源框架的扩展都是通过继承父类实现的。
4.提供产品或者项目的开放性。
缺点:
1.继承是侵入性的,只要继承就必须拥有父类的所有方法和属性;
2.降低了代码的灵活性。子类必须拥有父类的属性和方法,让子类中多了约束
3.增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
java中使用extends关键字来实现继承,采用的是单一继承的规则,C++则采用了多重继承的规则,即一个子类可以继承多个父类。从整体上上看,利大于弊,怎么才能更大的发挥“利”的作用呢?
解决方案就是引入里氏替换原则。什么是里氏替换原则呢?
该原则有两个定义:
该原则有两个定义:
第一种:如果每一个类型S的对象o1,都有一个类型T的对象o2,在以T定义的所有程序P中将所有的对象o2都替换为o1,而程序P的行为没有发生变化,那么S是T的子类。
意思:在一个程序中,如果可以将一个类T的对象全部替换为另一个类S的对象,而程序的行为没有发生变化,那么S是T的子类。
第二种:所有引用基类的地方必须能透明地使用其子类对象。
就是任何一个使用父类的地方,你都可以把它替换成它的子类,而不会发生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必可以替换。
里氏替换原则是继承复用的基石,它为良好的继承定义了一个规范,定义中包含了4层含义:
1.子类必须完全实现父类的方法。
我们以前做过的项目中,经常定义一个接口或者抽象类,然后编码实现,调用类则直接传入接口或者抽象类,其实这就是已经在使用历史替换原则了。
2.子类中可以增加自己特有的方法。
类都有自己的属性和方法,子类当然也不例外。除了从父类继承过来的,可以有自己独有的内容。为什么要单独列出来,是因为里氏替换原则是不可以反过来用的。也就是子类出现的地方,父类未必可以胜任。
3.当子类覆盖或实现父类的方法时,方法的输入参数(方法的形参)要比父类方法的输入参数更宽松。
public class LSP{
class Parent{
public void fun(HashMapmap){
System.out.println("父类被执行...");
}
}
class Sub extends Parent{
public void fun(Mapmap){
System.out.println("子类被执行...");
}
}
public static void main(String[]args){
System.out.print("父类的运行结果:");
LSP lsp = newLSP();
Parenta =lsp.newParent();
HashMap<Object,Object> map = newHashMap<Object,Object>();
a.fun(map);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.print("子类替代父类后的运行结果:");
LSP.Subb=lsp.newSub();
b.fun(map);
}
}
ps:这里子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行父类的重载方法。这符合里氏替换原则。
4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
package com.wln.principle.lvp;
import java.util.HashMap;
import java.util.Map;
public class LSP1 {
//当子类实现父类的抽象方法时,方法的后置条件要比父类更严格
abstract class Parent{
public abstract Map fun();
}
class Sub extends Parent{
@Override
public HashMap fun(){
HashMap map=new HashMap();
map.put("b", "子类被执行");
return map;
}
}
public static void main(String[] args) {
LSP1 lsp1=new LSP1();
Parent a=lsp1.new Sub();
System.out.println( a.fun());
}
}
里氏替换原则的主要作用如下。
1.里氏替换原则是实现开闭原则的重要方式之一。
2.它克服了继承中重写父类造成的可复用性变差的缺点。
3.它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
里面包含了三层含义:
核心思想:要面向接口编程,不要面向实现编程。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下5点,就能在项目中满足这个规则。
1.每个类尽量提供接口或抽象类,或者两者都具备。
2.变量的声明类型尽量是接口或者是抽象类。
3.任何类都不应该从具体类派生。
4.尽量不要覆写基类的方法
5.使用继承时结合里氏替换原则。
要为各个类建立他们需要的专用接口,而不要试图去建立一个庞大的接口供所有依赖它的类去调用。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下5个优点。
1.将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
2.接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
3.如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
4.使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
5.能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
1.6.1 迪米特发展的定义
它要求一个对象应该对其他对象有最少的了解。
通俗的说,一个类应该对自己需要耦合或调用的类知道的最少,被耦合或调用的类的内部是如何复杂都与我无关,我就知道你提供的public方法就好。
迪米特法则还有一个定义是:
只与你的直接朋友交谈,不跟“陌生人”说话(Talkonlytoyourimmediatefriendsandnottostrangers)。
其含义是:
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
什么叫做直接的朋友呢?每个对象都必然会和其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系有很多比如组合、聚合、依赖等等。包括以下几类:
1.当前对象本身(this)
2.当前对象的方法参数(以参数形式传入到当前对象方法中的对象)
3.当前对象的成员对象
4.如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
5.当前对象所创建的对象
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
从迪米特法则的定义和特点可知,它强调以下两点:
1.从依赖者的角度来说,只依赖应该依赖的对象。
2.从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下6点。
1.在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
2.在类的结构设计上,尽量降低类成员的访问权限。
3.在类的设计上,优先考虑将一个类设置成不变类。
4.在对其他类的引用上,将引用其他对象的次数降到最低。
5.不暴露类的属性成员,而应该提供相应的访问器(set和get方法)。
6.谨慎使用序列化(Serializable)功能。
举个栗子:明星平时档期都很满,例如拍电影、演出、粉丝见面会等等,那么他们的这些日程是怎么来的呢?一般都是由经纪人负责处理。这里的经纪人是明星的朋友,而见面会上的粉丝和拍电影或举办演出的媒体公司是陌生人,所以适合使用迪米特法则.
//场景类
public class Client {
public static void main(String[]args){
Agent agent=new Agent();
agent.setStar(new Star("迪丽热巴"));
agent.setFans(new Fans("小九"));
agent.setCompany(new Company("荔枝电视台"));
agent.meeting();
agent.business();
}
}
//经纪人class Agent{
private Star star;
private Fans fans;
private Company company;
public void setStar(Star star){
this.star=star;
}
public void setFans(Fans fans){
this.fans=fans;
}
public void setCompany(Company company){
this.company=company;
}
public void meeting(){
System.out.println(this.fans.getName()+"与明星"+this.star.getName()+"见面了。");
}
public void business(){
System.out.println(this.company.getName()+"与明星"+this.star.getName()+"商谈合作。");
}
}
//明星
class Star{
private String name;
public Star(String name){
this.name=name;
}
public String getName(){
returnname;
}
}
//粉丝
class Fans{
private String name;
public Fans(String name){
this.name=name;
}public String getName(){
returnname;
}
}
//公司
class Company{
private String name;
public Company(String name){
this.name=name;
}public String getName(){
return name;
}
}