设计模式从入门到精通
一. 课程导学及UML急速入门
1.1 本章导航
- 本章将通过以下六个方面来进行讲解UML:定义、特点、分类、类图、时序图、记忆技巧
- URL定义:
- 统一建模语言(英语:Unified Modeling Language,缩写UML)
- 非专利的第三代建模和规约语言
- URL的特点:
- UML是一种开放的方法
- 用于说明、可视化、构建和编写一个正在开发的面向对象的、软件密集系统的制品的开放方法。
- UML展现了一系列最佳工程实践,这些最佳实践在对大规模,复杂系统进行建模方面,特别是在软件架构层次已经被验证有效。
- UML2.2 分类:((UML2.2 中一共定义了14中图形,分类如下):
- 结构式图形:强调的是系统式的建模:
- 静态图(类图,对象图,包图)
- 实现图(组件图、部署图)
- 剖面图
- 复合结构图
- 行为式图形:强调系统建模中触发的事件:
- 活动图
- 状态图
- 用例图
- 交互式图形:属于行为式图形子集合,强调系统模型中资料流程:
- 通信图
- 交互概述图(UML2.0)
- 时序图(UML2.0)
- 时间图(UML2.0)
- 结构式图形:强调的是系统式的建模:
- UML类图:
- Class Diagram:用于表示类、接口、实例等之间相互的静态关系
- 虽然名字叫类图,但类图中并不只有类
- 记忆技巧:
- UML箭头方向:从子类指向父类
- 定义子类时需要通过extends关键字指定父类
- 子类一定是知道父类定义,但父类并不知道子类的定义
- 只有知道对方信息时才能指向对方
- 所以箭头方向是从子类指向父类
- 记忆技巧—实现-继承 | 虚线 - 实现
- 空心三角箭头:继承或实现
- UML箭头方向:从子类指向父类
二. 软件设计七大原则
2.1 本章导航
- 本章会讲解开闭、依赖倒置、单一职责、接口隔离原则还有迪米特法则
- 设计原则不是强行遵守,而是根据实际业务场景进行取舍,达到一个最合适的度。
2.2 开闭原则
- 开闭原则理论:
- 开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。(就像弹性打卡,每天工作八小时,这个每天工作八小时是不可变的,但是早来早走,晚来晚来晚走,对扩展开放。)
- 用抽象构建框架,用实现扩展细节(用固定的抽象来定义功能,再用实现类来实现具体的逻辑。)
- 优点:提高软件系统的可复用性及可维护性
开闭原则就是面向抽象编程,因为抽象是稳定的。
- 开闭原则coding:我们要实现一个功能,获取一门课程的id、名称、和价格,于是我们定义的代码如下
- 创建一个接口:
public interface ICourse{ Integer getId(); String getName(); Double getPrice(); }
- 创建一个实现类:
public class JavaCourse implements ICourse{ private Integer Id; private String name; private Double price; public JavaCourse(Integer id, String name, Double price){ this.Id = id; this.name = name; this.price = price; } public Integer getId(){ return this.Id; } public String getName(){ return this.name; } public Double getPrice(){ return this.price; } }
- 在后续的迭代中,我们需要对某一门指定的课程进行打折,打8折出售。于是我们应该如何改进我们的代码呢?将获取价格接口改为打折接口? 不符合开闭原则,满足了这个功能但又不满足其他功能。 所以解决办法是:我们打折的是某Java课程,那么我们可以在Java课程的类(JavaCourse)下继承,实现一个Java打折课程类,这样既保证了接口的稳定性,又实现了功能,且不耦合,这就是**对扩展开放,对修改关闭的开闭原则!**创建实现类如下:
public class JavaDiscountCourse extends JavaCourse{ public JavaDiscountCourse(Integer id, String name, Double price){ super(id,name,price); } public Double getOriginPrice(){ return super.getPrice(); } @Override public Double getPrice(){ return super.getPrice()*0.8; } }
- 测试类
public class Test{ public static void main(String[] args){ ICourse iCourse = new JavaDiscountCourse(11,"语文课",300d); JavaDiscountCourse javaCourse = (JavaDiscountCourse)iCourse; System.out.println("课程ID"+javaCourse.getId() + "课程名称:"+ javaCourse.getName()+"课程原价:"+javaCourse.getPrice()); } }
- 层级结构如图所示:
- 创建一个接口:
2.3 依赖倒置原则
- 理论:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节;细节应该依赖抽象
- 针对接口编程,不要针对实现编程
- 使用依赖倒置原则,可以减少类间的耦合行、提高系统稳定性,提高代码可读行和可维护性,可降低修改程序所造成的风险。
- coding:(以抽象为基础搭建的架构要比以细节搭建的架构更加稳定,接口能够制定好规范和契约)
- v1版本:面向实现编程:需求如下: 用户Geely要学习Java和前端课程:
- 创建Geely类:
public class Geely{ public void studyJavaCourse(){ System.out.ptintln("Geely在学习Java课程"); } public void studyFECourse(){ System.out.println("Geely在学习前端课程"); } }
- 创建测试类:
public class Test{ Geely geely = new Geely(); geely.studyJavaCourse(); geely.studyFECourse(); }
- 此刻,Geely又想学习Python课程怎么办呢?那Geely类增加Python方法,如下:
public class Geely{ public void studyJavaCourse(){ System.out.ptintln("Geely在学习Java课程"); } public void studyFECourse(){ System.out.println("Geely在学习前端课程"); } public void studyPythonCourse(){ System.out.println("Geely在学习Python课程"); } }
我们发现,每当我们上级想需要一个功能的时候,是需要低层来实现的,它的稳定性很差。根据依赖导致原则,高层次的模块是不应该依赖低层的模块的,这不符合依赖倒置原则,如何解决呢?使用抽象来改进代码。
- 创建Geely类:
- v2版本(方法抽象),使用抽象来实现,达到依赖导致原则;
- 定义抽象接口:
public interface ICourse{ void studyCourse(); }
- 学习java课程实现类:
public class JavaCourse implements ICourse{ @Override public void studyCourse(){ System.out.println("Geely在学习Java课程"); } }
- 学习前端课程实现类:
public class FECourse implements Icourse{ @Override public void studyCourse(){ System.out.println("Geely在学习FE课程"); } }
- Geely类:
public class Geely{ public void studyCourse(Icourse icourse){ iCourse.studyCourse(); } }
- 测试类:
public class Test{ public static void main(String[] args){ Geely geely = new Geely(); geely.studyCourse(new JavaCourse()); geely.studyCourse(new FECourse()); } }
使用了抽象,我们可以根据需要创建各种不同的实现类,既达到了接口、保持了接口功能的稳定性,同时遵从了依赖倒置原则,上层应用不再依赖Geely这个类了,而是根据需要去实现对应的Course类;
- 定义抽象接口:
- v3版本:通过构造函数注入:
- 接口及接口实现类与v2版本一致。
- Geely类:
public class Geely{ private ICourse iCourse; public Geely(ICourse iCourse){ this.iCourse = iCourse; } public void studyCourse(ICourse iCourse){ iCourse.studyCourse(); } }
- 测试类:
public class Test{ public static void main(String[] args){ Geely geely = new Geely(new JavaCourse()); geely.studyImooCourse(); } }
使用构造函数注入虽然避免了里面的方法需要单独引入接口,但是构造注入会导致只能使用一种课程,假如Geely要同时学习Java和前端课程,那就得需要创建两个类了,这显然不是我们想要的,使用set注入能够解决这一问题。
- v4版本,使用set注入:
- 接口及接口实现类与v2版本一致
- Geely:
public class Geely{ public void setCourse(ICourse iCourse){ this.iCourse = iCourse; } private ICourse iCourse; public void studyCourse(){ iCourse.studyCourse(); } }
- v1版本:面向实现编程:需求如下: 用户Geely要学习Java和前端课程:
在低层次模块我们可以进行扩展,在上层模块中就满足了依赖导致原则。
2.4 单一职责
- 理论:
- 定义:不要存在多于一个导致类变更的原因;
- 一个类/接口/方法只负责一项职责
- 优点:降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险。
- coding:
- 需求如下:实现一个类,鸵鸟用脚走,其他鸟儿用翅膀飞:
- 反例,直接实现类中加判断:
public class Bird{ public mainMoveMode(String birdName){ if("鸵鸟".equals(birdName)){ System.out.println(birdName+"用脚走"); }else{ System.out.println(birdName+"用翅膀飞"); } } }
例子中没有使用单一职责,如果出现了别的情况,比如某某鸟儿爬着走,某某鸟儿跳着走,是不是又要加很多if else 判断了?我们可以单一职责来解决这个问题,比如说创建一种飞着走的鸟儿类,创建一种用脚走的鸟儿类,这样对应的鸟儿可以使用对应的动作类,就可以完成功能,同时不会造成耦合度过高,同时满足了单一职责;
- 反例,直接实现类中加判断:
- 上面的需求举例到了实现类的单一职责,我们在接口的定义中也应该要满足单一原则。如下所示:
- 老接口如下:
public interface ICourse{ String getCourseName(); byte[] getCourseVideo(); void studyCourse(); void refundCourse(); }
这个接口存在的问题就是:上面是获取课程的两个方法,而下面的是学习课程和对课程退款的功能。一个接口中定义了不同职责的方法,会导致混乱。比如一个学习类,我只想实现getCourseName()方法和getCourseVideo()方法,但是继承了这个接口后必须全部实现,造成了职责不单一。
- 改造如下:
- 我们可以将获取课程名称和课程视频归类到一个接口,学习课程和对课程退款归类到一个接口中,当我们需要实现两边的接口的时候,可以在实现类中同时实现两个接口,也可以单独只实现我们需要的,这样灵活性更高。
- 类结构如下:
- 老接口如下:
- 需求如下:实现一个类,鸵鸟用脚走,其他鸟儿用翅膀飞:
2.5 接口隔离原则
- 理论:
- 定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
- 一个类对一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
- 注意适度原则,一定要适度;
- 使用接口隔离原则,符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。
我们在设计类的过程中,应该也要考虑以后可能会发生变化的地方,做一些预判。
- coding:
- 反例:
- 定义接口:
- dog 实现类:
- bird实现类:
我们发现了一个问题,狗实现类动物接口,但是它不能实现fly方法,因为狗不会飞。同时鸟儿实现了swim方法,但是鸟儿不会游泳。这说明了没有实现接口隔离原则,导致了有一些方法对于实现类来说不适用。
- 定义接口:
- 通过细化接口,将eat、fly、swim分为三个接口,实现接口隔离:
- 从图中可以看出,我们实现了三个Action,分别定义了eat()、swim()、fly() 方法,当我们需要对dog进行实现的时候,只需要继承IEatAnimalAction接口和ISwimAnimalAction 接口即可。
- 反例:
细化接口要适度,避免类膨胀;
2.6 迪米特法则讲解
-
理论:
- 定义:一个对象应该对其他对象保持最少的了解。又叫最少知道原则
- 尽量降低类之间的耦合
- 强调只和朋友交流,不和陌生人说话;
- 朋友:出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类;
-
coding:
- 未遵守迪米特法则之前:
- Boss类:
public class Boss{ public void commandCheckNumber(TeamLeader temLeader){ List<Course> courseList= new ArrayList<Course>(); for(int i = 0; i<20 ; i++){ courseList.add(new Course()); } teamLeader.checkNumberOfCourses(courseList); } }
- TeamLeader类
public class TeamLeader{ public void checkNumberOfCourses(List<Course> courseList){ System.out.println("在线课程的数量是CSDN暗余:"+courseList.size()); } }
- Boss类:
- 遵循迪米特法则之后:
- Boss 类:
public class Boss{ public void commandCheckNumber(TeamLeader teamLeader){ teamLeader.checkNumberOfCourse(); } }
- TeamLeader类:
public class TeamLeader{ public void checkNumberOfCourses(){ List<Course> courseList= new ArrayList<Course>(); for(int i = 0; i< 20; i++){ courseList.add(new Course()); } System.out.println("在线课程的数量是:"+courseList.size()); } }
- Boss 类:
- 未遵守迪米特法则之前:
Boss想从TeamLeader那里知道现有课程的总数。它们之间的调用关系应该为Boss—>TeamLeader—>Course。Boss与Course并无直接联系,所以在Boss类的方法中不应该出现Course类。这就是迪米特法则;
2.7 里氏替换原则
- 理论:
- 派生类(子类)可以在程式中代替基类(超类)对象。
- 作用:
- 里氏替换原则是实现开闭原则的重要方式之一。
- 它克服了继承中重写父类造成的可复用性变差的缺点。
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
- 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性、降低需求变更时引入的风险。
- 里氏替换原则的实现方法:
- 通俗讲就是: 子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
- 根据对上述语句的理解,对里氏替换原则的定义可以总结如下:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松。
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的方法更严格或相等。
2.8 合成复用原则
- 定义:
- 合成复用原则,要求在软件复用时,要先尽量使用组合或者聚合等关联关系实现,其次才考虑使用继承。也就是在一个新对象里通过关联的方式使用已有对象的一些方法和功能。
- 重要性:
- 通常类的复用可分为继承和合成,继承复用虽然简单,但是存在很大的缺点:
- 耦合度高,父类代码的修改会影响到子类,不利于代码的维护。
- 破坏了类的封装性,因为继承会将父类的实现细节暴露给紫烈,所以又叫做“白箱”复用。
- 限制了复用的灵活性,从父类继承来的实现是静态的,在运行期是无法改变的。
- 合成复用是将已有的对象作为新对象的成员变量来实现,新对象调用已有对象的功能,达到复用:
- 不会破坏封装性,因为新对象只能调用已有对象暴露出来的方法,所以又叫做“黑箱”复用。
- 耦合度低,已有对象的变化对新对象的影响较小,可以在新对象中根据需要调用已有对象的一些操作。
- 复用的灵活性高,可以在代码的运行中,动态选择相同类型的其他具体类。
- 通常类的复用可分为继承和合成,继承复用虽然简单,但是存在很大的缺点:
三. 简单工厂
3.1 简单工厂讲解
-
定义与类型:
- 定义:由工厂对象决定创建出哪一种产品类的实例
- 类型:创建型,但不属于GOF23种设计模式。
-
简单工厂-适用场景:
- 工厂类负责创建的对象比较少
- 客户端(应用层)只知道传入工厂类的参数,对于如何创建对象(逻辑)不关心。
-
简单工厂优点:
- 只需要传入一个正确的参数,就可以获取你所需要的对象而无需知道其创建细节。
-
简单工厂缺点:
- 工厂类的职责相对过重,增加新的产品,需要修改工厂类的判断逻辑,违背开闭原则。
3.2 简单工厂Conding
-
使用简单工厂前的准备工作:
- 创建抽象类Video:
public abstract class Video{ public abstract void product(); }
- 创建实现类java Video
public class JavaVideo extends Video{ @Override public void produce(){ System.out.println("录制Java课程视频"); } }
- 创建实现类python Video
public class PythonVideo extends Video{ @Override public void produce(){ System.out.println("录制Python课程视频"); } }
- 创建测试类:
public class Test{ public static void main(String[] args){ Video video = new PythonVideo(); video.produce(); } }
从测试类中,我们可以发现,每当我们需要PythonVideo类或者JavaVideo类时,会手动New创建一个,这会导致我们将创建逻辑显式的展示在我们的场景类中。我们可以将创建过程移植到简单工厂中,改进看下面。
- 创建抽象类Video:
-
使用简单工厂进行改造:
- VideoFactory类:
public class VideoFactory{ public Video getVideo(String type){ if("java".equalsIgnoreCase(type)){ return new JavaVideo(); }else if("python".equalsIgnoreCase(type)){ return new PythonVideo(); } return null; } }
- 测试类:
public class Test{ public static void main(String[] args){ VideoFactory videoFactory = new VideoFactory(); Video video = videoFacotry.getVideo("java"); if(video == null){ return; } video.produce(); } }
- 依赖关系如下:
通过简单工厂类,我们将创建类的具体实现细节隐藏掉了,通过一个工厂类就可以创建多种不同类型的类。但是存在一个问题,那就是创建类的时候不够灵活,每当需要一个全新的类的时候,就会要去修改工厂类中的创建类方法,所以它违背了开闭原则。
- VideoFactory类:
-
对工厂类进行升级改造(通过工厂+ 反射的方式来增强扩展性):
- 工厂类:
public class VideoFactory{ public Video getVideo(Class c){ Video video = null; try{ video = (Video)Class.forName(c.getName()).newInstance(); }catch(InstantiationException e){ e.printStackTrace(); }catch(IllegalAccessException e){ e.printStackTrace(); }catch(ClassNotFoundException e){ e.printStackTrace(); } return video; } }
- 测试类:
public class Test{ public static void main(String[] args){ VideoFactory videoFactory = new VideoFactory(); Video video = videoFacotry.getVideo(JavaVideo.class); if(video == null){ return; } video.produce(); } }
通过反射的方式增强了扩展性,即使增加了新的类型,但是只要它属于Video接口下的对象,那么就能够创建成功,且不需要对既有代码进行修改。
- 工厂类:
-
简单工厂源码分析:
- 时间日期类就使用到了简单工厂方式:
- 位置如下:
- 根据不同的语言和类型返回对应的实现类:
- 类继承关系如下:
- 位置如下:
- 时间日期类就使用到了简单工厂方式:
四. 工厂方法模式
4.1 工厂方法讲解
- 工厂方法- 定义与类型:
- 定义:定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行
- 类型:创建型
- 工厂方法- 适用场景:
- 创建对象需要大量重复代码
- 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
- 一个类通过其子类来指定创建哪个对象。
- 工厂方法优点:
- 用户只需要关心所需产品对应的工厂,无须关心创建细节
- 加入新产品符合开闭原则,提高可扩展性。
- 工厂方法缺点:
- 类的个数容易过多,增加复杂度
- 增加了系统的抽象性和理解难度。
4.2 抽象方法conding
- 新建抽象工厂类:
public abstract class VideoFactory{ public abstract Video getVideo(); }
在抽象工厂类中,我们使用的是abstract抽象类,而不是接口,是因为我们在这个类中可能会存在一些已知的方法,而接口类只能定义接口不能实现具体的逻辑,所以此处使用的是抽象类。
- Video类
public abstract class Video { public abstract void produce(); }
- JavaVideoFactory类
public class JavaVideoFactory extends VideoFactory{ @Override public Video getVideo(){ return new JavaVideo(); } }
- PythonVideoFactory类
public class PythonVideoFactory extends VideoFactory{ @Override public Video getVideo(){ return new PythonVideo(); } }
- 测试类:
public class Test{ public static void main(String[] args){ VideoFactory videoFactory = new PythonVideoFactory(); Video video = videoFactory.getVideo(); video.produce(); } }
- 类结构关系如下:
FEVideo 类与PythonVideo 类似,创建的一个新的类。其中都继承于Video抽象类,而工厂构造类都继承于VideoFactory,在Test类中需要创建具体的类的时候,只需要构造一个工厂类指向对应的子类即可。
工厂方法就是工厂类的一个方法,它是一个抽象方法,具体实现由其子类实现;
4.3 工厂方法源码解析
- Iterator中有一个抽象方法:
- ArrayList 继承了抽象类Iterator,然后并实现了抽象方法:
里面的Itr依赖于Iterator 接口类
- ArrayList继承关系图:
五. 抽象工厂模式
5.1 抽象工厂讲解
-
抽象工厂-定义与类型
- 定义: 抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口
- 无须指定它们具体的类
- 类型:创建型
-
抽象工厂-适用场景:
- 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
- 强调一系列相关的产品对象(属于同一产品族)一起适用创建对象需要大量重复的代码。
- 提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
-
抽象工厂- 优点
- 具体的产品在应用层代码隔离,无须关心创建细节
- 将一系列的产品族统一到一起创建
-
抽象工厂- 缺点
- 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口。
- 增加了系统的抽象性和理解难度。
-
抽象工厂 - 产品等级结构与产品族
5.2 Coding 抽象工厂
- 在没有使用设计模式来创建工厂类的时候,会存在一个问题,就是我们要创建一个类的对象,就要为他实现一个抽象工厂,那么会引起类爆炸。
- 如图所示:
为每个类创建一个工厂类,不是一个很好的设计。
- 如图所示:
- 使用抽象工厂创建对象:
- 需求:首先,一个课程拥有视频和笔记,同时视频具有扩展性,可能为java课程,也可能为python课程;
- 分析:综合来看,java 、python课程都属于课程, java视频、python视频都属于视频,java笔记、python笔记都属于笔记;
- 结构:
- 产品抽象工厂接口: 一个产品会有视频和笔记;
- 对应视频的工厂类: 比如java课程工厂类,会创建包含视频和笔记的对象;
- 视频接口:下面有java视频和python视频等
- 笔记接口:下面有java笔记和python笔记等
- 图示:
- 横向图示:
- Coding:
- 创建抽象工厂接口:
public interface CourseFactory{ Video getVideo(); Article getArticle(); }
- 创建抽象类Article:
public abstract class Article{ public abstract void produce(); }
- 创建抽象类Video:
public abstract class Video{ public abstract void produce(); }
- 创建Java 抽象工厂类:
public class JavaCourseFactory implements CourseFactory{ @Override public Video getVideo(){ return new JavaArticle(); } @Override public Article getArticle(){ return new JavaArticle(); } }
- 创建JavaArticle:
public class JavaArticle extends Article{ @Override public void produce(){ System.out.println("编写Java课程笔记"); } }
- 编写JavaVideo:
public class JavaVideo extends Video{ @Override public void produce(){ System.out.println("录制Java课程视频"); } }
- 当需要Python抽象工厂类的时候,只需要继承抽象工厂接口、具体的对象继承我们的Video或者Article 即可,不再赘述。
- 创建抽象工厂接口:
在Java的jdbc中,就大量使用了抽象工厂。建立连接的步骤是一致的,但是被操作的对象却各不相同;比如mysql、sqlserver等各种数据库通过对抽象的各自实现,让我们无感知的能够连接各自的的服务器;
六. 建造者模式
6.1 建造者讲解
- 定义与类型
- 定义:
- 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示;
- 用户只需指定需要建造的类型就可以得到它们,建造过程及细节不需要知道;
- 类型:创建型
- 定义:
- 建造者-适用场景:
- 如果一个对象有非常复杂的内部结构(很多属性)
- 想把复杂对象的创建和使用分离;
- 建造者-优点:
- 封装性好,创建和使用分离
- 扩展性好,建造类之间独立,一定程度上解耦;
- 建造者-缺点:
- 产生多余的Builder对象
- 产品内部发生变化,建造者都要修改,成本较大
建造者模式和工厂模式相同的地方都是用于创建对象,但是不同的地方是工厂模式更加注重于创建产品,而建造者模式更注意创建的顺序;建造者可以创建复杂的产品,可以控制顺序,而工厂模式创建出来的每个对象都是一样的(它不注重顺序及组成);
6.2 建造者模式Coding
- 使用链式调用使用建造者模式,UML类图图示:
-
- 代码演示:
- 创建Course类:
public class Course{ private String courseName; private String coursePPT; private String courseVideo; private String courseArticle; private String courseQA; public Course(CourseBuilder courseBuilder){ this.courseName = courseBuilder.courseName; this.coursePPT = courseBuilder.coursePPT; this.courseVideo = courseBuilder.courseVideo; this.courseArticle = courseBuilder.courseArticle; this.courseQA = courseBuilder.courseQA; } // 创建静态类,用于链式调用+建造者 public static class CourseBuilder{ private String courseName; private String coursePPT; private String courseVideo; private String courseArticle; private String courseQA; public CourseBuilder buildCourseName(String courseName){ this.courseName = courseName; return this; } public CourseBuilder buildCoursePPT(String coursePPT){ this.coursePPT = coursePPT; return this; } public CourseBuilder buildCourseVideo(String courseVideo){ this.courseName = courseVideo; return this; } public CourseBuilder buildCourseArticle(String courseArticle){ this.courseName = courseArticle; return this; } public CourseBuilder buildCoursQA(String courseQA){ this.courseName = courseQA; return this; } public Course build(){ return new Course(this); } } }
- 创建测试类验证功能:
public class Test{ public static void main(String[] args){ Course course = new Course.CourseBuilder().buildCourseName("java课程").buildCoursQA("问题和答案").build(); System.out.println(course); } }
- 创建Course类:
源码中StringBuilder/StringBuffer 使用到了建造者模式,它可以一直append 字符串;同时ImmutableSet中的add方法也是建造者模式,如: Set set = ImmutableSet.builder().add("a").add("b").build();
七. 单例模式
7.1 单例模式讲解
- 单例- 定义与类型
- 定义:保证一个类仅有一个实例,并提供一个全局访问点
- 类型:创建型
- 单例-适用场景:
- 想确保任何情况下都绝对只有一个实例
- 具体应用:
- 计数器,在单服务器情况下可以使用单例;
- 连接池
- 经常被使用且值不会发生变化的对象
- 单例的优点:
- 在内存里只有一个实例,减少了内存开销。
- 可以避免对资源的多重占用
- 设置全局访问点,严格控制访问
- 单例- 缺点:
- 没有接口,扩展困难
- 单例-重点:
- 私有构造器(将构造器私有化,避免从外部构造对象)
- 线程安全
- 延迟加载(使用它的时候再创建)
- 序列化和反序列化安全
- 防止反射攻击
- 实用技能:
- 反编译
- 内存原理
- 多线程Debug
- 单例-相关设计模式
- 单例模式和工厂模式
- 单例模式和享元模式
7.2 懒汉式
- 单线程模式下懒汉式(线程不安全):
public class LazySingleton{ private static LazySingleton lazySingleton = null; private LazySingleton(){ } public static LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
有线程安全问题,但是实现了延时加载的功能;
- 线程安全的懒汉式(但是性能不是很好)
public class LazySingleton{ private static LazySingleton lazySingleton = null; private LazySingleton(){ } public static synchronized LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
这种方式实际上就是加了一个synchronized关键字,使用同步的方式虽然避免了线程不安全,但是直接在静态方法上面加上synchronized的话,锁是锁住整个对象的。
7.3 双重检查懒汉式
public class LazySingleton{
private volatile static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
synchronized(LazySingleton.class){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
}
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
第一个判空出现在synchronized 的话,只会当实例为空的时候才有可能会阻塞,提高了性能;第二次判空是在加锁之后,这里面加锁比在方法上加锁性能更好。而LazySingleton 定义的时候使用了volatile关键字,是为了此实例不会出现重排序的现象;因为在计算机里面,指令个为了保证性能不一定会是依次执行,而new 关键字在底层可以理解为经历了三个步骤:1. 分配内存给这个对象,初始化对象,设置变量指向刚分配的内存对象,如果2步骤和3步骤发生了顺序调换,很可能在synchronnized外可能会获取到一个空引用;
7.4 静态内部类的单例模式
public class StaticInerClassSingleton{
private static class InnerClass{
private static StaticInnerClassSingLeton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}
7.5 饿汉式
public class HungrySingleton{
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
- 也可以使用静态代码块的方式:
public class HungrySingleton{
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
懒汉与饿汉的区别就在于是否是延迟加载;
7.6 序列化破坏单例模式
- 正常情况下在序列化中,我们写入的对象和反序列化的对象,它的指向地址不是一样的。如图所示:
- 解决办法:
- 我们在单例对象中定义一个readResolve方法,在反序列化的时候,会进行扫描,如果这个类有此方法,则会默认返回之前的单例对象,地址就不会发生变化了。
7.7 反射攻击的一些解决方案
-
问题:
-
解决方案:
7.8 Enum枚举单例 - 单例的最佳实践
- 枚举是实现单例模式的最佳实践,它是线程安全的,能够防止序列化破坏单例和反射攻击;
- 写法示例:
public enum EnumbInstance{ INSTANCE; private Object data; public Object getData(){ return data; } public void setData(Object data){ this.data = data; } public static EnumInstance getInstance(){ return INSTANCE; } }
7.9 容器单例模式-管理多单例
public class ContainerSingleton{
private ContainerSingleton(){}
private static Map<Stirng,Object> singletonMap = new HashMap<>();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containKey(key)){
singletonMap.put(key,instance);
}
}
}
publiuc static Object getInstance(String key){
return singletonMap.get(key);
}
}
如果存入相同key的value,HashMap会更新为最后一个相同key的value,而HashMap是线程不安全的,要注意此种情况;
7.10 基于ThrealLocal的“单例模式”
- 它相当于一个伪单例,通过以空间换时间的方案,对每个线程进行隔离;
- 代码如下:
public class ThreadLocalInstance{ private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal = new ThreadLocal<ThreadLocalInstance>(){ @Override protected ThreadLocalInstance initialValue(){ return new ThreadLocalInstance(); } }; private ThreadLocalInstance(){} public static ThreadLocalInstance getInstance(){ return threadLocalInstanceThreadLocal.get(); } }
八. 原型模式
8.1 原型模式讲解
- 定义与类型:
- 定义:指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,不需要知道任何创建的细节,不调用构造函数;
- 类型:创建型
- 适用场景:
- 类初始化消耗较多资源
- new 产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
- 构造函数比较复杂
- 循环体中生产大量对象时
- 原型模式优缺点:
- 优点:
- 原型模式性能比直接new一个对象性能高
- 简化创建过程
- 缺点:
- 必须配备克隆方法
- 对克隆复杂对象或对克隆出的对象进行复杂改造时,容易引入风险
- 深拷贝、浅拷贝要运用得当
- 优点:
- 扩展知识点:深克隆与浅克隆的区别
8.2 原型模式 conding
- 原型模式最直接的实现:
- 创建Mail对象
@Data @ToString public class Mail implements Cloneable{ private String name; private String emailAddress; private String content; public Mail(){} @Override protected Object clone() throws CloneNotSupportedException{ return super.clone(); } }
- 测试类:
public class Test{ public static void main(String[] args) throws CloneNotSupportedException{ Mail mail = new Mail(); mail.setContent("初始化模板"); for(int i = 0; i < 10; i++){ Mail mailTemp = (Mail)mail.clone(); mailTemp.setName("姓名"+i); mailTemp.setEmailAddress("姓名"+ i + "@imooc.com"); mailTemp.setContent("恭喜您,此次活动中奖了"); MailUtil.sendMail(mailTemp); } } }
当我们需要频繁的创建一个相同对象,且前期需要大量准备,则我们可以考虑使用原型模式;
- 创建Mail对象
- 使用抽象类来实现原型模式:
- 创建抽象类:
public abstract class A implements Cloneable{ @Override protected Object clone() throws CloneNotSupportedException{ return super.clone(); } }
- 创建B类,并直接创建测试方法测试克隆方法
public class B extends A{ public static void main(String[] args) throw CloneNotSupportedException{ B b = new B(); b.clone(); } }
当B使用克隆方法时,会使用抽象A类中的clone方法中去;打断点需要注意哦;
- 创建抽象类:
- 浅克隆存在的问题:
- 解决浅克隆的办法-深克隆:
如果一个单例对象继承了克隆方法,那么克隆方法存在破坏单例的风险;要么单例对象不实现克隆方法,要么在里面还是返回我们单例的实例;还有注意设计模式不是单打独斗,要灵活使用,可以多模式混用,具体与现实场景有关; 在ArrayList、HashMap中都有用到原型模式,可以在源码里面找到Clone 方法,他们都实现了Cloneable接口;感兴趣可以看看。
九. 外观模式
9.1 外观模式讲解
- 定义与类型:
- 定义:又叫门面模式,提供了一个统一的接口,用来访问子系统中的一群接口;外观模式定义了一个高层接口,让子系统更容易使用;
- 例子:比如在医院里,会有各种各样的病人,当病人需要看病的时候,它并不是直接去找指定的医生,而是经过了接待员(Facade),通过前台护士或者咨询台,会告诉不同的病人做不同的事情,而这个主入口,就相当于一个外观模式,或者叫做门面模式;
- 类型:结构型;
- 适用场景:
- 子系统越来越复杂,增加外观模式提供简单调用接口
- 构建多层系统结构,利用外观对象作为每层的入口,简化层间调用;
- 外观模式优缺点:
- 优点:
- 简化了调用过程,无须了解深入子系统,防止带来风险。
- 减少系统依赖,松散耦合。
- 更好的划分访问层次
- 符合迪米特法则,即最少知道原则;
- 缺点:
- 增加子系统,扩展子系统行为容易引入风险。
- 不符合开闭原则。
- 优点:
- 外观模式与其他相关的设计模式:
- 外观模式与和中介者模式: 中介者模式关注内部之间的交互,而外观模式是关注外部之间的交互;
- 外观模式和单例模式:外观模式的对象可以做成单例来使用;
- 外观模式可以通过抽象工厂获取子系统实例;
外观模式是迪米特法则的充分提现,即最少知道原则,降低了系统之间的耦合度,提升内聚;
9.2 coding
- 子系统角色中的类:
public class ModuleA {
//示意方法
public void testA(){
System.out.println("调用ModuleA中的testA方法");
}
}
public class ModuleB {
//示意方法
public void testB(){
System.out.println("调用ModuleB中的testB方法");
}
}
public class ModuleC {
//示意方法
public void testC(){
System.out.println("调用ModuleC中的testC方法");
}
}
- 门面角色类:
public class Facade {
//示意方法,满足客户端需要的功能
public void test(){
ModuleA a = new ModuleA();
a.testA();
ModuleB b = new ModuleB();
b.testB();
ModuleC c = new ModuleC();
c.testC();
}
}
- 客户端角色类
public class Client {
public static void main(String[] args) {
Facade facade = new Facade();
facade.test();
}
}
使用门面模式还有一个好处,就是能够选择性的暴露方法。 一个模块中定义的方法可以分为两部分,一部分是给子系统外部使用的,一部分是子系统内部模块之间相互调用时使用的。有了Facade类,那么用于子系统内部模块之间相互调用的方法就不用暴露给子系统外部了。
十. 装饰者模式
10.1 装饰者模式讲解
- 定义与类型:
- 定义:
- 在不改变原有度意向的基础之上,将功能附加到对象上;
- 提供了比继承更有弹性的替代方案(扩展原有对象功能)
- 类型:结构型
- 定义:
- 装饰者-适用场景:
- 扩展一个类的功能或给一个类添加附加职责
- 动态的给一个对象添加功能,这些功能可以再动态的撤销;
- 装饰者-优点:
- 继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能;
- 通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果;
- 符合开闭原则
- 装饰者-缺点:
- 会出现更多的代码,更多的类,增加程序复杂性
- 动态装饰时,多层装饰时会更复杂
- 装饰者-相关设计模式
- 装饰者模式和代理模式
- 装饰者模式和适配器模式
10.2 conding
- 有这样一个需求,我们经营了一个煎饼摊,顾客会来购买煎饼、也可能会购买加了鸡蛋的煎饼,也可能会购买加了鸡蛋、火腿肠的煎饼,用代码实现如下:
- v1 版本:
- 创建煎饼类:
public class Battercake{ protected String getDesc(){ return "煎饼"; } protected int cost(){ return 8; } }
被protected 修饰的属性,只允许在子类中重写;
- 创建加了鸡蛋的煎饼类:
public class BattercakeWithEgg extends Battercake{ @Override public String getDesc(){ return super.getDesc()+" 加一个鸡蛋"; } @Override public int cost(){ return super.cost()+1; } }
- 创建加了鸡蛋和香肠的煎饼类:
public class BattercakeWithEggSausage extends BattercakeWithEgg{ @Override public String getDesc(){ return super.getDesc() +" 加一根香肠"; } @Override public int cost(){ return super.cost(); } }
因为继承了加了鸡蛋的煎饼类,所以它会**"先加鸡蛋再加香肠"**;
- 测试类:
public class Test{ public static void main(String[] args){ Battercake battercake = new Battercake(); System.out.println(battercake.getDesc() + " 销售价格:"+ battercake.getCost()); Battercake battercake2 = new BattercakeWithEgg(); System.out.println(battercake2.getDesc() + " 销售价格:"+ battercake2.getCost()); Battercake battercake3 = new BattercakeWithEggSausage(); System.out.println(battercake3.getDesc() + " 销售价格:"+ battercake3.getCost()); } }
我们发现,使用类继承能够实现我们需要的业务需求;但是根据实际来看,顾客往往可能还需要三个鸡蛋的煎饼、五个火腿肠的煎饼、各种的随意组合,如果我们单纯使用类继承,是无法办到的,它既导致了类过于膨胀,且不具备扩展性;接下来我们看看装饰者模式是否能够解决这个问题!
- 创建煎饼类:
- v2版本:使用装饰者模式
- 创建煎饼抽象类,里面创建两个抽象方法:
public abstract class Abattercake{ protected abstract String getDesc(); protected abstract int cost(); }
- 创建煎饼的实现类:
public class Battercake extends Abattercake{ @Override protected String getDesc(){ return "煎饼"; } @Override protected int cost(){ return 8; } }
- 创建抽象的装饰者类,继承煎饼抽象类,让它与煎饼类建立联系;
public class AbstractDecorator extends Abatterrcake{ private Abattercake aBattercake; public AbstractDecorator(Abattercake aBattercake){ this.aBattercake = aBattercake; } @Override protected String getDesc(){ return this.aBattrercake.getDesc(); } @Override protected int cost(){ return this.aBattercake.cost(); } }
- 创建半装饰类 香肠类:
public class SausageDecorator extends AbstractDecorator{ public SausageDecorator(Abattercake aBattercake){ super(aBattercake); } @Override protected String getDesc(){ return super.getDesc()+" 加一个香肠"; } @Override protected int cost(){ return super.cost()+2; } }
- 创建半装饰类 加鸡蛋类:
public class EggDecorator extends AbstractDecorator{ public EggDecorator(ABattercake aBattercake){ super(aBattercake); } @Override protected String getDesc(){ return super.getDesc()+" 加一个鸡蛋"; } @Override protected int cost(){ return super.cost() +1; } }
- UML类图结构如下:
- 创建测试类:
public class Test{ public static void main(String[] args){ Abattercake aBattercake; // 要一个煎饼 aBattercake = new Battercake(); // 加一个鸡蛋 aBattercake = new EggDecorator(aBattercake); // 再加一个鸡蛋 aBattercake = new EggDecorator(aBattercake); // 再加一个火腿肠 aBattercake = new SausageDecorator(aBattercake); System.out.println(aBattercake.getDesc() +" 销售价格:"+aBattercake.cost()); } }
- 测试类打印结果如下:
- 创建煎饼抽象类,里面创建两个抽象方法:
我们使用了装饰者抽象类、同时利用继承关系,将对象不断的传入,然后进行累加,能够动态的控制煎饼、火腿肠和煎饼,既实现了需求,又满足了未来扩展需要,同时使代码更加简洁高效;在一些我们常用的工具中,InputStream拥有很多的子类,它也实现了装饰者模式;他们通过实现要被装饰的类,来扩展一些类的功能,同时不会对原有类造成侵入;
十一. 适配器模式
11.1 适配器模式讲解
- 适配器-定义与类型
- 定义: 将一个类的接口转换成客户期望的另一个接口
- 使原本接口不兼容的类可以一起工作
- 类型:结构型
- 适配器-适用场景:
- 已经存在的类,它的方法和需求不匹配时(方法结果相同或相似)
- 不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品、不同厂家造成功能类似而接口不同情况下的解决方案;
- 适配器-优点:
- 能提高类的透明性和复用,现有的类复用但不需要改变
- 目标类和适配器类解耦,提高程序扩展性
- 符合开闭原则
- 适配器-缺点:
- 适配器编写过程需要全面考虑,可能会增加系统的复杂性;
- 增加系统代码可读的难度;
- 适配器-扩展
- 对象适配器(使用委托机制)
- 类适配器(使用类继承来实现)
- 适配器-相关设计模式
- 适配器模式和外观模式(都是对现有的类进行封装,不同点在于适配器模式复用了原有的接口,而外观模式则定义了新的接口,适配器使两个接口协同工作,而外观模式则是提供了一个更加方便的访问入口,外观模式针对的粒度更大)
11.2 coding
- 类适配器模式:
-
创建被适配者Adaptee:
public class Adaptee{ public void adapteeRequest(){ System.out.println("被适配者的方法"); } }
-
定义请求接口Target:
public interface Target{ void request(); }
-
定义实现类,实现Target:
public class ConcreteTarget implements Target{ @Override public void request(){ System.out.println("concreateTarget目标方法"); } }
-
定义适配器类,通过继承 Target 接口,从而能够同步request方法,同时继承Adaptee,获取到它内部的adapteeRequest()方法,这样就能够在request内部去调用adapteeRequest方法,达到适配的效果:
public class Adapter extends Adaptee implements Target{ @Override public void request(){ super.adapteeRequest(); } }
-
类结构如图所示:
-
创建测试类,测试功能:
public class Test{ public static void main(String[] args){ Target target = new ConcreteTarget(); target.request(); Target adapterTarget = new Adapter(); adapterTarget.request(); } }
通过实现同一个接口,可以达到调用同一个方法的目的,而在内部继承被适配的类,可以将被适配的类的方法在适配器类中调用,达到适配的效果;
-
- 对象适配器模式:
- 定义Target:
public class ConcreteTarget implements Target{ @Override public void request(){ System.out.println("concreateTarget目标方法"); } }
- 创建被适配者Adaptee:
public class Adaptee{ public void adapteeRequest(){ System.out.println("被适配者的方法"); } }
- 创建实现类ConcreteTarget
public class ConcreteTarget implements Target{ @Override public void request(){ System.out.println("concreateTarget目标方法"); } }
以上三个跟类适配器中一样,没有发生变化,发生变化的是适配器本身;
- 通过继承Target接口获取通用方法,通过注入Adapter,直接调用Adapter对象的方法,达到对象适配器的效果:
public class Adapter implements Target{ private Adaptee adaptee = new Adaptee(); @Override public void request(){ adaptee.adapteeRequest(); } }
- 测试类:
public class Test{ public static void main(String[] args){ Target target = new ConcreteTarget(); target.request(); Target adapterTarget = new Adapter(); adapterTarget.request(); } }
- UML 类图结构如下:
- 定义Target:
- 场景示例:有这样一个场景,我们需要将一个220伏的交流电转为5伏的交流电,通过适配器后它能够输出为5伏,达到与其他电一致;
- 定义交流电220伏:
public class AC220{ public int outputAC220V(){ int output = 220; System.out.println("输出交流电"+output+"伏"); return output; } }
- 定义一个5伏的接口:
public interface DC5{ int outputDC5V(); }
- 定义一个适配器,将220伏经过这个适配器就会变成5伏:
public class PowerAdapter implements DC5{ private AC220 ac220 = new AC220(); @Override public int outputDC5V(){ int adapterInput = ac220.outputAC220V(); // 变压器 int adapterOutput = adapterInput/44; System.out.println("使用PowerAdapter输入AC:"+ adapterInput +"V"); return adapterOutput; } }
- 测试类如下:
- 定义交流电220伏:
11.3 有哪些源码应用到了适配器
- XmlAdapter:处理xml的序列化和反序列化;我们可以继承它去实现我们自己的序列化方法;
- jpa 中的JapVendorAdapter;
- Spring MVC 中的HandlerAdapter;
十二. 享元模式
12.1 享元模式讲解
-
定义与类型:
- 定义:提供了减少对象数量从而改善应用所需的对象结构的方式;
- 运用共享技术有效地支持大量细粒度的对象
- 类型:结构型;
一句话概括:减少对象创建,提高性能;
-
适用场景:
- 常常应用于系统底层的开发,以便解决系统的性能问题;
- 系统有大量相似对象,需要缓冲池的场景;
-
享元-优点:
- 减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率;
- 减少内存之外的其他资源占用;
-
享元-缺点:
- 关注内/外部状态,关注线程安全问题;
- 使系统、程序的逻辑复杂化
-
享元-扩展:
- 内部状态:在享元的内部不会随着环境的改变而改变的共享部分;可以理解为属性。
- 外部状态:随着环境的改变而改变的就是外部状态,这部分状态是不可以共享的状态;
-
享元-相关设计模式:
- 享元模式和代理模式:如果生产一个代理类需要花费很多时间,那么可以使用享元模式提高速度;
- 享元模式和单例模式:容器单例;
享元模式就是一个代码复用的思想,线程池就是其思想的一个实现之一;
12.2 conding
- 每个部门经理不一样,他们都需要做报告给领导Report, 但是部门经理的总数是固定的,我们怎么使用享元模式去达到部门经理的复用呢?
- 创建员工:
public interface Employee{ void report(); }
- 创建部门经理类:
public class Manager implements Employee{ @Override public void report(){ System.out.println(reportContent); } private String department; private String reportContent; public void setRepoirtContent(String reportContent){ this.reprotContent = reportContent; } public Manager(String department){ this.department = department; } }
- 结合了享元模式的部门经理工厂类:
public class EmployeeFactory{ private static final Map<String,Employee> EMPLOYEE_MAP = new HashMap<>(); public static Employee getManager(String department){ Manager manager = (Manager) EMPLOYEE_MAP.get(department); if(manager == null){ manager = new Manager(department); manager.setReportContent(department+"部门汇报:内容是..."); EMPLOYEE_MAP.put(department,manager); } return manager; } }
- 测试类:
public class Test{ private static final String departments[] = {"RD","QA","PM","ET"}; public static void main(String[] args){ for(int i = 0 ; i < 10 ; i++){ String department = departments[(int)Math.random()]; Manager manager = (Manager) EmployeeFactory.getManager(department); manager.report(); } } }
使用享元模式的思想,类只会被创建一次;
- 创建员工:
12.3 享元模式的应用
- Integer中的享元:
- 图示:
- 在Integer 的源码中,先进行判断,如果是在127范围以内的,就会使用已经创建好的缓存内的Integer 对象;
这里也是一个面试题,当两个Integer 大于127的时候,他们则不会相等;而在范围内时则是一个对象;
- 判断两个Integer是否相等:
第一个因为在127以内,则会复用Integer对象,则他们的地址值是一样的;而在第二个中,它的值大于127,则两个对象的地址值是不一样的,因为都是新生成的; 范围为: -128
127之间,Long类型的缓存也是-128127
- 图示:
十三. 组合模式
13.1 定义与类型
-
概述:
- 定义:将对象组合成树形结构以表示“部分-整体”的层次结构
- 组合模式使客户端对单个对象和组合对象保持一致的方式处理
- 类型:结构型
-
图示:
-
适用场景:
- 希望客户端可以忽略组合对象与单个对象的差异时;
- 处理一个树形结构时;
-
组合-优点:
- 清除地定义分层次的复杂对象,表示对象的全部或部分层次;
- 让客户端忽略了层次的差异,方便对整个层次结构进行控制
- 简化客户端代码
- 符合开闭原则
-
组合-缺点:
- 限制类型时会较为复杂
- 使设计变得更加抽象
13.2 Conding
- 业务场景:就拿一个课程网站来说,它会有很多课程,也有课程目录。课程有名称,有价格。比如Java课程,它属于Java目录下,Python目录下有很多Python课程;**如果我们使课程目录和课程继承同一个抽象类,那么就可以把这两个视为同一个对象来操作。具体操作有一些差别;
- 创建目录组件 CatelogComponent
public abstract class CatalogComponent{ public void add(CatalogComponent catalogComponent){ throw new UnsupportedOperationException("不支持添加操作!"); } public void remove(CatalogComponent catalogComponent){ throw new UnsupportedOperationException("不支持删除操作!"); } public String getName(CatalogComponent catalogComponent){ throw new UnsupportedOperationException("不支持获取名称操作!"); } public double getPrice(CatalogComponent catalogComponent){ throw new UnsupportedOperationException("不支持获取价格操作!"); } public void print(){ throw new UnsupportedOperationException("不支持打印操作!") } }
- 创建课程类 Course:
public class Course extends CatalogComponent{ private String name; private double price; public Course(String name, double price){ this.name = name; this.price = price; } @Override public String getName(CatalogComponent catalogComponent){ return this.name; } @Override public String getPrice(CatalogComponent catalogComponent){ return this.price; } @Override public void print(){ System.out.println("Course Name:"+name+" Price:"+price); } }
- 课程目录类: CourseCatalog
public class CourseCatalog extends CatalogComponent{ private List<CatalogComponent> items = new ArrayList<CatalogComponent>(); private String name; public CourseCatalog(String name){ this.name = name; } @Override public void add(CatalogComponent catalogComponent){ items.add(catalogComponent); } @Override public void remove(CatalogComponent catalogComponent){ items.remove(catalogComponent); } @Override public void print(){ for(CatalogComponent catalogComponent : items){ catalogComponent.print(); } } @Override public void getName(CatalogComponent catalogComponent){ return this.name; } }
子类没有实现父类的方法的时候,会自动执行父类的方法;
- UML类图如下:
课程Course 和课程目录 他们实际上是一个层级关系,Course 为Child 层级,而课程目录是parent层级;通过抽象类,使他们属于平级;
- 测试类如下:
public class Test{ public static void main(String[] args){ // 父子关系 CatalogComponent linuxCourse = new Course("Linux课程",11); CatalogComponent windowsCourse = new Course("Windows课程",11); CatalogComponent javaCourseCatalog = new CourseCatalog("Java课程目录",11); javaCousrseCatalog.add(linuxCourse); javaCousrseCatalog.add(windowsCourse); System.out.println(JSON.toJSONString(javaCourseCatalog)); // 平级关系 CatalogComponent parentCatalog = new CourseCatalog("暗余视频学习网"); parentCatalog.add(linuxCourse); parentCatalog.add(windowsCourse); parentCatalog.add(javaCourseCatalog); parentCatalog.print(); } }
在父子关系中,它可以用于树形结构。通过抽象方法,能够将目录和具体课程模糊自身的特点,并能够同时去添加(见'平级关系'下的代码),拥有极大的灵活性;我们还可以传入级别等进行标识和进行特殊的处理等;组合模式坑很多,要注意踩坑;
- 创建目录组件 CatelogComponent
13.3 源码学习
- SQL NODE
-
通过如图所示可以用以查看UML类图,对于理解源码的结构很有帮助!
-
UML:
通过此功能可以查看此接口下的实现类;
-
代码:
-
在SQL NODE中,每个Node的功能不尽相同,通过类似如上的方式可以将它们组合起来,这就是组合模式的妙用;
十四. 桥接模式
14.1 定义与类型
-
概述:
- 定义:将抽象部分与它的具体实现部分分离,使它们都可以独立地变化;
- 通过组合的方式建立两个类之间联系,而不是继承;
- 类型:结构型
可以防止类爆炸;
-
适用场景:
- 抽象和具体实现之间增加更多的灵活性
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
- 不希望使用继承,或因为多层继承导致系统类的个数剧增;
-
优点:
- 分离抽象部分及其具体实现部分
- 提高了系统的可扩展性
- 符合开闭原则
- 符合合成复用原则
-
缺点:
- 增加了系统的理解和设计难度
- 需要正确地识别出系统中两个独立变化的维度。
-
相关设计模式:
- 桥接模式和组合模式(组合更强调部分和整体的组合,而桥接模式更强调平行间的组合)
- 桥接模式和适配器模式(都是为了让两个东西配合工作,但是目的不一样,适配器是改变已有的接口,让他们相互间可以相互配合。而桥接模式是分离抽象和具体的实现,目的是为了分离。适配器模式可以把功能相似,但结构不同的类适配起来;而桥接模式是把类的抽象和类的实现分离开,在此基础上结合起来)
14.2 Coding
-
有这样一个需求: 首先有两家银行,每一家银行都有存款业务,存款业务分为定期存款和活期存款;我们要使用两个银行的不同存款类型进行操作,应该如何设计程序呢?
- 需求分析: 首先银行可以拥有很多种类,所以它应该有一个银行接口,不同的银行种类可以去实现这个银行接口。而账户可以定义一个账户接口,不同的账户类型也可以去实现这个接口;我们操作存款是通过银行去操作的,所以通过为银行接口传入一个账户接口,可以让两个不同种类的接口桥接起来,能够形成一个符合迪米特法则的设计;
- 原型图如图所示:
-
代码如下:
- 创建一个账号接口Account,创建两个方法,一个是打开账号方法,一个是查看账号的类型;
public interface Account{ Account openAccount(); void showAccountType(); }
- 创建一个定期账号,去实现Account接口:
public class DepositAccount implements Account{ @Override public Account openAccount(){ System.out.pintln("打开定期账号"); return new DepositAccount(); } @Override public void showAccountType(){ System.out.println("这是一个定期账号"); } }
- 创建一个活期账号SavingAccount
public class SavingAccount implements Account{ @Override public Account openAccount(){ System.out.println("打开活期账号") return new SavingAccount(); } @Override public void showAccountType(){ System.out.println("这是一个活期账号"); } }
- 创建一个银行类
public abstract class Bank{ protected Account account; public Bank(Account account){ this.account = account; } abstract Account openAccount(); }
- 创建ABCBank 实现类:
public class ABCBank extends Bank{ public ABCBank(Account account){ super(account); } @Override Account openAccount(){ System.out.println("打开中国农业银行账号") return account; } }
- 创建ICBC银行实现类
public class ICBCBank extends Bank{ public ICBCBank(Account account){ super(account); } @Override Account openAccount(){ System.out.println("打开中国工商银行账号"); return account; } }
- 目前创建的类结构图如下:
最简单的方式是继承的方案,但是它可扩展较低,不符合迪米特法则。通过桥接模式,我们可以将一个接口的实现类注入到操作类中,这样可以根据接口来将两个桥接起来;达到高内聚低耦合的目的;
- 创建测试类:
public class Test{ public static void main(String[] args){ Bank icbcBank = new ICBCBank(new DepositAccount()); Account icbcAccount = icbcBank.openAccount(); icbcAccount.showAccountType(); Bank abcBank = new ABCBank(new SavingAccount()); Account abcAccount = abcBank.openAccount(); abcAccount.showAccountType(); } }
我们可以将一些每个银行不同的业务逻辑以及账号的业务逻辑写在各自的实现类中,这样就可以直接调用了;
- 创建一个账号接口Account,创建两个方法,一个是打开账号方法,一个是查看账号的类型;
十五. 代理模式
15.1 代理模式讲解
- 代理-定义与类型
- 定义:代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能;
- 代理对象在客户端和目标对象之间起到中介作用
- 类型:结构型
- 代理-适用场景
- 保护对象
- 增强目标对象
- 代理-优点
- 代理模式能将代理对象与真实被调用的目标对象分离
- 一定程度上降低了系统的耦合度,扩展性好
- 保护目标对象
- 增强目标对象
- 代理-缺点
- 代理模式会造成系统设计中类的数目增加
- 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢
- 增加系统的复杂度
- 代理-扩展
- 静态代理:这种代理方式需要代理对象和目标对象实现一样的接口
- 优点:可以在不修改目标对象的前提下扩展目标对象的功能
- 缺点:
- 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类;
- 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改;
- 动态代理:动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理;
- 静态代理与动态代理的区别主要在:
- 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class 文件;
- 动态代理是在运行时动态生成的,即编译完成后没有实际的class 文件,而是在运行时动态生成类字节码,并加载到JVM中;
- 特点:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理;
- 静态代理与动态代理的区别主要在:
- CGLib代理(对final 的修饰符进行关注):是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展;
- 特点:
- JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口;如果想代理没有实现接口的类,就可以用CGLIB实现。
- CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多Aop的框架使用,例如Spring AOP和dynaop,为它们提供方法的interception(拦截)。
- CGLIB包的低层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类;不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉;
- 它与动态代理最大的区别是:
- 使用动态代理的对象必须实现一个或多个接口;
- 使用CGLIB代理的对象则无需实现接口,达到代理类无侵入;
- 特点:
- 静态代理:这种代理方式需要代理对象和目标对象实现一样的接口
- Spring代理选择
- 当Bean 有实现接口时,Spring 就会用JDK的动态代理
- 当Bean没有实现接口时,Spring使用CGlib
- 可以强制使用Cglib
- 在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
- 参考资料: docs.spring.io/spring/docs…
- 相关设计模式:
- 代理模式和装饰者模式:装饰者是为对象加上某些行为,而代理模式是增强行为;
- 代理模式和适配器模式:适配器可以改变目标对象的接口,而代理模式不会;
15.2 静态代理Coding
- 接口类:IUserDao
public interface IUserDao{ public void save(); }
- 目标对象:UserDao
public class UserDao implements IUserDao{ @Override public void save(){ System.out.println("保存数据"); } }
- 静态代理对象: UserDaoProxy 需要实现IUserDao接口!
public class UserDaoProxy implements IUserDao{ private IUserDao target; public UserDaoProxy(IUserDao target){ this.target = target; } @Override public void save(){ System.out.println("开启事务"); target.save(); System.out.println("提交事务"); } }
- 测试类: TestProxy
import org.junit.Test; public class StaticUserProxy{ @Test public void testSaticProxy{ // 目标对象 IUserDao target = new UserDao(); // 代理对象 UserDaoProxy proxy = new UserDaoProxy(target); proxy.save(); } }
15.3 动态代理
- 接口类:IUserDao
public interface IUserDao{ public void save(); }
- 目标对象:UserDao
public class UserDao implements IUserDao{ @Override public void save(){ System.out.println("保存数据"); } }
- 动态代理对象:ProxyFactory
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class ProxyFactory { private Object target;// 维护一个目标对象 public ProxyFactory(Object target) { this.target = target; } // 为目标对象生成代理对象 public Object getProxyInstance() { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("开启事务"); // 执行目标对象方法 Object returnValue = method.invoke(target, args); System.out.println("提交事务"); return null; } }); } }
- 测试类:TestProxy
import org.junit.Test; public class TestProxy{ @Test public void testDynamicProxy(){ IUserDao target = new UserDao(); System.out.println(target.getClass()); // 输出目标对象信息 IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance(); System.out.println(proxy.getClass()); // 输出代理对象信息 proxy.save(); // 执行代理方法 } }
15.4 cglib代理
- 引入maven依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.5</version> </dependency>
- 目标对象:UserDao
public class UserDao{ public void save(){ System.out.println("保存数据"); } }
- 代理对象: ProxyFactory
import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class ProxyFactory implements MethodInterceptor{ private Object target;//维护一个目标对象 public ProxyFactory(Object target) { this.target = target; } //为目标对象生成代理对象 public Object getProxyInstance() { //工具类 Enhancer en = new Enhancer(); //设置父类 en.setSuperclass(target.getClass()); //设置回调函数 en.setCallback(this); //创建子类对象代理 return en.create(); } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("开启事务"); // 执行目标对象的方法 Object returnValue = method.invoke(target, args); System.out.println("关闭事务"); return null; } }
- 测试类: TestProxy
public class TestProxy{ @Test public void testCglibProxy(){ // 目标对象 UserDao target = new UserDao(); System.out.println(target.getClass()); // 代理对象 UserDao proxy = (UserDao) new ProxyFactory(target).getProxyInstance(); System.out.println(proxy.getClass()); // 执行代理对象方法 proxy.save(); } }
十六. 模板方法模式
16.1 模板方法模式讲解
- 模板方法-定义与类型
- 定义:定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现;
- 模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤;
- 类型:行为型
- 模板方法-适用场景
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现;
- 各子类中公共的行为被提取出来并集中到一个公共父类中,从而避免代码重复;
- 模板方法-优点
- 提高复用性:将相同代码放到相同的父类中
- 提高扩展性:将不同的代码放到不同的子类中
- 符合开闭原则:通过对子类的扩展来增加新的行为,符合开闭原则;
- 模板方法-缺点
- 类数目增加
- 增加了系统实现的复杂度
- 继承关系自身缺点,如果父类添加新的抽象方法,所有子类都要改一遍;
- 钩子方法:
- 钩子方法源于设计模式中模板方法模式,模板方法模式中分为两大类:模板方法和基本方法,而基本方法又分为:抽象方法、具体方法,钩子方法。
- 对于钩子方法,是对于抽象方法或者接口定义的方法的一个空实现,在实际中的应用,比如说有一个接口,这个接口有7个方法,而你只想用其中一个方法,那么这时,你可以写一个抽象类实现这个接口,在这个抽象类里将你要用的那个方法设置为abstract,其他方法进行空实现,然后你再继承这个抽象类,就不需要实现其他不同的方法,这就是钩子方法的作用。
- 相关设计模式:
- 模板方法模式和工厂方法模式的区别:侧重点不同,工厂方法是类的创造模式,模板方法是类的行为模式;
- 模板方法模式和策略模式的区别:最重要的区别是,模板模式一般只针对一套算法,注重对同一个算法的不同细节进行抽象提供不同的实现。而策略模式注重多套算法多套实现;
16.2 模板方法coding
- 业务场景分析:假设我们要制作java视频课程和前端视频课程,那么我们可能需要一系列的步骤,比如都要制作PPT进行推广,需要制作各自的视频课程,同时需要提供各自的素材,而一些文字笔记则只有java需要而前端不需要,我们怎么实现这个功能呢?
业务分析:首先我们需要定义一个
- Coding
- 定义课程抽象类: ACourse
public abstract class ACourse{ protected final void makeCourse(){ this.makePPT(); this.makeVideo(); if(needWriteArticle()){ this.writeArticle(); } this.packageCourse(); } final void makePPT(){ System.out.println("制作PPT"); } final void makeVido(){ System.out.println("制作视频"); } final void writeArticle(){ System.out.println("编写文章"); } // 钩子方法 protected boolean needWriteArticle(){ return false; } // 素材 这个接口留给实现类来实现 abstract void packageCourse(); }
比如这个素材,可能不同课程的素材是不同的,这个可以留给实现类去实现;needWriteArticle这个方法会返回true或者false,我们默认是false所以不执行,如果实现类重写了这个方法并返回了true,则会执行writeArticle 这个功能;
- 定义后端课程实现类:DesignPatternCourse
public class DesignPatternCourse extends ACourse{ @Override void packageCourse(){ System.out.println("提供课程Java源代码"); } @Override protected boolean needWriteArticle(){ return true; } }
- 定义前端课程实现类: FECourse
public class FECourse extends ACourse{ @Override void packageCourse(){ System.out.println("提供课程的前端代码"); System.out.println("提供课程的图片等多媒体素材"); } }
- 测试类: Test
public class Test{ public static void main(String[] args){ System.out.println("后端设计模式课程 start ---"); ACourse designPatternCourse = new DesignPatternCourse(); designPatternCourse.makeCourse(); System.out.println("后端设计模式课程end ---"); System.out.println("前端课程start ---"); ACourse feCourse = new FECourse(); feCourse.makeCourse(); System.out.println("前端课程end ---"); } }
- (可选)可以将前端变成构造注入,不同的前端课程有的可能还是需要写文章,所以我们可以通过构造注入,来变成可选,FECourse 变成如下:
public class FECourse extends ACourse{ private boolean needWriteArticleFlag = false; @Override void packageCourse(){ System.out.println("提供课程的前端代码"); System.out.println("提供课程的图片等多媒体素材"); } public FECourse(boolean needWriteArticleFlag){ this.needWriteArticleFlag = needWriteArticleFlag; } @Override protected boolean needWriteArticle(){ return this.needWriteArticleFlag; } }
- 定义课程抽象类: ACourse
- UML 类图如下:
- 我们在一些实际应用场景中,就可以考虑使用模板方法模式;比如一个生成文件这个功能,可能会涉及到获取数据(由子类去实现),生成文件(指定txt、csv、excel等文件类型),上传到文件服务器获取url等步骤,它就可以使用模板方法来进行实现;
十七. 迭代器模式
17.1 迭代器模式讲解
- 定义与类型:
- 定义: 提供一种方法,顺序访问一个集合对象中的各个元素,而又不暴露该对象的内部表示
- 类型:行为型
- 适用场景
- 访问一个集合对象的内容而无需暴露它的内部表示
- 为遍历不同的集合结构提供一个统一的接口
- 优点
- 分离了集合对象的遍历行为
- 缺点
- 类的个数成对增加
- 相关设计模式,迭代器模式和访问者模式的区别:
- 迭代器模式提供一种方法,顺序访问一个集合对象中的各个元素,而又不暴露该对象的内部表示。该模式为遍历不同的集合结构提供了一个统一的接口。一般不会手写迭代器,都是使用封装好的,比如Java中的iterator;
- 访问者模式,它可以在不改变各元素的类的前提下,定义作用于这些元素的操作。该模式的主要目的是将数据与数据操作分离。被访问的类中需要传入访问者,并且在调用访问者方法的时候,需要将被访问的对象传入访问者的方法。
17.2 Conding 手写一个迭代器
- 定义课程类 Course
public class Course { private String name; public Course(String name) { this.name = name; } public String getName(){ return name; } }
- 定义一个课程处理类接口 CourseAggregate
public interface CourseAggregate { void addCourse(Course course); void removeCourse(Course course); CourseIterator getCourseIterator(); }
这个接口定义了三个方法,分别是新增课程,删除课程,获取这个课程的迭代器;
- 定义一个迭代器接口: CourseIterator
public interface CourseIterator{ Course nextCourse(); boolean isLastCourse(); }
- 创建一个课程实现: CourseAggregateImpl
public class CourseAggregateImpl implements CourseAggregate { private List courseList; public CourseAggregateImpl(){ this.courseList = new ArrayList(); }; @Override public void addCourse(Course course){ courseList.add(course); }; @Override public void removeCourse(Course course){ courseList.remove(course); }; @Override public CourseIterator getCourseIterator(){ return new CourseIteratorImpl(courseList); } }
- 创建迭代器实现: CourseIteratorImpl
public class CourseIteratorImpl implements CourseIterator{ private List courseList; int position; Course course; public CourseIteratorImpl(List courseList){ this.courseList = courseList; } @Override public Course nextCourse(){ System.out.println("返回课程,位置是:"+ position); course = (Course) courseList.get(position); position++; return course; } @Override public boolean isLastCourse(){ if(position < courseList.size()){ return false; } return true; } }
- 测试类: Test
public class Test { public static void main(String[] args){ Course course1 = new Course("Java 电商一期"); Course course2 = new Course("Java 电商二期"); Course course3 = new Course("Java 设计模式精讲"); Course course4 = new Course("Python课程"); Course course5 = new Course("算法课程"); Course course6 = new COurse("前端课程"); // 声明一个集合课程,并往里面添加课程 CourseAggregate courseAggregate = new CourseAggregateImpl(); courseAggregate.addCourse(course1); courseAggregate.addCourse(course2); courseAggregate.addCourse(course3); courseAggregate.addCourse(course4); courseAggregate.addCourse(course5); courseAggregate.addCourse(course6); System.out.println("----课程列表----"); printCourses(courseAggregate); courseAggregate.removeCourse(course4); courseAggregate.removeCourse(course5); System.out.println("------删除操作之后的课程列表----"); printCourses(courseAggregate); } public static void printCourses(CourseAggregate courseAggregate){ CourseIterator courseIterator = courseAggregate.getCourseIterator(); while(!courseIterator.isLastCourse()){ Course course = courseIterator.nextCourse(); System.out.println(course.getName()); } } }
- UML 类图如图所示:
在源码中,List 集合就有迭代器模式,每个List 的实现类就有对应的迭代器实现;
十八. 策略模式
18.1 策略模式讲解
- 定义与类型
- 定义:定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化不会影响到算法的用户;当有许多的if else 的时候,就可以考虑使用策略模式来处理
- 类型:行为型
- 适用场景
- 系统有很多类,而它们的去呗仅仅在于它们的行为不同,一个系统需要动态地在几种算法中选择鄂一中;
- 优点:
- 开闭原则
- 避免使用多重条件转移语句,比如很多的if else
- 提高算法的保密性和安全性;我们只需要知道策略名称和功能,而不需要知道内部实现细节;
- 缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类;
- 产生很多策略类
- 相关设计模式:
- 策略模式和工厂模式的区别:策略模式属于行为型模式,而工厂模式属于创建型模式;(工厂模式侧重于创建,而策略模式是使用已经创建好出来的对象)
- 策略模式和状态模式的区别:状态模式重点在各个状态之间的切换,从而做不同的事情,而策略模式更侧重于根据具体情况选择策略,并不涉及切换;
18.2 策略模式coding
- 业务场景:模拟一个促销场景和促销策略,这个促销场景有多种促销策略,比如满减、立减等
- 创建促销接口类:
public interface PromotionStrategy{ void doPromotion(); }
- 创建返现促销策略类:
public class FanXianPromotionStrategy implements PromotionStrategy{ @Override public void doPromotion(){ System.out.println("返现促销,返回的金额存放到用户的余额中"); } }
- 创建立减促销策略类:
public class LiJianPromotionStrategy implements PromotionStrategy{ @Override public void doPromotion(){ System.out.println("立减促销,课程的价格直接减去配置的价格"); } }
- 创建满减促销类
public class ManJianPromotionStrategy implements PromotionStrategy{ @Override public void doPromotion(){ System.out.println("满减促销,满200-20元"); } }
- 创建活动类 PromotionActivity
public class PromotionActivity{ private PromotionStrategy promotionStrategy; public PromotionActivity(PromotionStrategy promotionStrategy){ this.promotionStrategy = promotionStrategy; } public void executePromotionStrategy(){ promotionStrategy.doPromotion(); } }
- UML类图如下:
- 一个促销活动包含一种促销策略。促销策略接口被不同的促销类实现;
- 测试类Test
public class Test{ public static void main(String[] args){ PromotionActivity promotionActivity618 = new LiJianPromotionStrategy(); PromotionActivity promotionActivity1111 = new FanXianPromotionStrategy(); promotionActivity618.executePromotionStrategy(); promotionActivity1111.executePromotionStrategy(); } }
- 创建促销接口类:
- 代码优化:当我们执行某种策略的时候,会进行判断,则可能会有多个if else...,应该如何优化他们呢?
如图所示,当包含LIJIAN时,可能会执行立减的策略,依次类推,如果策略类很多的时候,if else 会带来代码臃肿,难以维护的问题;
- 优化如下:
- 创建策略模式工厂类:
public class PromotionStrategyFactory{ private static Map<String,PromotionStrategy> PROMOTION_STARTEGY_MAP = new HashMap<String,PromotionStrategy>(); private static final PromotionStrategy NON_PROMOTION = new EmptyPromotionStrategy(); static{ PROMOTION_STARTEGY_MAP.put(PromotionKey.LIJIAN,new LiJianPromotionStrategy()); PROMOTION_STARTEGY_MAP.put(PromotionKey.FANXIAN,new FanXianPromotionStrategy()); PROMOTION_STARTEGY_MAP.put(PromotionKey.MANJIAN,new ManJianPromotionStrategy()); } private PromotionStrategyFactory(){ } public static PromotionStrategy getPromotionStrategy(String promotionKey){ PromotionStrategy promotionStrategy = PROMOTION_STRATEGY_MAP.get(promotionKey); return promotionStrategy == null ? NON_PROMOTION : promotionStrategy; } private interface PromotionKey{ String LIJIAN = "LIJIAN"; String FANXIAN = "FANXIAN"; String MANJIAN = "MANJIAN"; } }
- 空策略(为了避免空指针创建的容错的空策略)
public class EmptyPromotionStrategy implements PromotionStrategy{ @Override public void doPromotion(){ System.out.println("无促销活动"); } }
- 测试类:
public class Test{ public static void main(String[] args){ String promotionKey = "MANJIANxxx"; PromotionActivity promotionActivity = new PromotionActivity(PromotionStrategyFactory.getPromotionStrategy(promotionKey)); promotionActivity.executePromotionStrategy(); } }
- 创建策略模式工厂类:
在很多开源框架源码中应用到了很多的策略模式,比如比较器,List 的Sort,TreeMap的compare,以及Resource
十九. 解释器模式
19.1 解释器模式讲解
- 定义与类型
- 定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子;它提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式事先了一个表达式接口,该接口解释一个特定上下文。这种模式被用在SQL解析、符号处理引擎等。
- 为了解释一种语言,而为语言创建的解释器;
- 类型:行为型;
- 适用场景
- 某个特定类型问题发生频率足够高
- 优点
- 语法由很多类表示,容易改变及扩展此“语言”
- 缺点:
- 当语法规则数目太多时,增加了复杂度
- 相关设计模式:
- 解释器模式和适配模式: 适配器模式不知道事先要适配的规则,解释器模式是要先把规则写好,然后根据规则去解释;
19.2 Conding
- 我们用经典的计算器来实现解释器模式
- 表达式接口 Expression
public interface Expression{ /** * 解释表达式的抽象方法 * @param map 比如现有表达式: a+b; 那么map中存放的就是{a = 10,b =20} * @return 返回解释后的值 */ int interpreter(Map<String,Integer> map); }
- 变量解析器类
public class VarExpression implements Expression{ // 公式中的变量 private String key; public VarExpression(String key){ this.key = key; } // 通过key获取所对应的值 @Override public int interpreter(Map<String,Integer> map){ return map.get(key); } }
- 运算符解析器类
public class SymbolExpression implements Expression{ /** * 假如现有表达式:a+b-c需要解析 * 分析: * 1、一个运算符连接的是它左右两个数字 * 2、如上表达式【+】号连接的是吧“a”和“b”,【-】号连接的是“a+b”和“c”。 * 3、经过分析我们将运算符连接的左右都看成是一个表达式也就是Expression. */ // 左表达式 protected Expression leftExpression; // 右表达式 protected Expression rightExpression; public SymbolExpression(Expression leftExpression,Expression rightExpression){ this.leftExpression = leftExpression; this.rightExpression = rightExpression; } // 不同种类的运算符由不同的运算符子类进行解析,所以该类不实现interpreter方法 @Override public int interpreter(Map<String,Integer> map){ return 0; } }
- 减法解析器 SubExpression
public class SubExpression extends SymboExpression{ public SubExpression(Expression leftExpression,Expression rightExpression){ super(leftExpression,rightExpression); } // 解释减法 @Override public int interpreter(Map<String,Integer> map){ return leftExpression.interpreter(map) - rightExpression.interpreter(map); } }
- 加法解析器
public class AddExpression extends SymbolExpression{ public AddExpression(Expression leftExpression,Expression rightExpression){ super(leftExpression,rightExpression); } // 解释加法 @Override public int interpreter(Map<String,Integer> map){ return leftExpression.interpreter(map) + rightExpression.interpreter(map); } }
- 计算器 => 对应Context角色
public class Calculator{ // 表达式 private Expression expression; public Calcuator(String strExpression){ char[] charArray = strExpression.toCharArray(); // 定义栈用于存储表达式,因示例简单故不考虑运算顺序。 Stack<Expression> stack = new Stack<>(); Expression left; Expression right; // 解析表达式 for(int i = 0; i < charArray.length; i++){ switch(charArray[i]){ case '+': // 获取左表达式 left = stack.pop(); // 定义右表达式 right = new VarExpression(String.valueOf(charArray[++i])); // 将合并为一个新的表达式并放入栈中 stack.push(new AddExpression(left,right)); break; case '-': // 过程跟加法一样 left = stack.pop(); right = new VarExpression(String.valueOf(charArray[++i])); stack.push(new SubExpression(left,right)); break; default: // 不是运算符 stack.push(new VarExpression(String.valueOf(charArray[i]))); break; } } // 遍历完成获取最终解析好的表达式 this.expression = stack.pop(); } /** * @param map 表达式对应的值 * @return 计算的结果 */ public int calculate(Map<String,Integer> map){ return this.expression.interpreter(map); } }
- 客户端测试类
public class Client{ public static void main(String[] args){ // 表达式 String strExpression = "a+b-c+d"; // 表达式对应的值 Map<String,Integer> map = new HashMap<>(); map.put("a",2); map.put("b",10); map.put("c",8); map.put("d",8); // 创建计算器 Calculator calculator = new Calculator(strExpression); // 计算 int result = calculator.calculate(map); System.out.println("表达式:"+ strExpression +"的计算结果为:"+ result); } }
- 执行结果:
在框架源码中,比如SPEL 表达式、Parse解析器等都用到了解释器模式,它是一个不是很常用的解析器,我们虽然可能不会经常去写解释器模式的代码,但是会经常接触和使用到通过此模式创建出来的一些工具类;
二十. 观察者模式
20.1 定义与类型
- 定义与类型:
- 定义:定义了对象之间的一对多依赖,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有依赖者(观察者)都会收到通知并更新;
- 在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。一个对象行为的改变能够通知到监听观察的人,同时,不同观察的人能够做出对应的不同处理行为,这就是观察者模式;
- 类型:行为性
- 适用场景:关联行为场景,创建触发机制
- 优点:
- 观察者和被观察者之间建立一个抽象的耦合
- 观察者模式支持广播通信
- 缺点:
- 观察者之间有过多的细节依赖、提高时间消耗及程序复杂度
- 使用要得到,要避免循环调用
- 模式的结构:观察者模式的主要角色如下:
- 抽象主题(Subject)角色:也叫目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法;
- 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知被调用。
- 结构图:
20.2 Coding
- 抽象类Subject:
public abstract class Subject{ protected List<Observer> observers = new ArrayList<Observer>(); // 增加观察者方法 public void add(Observer observer){ observers.add(observer); } // 删除观察者方法 public void remove(Observer observer){ observers.remove(observer); } // 通知观察者方法 public abstract void notifyObserver(); 通知观察者方法 }
- 具体目标ConcreteSubject.class ,对notifyObserver实现具体方法
public class ConcreteSubject extends Subject{ pubic void notifyObserver(){ System.out.println("具体目标已发生改变..."); for(Object obs: observers){ (Observer)obs.response(); } } }
- 抽象观察者接口 Observer
public interface Observer{ void response(); // 通过通知后做出的应对方法 }
- 具体观察者接口 ConcreteObserver1
public class ConcreteObserver1 implements Observer{ public void response(){ System.out.println("具体观察者1做出反应!"); } }
- 具体观察者2
public class ConcreateObserver2 implements Observer{ public void response(){ System.out.println("具体观察者2作出反应!"); } }
二十一. 备忘录模式
21.1 备忘录模式讲解
- 定义与类型
- 定义:保存一个对象的某个状态,以便在适当的时候恢复对象
- "后悔药"
- 类型:行为型
- 适用场景
- 保存及恢复数据相关业务场景
- 后悔的时候,即想恢复到之前的状态
- 优点:
- 为用户提供一种可恢复机制
- 存档信息的封装
- 缺点:
- 资源占用
- 相关设计模式:
- 与状态模式的区别:
- 备忘录模式在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。
- 状态模式就是复杂对象不同状态下的行为封装与状态转换。状态模式中的行为由状态决定,不同的状态下有不同的行为。其意图是让一个对象在其内部改变的时候其行为也随之改变。状态模式的和兴是状态与行为绑定,不同的状态对应不同的行为;
- 与状态模式的区别:
21.2 Coding
- 业务描述:我们有一个写文章,做保存的时候,会进行保存为一个暂存对象,通过一个集合进行存储,通过先进后出(栈)的方式进行存储和查询。我们存储的时候,总是最新的在上面,当要恢复记录的时候,也是最新的记录先进行恢复;
- 文章对象Article
public class Article{ private String title; private String content; private String imgs; public Article(String title,String content, String imgs){ this.title = title; this.content = content; this.imgs = imgs; } // getter、setter ... public ArticleMemento saveToMemento(){ ArticleMemento articleMemento = new ArticleMemento(this.title,this.content,this.imgs); return articleMemento; } public void undoFromMemento(ArticleMemento articleMemento){ this.title = articleMemento.getTitle(); this.content = articleMemento.getContent(); this.imgs = articleMemento.getImgs(); } }
- 文章快照 ArticleMemento
public class ArticleMemento{ private String title; private String content; private String imgs; public ArticleMemento(String title,String content, String imgs){ this.title = title; this.content = content; this.imgs = imgs; } public String getTitle(){return title;} public String getContent(){return content;} public String getImgs(){return imgs;}
这个快照类,可以根据实际来存储一些能够还原成原来对象的一些信息;故里面的变量和具体方法可以根据实际情况来
- 快照管理类: ArticleMementoManager
public class ArticleMementoManager{ private final Stack<ArticleMemento> ARTICLE_MEMENTO_STACK = new Stack<>(); public ArticleMemento getMemento(){ ArticleMemento articleMemento = ARTICLE_MEMENTO_STACK.pop } public void addMemento(ArticleMemento articleMemento){ ARTICLE_MEMENTO_STACK.push(articleMemento); } }
- 测试类Test
public class Test{ public static void main(String[] args){ ArticleMementoManager articleMementoManager = new ArticleManager(); Article article = new Article("如影随形的设计模式A","设计模式的具体内容","设计模式的图片"); // 将文章内容暂存,返回暂存对象 ArticleMemento articleMemento = article.saveToMemento(); // 获取暂存对象放入快照管理类 articleMementoManager.addMemento(articleMemento); // 修改文章内容 article.setTitle("随影随行的设计模式B"); // 此刻我们想要回归到之前的状态,则使用快照管理类进行回退 articleMemento = articleMementoManager.getMemento(); } }
备忘录模式可以将一些常用变量进行存储,然后通过管理类进行统一存储和还原;
- 文章对象Article
在工作流、涉及到可以回退的如文本编辑等功能,依据实际业务场景或许可以通过备忘录模式优雅的解决问题;
二十二. 命令模式
22.1 命令模式讲解
- 定义与类型
- 定义:将“请求”封装成对象,以便使用不同的请求
- 命令模式解决了应用程序中对象的职责以及它们之间的通信方式
- 类型:行为型
说白了,就是将一系列的请求命令封装起来,不直接调用真正执行者的方法,这样比较好扩展;
- 适用场景
- 请求调用者和请求接收者需要解耦,使得调用者和接受者不直接交互,需要抽象出等待执行的行为;
- 优点:
- 降低耦合
- 容易扩展新命令或者一组命令
- 缺点:
- 命令的无线扩展会增加类的数量,提高系统实现的复杂度;
- 相关设计模式:
- 备忘录模式:备忘录模式是能操作整个过程的,我们随时可以进行恢复,如果是命令模式的话,我们要去操作的就是只有一步一步的进行撤销操作的;
22.2 Conding
- 命令的执行者Receiver
public class Receiver{ public void action(){ System.out.println("命令执行了..."); } }
- 抽象命令Command
public interface Command{ // 调用命令 void execute(); }
- 具体命令类 ConcreteCommand
public class ConcreteCommand implements Command{ // 持有真正执行命令对象的引用 private Receiver receiver; public ConcreteCommand(Receiver receiver){ super(); this.receiver = receiver; } @Override public void execute(){ // 调用接收者执行命令的方法 receiver.action(); } }
- 命令发起者Invoker
// 请求者/调用者:发起执行命令请求的对象 public class Invoker{ // 持有命令对象的引用 private Command command; public Invoker(Command command){ super(); this.command = command; } public void call(){ // 请求者调用命令对象执行命令的那个execute方法 command.execute(); } }
- 测试类:
public class Test{ public static void main(String[] args){ // 通过请求者(invoker)调用命令对象(command),命令对象中调用了命令具体执行者(receiver) Commmand command = new ConcreteCommand(new Receiver()); Invoker invoker = new Invoker(command); invoker.call(); } }
二十三. 中介者模式
23.1 中介者模式讲解
- 定义与类型
- 定义一个封装一组对象如何交互的对象
- 通过使对象明确地相互引用来促进松散耦合,并允许独立地改变它们的交互;
- 类型:行为型
- 适用场景:
- 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解;
- 交互的公共行为,如果需要改变行为则可以增加新的中介者类
- 优点:
- 将一对多转化成了一对一,降低程序复杂度
- 类之间耦合
- 中介者模式-缺点:
- 中介者过多,导致系统复杂
- 其他设计模式:
- 观察者模式,有时候中介者模式会和观察者模式结合在一起使用;
23.2 Conding
- 描述业务场景: 有这样一个学习群,有一个中介者,它会提示说谁说了什么话。而不是当事人直接面对面对谁进行交流;
- 学习群中介者 StudyGroup
public class StudyGroup{ public static void showMessage(User user,String message){ System.out.println(new Date().toString() + "["+user.geName()+"]:"+ message ); } }
- 用户学生User
public class User{ private String name; public String getName(){return name;} public void setName(String name){this.name = name;} public User(String name){this.name = name;} public void sendMessage(String message){ StudyGroup.showMessage(this,message); } }
- 测试类Test
public class Test{ public static void main(String[] args){ User geely = new User("Geely"); User tom = new User("Tom"); geely.sendMessage(" Hey ! Tom! Let's learn Design Pattern..."); tom.sendMesage("OK! Geely"); } }
- 学习群中介者 StudyGroup
中介者模式适合做聊天室这种;
二十四. 责任链模式
24.1 责任链模式讲解
- 定义与类型
- 为请求创建一个接收此次请求对象的链
- 行为:请求型
- 适用场景:
- 一个请求的处理需要多个对象当中的一个或几个协作处理
- 优点:
- 请求的发送者和接收者(请求的处理)解耦
- 责任链可以动态组合
- 缺点:
- 责任链太长或者处理时间过长,影响性能
- 责任链有可能过多
24.2 Coding
- 业务场景描述:
- 课程类 Course:
public class Course{ private String name; private String article; private String video; // getter...setter... toString... }
- 审批抽象类 Approver
public abstract class Approver{ protected Approver approver; public void setNextApprover(Approver approver){ this.approver = approver; } public abstract void deploy(Course course); }
- 文章审批实现类 ArticleApprover
public class ArticleApprover extends Approver{ @Override public void deploy(Course course){ if(StringUtils.isNotEmpty(course.getArticle())){ System.out.println(course.getName()+"含有手记,批准"); } if(approver != null){ approver.deploy(course); } }else{ System.out.println(course.getName()+"不含有手记,不批准,流程结束"); return; } }
- 视频审批实现类 VedioApprover
public class VedioApprover extends Approver{ @Override public void deploy(Course course){ if(StringUtils.isNotEmpty(course.getVideo())){ System.out.println(course.getName()+"含有视频,批准"); } if(approver != null){ approver.deploy(course); } }else{ System.out.println(course.getName()+"不含有视频,不批准,流程结束"); return; } }
- 测试类 Test
public class Test{ public static void main(String[] args){ Approver articleApprover = new ArticleApprover(); Approver videoApprover = new VideoApprover(); Course course = new Course(); course.setName("Java 设计模式"); course.setArticle("Java 设计模式附带的文章"); course.setVideo("Java设计模式附带的视频"); articleApprover.setNextApprover(videoApprover); articleApprover.deploy(course); } }
- 课程类 Course:
责任链模式在源码中的应用有:Filter 过滤器、Spring Security等;
二十五. 访问者模式
25.1 访问者模式讲解
- 定义与类型:
- 封装作用于某数据结构(如List/Set/Map等)中的各元素的操作
- 可以在不改变各元素的类的前提下,定义作用于这些元素的操作;
- 类型:行为型;
- 它是最复杂的设计模式,并且使用频率不高,大多数情况下不需要使用访问者模式,一旦需要用到它时,那就真的需要使用了;
- 访问者模式是一种将数据操作和数据结构分离的设计模式;
- 适用场景:
- 对象结构比较稳定,但经常需要在此对象结构上定义新的操作;
- 需要对一个对象结构中的对象进行很多不同的并且不相干的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
- 优点:
- 各橘色职责分离,符合单一原则
- 具有优秀的扩展性:如果需要增加新的访问者,增加实现类ConcreteVisitor就可以快速扩展;
- 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化;
- 灵活性
- 缺点:
- 具体元素对访问者公布细节,违反了迪米特原则: CEO、CTO需要调用具体员工的方法;
- 具体元素变更时导致修改成本大:变更员工属性时,多个访问者都要修改;
- 违反了依赖导致原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象:访问者visit方法中,依赖了具体员工的具体访问法;
- 访问者模式UML类图:
- 角色介绍:
- Visitor: 接口或者抽象类,定义了对每个Element访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改Vistor接口,如果出现这种情况,则说明不适合使用访问者模式;
- ConcreteVistor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。
- Element: 元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
- ElementA、ElementB: 具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- ObjectStructure: 定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。
25.2 Coding
-
业务场景介绍:
- 年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。 由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。
-
代码实现:
- Staff类:
// 员工基类 public abstract class Staff { public String name; public int kpi; // 员工kpi public Staff(String name){ this.name = name; kpi = new Random().nextInt(10); } // 核心方法,接受Vistor的访问 public abstract void accept(Vistor vistor); }
Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。
- 工程师类 Engineer
// 工程师 public class Engineer extends Staff { public Engineer(String name){ super(name); } @Override public void accept(Visitor visitor){ visitor.visit(this); } // 工程师一年的代码数量 public int getCodeLines(){ return new Random().nextInt(10 * 100000); } }
- 经理类 Manager
public class Manager extends Staff { public Manager(String name){ super(name); } @Override public void accept(Visitor visitor){ visitor.visit(this); } // 一年做的产品数量 public int getProducts(){ return new Random().nextInt(10); } }
工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。
- 业务报表类 BusinessReport: 公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩
// 员工业务报表类 public class BusinessReport { private List<Staff> mStaffs = new LinkedList<>(); public BusinessReport() { mStaffs.add(new Manager("经理-A")); mStaffs.add(new Engineer("工程师-A")); mStaffs.add(new Engineer("工程师-B")); mStaffs.add(new Engineer("工程师-C")); mStaffs.add(new Manager("经理-B")); mStaffs.add(new Engineer("工程师-D")); } /** * 为访问者展示报表 * @param visitor 公司高层,如CEO、CTO */ public void showReport(Visitor visitor) { for (Staff staff : mStaffs) { staff.accept(visitor); } } }
- Visitor接口:Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数
public interface Visitor{ // 访问工程师类型 void visit(Engineer engineer); // 访问经理类型 void visit(Manager manager); }
首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。
- CEO访问者 CEOVisitor
public class CEOVisitor implements Visitor { @Override public void visit(Engineer engineer){ System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi); } @Override public void visit(Manager manager) { System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +", 新产品数量: " + manager.getProducts()); } }
- CTOVisitor
public class CTOVisitor implements Visitor { @Override public void visit(Engineer engineer) { System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines()); } @Override public void visit(Manager manager) { System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts()); } }
重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码
- 测试类 Test
public class Test{ public static void main(String[] args){ // 构建报表 BusinessReport report = new BusinessReport(); System.out.println("=====CEO 看报表======"); report.showReport(new CEOVisitor()); System.out.println("=====CTO 看报表======"); report.showReport(new CTOVisitor()); } }
- Staff类:
-
不使用Visitor模式,访问者代码如下:
public class ReportUtil { public void visit(Staff staff) { if (staff instanceof Manager) { Manager manager = (Manager) staff; System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi + ", 新产品数量: " + manager.getProducts()); } else if (staff instanceof Engineer) { Engineer engineer = (Engineer) staff; System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi); } } }
在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理,这就导致了 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这个 ReportUtil 就会很复杂。而使用 Visitor 模式,通过同一个函数对不同对元素类型进行相应对处理,使结构更加清晰、灵活性更高。
-
在上述示例中,Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码。 访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。
二十六. 状态模式
26.1 状态模式讲解
- 定义与类型
- 允许一个对象在其内部状态改变时,改变它的行为;
- 行为型
- 适用场景
- 一个对象存在多个状态(不同状态下行为不同),且状态可互相转换;
- 优点:
- 将不同的状态隔离
- 把各种状态的转换逻辑,分布到State的子类中,减少相互间依赖;
- 增加新的状态非常简单
- 缺点:
- 状态多的业务场景导致类数目增加,系统变复杂;
- 相关模式:
- 状态模式可以和享元模式配合使用;
26.2 Conding
- 业务需求描述:视频播放有很多状态: 播放、暂停、快进、停止状态,且在某些状态下,操作为其他状态可能会受限,比如停止状态则不能快进等;
- 代码演示:
- 视频状态类: CourseVideoState
public abstract class CourseVideoState { protected CourseVideoContext courseVideoContext; public void setCourseVideoContext(CourseVideoContext courseVideoContext){ this.courseVideoContext = courseVideoContext; } public abstract void play(); public abstract void speed(); public abstract void pause(); pulbic abstract void stop(); }
- 视频状态上下文: CourseVideoContext
public class CourseVideoContext{ private CourseVideoState courseVideoState; public final static PlayState PLAY_STATE = new PlayState(); public final static StopState STOP_STATE = new StopState(); public final static PauseState PAUSE_STATE = new PauseState(); public final static SpeedState SPEED_STATE = new SpeedSate(); public CourseVideoSate getCourseVideoState(){ return courseVideoState(); } public void setCourseVideoState(CourseVideoState courseVideoState){ this.courseVideoState = courseVideoState; this.courseVideoState.setCourseVideoContext(this); } public void play(){ this.courseVideoState.play(); } public void speed(){ this.courseVideoState.speed(); } public void stop(){ this.courseVideoState.stop(); } public void pause(){ this.courseVideoState.pause(); } }
- 状态具体实现类: PlayState.class
public class PlayState extends CourseVideoState{ @Override public void play(){ System.out.println("正常播放课程视频状态"); } @Override public void speed(){ super.courseVideoContext.setCourseVideoState(CourseVideo.SPEED_STATE); } @Override public void stop(){ super.courseVideoContext.setCourseVideoState(CourseVideo.STOP_STATE); } @Override public void pause(){ super.courseVideoContext.setCourseVideoState(CourseVideo.PAUSE_STATE); } }
- 快进状态具体实现类 SpeedState
public class SpeedState extends CourseVideoState{ @Override public void play(){ super.courseVideoContext.setCourseVideoState(CourseVideo.PLAY_STATE); } @Override public void speed(){ System.out.println("快进播放课程视频状态"); } @Override public void stop(){ super.courseVideoContext.setCourseVideoState(CourseVideo.STOP_STATE); } @Override public void pause(){ super.courseVideoContext.setCourseVideoState(CourseVideo.PAUSE_STATE); } }
- 暂停状态具体实现类 PauseState
public class StopState extends CourseVideoState{ @Override public void play(){ super.courseVideoContext.setCourseVideoState(CourseVideo.PLAY_STATE); } @Override public void speed(){ super.courseVideoContext.setCourseVideoState(CourseVideo.SPEED_STATE); } @Override public void stop(){ super.courseVideoContext.setCourseVideoState(CourseVideo.STOP_STATE); } @Override public void pause(){ System.out.println("暂停播放课程视频状态"); } }
- 停止状态具体实现类:StopState
public class StopState extends CourseVideoState{ @Override public void play(){ super.courseVideoContext.setCourseVideoState(CourseVideo.PLAY_STATE); } @Override public void speed(){ System.out.println("ERROR 停止状态不能快进"); } @Override public void stop(){ System.out.println("停止播放课程视频状态"); } @Override public void pause(){ System.out.println("ERROR 停止状态不能暂停"); } }
- 测试类Test
public class Test{ public static void main(String[] args){ CourseVideoContext courseVideoContext = new CourseVideoContext(); courseVideoContext.setCourseVideoState(new PlayState()); System.out.println("当前状态"+ courseVideoContext.getCourseVideoState().getClass().getSimpleName()); courseVideoContext.pause(); System.out.println("当前状态"+ courseVideoContext.getCourseVideoState().getClass().getSimpleName()); courseVideoContext.speed(); System.out.println("当前状态"+ courseVideoContext.getCourseVideoState().getClass().getSimpleName()); courseVideoContext.stop(); System.out.println("当前状态"+ courseVideoContext.getCourseVideoState().getClass().getSimpleName()); } }
- UML类图如下:
- 视频状态类: CourseVideoState
二十七. 课程总结
27.1 7大设计原则
27.2 创建型模式
黄色部分可能会经常用到
27.3 结构型模式
27.4 行为型模式
此篇博客码字9万余字,码字不易,看到这儿的您也不易,不要忘了收藏哦~~