一、设计模式
1.1 、Java 设计模式
1.1.1、几个经典的面试题
原型设计模式问题
- 1) 有请使用 UML 类图画出原型模式核心角色
- 2) 原型设计模式的深拷贝和浅拷贝是什么,并写出深拷贝的两种方式的源码(重写 clone 方法实现深拷贝、使用序列化来实现深拷贝)
- 3) 在 Spring 框架中哪里使用到原型模式,并对源码进行分析 beans.xml
- 4) Spring 中原型 bean 的创建,就是原型模式的应用
- 5) 代码分析+Debug 源码
设计模式的七大原则要求:
- 1) 七大设计原则核心思想
- 2) 能够以类图的说明设计原则
- 3) 在项目实际开发中,你在哪里使用到了 ocp 原则
设计模式常用的七大原则有:
- 1) 单一职责原则
- 2) 接口隔离原则
- 3) 依赖倒转原则
- 4) 里氏替换原则
- 5) 开闭原则 ocp
- 6) 迪米特法则
- 7) 合成复用原则
金融借贷平台项目:借贷平台的订单,有审核发布-抢单 等等 步骤,随着操作的不同,会改 变订单的状态, 项目中的这个模块实现就会使用 到状态模式,请你使用状态模式进行设计,并 完成实际代码
- 问题分析 :
- 这类代码难以应对变化,在添加一种状态时, 我们需要手动添加if/else,在添加一种功能时, 要对所有的状态进行判断。因此代码会变得越 来越臃肿,并且一旦没有处理某个状态,便会 发生极其严重的BUG,难以维护
- 问题分析 :
解释器设计模式
- 1) 介绍解释器设计模式是什么?
- 2) 画出解释器设计模式的UML类图, 分析设计模式中的各个角色是什 么?
- 3) 请说明Spring的框架中,哪里 使用到了解释器设计模式,并 做源码级别的分析
解释器模式在Spring框架应用的源码剖析
- 1) Spring框架中 SpelExpressionParser就使用到解释器模式
- 2) 代码分析+Debug源码+模式角色分析说明
单例设计模式一共有几种实现方式?请分别用代码实现,并说明各个实现方式的 优点和缺点?
单例设计模式一共有8种写法:
- 1) 饿汉式 两种
- 2) 懒汉式 三种
- 3) 双重检查
- 4) 静态内部类
- 5) 枚举
1.2、设计模式的重要性
1) 软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现) 的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人 在1990年代从建筑设计领域引入到计算机科学的
2) 大厦 VS 简易房
3) 拿实际工作经历来说, 当一个项目开发完后,如果客户提出增新功能,怎么办?。
4) 如果项目开发完后,原来程序员离职,你接手维护该项目怎么办? (维护性[可读性、 规范性])
5) 目前程序员门槛越来越高,一线IT公司(大厂),都会问你在实际项目中使用过什么 设计模式,怎样使用的,解决了什么问题。
6) 设计模式在软件中哪里?面向对象(oo)=>功能模块[设计模式+算法(数据结构)]=>框 架[使用到多种设计模式]=>架构 [服务器集群]
7) 如果想成为合格软件工程师,那就花时间来研究下设计模式是非常必要的.
二、设计模式七大原则
2.1、设计模式的目的
编写软件过程中,程序员面临着来自 耦合性,内聚性以及可维护性,可扩展性,重 用性,灵活性 等多方面的挑战,设计模式是为了让程序(软件),具有更好的:
- 1) 代码重用性 (即:相同功能的代码,不用多次编写)
- 2) 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
- 3) 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
- 4) 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
- 5) 使程序呈现高内聚,低耦合的特性
2.2 、设计模式七大原则
设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模 式的基础(即:设计模式为什么这样设计的依据)
设计模式常用的七大原则有:
- 1) 单一职责原则
- 2) 接口隔离原则
- 3) 依赖倒转(倒置)原则
- 4) 里氏替换原则
- 5) 开闭原则
- 6) 迪米特法则
- 7) 合成复用原则
2.3 、单一职责原则
2.3.1、单一职责的基本介绍
对类来说的,即一个类应该只负责一项职责。如类 A 负责两个不同职责:职责 1,职责2。当职责1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为 A1,A2
2.3.2、单一职责的案例分析
对类来说的,即一个类应该只负责一项职责。如类 A 负责两个不同职责:职责 1,职责2。当职责1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为 A1,A2 以交通工具案例讲解
1) 方案 一代码实现:
/**
* description
* 交通工具类
*
* @author
* @since 2022/11/17 15:19
*/
public class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在公路上跑...");
}
}
//再定义三个交通工具进行测试
/**
* description
* 以交通工具为例,第一种方案
*
* @author
* @since 2022/11/17 15:17
*/
public class SingleResponsibilityOne {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("摩托车");
vehicle.run("汽车");
vehicle.run("飞机");
}
}
- 输出结果如下:
由此分析出两个结论:
在方式 1 的 run 方法中,飞机和汽车、轮船的用途不同,但却出现飞机和轮船也在路上跑的情况,显然不合理
解决的方案非常的简单,根据交通工具运行方法不同,分解成不同类即可
- 2)方案二代码实现:思路(首先创建三个交通工具类,区分职责),然后再次执行对应方法进行测试
/**
* description
* 路上跑的交通工具
*
* @author
* @since 2022/11/17 15:33
*/
public class RoadVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在公路上跑...");
}
}
/**
* description
* 水里运行的交通工具
*
* @author
* @since 2022/11/17 15:36
*/
public class WaterVehicle {
public void run(String vehicle){
System.out.println(vehicle + "在水里运行");
}
}
/**
* description
* 天上飞的交通工具
*
* @author
* @since 2022/11/17 15:35
*/
public class AirVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在天上飞");
}
}
/**
* description
* 第二种解决方案
*
* @author
* @since 2022/11/17 16:20
*/
public class SingleResponsibilityTwo {
public static void main(String[] args) {
WaterVehicle waterVehicle = new WaterVehicle();
waterVehicle.run("轮船");
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("汽车");
AirVehicle airVehicle = new AirVehicle();
airVehicle.run("飞机");
}
}
输出结果如下:
方案二的分析:
1、遵守了单一职责原则
2、但是这样做的改动很大,即将类分解,同时要修改客户端
3、改进方案:直接修改Vehicle 类,改动的代码会比较少--->方案三
代码实现如下:
/**
* description
* 首先定义交通工具类
*
* @author
* @since 2022/11/17 15:19
*/
public class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + "在公路上跑...");
}
public void runAir(String vehicle){
System.out.println(vehicle + "在天上飞");
}
public void runWater(String vehicle){
System.out.println(vehicle + "在水里运行");
}
}
/**
* description
* 第三种解决方案测试
*
* @author
* @since 2022/11/17 16:28
*/
public class SingleResponsibilityThree {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.runAir("飞机");
vehicle.runWater("轮船");
}
}
//输出结果如下
第三种解决方案的分析:
1、这种修改方法没有对原来的类做大的修改,只是增加了方法
2、方式三一定程度上虽然没有在类做个级别上遵守单一职责原则,但是在方法级别上仍然遵守了单一职责原则
2.3.3、单一职责原则注意事项和细节
1)降低类的复杂度,一个类只负责一项职责
2)提高类的可读性,可维护性
3)降低变更引起的风险
4)通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则
2.4、 接口隔离原则(Interface Segregation Principle)
2.4.1、基本介绍
1) 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
2)先看一张图
2.4.2、应用实例
1) 类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,请编写代码完成此应用实例。
2) 代码实现-没有使用接口隔离原则代码
- 首先创建一个接口,并添加五个方法
- 再根据题意创建A、B、C、D四个实现类,其中
- B、D两个类实现接口中的五个方法
- A通过接口InterfaceOne 依赖(使用)B类,但是只用到1,2,3方法
- C通过接口InterfaceOne 依赖(使用)D类,但是只会用到1,4,5方法
- 代码实现如下:
/**
* description
* 在接口中定义五个方法
*
* @author
* @since 2022/11/17 20:43
*/
public interface InterfaceOne {
void operationOne();
void operationTwo();
void operationThree();
void operationFour();
void operationFive();
}
/**
* description
* 实现类B,实现接口内全部方法
*
* @author
* @since 2022/11/17 20:45
*/
public class B implements InterfaceOne {
@Override
public void operationOne() {
System.out.println("B 实现了operationOne");
}
@Override
public void operationTwo() {
System.out.println("B 实现了operationTwo");
}
@Override
public void operationThree() {
System.out.println("B 实现了operationThree");
}
@Override
public void operationFour() {
System.out.println("B 实现了operationFour");
}
@Override
public void operationFive() {
System.out.println("B 实现了operationFive");
}
}
/**
* description
* 实现类D,实现了接口内全部方法
*
* @author
* @since 2022/11/17 20:48
*/
public class D implements InterfaceOne {
@Override
public void operationOne() {
System.out.println("D 实现了operationOne");
}
@Override
public void operationTwo() {
System.out.println("D 实现了operationTwo");
}
@Override
public void operationThree() {
System.out.println("D 实现了operationThree");
}
@Override
public void operationFour() {
System.out.println("D 实现了operationFour");
}
@Override
public void operationFive() {
System.out.println("B 实现了operationFive");
}
}
/**
* description
* 实现类A通过接口InterfaceOne 依赖(使用)B类,但是只会用到1,2,3方法
*
* @author
* @since 2022/11/17 20:49
*/
public class A {
public void dependOne(InterfaceOne i){
i.operationOne();
}
public void dependTwo(InterfaceOne i){
i.operationTwo();
}
public void dependThree(InterfaceOne i){
i.operationThree();
}
}
/**
* description
* 通过接口InterfaceOne 依赖(使用)D类,但是只会用到1,4,5方法
*
* @author
* @since 2022/11/17 20:53
*/
public class C {
public void dependOne(InterfaceOne i){
i.operationOne();
}
public void dependFour(InterfaceOne i){
i.operationFour();
}
public void dependFive(InterfaceOne i){
i.operationFive();
}
}
2.4.3、应传统方法的问题和使用接口隔离原则改进
对传统方法问题的分析以及提出解决方案:
- 1)类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,如果接口Interface1 对于类A和类C来说不是最小接口,那么类 B 和类 D 必须去实现他们不需要的方法,就会造成资源浪费
- 2)按隔离原则应当这样处理: 将接口 InterfaceOne 拆分为独立的几个接口(这里我们拆分成 3 个接口),类 A 和类C 分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则
- 如图所示
代码实现如下:
/**
* description
*
* @author
* @since 2022/11/17 21:39
*/
public interface InterfaceOne {
void operationOne();
}
/**
* description
*
* @author
* @since 2022/11/17 21:39
*/
public interface InterfaceTwo {
void operationTwo();
void operationThree();
}
/**
* description
*
* @author
* @since 2022/11/17 21:39
*/
public interface InterfaceThree {
void operationFour();
void operationFive();
}
/**
* description
*
* @author
* @since 2022/11/17 21:44
*/
public class B implements InterfaceOne, InterfaceTwo {
@Override
public void operationOne() {
System.out.println("B 实现了operationOne");
}
@Override
public void operationTwo() {
System.out.println("B 实现了operationTwo");
}
@Override
public void operationThree() {
System.out.println("B 实现了operationThree");
}
}
/**
* description
*
* @author
* @since 2022/11/17 21:46
*/
public class D implements InterfaceOne, InterfaceThree {
@Override
public void operationOne() {
System.out.println("D 实现了operationOne");
}
@Override
public void operationFour() {
System.out.println("D 实现了operationFour");
}
@Override
public void operationFive() {
System.out.println("D 实现了operationFive");
}
}
/**
* description
* A类通过接口InterfaceOne、InterfaceTwo依赖(使用)B类,但是只会用到1,2,3方法
* @author
* @since 2022/11/17 21:48
*/
public class A {
public void dependOne(InterfaceOne i){
i.operationOne();
}
public void dependTwo(InterfaceTwo i){
i.operationTwo();
}
public void dependThree(InterfaceTwo i){
i.operationThree();
}
}
/**
* description
* C类通过接口InterfaceOne 、InterfaceThree依赖(使用)D类,但是只会用到1,4,5方法
*
* @author
* @since 2022/11/17 22:15
*/
public class C {
public void dependOne(InterfaceOne i) {
i.operationOne();
}
public void dependFour(InterfaceThree i) {
i.operationFour();
}
public void dependFive(InterfaceThree i) {
i.operationFive();
}
}
/**
* description
* 测试
*
* @author
* @since 2022/11/17 22:19
*/
public class Segregation {
public static void main(String[] args) {
A a = new A();
a.dependOne(new B()); //A类通过接口去依赖B类
a.dependTwo(new B());
a.dependThree(new B());
C c = new C();
c.dependOne(new D());
c.dependFour(new D());
c.dependFive(new D()); //C类通过接口去依赖D类
}
}
代码说明:
- 一个类通过一个接口去依赖另一个类,我们希望我们依赖的接口是最小的,用不到的方法就进行隔离
- 隔离的手法就是将接口拆成若干个接口,再进行依赖,这样就不会出现资源浪费的情况
2.4.4、接口隔离原则的个人理解
我们可以把接口理解成三个东西
- 一组 API 接口集合
- 它可以是某个微服务的接口,也可以是某个类库的接口等等。在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。例如:后台管理系统删除用户的接口。
- 单个 API 接口或函数
- 函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。比如,count() 实现的几种函数。
- OOP 中的接口概念
- 比如 Java 中的 interface。那接口的设计要尽量单一(避免一个大而全的Config接口),不要让接口的实现类和调用者,依赖不需要的接口函数。如:Redis、MySQL、Kafka部分配置信息热更新,新的监控功能需求
2.5、依赖倒转原则
2.5.1、基本介绍
依赖倒转原则(Dependence Inversion Principle)是指:
- 1) 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 2) 抽象不应该依赖细节,细节应该依赖抽象
- 3) 依赖倒转(倒置)的中心思想是面向接口编程
- 4) 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类
- 5) 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
2.5.2、应用实例
请编程完成 Person 接收消息 的功能。
1) 实现方案:代码实现(不使用依赖倒转)
/**
* description
* Person类,实现接收消息的功能
*
* @author
* @since 2022/11/17 23:24
*/
public class Person {
public void receive(Email email){
System.out.println(email.getInto());
}
}
/**
* description
* Email类,设置字符串信息模拟电子邮件信息
* @author
* @since 2022/11/17 23:25
*/
public class Email {
public String getInto(){
return "电子邮件信息: Hello,world";
}
}
/**
* description
* 测试传统方法 Person 接收消息 的功能
*
* @author
* @since 2022/11/17 23:20
*/
public class DependencyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
方式一分析:
1、简单,比较容易想到并实现
2、如果我们获取的对象比较复杂时,则需要新增类,同时Person也要增加相应的接受方法,需要维护和实现的成本加大
3、解决思路:引入一个抽象的接口 IReceiver,表示接收者,这样Person于接口发生依赖
- 3.1、因为比较复杂的对象属于接收者的范围,他们各自实现IReceiver 接口就符合依赖倒转原则
接下来是使用依赖倒转原则后的代码实现:
/**
* description
* Person类,实现接收消息的功能
*
* @author
* @since 2022/11/17 23:24
*/
public class Person {
public void receive(IReceiver iReceiver){
System.out.println(iReceiver.getInfo());
}
}
/**
* description
*
* @author
* @since 2022/11/17 23:25
*/
public class Email implements IReceiver {
@Override
public String getInfo() {
return "电子邮件信息: Hello,world";
}
}
/**
* description
* 定义一个微信消息
*
* @author
* @since 2022/11/17 23:46
*/
public class WeiXin implements IReceiver {
@Override
public String getInfo() {
return "微信消息:hello,ok";
}
}
/**
* description
* 测试依赖倒置原则实现方法 Person 接收消息 的功能
*
* @author
* @since 2022/11/17 23:20
*/
public class DependencyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
}
}
代码说明:
- 方式二采用了依赖倒转原则,即实现Person发生消息的功能是对接口的依赖而非子类本身
- 所以代码中添加了出邮箱以外的微信也可以轻松实现,即抽象不依赖细节(具体的子类),细节应该依赖抽象(接口)
2.5.3、依赖关系传递的三种方式和应用案例
1) 接口传递
2) 构造方法传递
3) setter 方式传递
- 接下来是三种方式的代码实现
/**
* description
* 电视开关的接口
*
* @author
* @since 2022/11/18 0:33
*/
public interface IOpenAndClose {
void open(ITV tv); //抽象方法,接受接口
}
/**
* description
* ITV接口,定义电视开关的接口
*
* @author
* @since 2022/11/18 0:33
*/
public interface ITV {
void play();
}
/**
* description
* 第一种方式:接口传递依赖
*
* @author
* @since 2022/11/18 0:23
*/
public class OpenAndCloseOne implements IOpenAndClose {
@Override
public void open(ITV tv) {
tv.play();
}
}
/**
* description
* 定义电视打开的方法
*
* @author
* @since 2022/11/18 0:37
*/
public interface IOpenAndCloseTwo {
void open(); //抽象方法
}
/**
* description
* ITV接口
*
* @author
* @since 2022/11/18 0:39
*/
public interface ITVTwo {
void play();
}
/**
* description
* 第二种方式:使用构造器传递依赖
*
* @author
* @since 2022/11/18 0:40
*/
public class OpenAndCloseTwo implements IOpenAndCloseTwo {
public ITVTwo itvTwo; //成员
public OpenAndCloseTwo(ITVTwo itvTwo) {
this.itvTwo = itvTwo; //使用构造器传递依赖
}
@Override
public void open() {
this.itvTwo.play();
}
}
/**
* description
* 定义电视打开方式的接口
*
* @author
* @since 2022/11/18 0:44
*/
public interface IOpenAndCloseThree {
void open(); //抽象方法
}
/**
* description
* ITV接口
*
* @author
* @since 2022/11/18 0:44
*/
public interface ITVThree {
void play();
}
public class OpenAndCloseThree {
private ITVThree itVThree;
public void setTv(ITVThree itvThree){ //定义setter方法
this.itVThree = itvThree;
}
public void open(){
this.itVThree.play();
}
}
2.5.4、依赖倒转原则的注意事项和细节
1) 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.
2) 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
3) 继承时遵循里氏替换原则
2.6 、里氏替换原则
2.6.1、OO 中的继承性的思考和说明
- 1) 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
- 2) 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障
- 3) 问题提出:在编程中,如何正确的使用继承? => 里氏替换原则
2.6.2、基本介绍
- 1) 里氏替换原则(Liskov Substitution Principle)在 1988 年,由麻省理工学院的以为姓里的女士提出的。
- 2) 如果对每个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序P 在所有的对象o1都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
- 3) 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
- 4) 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题。.
2.6.3、一个程序引出的问题和思考
/**
* description
* A类
*
* @author
* @since 2022/11/18 15:18
*/
public class A {
//返回两个数的差
public int funcOne(int num1,int num2){
return num1 - num2;
}
}
/**
* description
* B类,继承了A类并增加了新的功能:完成两个数相加,然后和9求和,并重写了A类的方法
*
* @author
* @since 2022/11/18 15:19
*/
public class B extends A {
@Override
public int funcOne(int num1, int num2) {
return num1 + num2;
}
public int funTwo(int a, int b){
return funcOne(a,b) + 9;
}
}
/**
* description
* 里氏替换原则的案例
*
* @author
* @since 2022/11/18 15:08
*/
public class LiskovOne {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3 = " + a.funcOne(11,3));
System.out.println("1-8 = " + a.funcOne(1,8));
System.out.println("-------------------");
B b = new B();
System.out.println("11-3 =" + b.funcOne(11,3));
System.out.println("1-8 =" + b.funcOne(1,8));
System.out.println("11+3+9=" + b.funTwo(11,3));
}
}
代码说明:首先在A类中定义两个数的差值,并在B类中重写A类的该方法,且B类在重写了该方法后在B类中重写了A类的方法,所以在程序测试的输出结果中,分割线前的结果是正确的,而重写了A类时再次调用方法进行计算就出现了偏差
2.6.4、解决方法
1) 我们发现原来运行正常的相减功能发生了错误。原因就是类 B 无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候
2) 通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替
3) 改进方案 :
- 即让A类和B类都继承另一个结构更简单的类,取消继承关系,改为依赖关系
4)代码实现如下:
/**
* description
* 把更加基础的方法和成员写到Base类中
*
* @author
* @since 2022/11/18 16:00
*/
public class Base {
}
/**
* description
* A类
*
* @author
* @since 2022/11/18 15:18
*/
public class A extends Base{
//返回两个数的差
public int funcOne(int num1,int num2){
return num1 - num2;
}
}
/**
* description
* B类,使用了里氏替换原则之后
*
* @author
* @since 2022/11/18 15:19
*/
public class B extends Base {
//如果B需要使用A类的方法,使用组合关系
private A a = new A();
public int funcOne(int num1, int num2) {
return num1 + num2;
}
public int funcTwo(int a, int b){
return funcOne(a,b) + 9;
}
//仍然想调用A的方法
public int funcThree(int a, int b){
return this.a.funcOne(a,b);
}
}
/**
* description
* 里氏替换原则的案例
*
* @author
* @since 2022/11/18 15:08
*/
public class LiskovTwo {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3 = " + a.funcOne(11,3));
System.out.println("1-8 = " + a.funcOne(1,8));
System.out.println("-------------------");
B b = new B();
//B类不再继承A类,因此调用者,不会再是求减法,调用完成目的就会很明确
System.out.println("11+3 =" + b.funcOne(11,3));
System.out.println("1+8 =" + b.funcOne(1,8));
System.out.println("11+3+9=" + b.funcTwo(11,3));
//若是有需要使用到减法,使用聚合就可以完成
System.out.println("11-3 =" + b.funcThree(11,3));
}
}
代码说明:将A类和B类的继承关系解除并让其都继承更简单的基类---Base类,达到通过聚合的方式进行依赖,解决了通过直接继承带来的入侵性,而是通过子类和父类共同继承更基层的基类,并使用聚合、依赖组合等方式就能实现其相同的功能
2.7 、开闭原则
2.7.1、基本介绍
1) 开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则
2) 一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)用抽象构建框架,用实现扩展细节。
3) 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
4) 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
2.7.2、看下面一段代码
- 看一个画图形的功能。
- 代码演示如下
/**
* description
* 矩形
*
* @author
* @since 2022/11/18 17:20
*/
public class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
/**
* description
* 圆形
*
* @author
* @since 2022/11/18 17:21
*/
public class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
/**
* description
* 这是一个用于绘图的类
*
* @author
* @since 2022/11/18 16:53
*/
public class GraphicEditor {
public void drawShape(Shape s){
if (s.m_type == 1){
drawRectangle(s);
}else if (s.m_type ==2){
drawCircle(s);
}
}
public void drawRectangle(Shape r){
System.out.println("绘制矩形");
}
public void drawCircle(Shape r){
System.out.println("绘制圆形");
}
}
/**
* description
* 基类
*
* @author
* @since 2022/11/18 16:57
*/
public class Shape {
int m_type;
}
/**
* description
* 开闭原则的案例
*
* @author
* @since 2022/11/18 16:52
*/
public class Ocp {
public static void main(String[] args) {
//测试并查看存在的问题
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
}
}
2.7.3、方式 1 的优缺点
代码说明:
- 1) 优点是比较好理解,简单易操作。
- 2) 缺点是违反了设计模式的 ocp 原则,即对扩展开放(提供方),对修改关闭(使用方)。即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码.
- 3) 比如我们这时要新增加一个图形种类 三角形,我们需要做修改且修改的地方较多,即使用方和调用方都需更改
- 4) 代码演示
2.7.4、改进的思路分析
思路:把创建 Shape 类做成抽象类,并提供一个抽象的 draw 方法,让子类去实现即可,这样我们有新的图形种类时,只需要让新的图形类继承 Shape,并实现 draw 方法即可,使用方的代码就不需要修改-> 满足了开闭原则
改进后的代码:
/**
* description
* 基类做成抽象类,并定义抽象方法
*
* @author
* @since 2022/11/18 16:57
*/
abstract class ShapeOne {
public abstract void draw(); //抽象方法
}
/**
* description
* 圆形
*
* @author
* @since 2022/11/18 17:21
*/
public class Circle extends ShapeOne {
Circle() {
super.m_type = 2;
}
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
/**
* description
* 三角形
*
* @author
* @since 2022/11/18 17:45
*/
public class Triangle extends ShapeOne {
Triangle() {
super.m_type = 3;
}
@Override
public void draw() {
System.out.println("绘制三角型");
}
}
/**
* description
* 矩形
*
* @author
* @since 2022/11/18 17:20
*/
public class Rectangle extends ShapeOne {
Rectangle() {
super.m_type = 1;
}
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
/**
* description
* 新增绘制其他图形的类
*
* @author
* @since 2022/11/18 21:26
*/
public class OtherGraphic extends ShapeOne {
OtherGraphic() {
super.m_type = 4;
}
@Override
public void draw() {
System.out.println("绘制其他图形");
}
}
/**
* description
* 开闭原则的案例的改进
*
* @author
* @since 2022/11/18 16:52
*/
public class Ocp {
public static void main(String[] args) {
//测试并查看存在的问题
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
graphicEditor.drawShape(new OtherGraphic());
}
}
代码说明:经开闭原则改进后的代码经测试后实现的功能与先前的完全一致,且在增加新功能时(新图形)只需在所需的子类实现抽象接口中的抽象方法即可,无需对已经实现的功能去做做修改,而是在其基础上新增,增加了代码的可维护性。
2.8 、迪米特法则
2.8.1、基本介绍
- 1) 一个对象应该对其他对象保持最少的了解
- 2) 类与类关系越密切,耦合度越大
- 3) 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息
- 4) 迪米特法则还有个更简单的定义:只与直接的朋友通信
- 5) 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
2.8.2、应用实例
1) 有一个学校,下属有各个学院和总部,现要求打印出学校总部员工 ID 和学院员工的id
2) 编程实现上面的功能, 看代码演示
/**
* description
* 员工类(学校总部员工)
*
* @author
* @since 2022/11/18 22:24
*/
public class Employee {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
/**
* description
* 学院员工类
*
* @author
* @since 2022/11/18 22:26
*/
public class CollegeEmployee {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
/**
* description
* 管理学院员工的类
*
* @author
* @since 2022/11/18 22:27
*/
public class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee(){
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里增加了十个员工到集合里
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id=" + i);
list.add(emp);
}
return list;
}
}
/**
* description
* 学校管理类
*
* @author
* @since 2022/11/18 22:31
*/
public class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) { //增加了五个员工到list
Employee emp = new Employee();
emp.setId("学校总部员工id=" + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工学校的方法
void printAllEmployee(CollegeManager sub){
//获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("-------------学院员工-------------");
for(CollegeEmployee e : list1){
System.out.println(e.getId());
}
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("--------------学校总部员工-----------");
for (Employee e : list2){
System.out.println(e.getId());
}
}
}
public class DemeterOne {
public static void main(String[] args) {
//创建了一个SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出了学院员工id和学校总部的员工id
schoolManager.printAllEmployee(new CollegeManager());
}
}
代码说明:代码中并没有过于复杂的结构,但是我们可以从中折射出迪米特法则中对于直接朋友和陌生朋友的定义
- SchoolManager 类中 Employee、CollegeManager都是它的直接朋友
- 因为Employee在对外除了提供的 public 方法中
- CollegeManager出现在方法参数中
- CollegeEmployee 是SchoolManager 类中的陌生类
- 因为它没有满足出现成员变量,方法参数,方法返回值任一条件中,而是以局部变量出现的,违背了迪米特法则
2.8.3、应用实例改进
1) 前面设计的问题在于 SchoolManager 中,CollegeEmployee 类并不是 SchoolManager 类的直接朋友(分析)
2) 按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合
3) 对代码按照迪米特法则 进行改进,发现问题并解决
4) 即将输出学院员工的方法,封装到CollegeManager类,然后再使用改对象调用即可
5) 中心思想就是把具体实现写道自己的类里,不要在别的类里写具体实现
6) 代码演示如下
/**
* description
* 学校管理类
*
* @author
* @since 2022/11/18 22:31
*/
public class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) { //增加了五个员工到list
Employee emp = new Employee();
emp.setId("学校总部员工id=" + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工学校的方法
void printAllEmployee(CollegeManager sub){
//将输出学院员工的方法,封装到CollegeManager类即可
sub.printEmployee();
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("--------------学校总部员工-----------");
for (Employee e : list2){
System.out.println(e.getId());
}
}
}
2.8.4、迪米特法则注意事项和细节
1) 迪米特法则的核心是降低类之间的耦合
2) 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系
2.9 、合成复用原则(Composite Reuse Principle)
原则是尽量使用合成/聚合的方式,而不是使用继承
- 即A类有三个方法,我们只是想让B类使用A类的方法,使用继承就会增加A类和B类的耦合性
- 那么不通过继承的方式,怎么让B类使用A的方法呢
- 方式一:在B类的方法参数里传入一个A的实例就可以使用A类的方法了——这种方式就是依赖
- 方式二:在B类中创建把A对象的实例,即把A变成成员变量也可以直接使用A类的方法——这种方式就是组合
- 方式三:在B类中创建对象A,并创建其set方法,就可以使用A类的方法——聚合
- 以上三种方式即达到了使用A类的方法,也没有使用继承的方式,这样做的好处是让A类和B类的耦合度降低
2.10 、设计原则核心思想
1) 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
2) 针对接口编程,而不是针对实现编程。
3) 为了交互对象之间的松耦合设计而努力
三、设计模式概述
3.1、 掌握设计模式的层次
1) 第 1 层:刚开始学编程不久,听说过什么是设计模式
2) 第 2 层:有很长时间的编程经验,自己写了很多代码,其中用到了设计模式,但是自己却不知道
3) 第 3 层:学习过了设计模式,发现自己已经在使用了,并且发现了一些新的模式挺好用的
4) 第 4 层:阅读了很多别人写的源码和框架,在其中看到别人设计模式,并且能够领会设计模式的精妙和带来的好处。
5) 第 5 层:代码写着写着,自己都没有意识到使用了设计模式,并且熟练的写了出来。
3.2 、设计模式介绍
1) 设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是某类问题的通用解决方案,设计模式(Design pattern)代表了最佳的实践。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
2) 设计模式的本质提高 软件的维护性,通用性和扩展性,并降低软件的复杂度。
3) <<设计模式>> 是经典的书,作者是 Erich Gamma、Richard Helm、Ralph Johnson 和John Vlissides Design(俗称 “四人组 GOF”)
4) 设计模式并不局限于某种语言,java,php,c++ 都有设计模式.3 设计模式类型
3.3、 设计模式类型
设计模式分为三种类型,共 23 种
1) 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式
2) 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
3) 行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter 模式)、状态模式、策略模式、职责链模式(责任链模式)。
四、单例设计模式
4.1 、单例设计模式介绍
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
比如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够,这是就会使用到单例模式。
4.2 、单例设计模式八种方式
单例模式有八种方式:
1) 饿汉式(静态常量)
2) 饿汉式(静态代码块)
3) 懒汉式(线程不安全)
4) 懒汉式(线程安全,同步方法)
5) 懒汉式(线程安全,同步代码块)
6) 双重检查
7) 静态内部类
8) 枚举
4.3 、饿汉式(静态常量)
饿汉式(静态常量)应用实例
步骤如下:
1) 构造器私有化 (防止 new )
2) 类的内部创建对象
3) 向外暴露一个静态的公共方法。getInstance
4) 代码实现
/**
* description
* 饿汉式—静态变量
*
* @author
* @since 2022/11/19 20:35
*/
public class Singleton {
//1、首先将构造器私有化,外部无法通过new创建对应的对象实例
private Singleton(){
}
//2、在本类内部创建对象实例
private final static Singleton instance = new Singleton();
//3、对外提供一个公有的静态方法,返回实例对象即可
public static Singleton getInstance(){
return instance;
}
}
/**
* description
* 单例模式的第一种写法—静态常量—饿汉式
*
* @author
* @since 2022/11/19 20:34
*/
public class SingletonOne {
public static void main(String[] args) {
//测试
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}
代码说明:
- 1) 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 2) 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 3) 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果
- 4) 结论:这种单例模式可用,可能造成内存浪费
4.4 、饿汉式(静态代码块)
- 代码演示:
/**
* description
* 饿汉式—静态变量
*
* @author
* @since 2022/11/19 20:35
*/
public class Singleton {
//1、首先将构造器私有化,外部无法通过new创建对应的对象实例
private Singleton() {
}
private static Singleton instance;
static{ //2、在静态代码块中创建单例对象
instance = new Singleton();
}
//3、对外提供一个公有的静态方法,返回实例对象即可
public static Singleton getInstance() {
return instance;
}
}
/**
* description
* 单例模式的第二种写法—静态代码块—饿汉式
*
* @author
* @since 2022/11/19 20:34
*/
public class SingletonTwo {
public static void main(String[] args) {
//测试
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}
代码说明:
1) 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
2) 结论:这种单例模式可用,但是可能造成内存浪费
4.5、懒汉式(线程不安全)
- 代码演示
/**
* description
* 懒汉式(线程不安全)
*
* @author
* @since 2022/11/19 21:52
*/
public class Singleton {
//1、创建一个没有初始化的静态对象
private static Singleton instance;
//2、将构造器私有化,外部无法通过new创建对应的对象实例
private Singleton() {
}
//3、提供一个静态的公有方法,当使用到该方法式才去创建相应实例,即懒汉式
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
/**
* description
* 单例模式的第三种写法—懒汉式
*
* @author
* @since 2022/11/19 21:46
*/
public class SingletonThree {
public static void main(String[] args) {
//测试
System.out.println("懒汉式,线程不安全");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}
代码分析:
1) 起到了 Lazy Loading 的效果,但是只能在单线程下使用。
2) 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
3) 结论:在实际开发中,不要使用这种方式.
4.6 、懒汉式(线程安全,同步方法)
- 代码实现
/**
* description
* 懒汉式(线程安全)
*
* @author
* @since 2022/11/19 21:52
*/
public class Singleton {
//1、创建一个没有初始化的静态对象
private static Singleton instance;
//2、将构造器私有化,外部无法通过new创建对应的对象实例
private Singleton() {
}
//3、提供一个静态的公有方法,加入了synchronized锁,解决线程安全问题
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
/**
* description
* 单例模式的第四种写法—懒汉式(线程安全)
*
* @author
* @since 2022/11/19 20:34
*/
public class SingletonFour {
public static void main(String[] args) {
//测试
System.out.println("懒汉式,线程安全");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}
代码分析:
- 1) 解决了线程安全问题
- 2) 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低
- 3) 结论:在实际开发中,不推荐使用这种方式
4.7 、懒汉式(线程不安全,同步代码块)
- 代码演示
/**
* description
* 懒汉式(线程不安全,同步代码块)
* @author
* @since 2022/11/19 22:50
*/
public class Singleton {
//1、创建一个没有初始化的静态对象
private static Singleton instance;
//2、将构造器私有化,外部无法通过new创建对应的对象实例
private Singleton(){
}
//3、提供一个静态的公有方法,在代码块加入了synchronized锁,但并没有解决线程安全问题
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
代码说明:
1) 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低, 改为同步产生实例化的的代码块
2) 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一 致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行, 另一个线程也通过了这个判断语句,这时便会产生多个实例
3) 结论:在实际开发中,不能使用这种方式
4.8 、双重检查
- 代码演示:
/**
* description
* 双重检查
*
* @author
* @since 2022/11/19 21:52
*/
public class Singleton {
//1、创建一个没有初始化的静态对象,使用volatile修饰,作用是能让此共享变量有修改值立刻更新到储存
private static volatile Singleton instance;
//2、将构造器私有化,外部无法通过new创建对应的对象实例
private Singleton() {
}
//3、提供一个静态的公有方法,加入了双重检查代码,解决线程安全问题,同时解决懒加载的问题
public static synchronized Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
//4、双重判断可以保证在多线程执行到这里的时候只有一个线程进入,保证了效率
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
/**
* description
* 单例模式的第六种写法—双重检查
*
* @author
* @since 2022/11/19 20:34
*/
public class SingletonSix {
public static void main(String[] args) {
//测试
System.out.println("双重检查");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}
代码优缺点说明:
1) Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
2) 这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接return 实例化对象,也避免的反复进行方法同步.
3) 线程安全;延迟加载;效率较高
4) 结论:在实际开发中,推荐使用这种单例设计模式
4.9 、静态内部类
- 代码实现:
/**
* description
* 静态内部类
*
* @author
* @since 2022/11/19 21:52
*/
public class Singleton {
//1、创建一个没有初始化的静态对象,使用volatile修饰,作用是能让此共享变量有修改值立刻更新到储存
private static volatile Singleton instance;
//2、将构造器私有化,外部无法通过new创建对应的对象实例
private Singleton() {
}
//3、写一个静态内部类,该类中有一个静态属性Singleton
private static class SingletonInstance{
private static final Singleton INSTANCE = new Singleton();
}
//4、提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static synchronized Singleton getInstance(){
return SingletonInstance.INSTANCE;
}
}
/**
* description
* 单例模式的第七种写法—静态内部类
*
* @author
* @since 2022/11/19 20:34
*/
public class SingletonSeven {
public static void main(String[] args) {
//测试
System.out.println("使用静态内部类完成单例模式");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}
代码分析:
1) 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
2) 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
3) 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
4) 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
5) 结论:推荐使用
4.10 、枚举
- 代码演示
/**
* description
* 使用枚举也可以使用单例
*
* @author
* @since 2022/11/19 23:52
*/
public enum Singleton {
INSTANCE;
public void sayOk() {
System.out.println("ok");
}
}
/**
* description
* 单例模式的第八种写法—枚举
*
* @author
* @since 2022/11/19 23:51
*/
public class SingletonEight {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2);
instance.sayOk();
}
}
代码说明:
1) 这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。 2) 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式
3) 结论:推荐使用
4.11 、单例模式在 JDK 应用的源码分析
- 1) 我们 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)
- 2) 代码分析+Debug 源码+代码说明
- 3) Runtime源码中使用了经典的单例模式中的饿汉式
4.12、 单例模式注意事项和细节说明
1) 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
2) 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
3) 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)
五、工厂模式
5.1、简单工厂模式
5.1.1、看一个具体的需求
看一个披萨的项目:要便于披萨种类的扩展,要便于维护
1) 披萨的种类很多(比如 GreekPizz、CheesePizz 等)
2) 披萨的制作有 prepare,bake, cut, box
3) 完成披萨店订购功能
5.1.2、使用传统的方式来完成
1) 思路分析(类图)
2)代码演示
/**
* description
* 披萨的抽象类
*
* @author
* @since 2022/11/20 15:56
*/
public abstract class Pizza {
//披萨名字
protected String name;
//准备原材料的方法,由于每种类型的披萨制作过程不一样所以定义一个抽象方法
public abstract void prepare();
//烘烤披萨的方法
public void bake() {
System.out.println(name + "制作完毕");
}
//分割披萨的方法
public void cut() {
System.out.println(name + "切割完毕");
}
//打包披萨的方法
public void box(){
System.out.println(name + "打包完毕");
}
//setter
public void setName(String name) {
this.name = name;
}
}
/**
* description
* 希腊披萨
*
* @author
* @since 2022/11/20 16:03
*/
public class GreekPizza extends Pizza {
@Override
public void prepare() {
System.out.println("准备希腊披萨需要用到的原材料");
}
}
/**
* description
* 中国披萨
*
* @author
* @since 2022/11/20 16:09
*/
public class ChinesePizza extends Pizza{
@Override
public void prepare() {
System.out.println("准备中国披萨需要用到的原材料");
}
}
/**
* description
* 奶酪披萨
*
* @author
* @since 2022/11/20 16:02
*/
public class CheesePizza extends Pizza {
@Override
public void prepare() {
System.out.println("准备奶酪披萨需要用到的材料");
}
}
/**
* description
* 披萨店,完成订购披萨
*
* @author
* @since 2022/11/20 16:05
*/
public class OrderPizza {
//构造器
public OrderPizza() {
Pizza pizza = null;
String orderType; //需要订购披萨的类型
do{
orderType = getType();
if ("greek".equals(orderType)){
pizza = new GreekPizza();
pizza.setName("希腊披萨");
} else if ("cheese".equals(orderType)) {
pizza = new CheesePizza();
pizza.setName("奶酪披萨");
} else if ("chinese".equals(orderType)){
pizza = new ChinesePizza();
pizza.setName("中国披萨");
} else {
break;
}
//输出披萨制作的过程
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}while (true);
}
//写一个方法,可以动态的获取客户希望订购的披萨种类
private String getType(){
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入您需要点的披萨种类:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
/**
* description
* 客户端--用于发起订购披萨
*
* @author
* @since 2022/11/20 16:18
*/
public class PizzaStore {
public static void main(String[] args) {
new OrderPizza();
}
}
5.1.3、传统的方式的优缺点
1) 优点是比较好理解,简单易操作。
2) 缺点是违反了设计模式的 ocp 原则,即对扩展开放,对修改关闭。即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码.
3) 比如我们这时要新增加一个 Pizza 的种类(Pepper 披萨),我们需要做如下修改. 如果我们增加一个 Pizza 类,只要是订购 Pizza 的代码都需要修改
4) 改进的思路分析
分析:修改代码可以接受,但是如果我们在其它的地方也有创建 Pizza 的代码,就意味着,也需要修改,而创建Pizza的代码,往往有多处。
思路:把创建 Pizza 对象封装到一个类中,这样我们有新的 Pizza 种类时,只需要修改该类就可,其它有创建到Pizza对象的代码就不需要修改了. --》这就是简单工厂模式
5.1.4、基本介绍
1) 简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式
2) 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为(代码)
3) 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式
5.1.5、使用简单工厂模式
1) 简单工厂模式的设计方案: 定义一个可以实例化 Pizaa 对象的类,封装创建对象的代码。
2)代码实现如下:
/**
* description
* 简单工厂类
*
* @author
* @since 2022/11/20 17:04
*/
public class SimpleFactory {
//根据orderType返回对应的Pizza对象
public Pizza createPizza(String orderType) {
Pizza pizza = null;
System.out.println("使用简单工厂模式");
if ("greek".equals(orderType)) {
pizza = new GreekPizza();
pizza.setName("希腊披萨");
} else if ("cheese".equals(orderType)) {
pizza = new CheesePizza();
pizza.setName("奶酪披萨");
} else if ("chinese".equals(orderType)) {
pizza = new ChinesePizza();
pizza.setName("中国披萨");
}
return pizza;
}
}
/**
* description
* 披萨店,完成订购披萨
*
* @author
* @since 2022/11/20 16:05
*/
public class OrderPizza {
//定义一个简单工厂对象
SimpleFactory simpleFactory;
Pizza pizza = null;
//定义构造器
public OrderPizza(SimpleFactory simpleFactory) {
setSimpleFactory(simpleFactory);
}
public void setSimpleFactory(SimpleFactory simpleFactory) {
String orderType = ""; //由用户输入的
this.simpleFactory = simpleFactory; //设置一个简单工厂对象
do {
orderType = getType();
pizza = this.simpleFactory.createPizza(orderType);
//输出披萨信息
if (pizza != null) { //订购成功
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} else {
System.out.println("订购披萨失败");
break;
}
} while (true);
}
//写一个方法,可以动态的获取客户希望订购的披萨种类
private String getType() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入您需要点的披萨种类:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
/**
* description
* 客户端--用于发起订购披萨
*
* @author
* @since 2022/11/20 16:18
*/
public class PizzaStore {
public static void main(String[] args) {
//使用简单工厂模式
new OrderPizza(new SimpleFactory());
System.out.println("退出程序");
}
}
代码说明: 简单工厂模式提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程,只需知道具体产品类所对应的参数即可,通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
但缺点在于不符合“开闭原则”,每次添加新产品就需要修改工厂类。在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展维护,并且工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
5.2 、工厂方法模式
5.2.1、看一个新的需求
披萨项目新的需求:客户在点披萨时,可以点不同口味的披萨,比如 北京的奶酪pizza、北京的胡椒pizza或者是伦敦的奶酪 pizza、伦敦的胡椒 pizza
5.2.2、思路 1
使用简单工厂模式,创建不同的简单工厂类,比如 BJPizzaSimpleFactory、LDPizzaSimpleFactory 等等.从当前这个案例来说,也是可以的,但是考虑到项目的规模,以及软件的可维护性、可扩展性并不是特别好
5.2.3、思路 2
使用另一种工厂模式:工厂方法模式
5.2.4、工厂方法模式介绍
1) 工厂方法模式设计方案:将披萨项目的实例化功能抽象成抽象方法,在不同的口味点餐子类中具体实现。
2) 工厂方法模式:定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
5.2.5、工厂方法模式应用案例
1) 披萨项目新的需求:客户在点披萨时,可以点不同口味的披萨,比如 北京的奶酪pizza、北京的胡椒pizza或者是伦敦的奶酪 pizza、伦敦的胡椒 pizza
2) 思路分析图解
3) 代码实现
/**
* description
* 北京披萨子类工厂
*
* @author
* @since 2022/11/20 23:27
*/
public class BJorderPizza extends OrderPizza {
@Override
Pizza createPizza(String orderType) {
Pizza pizza = null;
if ("cheese".equals(orderType)){
pizza = new BJCheesePizza();
} else if ("pepper".equals(orderType)){
pizza = new BJPepperPizza();
}
return pizza;
}
}
/**
* description
* 伦敦披萨子类工厂
*
* @author
* @since 2022/11/20 23:27
*/
public class LDorderPizza extends OrderPizza {
@Override
Pizza createPizza(String orderType) {
Pizza pizza = null;
if ("cheese".equals(orderType)){
pizza = new LDCheesePizza();
} else if ("pepper".equals(orderType)){
pizza = new LDPepperPizza();
}
return pizza;
}
}
/**
* description
* 披萨店,完成订购披萨
*
* @author
* @since 2022/11/20 16:05
*/
public abstract class OrderPizza {
//定义一个抽象方法,createPizza,让各个工厂子类自己实现
abstract Pizza createPizza(String orderType);
//构造器
public OrderPizza() {
Pizza pizza = null;
String orderType; //需要订购披萨的类型
do {
orderType = getType();
pizza = createPizza(orderType); //抽象方法,由工厂子类完成
//输出披萨制作的过程
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} while (true);
}
//写一个方法,可以动态的获取客户希望订购的披萨种类
private String getType() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入您需要点的披萨种类:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
代码说明:工厂方法模式将工厂抽象化,并定义一个创建对象的接口。每增加新产品,只需增加该产品以及对应的具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码。在使用时,用于只需知道产品对应的具体工厂,关注具体的创建过程,甚至不需要知道具体产品类的类名,当我们选择哪个具体工厂时,就已经决定了实际创建的产品是哪个了。
但缺点在于,每增加一个产品都需要增加一个具体产品类和实现工厂类,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
5.3、 抽象工厂模式
5.3.1、基本介绍
1) 抽象工厂模式:定义了一个 interface 用于创建相关或有依赖关系的对象簇,而无需指明具体的类
2) 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合。
3) 从设计层面看,抽象工厂模式就是对简单工厂模式的改进(或者称为进一步的抽象)。
4) 将工厂抽象成两层,AbsFactory(抽象工厂) 和 具体实现的工厂子类。程序员可以根据创建对象类型使用对应的工厂子类。这样将单个的简单工厂类变成了工厂簇,更利于代码的维护和扩展。
5) 类图
5.3.2、抽象工厂模式应用实例
- 使用抽象工厂模式来完成披萨项目
/**
* description
* 抽象工厂模式的抽象层
*
* @author
* @since 2022/11/21 15:32
*/
public interface AbsFactory {
//让下面的工厂子类来具体实现
Pizza createPizza(String OrderType);
}
/**
* description
* 北京披萨的子类工厂
*
* @author
* @since 2022/11/21 15:34
*/
public class BJFactory implements AbsFactory {
@Override
public Pizza createPizza(String OrderType) {
System.out.println("使用的是抽象工厂模式");
Pizza pizza = null;
if ("cheese".equals(OrderType)) {
pizza = new BJCheesePizza();
} else if ("pepper".equals(OrderType)) {
pizza = new BJPepperPizza();
}
return pizza;
}
}
/**
* description
* 伦敦披萨的子类工厂
*
* @author
* @since 2022/11/21 15:36
*/
public class LDFactory implements AbsFactory {
@Override
public Pizza createPizza(String OrderType) {
System.out.println("使用的是抽象工厂模式");
Pizza pizza = null;
if ("cheese".equals(OrderType)) {
pizza = new LDCheesePizza();
} else if ("pepper".equals(OrderType)) {
pizza = new LDPepperPizza();
}
return pizza;
}
}
/**
* description
* 披萨店,完成订购披萨
*
* @author
* @since 2022/11/21 15:40
*/
public class OrderPizza {
AbsFactory factory;
//构造器
public OrderPizza(AbsFactory factory) {
setFactory(factory);
}
private void setFactory(AbsFactory factory) {
Pizza pizza = null;
String orderType = ""; //用户输入
this.factory = factory;
do {
orderType = getType();
//factory可能是北京的工厂子类,也可能是伦敦的
pizza = factory.createPizza(orderType);
if (pizza != null){ //订购ok
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} else {
System.out.println("订购失败");
break;
}
} while (true);
}
//写一个方法,可以动态的获取客户希望订购的披萨种类
private String getType() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入您需要点的披萨种类:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
/**
* description
* 客户端
*
* @author
* @since 2022/11/21 15:56
*/
public class PizzaStore {
public static void main(String[] args) {
new OrderPizza(new BJFactory());
}
}
代码说明:抽象工厂模式主要用于创建相关对象的家族。当一个产品族中需要被设计在一起工作时,通过抽象工厂模式,能够保证客户端始终只使用同一个产品族中的对象;并且通过隔离具体类的生成,使得客户端不需要明确指定具体生成类;所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
但该模式的缺点在于添加新的行为时比较麻烦,如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。
5.4、 工厂模式在 JDK-Calendar 应用的源码分析
- 源码部分
public static Calendar getInstance(TimeZone zone)
{
return createCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));
}
/**
* 使用默认时区和指定的地区获取日历。返回的<code>Calendar<code>基于默认时区的当前时间和给定的地区
*
* @param 为周数据设置locale
* @return 一个日历.
*/
public static Calendar getInstance(Locale aLocale)
{
return createCalendar(TimeZone.getDefault(), aLocale);
}
/**
* 获取具有指定时区和地区的日历。返回的<code>Calendar<code>基于给定时区和给定地区的当前时间
*
* @param 设置要使用的时区
* @param 为周数据设置locale
* @return 一个日历.
*/
public static Calendar getInstance(TimeZone zone,
Locale aLocale)
{
return createCalendar(zone, aLocale);
}
private static Calendar createCalendar(TimeZone zone,
Locale aLocale)
{
CalendarProvider provider =
LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
.getCalendarProvider();
if (provider != null) {
try {
return provider.getInstance(zone, aLocale);
} catch (IllegalArgumentException iae) {
// fall back to the default instantiation
}
}
Calendar cal = null;
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType("ca");
if (caltype != null) {
switch (caltype) {
case "buddhist":
cal = new BuddhistCalendar(zone, aLocale);
break;
case "japanese":
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case "gregory":
cal = new GregorianCalendar(zone, aLocale);
break;
}
}
}
if (cal == null) {
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
&& aLocale.getCountry() == "JP") {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
}
5.5、 工厂模式小结
工厂模式小结:
- 三种工厂模式 (简单工厂模式、工厂方法模式、抽象工厂模式)
工厂方法模式与抽象工厂模式的区别在于:
- (1)工厂方法只有一个抽象产品类和一个抽象工厂类,但可以派生出多个具体产品类和具体工厂类,每个具体工厂类只能创建一个具体产品类的实例。
- (2)抽象工厂模式拥有多个抽象产品类(产品族)和一个抽象工厂类,每个抽象产品类可以派生出多个具体产品类;抽象工厂类也可以派生出多个具体工厂类,同时每个具体工厂类可以创建多个具体产品类的实例
工厂模式的意义 :
- 将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性。
- 设计模式的依赖抽象原则
六、原型模式
6.1 、克隆羊问题
- 需求:现在有一只羊 tom,姓名为: tom, 年龄为:1,颜色为:白色,请编写程序创建和tom 羊属性完全相同的10只羊。
6.2 、传统方式解决克隆羊问题
1) 思路分析(图解)
代码实现
/**
* description
* 原型模式——克隆羊问题的传统解决方式——相当于羊的实体类
*
* @author
* @since 2022/11/21 17:20
*/
public class SheepOne {
private String name; //名字
private int age; //年龄
private String color; //颜色
//有参构造、getter、setter、toString方法生成....
}
public class ClientOne {
public static void main(String[] args) {
SheepOne sheepOne = new SheepOne("tom", 1, "白色");
SheepOne sheepOne1 = new SheepOne(sheepOne.getName(), sheepOne.getAge(), sheepOne.getColor());
SheepOne sheepOne2 = new SheepOne(sheepOne.getName(), sheepOne.getAge(), sheepOne.getColor());
SheepOne sheepOne3 = new SheepOne(sheepOne.getName(), sheepOne.getAge(), sheepOne.getColor());
...
System.out.println(sheepOne);
System.out.println(sheepOne1);
System.out.println(sheepOne2);
System.out.println(sheepOne3);
}
}
6.3 、传统的方式的优缺点
1) 优点是比较好理解,简单易操作。
2) 在创建新的对象时,总是需要重新获取原始对象的属性,如果创建的对象比较复杂时,效率较低
3) 总是需要重新初始化对象,而不是动态地获得对象运行时的状态, 不够灵活
4) 改进的思路分析
思路:
- Java 中 Object 类是所有类的根类,Object 类提供了一个 clone()方法,该方法可以将一个Java 对象复制一份,但是需要实现 clone 的 Java 类必须要实现一个接口 Cloneable,该接口表示该类能够复制且具有复制的能力=>原型模式
6.4、 原型模式-基本介绍
基本介绍
- 1) 原型模式(Prototype 模式)是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
- 2) 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
- 3) 工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即 对象.clone()
- 4) 形象的理解:孙大圣拔出猴毛, 变出其它孙大圣
6.5 、原型模式原理结构图-uml 类图
- 原理结构图说明
- 1) Prototype : 原型类,声明一个克隆自己的接口
- 2) ConcretePrototype: 具体的原型类, 实现一个克隆自己的操作
- 3) Client: 让一个原型对象克隆自己,从而创建一个新的对象(属性一样)
6.6、 原型模式解决克隆羊问题的应用实例
使用原型模式改进传统方式,让程序具有更高的效率和扩展性
- 代码实现如下
/**
* description
* 原型模式——克隆羊问题的传统解决方式——相当于羊的实体类
*
* @author
* @since 2022/11/21 17:20
*/
public class SheepOne {
private String name; //名字
private int age; //年龄
private String color; //颜色
//有参构造、getter、setter、toString方法生成....
}
/**
* 克隆该实例,使用默认的克隆方法完成
* @return 返回该实例对应对象
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() {
SheepTwo sheepTwo = null;
try {
sheepTwo = (SheepTwo) super.clone();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return sheepTwo;
}
}
/**
* description
* 原型解决克隆羊的问题
*
* @author
* @since 2022/11/21 17:23
*/
public class ClientTwo {
public static void main(String[] args) {
System.out.println("使用原型模式完成对象的克隆");
SheepTwo sheepTwo = new SheepTwo("Tom",2,"黑色");
SheepTwo sheepTwo2 = (SheepTwo) sheepTwo.clone(); //克隆
SheepTwo sheepTwo3 = (SheepTwo) sheepTwo.clone(); //克隆
SheepTwo sheepTwo4 = (SheepTwo) sheepTwo.clone(); //克隆
System.out.println("sheepTwo" + sheepTwo);
System.out.println("sheepTwo2" + sheepTwo2);
System.out.println("sheepTwo3" + sheepTwo3);
System.out.println("sheepTwo4" + sheepTwo4);
}
}
6.7 、原型模式在 Spring 框架中源码分析
1) Spring 中原型 bean 的创建,就是原型模式的应用
2) 代码分析+Debug 源码2
3)创建一个实体类对象并且配置相应的bean,在测试类中测试并debug,进入源码
6.8 、深入讨论-浅拷贝和深拷贝
6.8.1、浅拷贝的介绍
1) 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
2) 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
3) 前面我们克隆羊就是浅拷贝
4) 浅拷贝是使用默认的 clone()方法来实现 sheep = (Sheep) super.clone();
6.8.2、深拷贝基本介绍
1) 复制对象的所有基本数据类型的成员变量值
2) 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
3) 深拷贝实现方式 1:重写 clone 方法来实现深拷贝
4) 深拷贝实现方式 2:通过对象序列化实现深拷贝(推荐)
6.9 、深拷贝应用实例
1) 使用 重写 clone 方法实现深拷贝
2) 使用序列化来实现深拷贝
3) 代码演示
/**
* description
* 深拷贝的原型模式案例
*
* @author
* @since 2022/11/21 22:32
*/
public class DeepProtoType implements Cloneable, Serializable {
public String name; //String属性
public DeepCloneableTarget deepCloneableTarget; //引用类型
public DeepProtoType() {
}
//完成深拷贝实现--方式一:使用clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null; //要生成的对象
//这里完成对基本数据类型(属性)和String的克隆
deep = super.clone();
//对引用类型的属性进行单独处理
DeepProtoType deepProtoType = (DeepProtoType) deep;
deepProtoType.deepCloneableTarget = (DeepCloneableTarget) deepCloneableTarget.clone();
return deepProtoType;
}
//深拷贝的第二种方式:通过序列化实现深拷贝(推荐)
public Object deepClone() {
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//先进行序列化操作
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this); //把当前对象以对象流的方式输出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
DeepProtoType copyObj = (DeepProtoType) ois.readObject();
return copyObj;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
//关闭流
try {
if (bos != null) {
bos.close();
}
if (oos != null) {
oos.close();
}
if (bis != null) {
bos.close();
}
if (ois != null) {
ois.close();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
}
}
}
/**
* description
* 深拷贝的案例
*
* @author
* @since 2022/11/21 22:11
*/
public class DeepCloneableTarget implements Serializable,Cloneable {
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
//因为该类的属性都是String,所以这里使用默认的clone完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/**
* description
* 测试深拷贝案例是否正常
*
* @author
* @since 2022/11/21 22:39
*/
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType p = new DeepProtoType();
p.name = "武松";
p.deepCloneableTarget = new DeepCloneableTarget("老虎", "景阳虎");
//使用方式一:调用clone的方式进行深拷贝
DeepProtoType p2 = (DeepProtoType) p.clone();
System.out.println("p.name=" + p.name + " p.deepCloneableTarget=" + p.deepCloneableTarget.hashCode());
System.out.println("p2.name=" + p2.name + " p2.deepCloneableTarget=" + p2.deepCloneableTarget.hashCode());
//使用方式二完成深拷贝
DeepProtoType p3 = (DeepProtoType) p.deepClone();
System.out.println("p3.name=" + p3.name + " p3.deepCloneableTarget=" + p3.deepCloneableTarget.hashCode());
}
}
6.10、 原型模式的注意事项和细节
1) 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
2) 不用重新初始化对象,而是动态地获得对象运行时的状态
3) 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码
4) 在实现深克隆的时候可能需要比较复杂的代码
5) 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了 ocp 原则
七、建造者模式
7.1、 盖房项目需求
1) 需要建房子:这一过程为打桩、砌墙、封顶
2) 房子有各种各样的,比如普通房,高楼,别墅,各种房子的过程虽然一样,但是要求不要相同的.
3) 请编写程序,完成需求.
7.2、 传统方式解决盖房需求
1) 思路分析(图解)
2) 代码演示
/**
* description
* 使用传统方式解决建造问题,创建房子的抽象类
*
* @author
* @since 2022/11/22 17:17
*/
public abstract class AbstractHouse {
//打地基
public abstract void buildBasic();
//砌墙
public abstract void buildWall();
//封顶
public abstract void roofed();
//将盖房子的工作排序
public void build(){
buildBasic();
buildWall();
roofed();
}
}
/**
* description
* 使用传统方式解决建造问题,创建房子的子类——高楼大厦的建造方法
*
* @author
* @since 2022/11/22 17:22
*/
public class CommonHose extends AbstractHouse{
@Override
public void buildBasic() {
System.out.println("给普通房子打地基");
}
@Override
public void buildWall() {
System.out.println("给普通房子砌墙");
}
@Override
public void roofed() {
System.out.println("给普通房子封顶");
}
}
/**
* description
* 使用传统方式解决建造问题,创建房子的子类——普通房子的建造方法
*
* @author
* @since 2022/11/22 17:21
*/
public class HighBuilding extends AbstractHouse{
@Override
public void buildBasic() {
System.out.println("给高楼大厦打地基");
}
@Override
public void buildWall() {
System.out.println("给高楼大厦砌墙");
}
@Override
public void roofed() {
System.out.println("给高楼大厦封顶");
}
}
/**
* description
* 传统方式解决建造问题
*
* @author
* @since 2022/11/22 17:26
*/
public class Client {
public static void main(String[] args) {
CommonHose commonHose = new CommonHose();
commonHose.build();
HighBuilding highBuilding = new HighBuilding();
highBuilding.build();
}
}
7.3 传统方式的问题分析
代码说明:
- 1) 优点是比较好理解,简单易操作
- 2) 设计的程序结构,过于简单,没有设计缓存层对象,程序的扩展和维护不好. 也就是说,这种设计方案,把产品(即:房子) 和 创建产品的过程(即:建房子流程) 封装在一起,耦合性增强了。
- 3) 解决方案:将产品和产品建造过程解耦 => 建造者模式.
7.4 、建造者模式基本介绍
基本介绍
- 1) 建造者模式(Builder Pattern) 又叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
- 2) 建造者模式 是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。
7.5 、建造者模式的四个角色
1) Product(产品角色): 一个具体的产品对象。
2) Builder(抽象建造者): 创建一个 Product 对象的各个部件指定的 接口/抽象类。
3) ConcreteBuilder(具体建造者): 实现接口,构建和装配各个部件。
4) Director(指挥者): 构建一个使用 Builder 接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。
7.6、 建造者模式原理类图
7.7 、建造者模式解决盖房需求应用实例
1) 需要建房子:这一过程为打桩、砌墙、封顶。不管是普通房子也好,别墅也好都需要经历这些过程,下面我们使用建造者模式(Builder Pattern)来完成
2) 思路分析图解(类图)
3) 代码实现
/**
* description
* 产品类 -->在建造者模式中对应的角色-->Product
*
* @author
* @since 2022/11/22 20:56
*/
public class House {
private String baise; //地基
private String wall; //墙
private String roofed; //屋顶
//getter、setter方法...
}
/**
* description
* 在建造者模式中对应的角色-->抽象的建造者-->负责建造的具体流程
*
* @author
* @since 2022/11/22 21:04
*/
public abstract class HouseBuilder {
protected House house = new House();
/**
* 将建造的流程写好,抽象的方法
* 打地基
*/
public abstract void buildBasic();
//砌墙
public abstract void buildWall();
//封顶
public abstract void roofed();
//建造房子完毕后,将产品返回
public House buildHouse() {
return house;
}
}
/**
* description
* 具体的实现子类--->建造过程写在子类里,产品本身属性封装到HouseBuilder里
* 即把产品本身和制造流程进行了解耦
*
* @author
* @since 2022/11/22 21:14
*/
public class CommonHouse extends HouseBuilder{
@Override
public void buildBasic() {
System.out.println("普通房子打地基5米");
}
@Override
public void buildWall() {
System.out.println("普通房子砌墙10厘米");
}
@Override
public void roofed() {
System.out.println("普通房子的屋顶");
}
}
/**
* description
* 具体的实现子类--->建造过程写在子类里,产品本身属性封装到HouseBuilder里
* 即把产品本身和制造流程进行了解耦
*
* @author
* @since 2022/11/22 21:20
*/
public class HightBuilding extends HouseBuilder{
@Override
public void buildBasic() {
System.out.println("高楼大厦打地基100米");
}
@Override
public void buildWall() {
System.out.println("高楼大厦砌墙20米");
}
@Override
public void roofed() {
System.out.println("高楼大厦的封顶");
}
}
/**
* description
* 建造者模式中的指挥者,动态的指定制作流程,并且返回具体的产品
*
* @author
* @since 2022/11/22 21:23
*/
public class HouseDirector {
//初始化
HouseBuilder houseBuilder = null;
//通过构造器传入
public HouseDirector(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
//通过setter传入
public void setHouseBuilder(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
//如何处理建造房子的流程,交给指挥者
public House constructHouse(){
houseBuilder.buildBasic();
houseBuilder.buildWall();
houseBuilder.roofed();
return houseBuilder.buildHouse();
}
}
/**
* description
* 客户端
*
* @author
* @since 2022/11/22 21:22
*/
public class Client {
public static void main(String[] args) {
//盖普通房子
CommonHouse commonHouse = new CommonHouse();
//准备创建房子的指挥者
HouseDirector houseDirector = new HouseDirector(commonHouse);
//完成盖房子的操作并返回盖好的产品
House house = houseDirector.constructHouse();
}
}
代码说明:
以上建造房子的过程,把具体实现和建造房子分离了,建造房子和建造房子过程分离了,也就是我们常说的解耦。
指挥者可以随时修房子的建造过程,不影响交付给客户的产品;也可以随时换掉建房子的人,不再依赖具体的人。也就是不依赖细节,都依赖于抽象,使整个房子建造过程更稳定。
建造者模式适用于所创建的产品具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,如造飞机和装电脑
7.8 、建造者模式在 JDK 的应用和源码分析
1) java.lang.StringBuilder 中的建造者模式
2) 代码说明+Debug 源码
3) 源码中建造者模式角色分析
- Appendable 接口定义了多个 append 方法(抽象方法), 即 Appendable 为抽象建造者, 定义了抽象方法
- AbstractStringBuilder 实现了 Appendable 接口方法,这里的 AbstractStringBuilder 已经是建造者,只是不能实例化
- StringBuilder 即充当了指挥者角色,同时充当了具体的建造者,建造方法的实现是由AbstractStringBuilder 完成, 而 StringBuilder 继承了 AbstractStringBuilder
7.9、 建造者模式的注意事项和细节
1) 客户端(使用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
2) 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象
3) 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程
4) 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”
5) 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
6) 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,因此在这种情况下,要考虑是否选择建造者模式.
7) 抽象工厂模式 VS 建造者模式 抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品
八、适配器设计模式
8.1、 现实生活中的适配器例子
泰国插座用的是两孔的(欧标),可以买个多功能转换插头 (适配器) ,这样就可以使用了
8.2 、基本介绍
1) 适配器模式(Adapter Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper)
2) 适配器模式属于结构型模式
3) 主要分为三类:类适配器模式、对象适配器模式、接口适配器模式
8.3 、工作原理
1) 适配器模式:将一个类的接口转换成另一种接口.让原本接口不兼容的类可以兼容
2) 从用户的角度看不到被适配者,是解耦的
3) 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法
4) 用户收到反馈结果,感觉只是和目标接口交互,如图
8.4 、类适配器模式
8.4.1、类适配器模式介绍
基本介绍:Adapter 类,通过继承 src 类,实现 dst 类接口,完成 src->dst 的适配
8.4.2、类适配器模式应用实例
1) 应用实例说明 以生活中充电器的例子来讲解适配器,充电器本身相当于 Adapter,220V 交流电相当于src (即被适配者),我们的目 dst(即 目标)是 5V 直流电
2) 思路分析(类图)
3) 代码实现
/**
* description
* 类适配器模式中类适配器的案例——被适配者
*
* @author
* @since 2022/11/23 15:22
*/
public class Voltage220V {
/**
* 输出220V的电压
*
* @return 电压
*/
public int output220V() {
int src = 220;
System.out.println("电压=" + src + "伏");
return src;
}
}
/**
* description
* 类适配器模式中——适配接口
*
* @author
* @since 2022/11/23 15:33
*/
public interface IVoltage5V {
/**
* 输出五伏的方法
*/
int output5V();
}
/**
* description
* 类适配器模式中——适配器类
*
* @author
* @since 2022/11/23 15:37
*/
public class VoltageAdapter extends Voltage220V implements IVoltage5V {
@Override
public int output5V() {
//获取到220V的电压
int srcV = output220V();
return srcV / 44;
}
}
/**
* description
* 手机
*
* @author
* @since 2022/11/23 15:40
*/
public class Phone {
/**
* 充电的方法
*
* @param iVoltage5V 适配接口
*/
public void charging(IVoltage5V iVoltage5V) {
if (iVoltage5V.output5V() == 5) {
System.out.println("电压为5V,可以充电");
} else if (iVoltage5V.output5V() > 5) {
System.out.println("电压大于5V,无法充电");
}
}
}
/**
* description
* 客户端
*
* @author
* @since 2022/11/23 15:44
*/
public class Client {
public static void main(String[] args) {
System.out.println("现在使用的是类适配器模式");
Phone phone = new Phone();
phone.charging(new VoltageAdapter());
}
}
8.4.3、类适配器模式注意事项和细节
1) Java 是单继承机制,所以类适配器需要继承 src 类这一点算是一个缺点, 因为这要求dst 必须是接口,有一定局限性;
2) src 类的方法在 Adapter 中都会暴露出来,也增加了使用的成本。
3) 由于其继承了 src 类,所以它可以根据需求重写 src 类的方法,使得 Adapter 的灵活性增强了
8.5 、对象适配器模式
8.5.1、对象适配器模式介绍
1) 基本思路和类的适配器模式相同,只是将 Adapter 类作修改,不是继承 src 类,而是持有src 类的实例,以解决兼容性的问题。 即:持有 src 类,实现 dst 类接口,完成 src->dst 的适配
2) 根据“合成复用原则”,在系统中尽量使用关联关系(聚合)来替代继承关系。
3) 对象适配器模式是适配器模式常用的一种
8.5.2、对象适配器模式应用实例
1) 应用实例说明 以生活中充电器的例子来讲解适配器,充电器本身相当于 Adapter,220V 交流电相当于src (即被适配者),我们的目 dst(即目标)是 5V 直流电,使用对象适配器模式完成。
2) 思路分析(类图):只需修改适配器即可, 如下:
3) 代码实现
/**
* description
* 对象适配器模式中类适配器的案例——被适配者
*
* @author
* @since 2022/11/23 15:22
*/
public class Voltage220V {
/**
* 输出220V的电压
*
* @return 电压
*/
public int output220V() {
int src = 220;
System.out.println("电压=" + src + "伏");
return src;
}
}
/**
* description
* 对象适配器模式中——适配接口
*
* @author
* @since 2022/11/23 15:33
*/
public interface IVoltage5V {
/**
* 输出五伏的方法
*/
int output5V();
}
/**
* description
* 对象适配器模式中——适配器类
*
* @author
* @since 2022/11/23 15:37
*/
public class VoltageAdapter implements IVoltage5V {
private Voltage220V voltage220V; //关联关系中的聚合关系
//通过构造器传入一个voltage220V的实例
public VoltageAdapter(Voltage220V voltage220V) {
this.voltage220V = voltage220V;
}
@Override
public int output5V() {
int dst = 0;
if (voltage220V != null) {
int src = voltage220V.output220V(); //获取220V的电压
System.out.println("使用对象适配器进行转换");
dst = src / 44;
System.out.println("适配完成,输出的电压=" + dst + "V");
}
return dst;
}
}
/**
* description
* 手机
*
* @author
* @since 2022/11/23 15:40
*/
public class Phone {
/**
* 充电的方法
*
* @param iVoltage5V 适配接口
*/
public void charging(IVoltage5V iVoltage5V) {
if (iVoltage5V.output5V() == 5) {
System.out.println("电压为5V,可以充电");
} else if (iVoltage5V.output5V() > 5) {
System.out.println("电压大于5V,无法充电");
}
}
}
/**
* description
* 客户端
*
* @author
* @since 2022/11/23 15:44
*/
public class Client {
public static void main(String[] args) {
System.out.println("现在使用的是对象适配器模式");
Phone phone = new Phone();
phone.charging(new VoltageAdapter(new Voltage220V()));
}
}
8.5.3、对象适配器模式注意事项和细节
1) 对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。 根据合成复用原则,使用组合替代继承, 所以它解决了类适配器必须继承 src 的局限性问题,也不再要求dst 必须是接口。
2) 使用成本更低,更灵活
8.6、 接口适配器模式
8.6.1、接口适配器模式介绍
1) 一些书籍称为:适配器模式(Default Adapter Pattern)或缺省适配器模式。
2) 核心思路:当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
3) 适用于一个接口不想使用其所有的方法的情况。
8.6.2、接口适配器模式应用实例
1) Android 中的属性动画 ValueAnimator 类可以通过 addListener(AnimatorListener listener)方法添加监听器,那么常规写法如右:
2) 有时候我们不想实现 Animator.AnimatorListener 接口的全部方法,我们只想监听 onAnimationStart,我们会如下写
3) AnimatorListenerAdapter 类,就是一个 接口适配器,代码如下图:它空实现了 Animator.AnimatorListener 类(src)的所 有方法.
4) AnimatorListener 是一个接口.
5) 程序里的匿名内部类就是 Listener 具体实现类
6) 案例说明
/**
* description
* 接口适配器的案例
*
* @author
* @since 2022/11/23 20:30
*/
public interface InterfaceFour {
void m1();
void m2();
void m3();
void m4();
}
/**
* description
* 接口适配器中的抽象类,将InterfaceFour中的所有方法进行默认实现
*
* @author
* @since 2022/11/23 20:41
*/
public abstract class AbsAdapter implements InterfaceFour {
@Override
public void m1() {
}
@Override
public void m2() {
}
@Override
public void m3() {
}
@Override
public void m4() {
}
}
/**
* description
* 客户端使用接口适配器
*
* @author
* @since 2022/11/23 20:45
*/
public class Client {
public static void main(String[] args) {
//创建抽象类的实例,由于抽象类内已经默认实现,所以这里不用实现了
AbsAdapter adapter = new AbsAdapter() {
//只需要去覆盖我们需要使用的接口方法
@Override
public void m1() {
System.out.println("使用了m1的方法");
}
};
adapter.m1();
}
}
代码说明:使用抽象类默认实现了接口中的全部方法,在客户端中就具体实现需要去适配的方法即可
8.7 、适配器模式在 SpringMVC 框架应用的源码剖析
1) SpringMvc 中的 HandlerAdapter, 就使用了适配器模式
2) SpringMVC 处理请求的流程回顾
3) 使用 HandlerAdapter 的原因分析:
可以看到处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果需要直接调用Controller 方法,需要调用的时候就得不断是使用 if else 来进行判断是哪一种子类然后执行。那么如果后面要扩展Controller,就得修改原来的代码,这样违背了 OCP 原则。
4) 代码分析+Debug 源码
5)动手写springMvc 通过适配器设计模式获取到对应 Controller
- 说明:
- Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类
- 适配器代替controller执行相应的方法
- 扩展Controller 时,只需要增加一个适配器类就完成了SpringMVC的扩展了这就是设计模式的力量
6)对源码进行总结,SpringMVC 中的 HandlerAdapter用到适配器模式的流程:
将request交给HttpServletRequest
通过request可以得到请求对应的控制器,或者叫处理器;即controller/handler
根据handler得到对应的适配器
通过适配器去调用对应controller的方法,并返回ModelAndView(表明看执行的是适配器的handle方法,实际上适配器执行的就是对应controller的方法)
有了适配器模式,最终是通过具体的适配器类来调用controller中的方法,那么为什么不直接去调用controller中的方法,而要通过适配器来调用呢,这不是多此一举吗?
当然不是多此一举,可以看到处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果需要直接调用 Controller 方法,需要调用的时候就得不断是使用 if else 来进行判断是哪一种子类然后执行。那么如果后面要扩展 Controller,就得修改原来的代码,这样违背了 OCP 原则。
另外,把每个controller的方法差异封装到各个 HandlerAdapter的实现类里,对外只暴露统一的handle方法。
前面在类适配器模式我们提到,由于适配器类继承了被适配器类的方法,因此在适配器类中我们可以会直接调用被适配器类的方法,这样就使得被适配器类的方法在 Adapter 中会暴露出来,增加了使用的成本;而SpringMVC中的HandlerAdapte使用了适配器模式后就解决了这个问题。
8.8 、适配器模式的注意事项和细节
1) 三种命名方式,是根据 src 是以怎样的形式给到 Adapter(在 Adapter 里的形式)来命名的。
2) 类适配器:以类给到,在 Adapter 里,就是将 src 当做类,继承 对象适配器:以对象给到,在 Adapter 里,将 src 作为一个对象,持有 接口适配器:以接口给到,在 Adapter 里,将 src 作为一个接口,实现
3) Adapter 模式最大的作用还是将原本不兼容的接口融合在一起工作。
4) 实际开发中,实现起来不拘泥于我们讲解的三种经典形式
九、桥接模式
9.1、手机操作问题
现在对不同手机类型的不同品牌实现操作编程(比如:开机、关机、上网,打电话等),如图
9.2、 传统方案解决手机操作问题
传统方法对应的类图
9.3 、传统方案解决手机操作问题分析
1) 扩展性问题(类爆炸),如果我们再增加手机的样式(旋转式),就需要增加各个品牌手机的类,同样如果我们增加一个手机品牌,也要在各个手机样式类下增加。
2) 违反了单一职责原则,当我们增加手机样式时,要同时增加所有品牌的手机,这样增加了代码维护成本.
3) 解决方案-使用桥接模式
9.4、 桥接模式(Bridge)-基本介绍
基本介绍
1) 桥接模式(Bridge 模式)是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
2) 是一种结构型设计模式
3) Bridge 模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。它的主要特点是把抽象(Abstraction)与行为实现(Implementation)分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展
9.5 桥接模式(Bridge)-原理类图
上图做了说明
- 1) Client 类:桥接模式的调用者
- 2) 抽象类(Abstraction) :维护了 Implementor / 即它的实现类 ConcreteImplementorA.., 二者是聚合关系, Abstraction充当桥接类
- 3) RefinedAbstraction : 是 Abstraction 抽象类的子类
- 4) Implementor : 行为实现类的接口
- 5) ConcreteImplementorA /B :行为的具体实现类
- 6) 从 UML 图:这里的抽象类和接口是聚合的关系,其实调用和被调用关系
9.6、 桥接模式解决手机操作问题
使用桥接模式改进传统方式,让程序具有搞好的扩展性,利用程序维护
1) 应用实例说明(和前面要求一样)
2) 使用桥接模式对应的类图
3) 代码实现
/**
* description
* 手机品牌的接口
*
* @author
* @since 2022/11/24 16:10
*/
public interface Brand {
/**
* 开机
*/
void open();
/**
* 关机
*/
void close();
/**
* 打电话
*/
void call();
}
/**
* description
* 手机的抽象类——充当桥梁的作用
*
* @author
* @since 2022/11/24 16:15
*/
public abstract class Phone {
//组合品牌
private Brand brand;
//构造器
public Phone(Brand brand) {
this.brand = brand;
}
//相关的方法
protected void open() {
this.brand.open();
}
protected void close() {
this.brand.close();
}
protected void call() {
this.brand.call();
}
}
/**
* description
* 具体的实现类——折叠式手机的样式继承抽象类Phone
*
* @author
* @since 2022/11/24 16:18
*/
public class FoldedPhone extends Phone{
public FoldedPhone(Brand brand) {
super(brand);
}
@Override
protected void open() {
super.open();
System.out.println("折叠样式手机");
}
@Override
protected void close() {
super.close();
System.out.println("折叠样式手机");
}
@Override
protected void call() {
super.call();
System.out.println("折叠样式手机");
}
}
/**
* description
* 具体的实现类——直立式手机的样式继承抽象类Phone
*
* @author
* @since 2022/11/24 16:34
*/
public class UpRightPhone extends Phone {
public UpRightPhone(Brand brand) {
super(brand);
}
@Override
protected void open() {
super.open();
System.out.println("直立式手机");
}
@Override
protected void close() {
super.close();
System.out.println("直立式手机");
}
@Override
protected void call() {
super.call();
System.out.println("直立式手机");
}
}
/**
* description
* 手机品牌的具体实现类
*
* @author
* @since 2022/11/24 16:14
*/
public class ViVo implements Brand {
@Override
public void open() {
System.out.println("ViVo手机开机");
}
@Override
public void close() {
System.out.println("ViVo手机关机");
}
@Override
public void call() {
System.out.println("ViVo打电话");
}
}
/**
* description
* 手机品牌的具体实现类
*
* @author
* @since 2022/11/24 16:12
*/
public class XiaoMi implements Brand{
@Override
public void open() {
System.out.println("小米手机开机");
}
@Override
public void close() {
System.out.println("小米手机关机");
}
@Override
public void call() {
System.out.println("小米手机打电话");
}
}
/**
* description
* 客户端
*
* @author
* @since 2022/11/24 16:23
*/
public class Client {
public static void main(String[] args) {
//获取折叠式手机(样式+品牌)
Phone phone = new FoldedPhone(new XiaoMi());
phone.open();
phone.call();
phone.close();
System.out.println("==================================");
Phone phone1 = new UpRightPhone(new ViVo());
phone1.open();
phone1.call();
phone1.close();
}
}
代码说明:桥接模式的优点和好处
- 分离抽象部分及其具体实现部分
- 提高了系统的扩展性
- 符合开闭原则
- 符合合成复用原则
9.7、 桥接模式在 JDBC 的源码剖析
桥接模式在 JDBC 的源码剖析
1) Jdbc 的 Driver 接口,如果从桥接模式来看,Driver 就是一个接口,下面可以有MySQL 的Driver,Oracle的Driver,这些就可以当做实现接口类
2) 代码分析+Debug 源码
对 jdbc 源码分析的类图
9.8 、桥接模式的注意事项和细节
1) 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统。
2) 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成。
3) 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
4) 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程
5) 桥接模式要求正确识别出系统中两个独立变化的维度(抽象、和实现),因此其使用范围有一定的局限性,即需要有这样的应用场景。 桥接模式其它应用场景
- 桥接模式其它应用场景
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用
9.9、 常见的应用场景:
1) -JDBC 驱动程序
2) -银行转账系统 转账分类: 网上转账,柜台转账,AMT 转账 转账用户类型:普通用户,银卡用户,金卡用户..
3) -消息管理 消息类型:即时消息,延时消息 消息分类:手机短信,邮件消息,QQ 消息...
十、装饰者设计模式
10.1、星巴克咖啡问题
1) 咖啡种类/单品咖啡:Espresso(意大利浓咖啡)、ShortBlack、LongBlack(美式咖啡)、Decaf(无因咖啡)
2) 调料:Milk、Soy(豆浆)、Chocolate
3) 要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便
4) 使用 OO 的来计算不同种类咖啡的费用: 客户可以点单品咖啡,也可以单品咖啡+调料组合
10.2、 方案 1-解决星巴克咖啡订单项目
10.3、 方案 1-解决星巴克咖啡订单问题分析
1) Drink 是一个抽象类,表示饮料
2) des 就是对咖啡的描述, 比如咖啡的名字
3) cost() 方法就是计算费用,Drink 类中做成一个抽象方法.
4) Decaf 就是单品咖啡, 继承 Drink, 并实现 cost
5) Espress && Milk 就是单品咖啡+调料, 这个组合很多
6) 问题:这样设计,会有很多类,当我们增加一个单品咖啡,或者一个新的调料,类的数量就会倍增,就会出现类爆炸
10.4、 方案 2-解决星巴克咖啡订单(好点)
1) 前面分析到方案 1 因为咖啡单品+调料组合会造成类的倍增,因此可以做改进,将调料内置到Drink类,这样就不会造成类数量过多。从而提高项目的维护性(如图)
2) 说明: milk,soy,chocolate 可以设计为 Boolean,表示是否要添加相应的调料
10.5、 方案 2-解决星巴克咖啡订单问题分析
1) 方案 2 可以控制类的数量,不至于造成很多的类
2) 在增加或者删除调料种类时,代码的维护量很大
3) 考虑到用户可以添加多份 调料时,可以将 hasMilk 返回一个对应 int
4) 考虑使用 装饰者 模式
10.6、 装饰者模式定义
1) 装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则(ocp)
2) 这里提到的动态的将新功能附加到对象和 ocp 原则,在后面的应用实例上会以代码的形式体现,请注意体会。
10.7、 装饰者模式原理
1) 装饰者模式就像打包一个快递 主体:比如:陶瓷、衣服 (Component) // 被装饰者 包装:比如:报纸填充、塑料泡沫、纸板、木板(Decorator)
2) Component 主体:比如类似前面的 Drink
3) ConcreteComponent 和 Decorator ConcreteComponent:具体的主体, 比如前面的各个单品咖啡
4) Decorator: 装饰者,比如各调料. 在如图的 Component 与 ConcreteComponent 之间,如果 ConcreteComponent 类很多,还可以设计一个缓冲层,将共有的部分提取出来,抽象层一个类。
10.8 、装饰者模式解决星巴克咖啡订单
10.9、 装饰者模式下的订单:2 份巧克力+一份牛奶的 LongBlack
10.10、装饰者模式咖啡订单项目应用实例
- 咖啡订单项目包结构
- 代码实现如下
/**
* description
* 抽象类——饮品
*
* @author
* @since 2022/11/25 17:51
*/
public abstract class Drink {
public String description; //对饮品的描述
private float price = 0.0f; //饮品价格
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
/**
* 计算费用的抽象方法,由继承抽象类的子类来具体实现
*
* @return
*/
public abstract float cost();
}
/**
* description
* Drink的实现子类——单品咖啡
*
* @author
* @since 2022/11/25 17:57
*/
public class Coffee extends Drink {
@Override
public float cost() {
return super.getPrice();
}
}
/**
* description
* Coffee的具体实现子类——意大利咖啡
*
* @author
* @since 2022/11/25 17:58
*/
public class Espresso extends Coffee{
//构造器,用于构造咖啡的价格和描述
public Espresso() {
setDescription("意大利咖啡");
setPrice(6.0f);
}
}
/**
* description
* Coffee的具体实现子类——美式咖啡
*
* @author
* @since 2022/11/25 18:01
*/
public class LongBlack extends Coffee{
//构造器,用于构造咖啡的价格和描述
public LongBlack() {
setDescription("美式咖啡");
setPrice(5.0f);
}
}
/**
* description
* Coffee的具体实现子类——浓缩咖啡
*
* @author
* @since 2022/11/25 18:02
*/
public class ShortBlack extends Coffee{
//构造器,用于构造咖啡的价格和描述
public ShortBlack() {
setDescription("浓缩咖啡");
setPrice(4.0f);
}
}
/**
* description
* Coffee的具体实现子类——无咖啡因咖啡
*
* @author
* @since 2022/11/25 18:05
*/
public class Decaf extends Coffee{
//构造器,用于构造咖啡的价格和描述
public Decaf() {
setDescription("浓缩咖啡");
setPrice(9.0f);
}
}
/**
* description
* 装饰者——即咖啡中的调料
*
* @author
* @since 2022/11/25 18:07
*/
public class Decorator extends Drink {
//单品咖啡
private Drink obj;
public Decorator(Drink obj) { //体现出组合的关系
this.obj = obj;
}
@Override
public float cost() {
//先拿到自己的价格,再加上调料的价格
return super.getPrice() + obj.cost();
}
@Override
public String getDescription() {
//obj.getDescription()输出了被装饰者的描述信息
return super.getDescription() + " " + super.getPrice() + " " + obj.getDescription();
}
}
/**
* description
* 装饰者具体的实现子类——调料——巧克力
*
* @author
* @since 2022/11/25 18:12
*/
public class Chocolate extends Decorator{
//构造器,用于构造调料的价格和描述
public Chocolate(Drink obj) {
super(obj);
setDescription("巧克力");
setPrice(3.0f);
}
}
/**
* description
* 装饰者具体的实现子类——调料——牛奶
*
* @author
* @since 2022/11/25 18:15
*/
public class Milk extends Decorator{
//构造器,用于构造调料的价格和描述
public Milk(Drink obj) {
super(obj);
setDescription("牛奶");
setPrice(4.0f);
}
}
/**
* description
* 装饰者具体的实现子类——调料——豆浆
* @author
* @since 2022/11/25 18:17
*/
public class Soy extends Decorator{
//构造器,用于构造调料的价格和描述
public Soy(Drink obj) {
super(obj);
setDescription("豆浆");
setPrice(3.0f);
}
}
/**
* description
* 客户端,用于模拟咖啡店下订单
* 加两份巧克力和一份牛奶的美式咖啡
*
* @author
* @since 2022/11/25 18:19
*/
public class CoffeeBar {
public static void main(String[] args) {
// 装饰者模式下的订单:2 份巧克力+一份牛奶的 LongBlack
// 1. 点一份 LongBlack
Drink order = new LongBlack();
System.out.println("描述=" + order.getDescription() + " 费用 =" + order.cost());
// 2. order 加入一份牛奶
order = new Milk(order);
System.out.println("在此基础上加入一份牛奶的费用 =" + order.cost());
// 3. order 加入一份巧克力
order = new Chocolate(order);
System.out.println("咖啡加入一份牛奶,再加入一份巧克力的费用 =" + order.cost());
// 4. order 加入一份巧克力
order = new Chocolate(order);
System.out.println("咖啡加入一份牛奶 加入两份巧克力 费用 =" + order.cost());
}
}
10.11、装饰者模式在 JDK 应用的源码分析
Java 的 IO 结构,FilterInputStream 就是一个装饰者
源码说明
/**
* description
* 装饰者模式在jdk源码中的应用——IO流,对源码的说明如下
* 1. InputStream 是抽象类, 类似我们前面讲的 Drink,也就是一个模糊的抽象类
* 2. FileInputStream 是 InputStream 子类,类似我们前面的 DeCaf, LongBlack,即抽象类的具体实现产品
* 3. FilterInputStream 是 InputStream 子类:类似我们前面 的 Decorator 修饰者
* 4. DataInputStream 是 FilterInputStream 子类,具体的修饰者,类似前面的Milk, Soy 等
* 5. FilterInputStream 类 有 protected volatile InputStream in; 即含被装饰者
* 6. 分析得出在 jdk 的 io 体系中,就是使用装饰者模式
*
* @author
* @since 2022/11/25 22:05
*/
public class Decorator {
public static void main(String[] args) throws Exception {
DataInputStream dis = new DataInputStream(new FileInputStream("D:\\abc.txt"));
System.out.println(dis.read());
dis.close();
}
}
十一、组合模式
11.1 看一个学校院系展示需求
编写程序展示一个学校院系结构:需求是这样,要在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系。如图:
11.2、 传统方案解决学校院系展示(类图)
11.3 、传统方案解决学校院系展示存在的问题分析
1) 将学院看做是学校的子类,系是学院的子类,这样实际上是站在组织大小来进行分层次的
2) 实际上我们的要求是 :在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系,因此这种方案,不能很好实现的管理的操作,比如对学院、系的添加,删除,遍历等
3) 解决方案:把学校、院、系都看做是组织结构,他们之间没有继承的关系,而是一个树形结构,可以更好的实现管理操作。 => 组合模式
11.4、 组合模式基本介绍
- 基本介绍
- 1) 组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示“整体-部分”的层次关系
- 2) 组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
- 3) 这种类型的设计模式属于结构型模式
- 4) 组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象
11.5 、组合模式原理类图
对原理结构图的说明-即(组合模式的角色及职责)
1) Component :这是组合中对象声明接口,在适当情况下,实现所有类共有的接口默认行为,用于访问和管理Component 子部件, Component 可以是抽象类或者接口
2) Leaf : 在组合中表示叶子节点,叶子节点没有子节点
3) Composite :非叶子节点, 用于存储子部件, 在 Component 接口中实现 子部件的相关操作,比如增加(add), 删除。
11.6 、组合模式解决学校院系展示的 应用实例
应用实例要求
1) 编写程序展示一个学校院系结构:需求是这样,要在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系。
2) 思路分析和图解(类图)
3) 代码实现
/**
* description
* 是组合中对象声明接口——即组合模式中的Component,可以是抽象类、类,亦可是接口
*
* @author
* @since 2022/11/28 16:53
*/
public abstract class OrganizationComponent {
private String name; //名字
private String description; //说明
/**
* 叶子节点——添加的方法
*
* @param component 抽象类
*/
protected void add(OrganizationComponent component) {
//默认实现
throw new UnsupportedOperationException();
}
/**
* 叶子节点——删除的方法
*
* @param component 抽象类
*/
protected void remove(OrganizationComponent component) {
//默认实现
throw new UnsupportedOperationException();
}
/**
* 构造器
*
* @param name 名字
* @param description 说明
*/
public OrganizationComponent(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
/**
* 打印方法,可以做成抽象方法,子类都需要实现
*/
protected abstract void print();
}
/**
* description
* 非叶子节点,用于存储子部件——大学
*
* @author
* @since 2022/11/28 17:01
*/
public class University extends OrganizationComponent {
//聚合到集合里
List<OrganizationComponent> organizationComponents = new ArrayList<>();
//构造器
public University(String name, String description) {
super(name, description);
}
//重写add方法
@Override
protected void add(OrganizationComponent component) {
organizationComponents.add(component);
}
//重写remove方法
@Override
protected void remove(OrganizationComponent component) {
organizationComponents.remove(component);
}
//重写get方法
@Override
public String getName() {
return super.getName();
}
//重写get方法
@Override
public String getDescription() {
return super.getDescription();
}
//print方法,就是输出包含的学院
@Override
protected void print() {
System.out.println("------------------" + getName() + "---------------------");
//遍历organizationComponents
for (OrganizationComponent organizationComponents : organizationComponents) {
organizationComponents.print();
}
}
}
/**
* description
* 叶子节点——叶子节点没有子节点,不用重写add和remove方法
*
* @author
* @since 2022/11/28 17:18
*/
public class Department extends OrganizationComponent{
public Department(String name, String description) {
super(name, description);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getDescription() {
return super.getDescription();
}
@Override
protected void print() {
System.out.println(getName());
}
}
/**
* description
* 非叶子节点,用于存储子部件——学院
*
* @author
* @since 2022/11/28 17:13
*/
public class College extends OrganizationComponent {
//List 中存放的是Department
List<OrganizationComponent> organizationComponents = new ArrayList<>();
//构造器
public College(String name, String description) {
super(name, description);
}
//重写add方法
@Override
protected void add(OrganizationComponent component) {
//将来实际业务中,College的add 方法和 University add方法业务逻辑不一定完全相同
organizationComponents.add(component);
}
//重写remove方法
@Override
protected void remove(OrganizationComponent component) {
organizationComponents.remove(component);
}
//重写get方法
@Override
public String getName() {
return super.getName();
}
//重写get方法
@Override
public String getDescription() {
return super.getDescription();
}
//print方法,就是输出包含的学院
@Override
protected void print() {
System.out.println("------------------" + getName() + "---------------------");
//遍历organizationComponents
for (OrganizationComponent organizationComponents : organizationComponents) {
organizationComponents.print();
}
}
}
/**
* description
* 客户端
*
* @author
* @since 2022/11/28 17:37
*/
public class Client {
public static void main(String[] args) {
//从大到小创建对象
OrganizationComponent university = new University("清华大学", "中国最好的大学");
//创建学院
OrganizationComponent computerCollege = new College("计算机学院", "与电脑沟通的语言");
OrganizationComponent messageCollege = new College("信息工程学院", "信息改变生活");
//创建学院下面的系(专业)
computerCollege.add(new Department("软件工程","软件改变生活"));
computerCollege.add(new Department("网络工程","网络改变生活"));
computerCollege.add(new Department("计算机科学与技术","科学改变生活"));
//创建学院下面的系(专业)
messageCollege.add(new Department("信息工程","信息改变生活"));
messageCollege.add(new Department("通信工程","通信改变生活"));
//将学院加入到学校中
university.add(computerCollege);
university.add(messageCollege);
university.print();
}
}
11.7 、组合模式在 JDK 集合的源码分析
1) Java 的集合类-HashMap 就使用了组合模式
2) 代码分析+Debug 源码
3) 类图
4)对于组合模式在源码中的说明
- 1、Map就是一个抽象的构建(类似于Component)
- 2、HashMap是一个中间的构建(composite),实现/继承了相关方法
- 类似于put()、putall() 方法
- 3、Node 是HashMap的静态内部类,雷士Leaf叶子节点,这里就没有put,putall方法
- static class Node implements Map.Entry
11.8 、组合模式的注意事项和细节
1) 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。
2) 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何改动.
3) 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构
4) 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式.
5) 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
十二、外观模式
12.1、影院管理项目
组建一个家庭影院:
DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机
要求完成使用家庭影院的功能,其过程为:
直接用遥控器:统筹各设备开关 开爆米花机 放下屏幕 开投影仪 开音响 开 DVD,选 dvd 去拿爆米花 调暗灯光 播放 观影结束后,关闭各种设备
12.2、传统方式解决影院管理
12.3、传统方式解决影院管理问题分析
1) 在 ClientTest 的 main 方法中,创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程混乱,没有清晰的过程
2) 不利于在 ClientTest 中,去维护对子系统的操作
3) 解决思路:定义一个高层接口,给子系统中的一组接口提供一个一致的界面(比如在高层接口提供四个方法ready, play, pause, end ),用来访问子系统中的一群接口
4) 也就是说 就是通过定义一个一致的接口(界面类),用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节 => 外观模式
12.4、外观模式基本介绍
基本介绍
- 1) 外观模式(Facade),也叫“过程模式:外观模式为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
- 2) 外观模式通过定义一个一致的接口,用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节
12.5、外观模式原理类图
对类图说明(分类外观模式的角色)
1) 外观类(Facade): 为调用端提供统一的调用接口, 外观类知道哪些子系统负责处理请求,从而将调用端的请求代理给适当子系统对象
2) 调用者(Client): 外观接口的调用者
3) 子系统的集合:指模块或者子系统,处理 Facade 对象指派的任务,他是功能的实际提供者
12.6、外观模式解决影院管理
12.6.1、传统方式解决影院管理说明
1) 外观模式可以理解为转换一群接口,客户只要调用一个接口,而不用调用多个接口才能达到目的。比如:在pc上安装软件的时候经常有一键安装选项(省去选择安装目录、安装的组件等等),还有就是手机的重启功能(把关机和启动合为一个操作)。
2) 外观模式就是解决多个复杂接口带来的使用困难,起到简化用户操作的作用
3) 示意图说明
12.6.2、外观模式应用实例
1) 应用实例要求
2) 使用外观模式来完成家庭影院项目
3) 思路分析和图解(类图)
4)代码实现
/**
* description
* 外观模式案例的代码实现——电视机,即外观模式的子系统
*
* @author
* @since 2022/11/29 16:53
*/
public class DVDPlayer {
//使用单例模式中的饿汉式
private static DVDPlayer instance = new DVDPlayer();
/**
* 单例模式——饿汉式
*
* @return DVDPlayer的实例
*/
public static DVDPlayer getInstance() {
return instance;
}
/**
* 打开的方法
*/
public void on() {
System.out.println("DVD 打开了");
}
/**
* 关闭的方法
*/
public void off() {
System.out.println("DVD 关闭了");
}
/**
* 播放的方法
*/
public void play() {
System.out.println("DVD 开始播放");
}
/**
* 暂停的方法
*/
public void pause() {
System.out.println("DVD 暂停");
}
}
/**
* description
* 外观模式案例的代码实现——爆米花机,即外观模式的子系统
*
* @author
* @since 2022/11/29 17:01
*/
public class Popcorn {
//使用单例模式中的饿汉式
private static Popcorn instance = new Popcorn();
/**
* 单例模式——饿汉式
*
* @return Popcorn的实例
*/
public static Popcorn getInstance() {
return instance;
}
/**
* 打开的方法
*/
public void on() {
System.out.println("爆米花机 打开了");
}
/**
* 关闭的方法
*/
public void off() {
System.out.println("爆米花机 关闭了");
}
/**
* 暂停的方法
*/
public void pause() {
System.out.println("爆米花机 暂停");
}
/**
* 爆米花机满了的方法
*/
public void full(){
System.out.println("爆米花机 满了");
}
/**
* 爆米花机出爆米花的方法
*/
public void pop(){
System.out.println("爆米花机 正在出爆米花");
}
}
/**
* description
* 外观模式案例的代码实现——投影仪,即外观模式的子系统
*
* @author
* @since 2022/11/29 17:07
*/
public class Project {
//使用单例模式中的饿汉式
private static Project instance = new Project();
/**
* 单例模式——饿汉式
*
* @return Popcorn的实例
*/
public static Project getInstance() {
return instance;
}
/**
* 打开的方法
*/
public void on() {
System.out.println("投影仪 打开了");
}
/**
* 关闭的方法
*/
public void off() {
System.out.println("投影仪 关闭了");
}
/**
* 暂停的方法
*/
public void pause() {
System.out.println("投影仪 暂停");
}
/**
* 聚焦的方法
*/
public void focus() {
System.out.println("投影仪 聚焦");
}
}
/**
* description
* 外观模式案例的代码实现——立体声,即外观模式的子系统
*
* @author
* @since 2022/11/29 17:11
*/
public class Stereo {
//使用单例模式中的饿汉式
private static Stereo instance = new Stereo();
/**
* 单例模式——饿汉式
*
* @return Popcorn的实例
*/
public static Stereo getInstance() {
return instance;
}
/**
* 打开的方法
*/
public void open(){
System.out.println("立体声打开了");
}
/**
* 关闭的方法
*/
public void off(){
System.out.println("立体声关闭了");
}
/**
* 调大音量的方法
*/
public void changeUp(){
System.out.println("音量调高");
}
/**
* 调小音量的方法
*/
public void changeDown(){
System.out.println("音量调低");
}
}
/**
* description
* 外观模式案例的代码实现——灯光,即外观模式的子系统
*
* @author
* @since 2022/11/29 17:17
*/
public class ThreaderLights {
//使用单例模式中的饿汉式
private static ThreaderLights instance = new ThreaderLights();
/**
* 单例模式——饿汉式
*
* @return Popcorn的实例
*/
public static ThreaderLights getInstance() {
return instance;
}
/**
* 打开的方法
*/
public void open(){
System.out.println("灯光打开了");
}
/**
* 关闭的方法
*/
public void off(){
System.out.println("灯光关闭了");
}
/**
* 灯光调亮的方法
*/
public void changeBright(){
System.out.println("灯光调亮");
}
/**
* 灯光调暗的方法
*/
public void changeDark(){
System.out.println("灯光调暗");
}
}
/**
* description
* 外观模式案例的代码实现——影院外观,即外观模式的外观类
*
* @author
* @since 2022/11/29 17:21
*/
public class HomeTheaterFacade {
//首先定义各个子系统的对象——电视机
private DVDPlayer dvdPlayer;
//投影仪
private Project pojector;
//爆米花机
private Popcorn popcorn;
//屏幕
private Screen screen;
//立体声
private Stereo stereo;
//灯光
private ThreaderLights threaderLights;
//在构造器中初始化所有的子系统对象,由于创建对象时使用了单例模式所以直接使用类去调用方法初始即可
public HomeTheaterFacade() {
super();
this.dvdPlayer = DVDPlayer.getInstance();
this.pojector = Project.getInstance();
this.popcorn = Popcorn.getInstance();
this.screen = Screen.getInstance();
this.stereo = Stereo.getInstance();
this.threaderLights = ThreaderLights.getInstance();
}
/**
* 通过这样处理后我们希望吧所有的操作分成四步
* 第一步:准备
*/
public void ready() {
popcorn.on();
popcorn.pop();
popcorn.full();
screen.down();
pojector.on();
stereo.open();
dvdPlayer.on();
threaderLights.changeDark();
}
/**
* 第二步:播放
*/
public void play() {
dvdPlayer.play();
stereo.changeUp();
}
/**
* 第三步:暂停
*/
public void pause() {
dvdPlayer.pause();
}
/**
* 第四步:结束
*/
public void end() {
dvdPlayer.off();
pojector.off();
popcorn.off();
screen.up();
stereo.off();
threaderLights.changeBright();
}
}
/**
* description
* 外观模式案例的代码实现——客户端,即外观模式的调用者
*
* @author
* @since 2022/11/29 17:20
*/
public class Client {
public static void main(String[] args) {
//统一调用
HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade();
homeTheaterFacade.ready();
homeTheaterFacade.play();
homeTheaterFacade.pause();
homeTheaterFacade.end();
}
}
12.7、外观模式在 MyBatis 框架应用的源码分析
1) MyBatis 中的 Configuration 去创建 MetaObject 对象使用到外观模式
2) 代码分析+Debug 源码+示意图
3) 对源码中使用到的外观模式的角色类图
12.8、外观模式的注意事项和细节
1) 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
2) 外观模式对客户端与子系统的耦合关系 - 解耦,让子系统内部的模块更易维护和扩展
3) 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
4) 当系统需要进行分层设计时,可以考虑使用 Facade 模式,即外观模式
5) 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facade 类交互,提高复用性
6) 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的。
十三、享元模式
13.1、展示网站项目需求
小型的外包项目,给客户 A 做一个产品展示网站,客户 A 的朋友感觉效果不错,也希望做这样的产品展示网站,但是要求都有些不同:
1) 有客户要求以新闻的形式发布
2) 有客户人要求以博客的形式发布
3) 有客户希望以微信公众号的形式发布
13.2、传统方案解决网站展现项目
1) 直接复制粘贴一份,然后根据客户不同要求,进行定制修改
2) 给每个网站租用一个空间
3) 方案设计示意图
13.3、传统方案解决网站展现项目-问题分析
1) 需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,造成服务器的资源浪费
2) 解决思路:整合到一个网站中,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源
3) 对于代码来说,由于是一份实例,维护和扩展都更加容易
4) 上面的解决思路就可以使用 享元模式 来解决
13.4、享元模式基本介绍
基本介绍
- 1) 享元模式(Flyweight Pattern) 也叫 蝇量模式: 运用共享技术有效地支持大量细粒度的对象
- 2) 常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个
- 3) 享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率
- 4) 享元模式经典的应用场景就是池技术了,String 常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式
13.5、享元模式的原理类图
对类图的说明
对原理图的说明-即(模式的角色及职责)
- 1) FlyWeight 是抽象的享元角色, 他是产品的抽象类, 同时定义出对象的外部状态和内部状态(后面介绍) 的接口或实现
- 2) ConcreteFlyWeight 是具体的享元角色,是具体的产品类,实现抽象角色定义相关业务
- 3) UnSharedConcreteFlyWeight 是不可共享的角色,一般不会出现在享元工厂。
- 4) FlyWeightFactory 享元工厂类,用于构建一个池容器(集合), 同时提供从池中获取对象方法
13.6、内部状态和外部状态
比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多一点,所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化的,所以棋子坐标就是棋子的外部状态
1) 享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态了,即将对象的信息分为两个部分:内部状态和外部状态
2) 内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变
3) 外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态
4) 举个例子:围棋理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就很好的解决了对象的开销问题
13.7、享元模式解决网站展现项目
1) 应用实例要求 使用享元模式完成,前面提出的网站外包问题
2) 思路分析和图解(类图)
3) 代码实现
/**
* description
* 享元模式中——抽象的享元角色
*
* @author xujicheng
* @since 2022年11月30日 22:00
*/
public abstract class WebSize {
/**
* 抽象的享元角色
*
* @param user 外部状态
*/
public abstract void use(User user); //抽象方法
}
/**
* description
* 享元模式中——具体的享元角色(产品类)
*
* @author xujicheng
* @since 2022年11月30日 22:02
*/
public class ConcreteWebSize extends WebSize {
//共享的部分,内部状态
private String type; //网站发布的形式/类型
//构造器
public ConcreteWebSize(String type) {
this.type = type;
}
@Override
public void use(User user) {
System.out.println("网站的发布形式为:" + type + "正在使用中...使用者是:" + user.getName());
}
}
/**
* description
* 享元模式中——享元工厂类,网站工厂类,根据需求返回一个具体的网站
*
* @author xujicheng
* @since 2022年11月30日 22:04
*/
public class WebSizeFactory {
//集合,充当池的作用
private HashMap<String, ConcreteWebSize> pool = new HashMap<>();
/**
* 根据网站发布形式返回一个网站,如果没有就创建一个网站并放入到池中,并返回
* @param type 网站类型
* @return 具体的网站
*/
public WebSize getWebSizeCategory(String type){
if (!pool.containsKey(type)){
pool.put(type, new ConcreteWebSize(type));
}
return pool.get(type);
}
//获取网站分类的总数(池中有多少个网站类型)
public int getWebSizeCount(){
return pool.size();
}
}
/**
* description
* 享元模式中的外部状态
*
* @author xujicheng
* @since 2022年11月30日 22:21
*/
public class User {
private String name; //用户的名字
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年11月30日 22:11
*/
public class Client {
public static void main(String[] args) {
//创建一个工厂类
WebSizeFactory factory = new WebSizeFactory();
//客户要一个以新闻形式发布的网站
WebSize webSizeOne = factory.getWebSizeCategory("新闻");
webSizeOne.use(new User("Tom"));
//客户要一个以博客形式发布的网站
WebSize webSizeTwo = factory.getWebSizeCategory("博客");
webSizeTwo.use(new User("Jack"));
//客户要一个以博客形式发布的网站
WebSize webSizeThree = factory.getWebSizeCategory("博客");
webSizeThree.use(new User("Jenny"));
//客户要一个以博客形式发布的网站
WebSize webSizeFour = factory.getWebSizeCategory("博客");
webSizeFour.use(new User("Bob"));
System.out.println("网站的分类共有=" + factory.getWebSizeCount() + "种");
}
}
代码说明:
以上调用客户端输出结果如下:
创建了四个网站,但是网站的分类只有两种,就避免了创建相同对象造成的性能浪费
这就是比较经典的享元模式,核心在于能够把外部状态和内部状态分开,分开后在提供的能够把共享的部分进行共享,即只有在类型不同的适合才回去创建,有相同类型的适合无需重复创建,避免了性能浪费
13.8、享元模式在 JDK-Interger 的应用源码分析
1) Integer 中的享元模式
2) 代码分析+Debug 源码+说明
3)代码说明:
- 如果 Integer.valueOf(x) x 在 -128 --- 127 之间,就是使用享元模式,并返回,如果不在这个范围类,则仍然 创建一个新对象返回
- 小结:
- 在 valueOf 方法中,先判断值是否在 IntegerCache 中,如果不在,就创建新的Integer(new), 否则,就直接从 缓存池返回即可
- 如果使用valueOf 方法,就使用到享元模式
- 如果使用 valueOf 方法得到一个 Integer 实例,范围在 -128 - 127 ,执行速度比新创建的对象快
13.9、享元模式的注意事项和细节
1) 在享元模式这样理解,“享”就表示共享,“元”表示对象
2) 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式
3) 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable 存储
4) 享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率
5) 享元模式提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方.
6) 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制。
7) 享元模式经典的应用场景是需要缓冲池的场景,比如 String 常量池、1数据库连接池
十四、代理模式
14.1、代理模式(Proxy)
14.1.1、代理模式的基本介绍
1) 代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
2) 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
3) 代理模式有不同的形式, 主要有三种 静态代理、动态代理 (JDK 代理、接口代理)和Cglib 代理(可以在内存动态的创建对象,而不需要实现接口, 他是属于动态代理的范畴) 。
4) 代理模式示意图
14.2、静态代理
14.2.1、静态代码模式的基本介绍
静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类
14.2.2、应用实例
具体要求
1) 定义一个接口:ITeacherDao
2) 目标对象 TeacherDAO 实现接口 ITeacherDAO
3) 使用静态代理方式,就需要在代理对象 TeacherDAOProxy 中也实现 ITeacherDAO
4) 调用的时候通过调用代理对象的方法来调用目标对象.
5) 特别提醒:代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法
思路分析图解(类图)
代码实现
/**
* description
* 静态代理的案例——教师的接口
*
* @author xujicheng
* @since 2022年12月02日 15:09
*/
public interface ITeacherDao {
void teach(); //授课方法
}
/**
* description
* 教师的实体类对象
*
* @author xujicheng
* @since 2022年12月02日 15:10
*/
public class TeacherDao implements ITeacherDao{
@Override
public void teach() {
System.out.println("老师授课中......");
}
}
/**
* description
* 老师的静态代理对象
*
* @author xujicheng
* @since 2022年12月02日 15:13
*/
public class TeacherDaoProxy implements ITeacherDao{
private ITeacherDao target; //目标对象,通过接口来聚合
//构造器
public TeacherDaoProxy(ITeacherDao target) {
this.target = target;
}
@Override
public void teach() {
System.out.println("开始代理,完成某些操作...");
target.teach();
System.out.println("代理结束...");
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月02日 15:19
*/
public class Client {
public static void main(String[] args) {
//创建目标对象(被代理对象)
TeacherDao teacherDao = new TeacherDao();
//创建代理对象,同时将被代理对象传递给代理对象
TeacherDaoProxy teacherDaoProxy = new TeacherDaoProxy(teacherDao);
//通过代理对象,调用到被代理对象的方法,即执行的是代理对象的方法,代理对象再调用目标方法
teacherDaoProxy.teach();
}
}
14.2.3、静态代理优缺点
1) 优点:在不修改目标对象的功能前提下, 能通过代理对象对目标功能扩展
2) 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类
3) 一旦接口增加方法,目标对象与代理对象都要维护
14.3、动态代理
14.3.1、动态代理模式的基本介绍
1) 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理
2) 代理对象的生成,是利用 JDK 的 API,动态的在内存中构建代理对象
3) 动态代理也叫做:JDK 代理、接口代理
14.3.2、JDK 中生成代理对象的 API
1) 代理类所在包:java.lang.reflect.Proxy
2) JDK 实现代理只需要使用 newProxyInstance 方法,但是该方法需要接收三个参数,完整的写法是: static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h )
14.3.3、动态代理应用实例
应用实例要求 将前面的静态代理改进成动态代理模式(即:JDK 代理模式)
思路图解(类图)
代码实现
/**
* description
* 动态代理的案例——教师接口
*
* @author xujicheng
* @since 2022年12月02日 18:31
*/
public interface ITeacherDao {
void teach(); //授课的方法
}
/**
* description
* 动态代理的案例——目标对象,即教师的实体类
*
* @author xujicheng
* @since 2022年12月02日 18:32
*/
public class TeacherDao implements ITeacherDao{
@Override
public void teach() {
System.out.println("老师正在授课...");
}
}
/**
* description
* 动态代理的案例——代理工厂
*
* @author xujicheng
* @since 2022年12月02日 18:33
*/
public class ProxyFactory {
//维护一个目标对象,以Object的对象维护即可
private Object target;
//通过构造器初始化目标对象
public ProxyFactory(Object target) {
this.target = target;
}
//给目标对象生成一个代理对象
public Object getProxyInstance() {
/*
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
对当前的静态方法的参数进行说明:
1、ClassLoader loader : 指定当前目标对象使用的类加载器,获取加载器的方法是固定的
2、Class<?>[] interfaces : 目标对象实现的接口类型,使用泛型方法确认类型
3、InvocationHandler h :事件处理,执行目标对象方法时,它会触发事件处理器的方法,
会把当前执行的目标对象方法作为一个参数传入
*/
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("JDK代理开始");
//通过反射机制调用目标对象的方法
Object returnVal = method.invoke(target, args);
System.out.println("JDK代理提交");
return returnVal;
}
});
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月02日 18:46
*/
public class Client {
public static void main(String[] args) {
//创建目标对象
ITeacherDao target = new TeacherDao();
//给目标对象创建代理对象,返回的类型就是代理对象,可以转为ITeacherDao
ITeacherDao proxyInstance = (ITeacherDao) new ProxyFactory(target).getProxyInstance();
// proxyInstance=class com.sun.proxy.$Proxy0 内存中动态生成了代理对象
System.out.println("proxyInstance=" + proxyInstance.getClass());
//通过代理对象来调用目标对象的方法
proxyInstance.teach();
}
}
14.4、Cglib 代理
14.4.1、1 Cglib 代理模式的基本介绍
1) 静态代理和 JDK 代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理-这就是 Cglib 代理
2) Cglib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展, 有些书也将Cglib代理归属到动态代理。
3) Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口.它广泛的被许多AOP的框架使用,例如 Spring AOP,实现方法拦截
4) 在 AOP 编程中如何选择代理模式:
- 4.1、目标对象需要实现接口,用 JDK 代理
- 4.2、目标对象不需要实现接口,用 Cglib 代理
5) Cglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类
14.4.2、Cglib 代理模式实现步骤
1)首先导入Cglib的依赖
<!-- https://mvnrepository.com/artifact/cglib/cglib --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
2) 在内存中动态构建子类,注意代理的类不能为 final,否则报错
- java.lang.IllegalArgumentException:
3) 目标对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.
14.4.3、Cglib 代理模式应用实例
应用实例要求 :将前面的案例用 Cglib 代理模式实现
思路图解(类图)
代码实现+Debug 源码
/**
* description
* cglib案例的目标对象
*
* @author xujicheng
* @since 2022年12月02日 20:41
*/
public class TeacherDao {
public void teach(){
System.out.println("老师授课中...我是cglib代理,不需要实现接口");
}
}
/**
* description
* cglib代理的案例——代理工厂
* @author xujicheng
* @since 2022年12月02日 20:43
*/
public class ProxyFactory implements MethodInterceptor {
//维护一个目标对象
private Object target;
//构造器,传入并初始化一个被代理的对象
public ProxyFactory(Object target) {
this.target = target;
}
//返回一个代理对象(当前目标对象target的代理对象)
public Object getProxyInstance(){
//1、创建一个工具类
Enhancer enhancer = new Enhancer();
//2、设置父类
enhancer.setSuperclass(target.getClass());
//3、设置回调函数
enhancer.setCallback(this);
//4、创建子类对象,即代理对象
return enhancer.create();
}
//重写 intercept 方法,会调用目标对象的相关方法
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib代理模式开始...");
Object result = method.invoke(target, objects);
System.out.println("cglib代理提交...");
return result;
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月02日 20:56
*/
public class Client {
public static void main(String[] args) {
//创建目标对象
TeacherDao target = new TeacherDao();
//获取到代理对象,并且将目标对象传递给代理对象
TeacherDao proxyInstance = (TeacherDao) new ProxyFactory(target).getProxyInstance();
//执行代理对象的方法,触发intercept方法从而实现对目标对象的调用
proxyInstance.teach();
}
}
14.4.4、几种常见的代理模式介绍— 几种变体
1) 防火墙代理
- 内网通过代理穿透防火墙,实现对公网的访问。
2) 缓存代理
- 比如:当请求图片文件等资源时,先到缓存代理取,如果取到资源则 ok,如果取不到资源,再到公网或者数据库取,然后缓存。
3) 远程代理
- 远程对象的本地代表,通过它可以把远程对象当本地对象来调用。远程代理通过网络和真正的远程对象沟通信息。
4) 同步代理:主要使用在多线程编程中,完成多线程间同步工作
十五、模板方法模式
15.1、豆浆制作问题
编写制作豆浆的程序,说明如下
- 1) 制作豆浆的流程 选材--->添加配料--->浸泡--->放到豆浆机打碎
- 2) 通过添加不同的配料,可以制作出不同口味的豆浆
- 3) 选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的
- 4) 请使用 模板方法模式 完成 (说明:因为模板方法模式,比较简单,很容易就想到这个方案,因此就直接使用,不再使用传统的方案来引出模板方法模式 )
15.2、模板方法模式基本介绍
基本介绍
- 1) 模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
- 2) 简单说,模板方法模式 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤
- 3) 这种类型的设计模式属于行为型模式。
15.3、模板方法模式原理类图
15.3.1、模板方法模式的原理类图
对原理类图的说明-即(模板方法模式的角色及职责)
- 1) AbstractClass 抽象类, 类中实现了模板方法(template),定义了算法的骨架,具体子类需要去实现其它的抽象方法 operationr2,3,4
- 2) ConcreteClass 实现抽象方法 operationr2,3,4, 以完成算法中特点子类的步骤
15.4、模板方法模式解决豆浆制作问题
1) 应用实例要求 编写制作豆浆的程序,说明如下: 制作豆浆的流程 选材--->添加配料--->浸泡--->放到豆浆机打碎 通过添加不同的配料,可以制作出不同口味的豆浆 选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的(红豆、花生豆浆。。。)
2) 思路分析和图解(类图)
3)代码实现
/**
* description
* 模板方法模式——抽象的豆浆类
*
* @author xujicheng
* @since 2022年12月02日 21:25
*/
public abstract class SoyaMilk {
//模板方法:make,模板方法可以做成final,不让子类去覆盖
final void make(){
select();
addCondiments();
soak();
beat();
}
//选材料的方法
void select(){
System.out.println("第一步:选择好新鲜的黄豆");
}
//添加不同的配料方法,做成抽象方法留给子类具体实现
abstract void addCondiments();
//浸泡的方法
void soak(){
System.out.println("第三步: 黄豆和配料开始浸泡,需要浸泡三小时");
}
//打豆浆的方法
void beat(){
System.out.println("第四步: 黄豆和配料放在豆浆机里打碎");
}
}
/**
* description
* 模板方法模式——红豆豆浆
*
* @author xujicheng
* @since 2022年12月02日 21:32
*/
public class RedBeanSoyaMilk extends SoyaMilk{
@Override
void addCondiments() {
System.out.println("第二步:加入上好的红豆");
}
}
/**
* description
* 模板方法模式——花生豆浆
*
* @author xujicheng
* @since 2022年12月02日 21:56
*/
public class PeanutSoyaMilk extends SoyaMilk{
@Override
void addCondiments() {
System.out.println("第二步:加入上好的花生");
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月02日 22:27
*/
public class Client {
public static void main(String[] args) {
//制作红豆豆浆
System.out.println("制作红豆豆浆");
SoyaMilk redBeanSoyaMilk = new RedBeanSoyaMilk();
redBeanSoyaMilk.make();
//制作花生豆浆
System.out.println("制作花生豆浆");
SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
peanutSoyaMilk.make();
}
}
15.5、模板方法模式的钩子方法
1) 在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。
2) 还是用上面做豆浆的例子来讲解,比如,我们还希望制作纯豆浆,不添加任何的配料,请使用钩子方法对前面的模板方法进行改造
3)代码实现
- 在抽象类 SoyaMilk中添加钩子方法以及判断逻辑即可
/** * description * 模板方法模式——抽象的豆浆类 * * @author xujicheng * @since 2022年12月02日 21:25 */ public abstract class SoyaMilk { //模板方法:make,模板方法可以做成final,不让子类去覆盖 final void make(){ select(); if (customerWantCondiments()){ //判断是否需要添加配料 addCondiments(); } soak(); beat(); } //选材料的方法 void select(){ System.out.println("第一步:选择好新鲜的黄豆"); } //添加不同的配料方法,做成抽象方法留给子类具体实现 abstract void addCondiments(); //浸泡的方法 void soak(){ System.out.println("第三步: 黄豆和配料开始浸泡,需要浸泡三小时"); } //打豆浆的方法 void beat(){ System.out.println("第四步: 黄豆和配料放在豆浆机里打碎"); } //钩子方法,决定是否需要添加配料 boolean customerWantCondiments(){ return true; } } /** * description * 使用模板方法模式中钩子方法创建的子类对象——纯豆浆 * * @author xujicheng * @since 2022年12月02日 22:34 */ public class PureSoyaMilk extends SoyaMilk{ @Override void addCondiments() { //空实现即可 } @Override boolean customerWantCondiments() { return false; } }
代码说明,当模板模式中添加了钩子方法,它可以比较轻松的在你的模板模式中自行选择要不要调用模板
15.6、模板方法模式在 Spring 框架应用的源码分析
1) Spring IOC 容器初始化时运用到的模板方法模式
2) 代码分析+角色分析+说明类图
3) 针对源码的类图(说明层次关系)
15.7、模板方法模式的注意事项和细节
1) 基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
2) 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。3) 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。 4) 该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大
5) 一般模板方法都加上 final 关键字, 防止子类重写模板方法.
6) 模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤 ,这一系列的步骤基本相同,但其个别步骤在实现时 可能不同,通常考虑用模板方法模式来处理
十六、命令模式
16.1、智能生活项目需求
看一个具体的需求
1) 我们买了一套智能家电,有照明灯、风扇、冰箱、洗衣机,我们只要在手机上安装app 就可以控制对这些家电工作。
2) 这些智能家电来自不同的厂家,我们不想针对每一种家电都安装一个 App,分别控制,我们希望只要一个app就可以控制全部智能家电。
3) 要实现一个 app 控制所有智能家电的需要,则每个智能家电厂家都要提供一个统一的接口给app 调用,这时就可以考虑使用命令模式。
4) 命令模式可将“动作的请求者”从“动作的执行者”对象中解耦出来. 5) 在我们的例子中,动作的请求者是手机 app,动作的执行者是每个厂商的一个家电产品
16.2、命令模式基本介绍
1) 命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个, 我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计
2) 命名模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦。
3) 在命名模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作
4) 通俗易懂的理解:将军发布命令,士兵去执行。其中有几个角色:将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)。 Invoker 是调用者(将军),Receiver 是被调用者(士兵),MyCommand 是命令,实现了Command 接口,持有接收对象
16.3、命令模式的原理类图
对原理类图的说明-即(命名模式的角色及职责)
1) Invoker 是调用者角色
2) Command: 是命令角色,需要执行的所有命令都在这里,可以是接口或抽象类
3) Receiver: 接受者角色,知道如何实施和执行一个请求相关的操作
4) ConcreteCommand: 将一个接受者对象与一个动作绑定,调用接受者相应的操作,实现execute
16.4、命令模式解决智能生活项目
应用实例要求
1) 编写程序,使用命令模式 完成前面的智能家电项目
2) 思路分析和图解
3)代码实现
/**
* description
* 命令模式中——命令接口
* @author xujicheng
* @since 2022年12月04日 10:17
*/
public interface Command {
//执行动作(操作)
void execute();
//撤销动作(操作)
void undo();
}
/**
* description
* 命令模式中——电灯打开的命令
*
* @author xujicheng
* @since 2022年12月04日 10:31
*/
public class LightOnCommand implements Command {
//聚合接受者(LightReceiver)
LightReceiver light;
//构造器,用于初始化接收者对象
public LightOnCommand(LightReceiver light) {
this.light = light;
}
@Override
public void execute() {
//调用接受者的方法
light.on();
}
@Override
public void undo() {
//调用接受者的方法
light.off();
}
}
/**
* description
* 命令模式中——电灯关闭的命令
*
* @author xujicheng
* @since 2022年12月04日 10:31
*/
public class LightOffCommand implements Command {
//聚合接受者(LightReceiver)
LightReceiver light;
//构造器,用于初始化接收者对象
public LightOffCommand(LightReceiver light) {
this.light = light;
}
@Override
public void execute() {
//调用接受者的方法
light.off();
}
@Override
public void undo() {
//调用接受者的方法
light.on();
}
}
/**
* description
* 没有任何命令,表示空执行,用于初始化每个按钮,当调用空命令时,对象什么都不做即可
* 这样也是一种设计模式,这样可以省掉非空操作
*
* @author xujicheng
* @since 2022年12月04日 10:46
*/
public class NoCommand implements Command {
@Override
public void execute() {
}
@Override
public void undo() {
}
}
/**
* description
* 遥控器
*
* @author xujicheng
* @since 2022年12月04日 10:57
*/
public class RemoteController {
//开按钮的命令数组
Command[] onCommands;
//关按钮的命令数组
Command[] offCommands;
//执行撤销的命令
Command undoCommand;
//写一个构造器完成对按钮的初始化
public RemoteController() {
onCommands = new Command[5];
offCommands = new Command[5];
for (int i = 0; i < 5; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
}
//给我们的按钮设置需要的命令即可
public void setCommand(int no, Command onCommand, Command offCommand) {
onCommands[no] = onCommand;
offCommands[no] = offCommand;
}
//按下开的按钮
public void onButtonWasPushed(int no) {
//找到按下的开按钮并调用对应的方法
onCommands[no].execute();
//记录这一次的操作用于撤销
undoCommand = onCommands[no];
}
//按下关的按钮
public void offButtonWasPushed(int no) {
//找到按下的关按钮并调用对应的方法
offCommands[no].execute();
//记录这一次的操作用于撤销
undoCommand = offCommands[no];
}
//按下撤销的按钮
public void undoButtonWasPushed() {
undoCommand.undo();
}
}
/**
* description
* 命令模式中的接受者角色
*
* @author xujicheng
* @since 2022年12月04日 10:33
*/
public class LightReceiver {
void on(){
System.out.println("电灯打开了.. ");
}
void off(){
System.out.println("电灯关闭了.. ");
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月04日 11:25
*/
public class Client {
public static void main(String[] args) {
//调用查看命令模式如何通过遥控器完成对电灯的操作
LightReceiver lightReceiver = new LightReceiver();
//创建电灯相关的打开命令
LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
//创建电灯相关的关闭命令
LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
//需要一个遥控器来控制
RemoteController remoteController = new RemoteController();
//给我们的遥控器设置相关命令,比如no == 0的是电灯的开和关的操作
remoteController.setCommand(0,lightOnCommand,lightOffCommand);
System.out.println("=========按下电灯的开按钮========");
remoteController.onButtonWasPushed(0);
System.out.println("=========按下电灯的关按钮========");
remoteController.offButtonWasPushed(0);
System.out.println("=========按下撤销按钮========");
remoteController.undoButtonWasPushed();
}
}
经测试的输出结果为:
同时我们发现,这个模式的扩展性非常好,当我们加入一个电视时,只需要增加电视开关的具体命令和电视接收者即可,遥控器不需要做任何变化,符合开闭原则,实现扩展的代码如下:
** * description * 命令模式中的接收者——电视的接受者,用于接收具体命令 * * @author xujicheng * @since 2022年12月04日 11:38 */ public class TVReceiver { public void on(){ System.out.println("电视打开了.. "); } public void off(){ System.out.println("电视关闭了.. "); } } /** * description * 命令模式中——电视关闭的命令 * * @author xujicheng * @since 2022年12月04日 11:37 */ public class TVOffCommand implements Command{ //聚合接受者(TVReceiver) TVReceiver TV; //构造器,用于初始化接收者对象 public TVOffCommand(TVReceiver TV) { this.TV = TV; } @Override public void execute() { //调用接受者的方法 TV.off(); } @Override public void undo() { //调用接受者的方法 TV.on(); } } /** * description * 命令模式中——电视打开的命令 * * @author xujicheng * @since 2022年12月04日 11:38 */ public class TVOnCommand implements Command { //聚合接受者(TVReceiver) TVReceiver TV; //构造器,用于初始化接收者对象 public TVOnCommand(TVReceiver TV) { this.TV = TV; } @Override public void execute() { //调用接受者的方法 TV.on(); } @Override public void undo() { //调用接受者的方法 TV.off(); } } /** * description * 客户端 * * @author xujicheng * @since 2022年12月04日 11:25 */ public class Client { public static void main(String[] args) { //需要一个遥控器来控制 RemoteController remoteController = new RemoteController(); //对电视剧使用遥控器操作电视机 System.out.println("=========使用遥控操作电视========"); TVReceiver tvReceiver = new TVReceiver(); TVOnCommand tvOnCommand = new TVOnCommand(tvReceiver); TVOffCommand tvOffCommand = new TVOffCommand(tvReceiver); //给我们的遥控器设置相关命令,比如no == 1的是电视的开和关的操作 remoteController.setCommand(1,tvOnCommand,tvOffCommand); System.out.println("=========按下电视的开按钮========"); remoteController.onButtonWasPushed(1); System.out.println("=========按下电视的关按钮========"); remoteController.offButtonWasPushed(1); System.out.println("=========按下电视撤销按钮========"); remoteController.undoButtonWasPushed(); } }
- 执行结果如下:
16.5、 命令模式在 Spring 框架 JdbcTemplate 应用的源码分析
1) Spring 框架的 JdbcTemplate 就使用到了命令模式
2) 代码分析
3) 模式角色分析说明
- StatementCallback 接口 ,类似命令接口(Command)
- class QueryStatementCallback implements StatementCallback, SqlProvider , 匿名内部类,实现了命令接口,同时也充当命令接收者
- 命令调用者 是 JdbcTemplate , 其中 execute(StatementCallback action) 方法中,调用action.doInStatement 方法. 不同的 实现 StatementCallback 接口的对象,对应不同的 doInStatemnt 实现逻辑
- 另外实现 StatementCallback 命令接口的子类还有 QueryStatementCallback
16.6、命令模式的注意事项和细节
1) 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:”请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用。
2) 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令
3) 容易实现对请求的撤销和重做
4) 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意
5) 空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一个按键都要判空,这给我们编码带来一定的麻烦。
6) 命令模式经典的应用场景:界面的一个按钮都是一条命令、模拟 CMD(DOS 命令)订单的撤销/恢复、触发-反馈机制
十七、访问者模式
17.1、测评系统的需求
完成测评系统需求
1) 将观众分为男人和女人,对歌手进行测评,当看完某个歌手表演后,得到他们对该歌手不同的评价(评价有不同的种类,比如 成功、失败 等)
2) 传统方案
17.2、传统方式的问题分析
1) 如果系统比较小,还是 ok 的,但是考虑系统增加越来越多新的功能时,对代码改动较大,违反了ocp 原则,不利于维护 2) 扩展性不好,比如 增加了 新的人员类型,或者管理方法,都不好做
3) 引出我们会使用新的设计模式 – 访问者模式
17.3、访问者模式基本介绍
1) 访问者模式(Visitor Pattern),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
2) 主要将数据结构与数据操作分离,解决 数据结构和操作耦合性问题
3) 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
4) 访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决
对原理类图的说明- 即(访问者模式的角色及职责)
1) Visitor 是抽象访问者,为该对象结构中的 ConcreteElement 的每一个类声明一个 visit 操作
2) ConcreteVisitor :是一个具体的访问者实现每个有 Visitor 声明的操作,是每个操作实现的部分.
3) ObjectStructure 能枚举它的元素, 可以提供一个高层的接口,用来允许访问者访问元素
4) Element 定义一个 accept 方法,接收一个访问者对象
5) ConcreteElement 为具体元素,实现了 accept 方法
17.5、 访问者模式应用实例
应用实例要求
1) 将人分为男人和女人,对歌手进行测评,当看完某个歌手表演后,得到他们对该歌手不同的评价(评价有不同的种类,比如 成功、失败 等),请使用访问者模式来说实现
2) 思路分析和图解(类图)
3) 代码实现
/**
* description
* 访问者模式中——抽象访问者
*
* @author xujicheng
* @since 2022年12月04日 21:42
*/
public abstract class Action {
//得到男性的测评
public abstract void getManResult(Man man);
//得到女性的测评
public abstract void getWomanResult(Woman woman);
}
/**
* description
* 访问者模式中——接收访问者对象的抽象类
*
* @author xujicheng
* @since 2022年12月04日 21:47
*/
public abstract class Person {
//提供一个方法让访问者可以访问
public abstract void accept(Action action);
}
/**
* description
* 访问者模式中——接收访问者对象的子类
* 说明:
* 1、这里使用到了双分派,即首先在客户端程序中将具体的状态作为参数传递到Man中(第一次分派)
* 2、在Man 类中调用了作为参数的 "具体方法"中的方法,getManResult,同时将自己作为参数传入
* 完成了第二次分派,使用双分派的好处是达到解耦的效果,且程序处理起来会比较简单。
*
* @author xujicheng
* @since 2022年12月04日 21:48
*/
public class Man extends Person {
@Override
public void accept(Action action) {
action.getManResult(this);
}
}
/**
* description
* 访问者模式中——接收访问者对象的子类
* 说明:
* 1、这里使用到了双分派,即首先在客户端程序中将具体的状态作为参数传递到Woman中(第一次分派)
* 2、在Woman 类中调用了作为参数的 "具体方法"中的方法,即getWomanResult,同时将自己作为参数传入
* 完成了第二次分派,使用双分派的好处是达到解耦的效果,且程序处理起来会比较简单。
*
* @author xujicheng
* @since 2022年12月04日 21:49
*/
public class Woman extends Person {
@Override
public void accept(Action action) {
action.getWomanResult(this);
}
}
/**
* description
* 访问者模式中——抽象访问者的具体实现子类
*
* @author xujicheng
* @since 2022年12月04日 21:50
*/
public class Success extends Action{
@Override
public void getManResult(Man man) {
System.out.println("男人给的评价是该歌手很成功");
}
@Override
public void getWomanResult(Woman woman) {
System.out.println("男人给的评价是该歌手很成功");
}
}
/**
* description
* 访问者模式中——抽象访问者的具体实现子类
*
* @author xujicheng
* @since 2022年12月04日 22:01
*/
public class Fail extends Action{
@Override
public void getManResult(Man man) {
System.out.println("男人给的评价是该歌手很失败");
}
@Override
public void getWomanResult(Woman woman) {
System.out.println("女人给的评价是该歌手很失败");
}
}
/**
* description
* 访问者模式中的数据结构——能枚举它的元素, 可以提供一个高层的接口,用来允许访问者访问元素
*
* @author xujicheng
* @since 2022年12月04日 22:11
*/
public class ObjectStructure {
//维护了一个集合
private List<Person> persons = new LinkedList<>();
//增加到list
public void attach(Person p){
persons.add(p);
}
//移除
public void detach(Person p){
persons.remove(p);
}
//显示测评情况
public void display(Action action){
for (Person p : persons){
p.accept(action);
}
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月04日 22:18
*/
public class Client {
public static void main(String[] args) {
//创建ObjectStructure
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.attach(new Man());
objectStructure.attach(new Woman());
//成功
Success success = new Success();
objectStructure.display(success);
System.out.println("=========================");
Fail fail = new Fail();
objectStructure.display(fail);
}
}
小结:以上述实例为例,假设我们要添加一个 Wait 的状态类,考察 Man 类和 Woman 类的反应,由于使用了双分派,只需增加一个 Action 子类即可在客户端调用即可,不需要改动任何其他类的代码。
/**
* description
* 访问者模式中——抽象访问者的具体实现子类
*
* @author xujicheng
* @since 2022年12月04日 22:27
*/
public class Wait extends Action {
@Override
public void getManResult(Man man) {
System.out.println("男人给的评价是待定");
}
@Override
public void getWomanResult(Woman woman) {
System.out.println("女人给的评价是待定");
}
}
17.6、访问者模式的注意事项和细节
优点
1) 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
2) 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统
缺点
- 1) 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的, 这样造成了具体元素变更比较困难
2) 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
3) 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的
十八、迭代器模式
18.1、看一个具体的需求
编写程序展示一个学校院系结构:需求是这样,要在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系。如图:
18.2、传统的设计方案(类图)
18.3、 传统的方式的问题分析
1) 将学院看做是学校的子类,系是学院的子类,这样实际上是站在组织大小来进行分层次的
2) 实际上我们的要求是 :在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系,因此这种方案,不能很好实现的遍历的操作
3) 解决方案:=> 迭代器模式
18.4、迭代器模式基本介绍
基本介绍
- 1) 迭代器模式(Iterator Pattern)是常用的设计模式,属于行为型模式
- 2) 如果我们的集合元素是用不同的方式实现的,有数组,还有 java 的集合类,或者还有其他方式,当客户端要遍历这些集合元素的时候就要使用多种遍历方式,而且还会暴露元素的内部结构,可以考虑使用迭代器模式解决。
- 3) 迭代器模式,提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示,即:不暴露其内部的结构。
18.5、迭代器模式的原理类图
对原理类图的说明-即(迭代器模式的角色及职责)
- 1) 抽象迭代器:Iterator : 迭代器接口,是系统提供,含有 hasNext, next, remove
- 2) 具体聚合:ConcreteIterator : 具体的迭代器类,管理迭代
- 3) 抽象聚合:Aggregate :一个统一的聚合接口, 将客户端和具体聚合解耦
- 4) 具体迭代器:ConcreteAggreage : 具体的聚合持有对象集合, 并提供一个方法,返回一个迭代器,该迭代器可以正确遍历集合
- 5) Client :客户端, 通过 Iterator 和 Aggregate 依赖子类
18.6、迭代器模式应用实例
1) 应用实例要求 编写程序展示一个学校院系结构:需求是这样,要在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系。
2) 设计思路分析
3) 代码实现
/** * description * 系的实体类,实现对应的getter和setter方法和用于初始化的构造器 * * @author xujicheng * @since 2022年12月07日 21:39 */ public class Department { private String name; private String description; public Department(String name, String description) { this.name = name; this.description = description; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } /** * description * 计算机工程学院 * * @author xujicheng * @since 2022年12月07日 21:42 */ public class ComputerCollegeIterator implements Iterator { //计算机工程学院是以数组的方式存放 Department[] departments; int position = 0; //遍历的位置 public ComputerCollegeIterator(Department[] departments) { this.departments = departments; } //判断是否还有下一个元素 @Override public boolean hasNext() { if (position >= departments.length || departments[position] == null){ return false; } else { return true; } } @Override public Object next() { Department department = departments[position]; position +=1; //将position后移一位 return department; } //删除的方法默认空实现012 @Override public void remove() { } } /** * description * 信息工程学院 * * @author xujicheng * @since 2022年12月07日 21:43 */ public class InfoCollegeIterator implements Iterator { //信息工程学院是以List方式存放 List<Department> departmentlist; int index = -1; //索引 public InfoCollegeIterator(List<Department> departmentlist) { this.departmentlist = departmentlist; } //判断list中还有没有下一个元素 @Override public boolean hasNext() { if (index >= departmentlist.size() - 1) { return false; } else { index += 1; return true; } } @Override public Object next() { return departmentlist.get(index); } //空实现remove方法 @Override public void remove() { } } /** * description * 一个统一的聚合接口, 将客户端和具体聚合解耦 * * @author xujicheng * @since 2022年12月07日 22:02 */ public interface Collage { String getName(); /** * 增加系的方法 * * @param name 系的名字 * @param description 系的描述 */ void addDepartment(String name, String description); //返回一个迭代器,用于遍历 Iterator createIterator(); } /** * description * 具体的学院子类 * * @author xujicheng * @since 2022年12月07日 22:13 */ public class ComputerCollege implements Collage { Department[] departments; int numOfDepartment = 0; //保存当前数组的对象个数 public ComputerCollege() { departments = new Department[5]; addDepartment("Java专业","Java是全世界最好的编程语言"); addDepartment("PHP专业","PHP是全世界最好的编程语言"); addDepartment("C++专业","C++是全世界最好的编程语言"); addDepartment("GO专业","GO是全世界最好的编程语言"); addDepartment("JSP专业","JSP是全世界最好的编程语言"); } @Override public String getName() { return "计算机学院"; } @Override public void addDepartment(String name, String description) { Department department = new Department(name, description); departments[numOfDepartment] = department; numOfDepartment += 1; } @Override public Iterator createIterator() { return new ComputerCollegeIterator(departments); } } /** * description * 具体的学院子类——信息学院 * * @author xujicheng * @since 2022年12月07日 22:21 */ public class InfoCollege implements Collage { List<Department> departmentList; public InfoCollege() { departmentList = new ArrayList<Department>(); addDepartment("信息安全专业","信息安全专业的描述"); addDepartment("网络安全专业","网络安全专业的描述"); addDepartment("服务器安全专业","服务器安全专业的描述"); } @Override public String getName() { return "信息工程学院"; } @Override public void addDepartment(String name, String description) { Department department = new Department(name, description); departmentList.add(department); } @Override public Iterator createIterator() { return new InfoCollegeIterator(departmentList); } } /** * description * 输出类,用于输出学院系的所有信息 * * @author xujicheng * @since 2022年12月07日 22:27 */ public class OutPutImpl { //学院集合 List<Collage> collageList; //构造器,用于初始化 public OutPutImpl(List<Collage> collageList) { this.collageList = collageList; } //遍历所有的学院,然后调用printDepartment输出各个学院的系 public void printCollege() { //从collageList取出所有的学院 Iterator<Collage> iterator = collageList.iterator(); while (iterator.hasNext()) { //取出一个学院 Collage collage = iterator.next(); System.out.println("=======" + collage.getName() + "========"); printDepartment(collage.createIterator()); //得到对应的迭代器 } } /** * 学院输出系 * * @param iterator 迭代器 */ public void printDepartment(Iterator iterator) { while (iterator.hasNext()) { Department d = (Department) iterator.next(); System.out.println(d.getName()); } } } /** * description * 客户端 * * @author xujicheng * @since 2022年12月07日 22:35 */ public class Client { public static void main(String[] args) { //创建学院 List<Collage> collageList = new ArrayList<>(); ComputerCollege computerCollege = new ComputerCollege(); InfoCollege infoCollege = new InfoCollege(); collageList.add(computerCollege); collageList.add(infoCollege); //创建一个输出的对象 OutPutImpl outPut = new OutPutImpl(collageList); outPut.printCollege(); } }
当我们在使用 Java 开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现
java.util.Iterable
并实现其中的 iterator() 方法使其返回一个java.util.Iterator
的实现类就可以了
18.7、迭代器模式在 JDK-ArrayList 集合应用的源码分析
1) JDK 的 ArrayList 集合中就使用了迭代器模式
2) 代码分析+类图+说明
3) 对类图的角色分析和说明
- 内部类 Itr 充当具体实现迭代器 Iterator 的类, 作为 ArrayList 内部类
- List 就是充当了聚合接口,含有一个 iterator() 方法,返回一个迭代器对象
- ArrayList 是实现聚合接口 List 的子类,实现了 iterator()
- Iterator 接口系统提供
- 迭代器模式解决了 不同集合(ArrayList ,LinkedList) 统一遍历问题
18.8、迭代器模式的注意事项和细节
优点
1) 提供一个统一的方法遍历对象,客户不用再考虑聚合的类型,使用一种方法就可以遍历对象了。
2) 隐藏了聚合的内部结构,客户端要遍历聚合的时候只能取到迭代器,而不会知道聚合的具体组成。
3) 提供了一种设计思想,就是一个类应该只有一个引起变化的原因(叫做单一责任原则)。在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。而如果遍历方式改变的话,只影响到了迭代器。
4) 当要展示一组相似对象,或者遍历一组相同对象时使用, 适合使用迭代器模式
缺点
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
- 对于比较简单的遍历(数组或者有序列表),使用迭代器方式遍历较为繁琐而且遍历效率不高,使用迭代器的方式比较适合那些底层以链表形式实现的集合
18.9、迭代器模式的使用场景:
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 需要为聚合对象提供多种遍历方式。
- 为遍历不同的聚合结构提供一个统一的接口。
- JAVA 中的 iterator。
十九、观察者模式
19.1、天气预报项目需求,具体要求如下:
1) 气象站可以将每天测量到的温度,湿度,气压等等以公告的形式发布出去(比如发布到自己的网站或第三方)。
2) 需要设计开放型 API,便于其他第三方也能接入气象站获取数据。
3) 提供温度、气压和湿度的接口
4) 测量数据更新时,要能实时的通知给第三方
19.2、天气预报设计方案 1-普通方案
19.2.1、WeatherData 类
传统的设计方案
- 代码实现
/**
* description
* 显示当前天气情况(可以理解成气象站自己的网站)
*
* @author xujicheng
* @since 2022年12月07日 23:03
*/
public class CurrentConditions {
private float temperature; //温度
private float pressure; //气压
private float humidity; //湿度
//更新天气情况,由WeatherDate来调用,使用推送模式
public void update(float temperature, float pressure, float humidity) {
this.temperature = temperature;
this.pressure = pressure;
this.humidity = humidity;
display();
}
//用于显示
public void display() {
System.out.println("***Today mTemperature: " + temperature + "***");
System.out.println("***Today mPressure: " + pressure + "***");
System.out.println("***Today mHumidity: " + humidity + "***");
}
}
/**
* description
* 核心类,包含最新的天气情况,包含CurrentConditions对象,数据有更新时主动调用update方法
*
* @author xujicheng
* @since 2022年12月07日 23:03
*/
public class WeatherData {
private float temperature;
private float pressure;
private float humidity;
private CurrentConditions currentConditions;
public WeatherData(CurrentConditions currentConditions) {
this.currentConditions = currentConditions;
}
public float getTemperature() {
return temperature;
}
public float getPressure() {
return pressure;
}
public float getHumidity() {
return humidity;
}
public void dataChange() {
//调用接入方的update方法
currentConditions.update(getTemperature(), getPressure(), getHumidity());
}
//当数据有更新时,就调用setData
public void setData(float temperature, float pressure, float humidity) {
this.temperature = temperature;
this.pressure = pressure;
this.humidity = humidity;
//将最新的信息推送给接入方
dataChange();
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class Client {
public static void main(String[] args) {
//创建接入方
CurrentConditions currentConditions = new CurrentConditions();
//创建WeatherData对象并将接入方聚合到WeatherData中
WeatherData weatherData = new WeatherData(currentConditions);
//更新天气情况
weatherData.setData(30, 150, 40);
}
}
问题分析
- 1) 其他第三方接入气象站获取数据的问题
- 2) 无法在运行时动态的添加第三方 (新浪网站)
- 3) 违反 ocp 原则=>观察者模式
- 在 WeatherData 中,当增加一个第三方,都需要创建一个对应的第三方的公告板对象,并加入到dataChange, 不利于维护,也不是动态加入
public void dataChange() {
currentConditions.update(getTemperature(), getPressure(), getHumidity());
}
19.3、 观察者模式原理
1) 观察者模式类似订牛奶业务
2) 奶站/气象局:Subject
3) 用户/第三方网站:Observer
- Subject:登记注册、移除和通知
- 1) registerObserver 注册
- 2) removeObserver 移除
- 3) notifyObservers() 通知所有的注册的用户,根据不同需求,可以是更新数据,让用户来取,也可能是实施推送,看具体需求定
- Observer:接收输入
- 观察者模式:对象之间多对一依赖的一种设计方案,被依赖的对象为 Subject,依赖的对象为Observer,Subject 通知 Observer 变化,比如这里的奶站是 Subject,是 1 的一方。用户时 Observer,是多的一方。
19.4、观察者模式解决天气预报需求
19.4.1 、类图说明
19.4.2、角色分析
Subject(目标):被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的Vector 容器(之所以使用Vector而不使用List,是因为多线程操作时,Vector在是安全的,而List则是不安全的),这个Vector容器是被观察者类的核心,另外还有三个方法:attach方法是向这个容器中添加观察者对象;detach方法是从容器中移除观察者对象;notify方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。
ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。
Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法
update()
,因此又称为抽象观察者。ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 Observer 中定义的 update()方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。
19.4.3、代码实现
/**
* description
* 新的观察者——百度
*
* @author xujicheng
* @since 2022年12月08日 0:07
*/
public class BaiduSite implements Observer {
private float temperature; //温度
private float pressure; //气压
private float humidity; //湿度
//更新天气情况,由WeatherDate来调用,使用推送模式
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.pressure = pressure;
this.humidity = humidity;
display();
}
//用于显示
public void display() {
System.out.println("***百度网站气温: " + temperature + "***");
System.out.println("***百度网站气压: " + pressure + "***");
System.out.println("***百度网站湿度: " + humidity + "***");
}
}
/**
* description
* 观察者——具体的实现子类——气象站
*
* @author xujicheng
* @since 2022年12月07日 23:40
*/
public class CurrentConditions implements Observer{
private float temperature; //温度
private float pressure; //气压
private float humidity; //湿度
//更新天气情况,由WeatherDate来调用,使用推送模式
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.pressure = pressure;
this.humidity = humidity;
display();
}
//用于显示
public void display() {
System.out.println("***Today mTemperature: " + temperature + "***");
System.out.println("***Today mPressure: " + pressure + "***");
System.out.println("***Today mHumidity: " + humidity + "***");
}
}
/**
* description
* 观察者接口,由观察者来实现
*
* @author xujicheng
* @since 2022年12月07日 23:37
*/
public interface Observer {
/**
* 更新的方法
*
* @param temperature 温度
* @param humidity 气压
* @param pressure 湿度
*/
void update(float temperature, float humidity, float pressure);
}
/**
* description
* 接口,让WeatherData来实现
*
* @author xujicheng
* @since 2022年12月07日 23:35
*/
public interface Subject {
//注册
void registerObserver(Observer o);
//删除
void removeObserver(Observer o);
//通知
void notifyObservers();
}
/**
* description
* 核心类,包含观察者集合,包含CurrentConditions对象,数据有更新时主动调用update方法,通知接入方
*
* @author xujicheng
* @since 2022年12月07日 23:03
*/
public class WeatherData implements Subject {
private float temperature;
private float pressure;
private float humidity;
//观察者集合
private ArrayList<Observer> observers;
public WeatherData() {
observers = new ArrayList<Observer>();
}
public float getTemperature() {
return temperature;
}
public float getPressure() {
return pressure;
}
public float getHumidity() {
return humidity;
}
public void dataChange() {
//调用接入方的update方法
notifyObservers();
}
//当数据有更新时,就调用setData
public void setData(float temperature, float pressure, float humidity) {
this.temperature = temperature;
this.pressure = pressure;
this.humidity = humidity;
//调用 dataChange,将最新的信息 推送给 接入方 currentConditions
dataChange();
}
//注册一个观察者
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
//移除一个观察者
@Override
public void removeObserver(Observer o) {
if (observers.contains(o)) {
observers.remove(o);
}
}
//遍历所有的观察者,并通知
@Override
public void notifyObservers() {
for (int i = 0; i < observers.size(); i++) {
observers.get(i).update(getTemperature(), getPressure(), getHumidity());
}
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月07日 23:49
*/
public class Client {
public static void main(String[] args) {
//创建一个WeatherData
WeatherData weatherData = new WeatherData();
//创建观察者
CurrentConditions currentConditions = new CurrentConditions();
BaiduSite baiduSite = new BaiduSite();
//注册到WeatherData
weatherData.registerObserver(currentConditions);
weatherData.registerObserver(baiduSite);
//测试
System.out.println("通知各个注册的观察者");
weatherData.setData(10f,100f,30.3f);
}
}
19.4.3、 观察者模式的优缺点
优点
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系
- 目标与观察者之间建立了一套触发机制
- 支持广播通信
- 符合“开闭原则”的要求
缺点
- 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用
- 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率
19.5、观察者模式在 Jdk 应用的源码分析
1) Jdk 的 Observable 类就使用了观察者模式
2) 代码分析+模式角色分析
3) 模式角色分析
- Observable 的作用和地位等价于 我们前面讲过 Subject
- Observable 是类,不是接口,类中已经实现了核心的方法 ,即管理 Observer 的方法add.. delete .. notify...
- Observer 的作用和地位等价于我们前面讲过的 Observer, 有 update
- Observable 和 Observer 的使用方法和前面讲过的一样,只是 Observable 是类,通过继承来实现观察者模式
二十、中介者模式
20.1、智能家庭项目
智能家庭项目:
1) 智能家庭包括各种设备,闹钟、咖啡机、电视机、窗帘 等
2) 主人要看电视时,各个设备可以协同工作,自动完成看电视的准备工作,比如流程为:闹铃响起->咖啡机开始做咖啡->窗帘自动落下->电视机开始播放
20.2、传统方案解决智能家庭管理问题
20.3、传统的方式的问题分析
1) 当各电器对象有多种状态改变时,相互之间的调用关系会比较复杂
2) 各个电器对象彼此联系,你中有我,我中有你,不利于松耦合.
3) 各个电器对象之间所传递的消息(参数),容易混乱
4) 当系统增加一个新的电器对象时,或者执行流程改变时,代码的可维护性、扩展性都不理想考虑中介者模式
20.4、中介者模式基本介绍
基本介绍
- 1) 中介者模式(Mediator Pattern),用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
- 2) 中介者模式属于行为型模式,使代码易于维护
- 3) 比如 MVC 模式,C(Controller 控制器)是 M(Model 模型)和 V(View 视图)的中介者,在前后端交互时起到了中间人的作用
20.5、中介者模式的原理类图
对原理类图的说明-即(中介者模式的角色及职责)
- 1) Mediator 就是抽象中介者,定义了同事对象到中介者对象的接口
- 2) Colleague 是抽象同事类
- 3) ConcreteMediator 具体的中介者对象, 实现抽象方法, 他需要知道所有的具体的同事类,即以一个集合来管理HashMap,并接受某个同事对象消息,完成相应的任务
- 4) ConcreteColleague 具体的同事类,会有很多, 每个同事只知道自己的行为,而不了解其他同事类的行为(方法),但 是他们都依赖中介者对象
20.6、中介者模式应用实例-智能家庭管理
1) 应用实例要求 完成前面的智能家庭的项目,使用中介者模式
2) 思路分析和图解(类图)
3) 中介者模式——智能家庭的操作流程
- 1、创建ConcreMediator对象
- 2、在创建各个同事类对象,比如Alarm、CoffeeMachine、TV...
- 3、在创建同事类对象的时候,就直接通过构造器,加入到colleagueMap
- 4、同事类对象,可以调用sendMessage,最终会去调用ConcreteMediator
- 5、getMessage 会根据收到的同事对象发出的消息来协调调用其他的同事对象,完成任务
- 6、可以看到getMessage是核心方法,完成相应任务
/**
* description
* 同事抽象类
*
* @author xujicheng
* @since 2022年12月08日 23:23
*/
public abstract class Colleague {
private Mediator mediator;
public String name;
public Colleague(Mediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
public Mediator GetMediator() {
return this.mediator;
}
public abstract void SendMessage(int stateChange);
}
/**
* description
* 具体的同事类——闹钟
*
* @author xujicheng
* @since 2022年12月08日 23:23
*/
public class Alarm extends Colleague {
//构造器
public Alarm(Mediator mediator, String name) {
super(mediator, name);
//在创建同事对象时,将自己放入到ConcreteMediator 对象中
mediator.Register(name, this);
}
public void SendAlarm(int stateChange) {
SendMessage(stateChange);
}
@Override
public void SendMessage(int stateChange) {
//调用的中介者对象的GetMessage
this.GetMediator().GetMessage(stateChange, this.name);
}
}
/**
* description
* 具体的同事类——电视
*
* @author xujicheng
* @since 2022年12月08日 23:23
*/
public class TV extends Colleague {
//构造器
public TV(Mediator mediator, String name) {
super(mediator, name);
//在创建同事对象时,将自己放入到ConcreteMediator 对象中
mediator.Register(name, this);
}
@Override
public void SendMessage(int stateChange) {
//调用的中介者对象的GetMessage
this.GetMediator().GetMessage(stateChange, this.name);
}
public void StartTv() {
System.out.println("It's time to StartTv!");
}
public void StopTv() {
System.out.println("StopTv!");
}
}
/**
* description
* 中介者
*
* @author xujicheng
* @since 2022年12月08日 23:23
*/
public abstract class Mediator {
//将中介者对象加入到结合中
public abstract void Register(String colleagueName, Colleague colleague);
//接收消息,具体的同事对象发出的
public abstract void GetMessage(int stateChange, String colleagueName);
public abstract void SendMessage();
}
/**
* description
* 具体的中介者类
*
* @author xujicheng
* @since 2022年12月08日 23:23
*/
public class ConcreteMediator extends Mediator {
//集合,放入了所有的同事对象
private HashMap<String, Colleague> colleagueMap;
private HashMap<String, String> interMap;
public ConcreteMediator() {
colleagueMap = new HashMap<String, Colleague>();
interMap = new HashMap<String, String>();
}
@Override
public void Register(String colleagueName, Colleague colleague) {
colleagueMap.put(colleagueName, colleague);
if (colleague instanceof Alarm) {
interMap.put("Alarm", colleagueName);
} else if (colleague instanceof CoffeeMachine) {
interMap.put("CoffeeMachine", colleagueName);
} else if (colleague instanceof TV) {
interMap.put("TV", colleagueName);
} else if (colleague instanceof Curtains) {
interMap.put("Curtains", colleagueName);
}
}
/**
* 具体中介者的核心方法
* 1、根据得到消息,完成对应任务
* 2、中介者在这个方法中协调各个具体的同事类,完成任务
*/
@Override
public void GetMessage(int stateChange, String colleagueName) {
if (colleagueMap.get(colleagueName) instanceof Alarm) {
if (stateChange == 0) {
((CoffeeMachine) (colleagueMap.get(interMap
.get("CoffeeMachine")))).StartCoffee();
((TV) (colleagueMap.get(interMap.get("TV")))).StartTv();
} else if (stateChange == 1) {
((TV) (colleagueMap.get(interMap.get("TV")))).StopTv();
}
} else if (colleagueMap.get(colleagueName) instanceof CoffeeMachine) {
((Curtains) (colleagueMap.get(interMap.get("Curtains"))))
.UpCurtains();
} else if (colleagueMap.get(colleagueName) instanceof TV) {
} else if (colleagueMap.get(colleagueName) instanceof Curtains) {
}
}
@Override
public void SendMessage() {
}
}
public class ClientTest {
public static void main(String[] args) {
//创建一个中介者对象
Mediator mediator = new ConcreteMediator();
//创建了一个闹钟Alarm 并加入到ConcreteMediator 对象的HashMap中
Alarm alarm = new Alarm(mediator, "alarm");
//创建了CoffeeMachine 对象,并且加入到ConcreteMediator 对象的HashMap中
CoffeeMachine coffeeMachine = new CoffeeMachine(mediator,
"coffeeMachine");
//创建了Curtains 对象,并且加入到 ConcreteMediator对象的HashMap中p
Curtains curtains = new Curtains(mediator, "curtains");
TV tV = new TV(mediator, "TV");
//让闹钟发出了消息
alarm.SendAlarm(0);
coffeeMachine.FinishCoffee();
alarm.SendAlarm(1);
}
}
20.7、中介者模式的注意事项和细节
1) 多个类相互耦合,会形成网状结构, 使用中介者模式将网状结构分离为星型结构,进行解耦
2) 减少类间依赖,降低了耦合,符合迪米特原则
3) 中介者承担了较多的责任,一旦中介者出现了问题,整个系统就会受到影响
4) 如果设计不当,中介者对象本身变得过于复杂,这点在实际使用时,要特别注意
二十一、备忘录模式
21.1、游戏角色状态恢复问题
- 游戏角色有攻击力和防御力,在大战 Boss 前保存自身的状态(攻击力和防御力),当大战Boss 后攻击力和防御力下降,从备忘录对象恢复到大战前的状态
21.2、传统方案解决游戏角色恢复
21.3、传统的方式的问题分析
1) 一个对象,就对应一个保存对象状态的对象, 这样当我们游戏的对象很多时,不利于管理,开销也很大.
2) 传统的方式是简单地做备份,new 出另外一个对象出来,再把需要备份的数据放到这个新对象,但这就暴露了对象内部的细节
3) 解决方案: => 备忘录模式
21.4、备忘录模式基本介绍
- 基本介绍
- 1) 备忘录模式(Memento Pattern)在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态
- 2) 可以这里理解备忘录模式:现实生活中的备忘录是用来记录某些要去做的事情,或者是记录已经达成的共同意见的事情,以防忘记了。而在软件层面,备忘录模式有着相同的含义,备忘录对象主要用来记录一个对象的某种状态,或者某些数据,当要做回退时,可以从备忘录对象里获取原来的数据进行恢复操作
- 3) 备忘录模式属于行为型模式
21.5、备忘录模式的原理类图
对原理类图的说明-即(备忘录模式的角色及职责)
- 1) originator : 对象(需要保存状态的对象)
- 2) Memento : 备忘录对象,负责保存好记录,即 Originator 内部状态
- 3) Caretaker: 守护者对象,负责保存多个备忘录对象, 使用集合管理,提高效率
- 4) 说明:如果希望保存多个 originator 对象的不同时间的状态,也可以,只需要HashMap
代码实现
/** * description * 备忘录对象,负责保存好记录,即 Originator 内部状态 * * @author xujicheng * @since 2022年12月09日 15:51 */ public class Memento { private String state; //状态信息 //构造器 public Memento(String state) { this.state = state; } public String getState() { return state; } } /** * description * 原始对象(需要保存状态的对象) * * @author xujicheng * @since 2022年12月09日 15:49 */ public class Originator { private String state; //状态信息 public String getState() { return state; } public void setState(String state) { this.state = state; } //编写一个方法,可以保存一个状态对象Memento public Memento saveMemento() { return new Memento(state); } /** * 通过备忘录状态,恢复状态 * * @param memento 备忘录对象 */ public void getStateFromMemento(Memento memento) { state = memento.getState(); } } /** * description * 守护者对象,负责保存多个备忘录对象, 使用集合管理,提高效率 * * @author xujicheng * @since 2022年12月09日 15:55 */ public class Caretaker { //集合,用于管理多个备忘录对象 private List<Memento> mementoList = new ArrayList<Memento>(); /** * 添加备忘录对象到集合的方法 * * @param memento 备忘录对象 */ public void add(Memento memento) { mementoList.add(memento); } /** * 获取到Index个备忘录对象 * * @param index 计数器 * @return */ public Memento get(int index) { return mementoList.get(index); } } /** * description * 客户端 * * @author xujicheng * @since 2022年12月09日 15:59 */ public class Client { public static void main(String[] args) { //创建一个原生对象 Originator originator = new Originator(); //创建一个守护者对象 Caretaker caretaker = new Caretaker(); //给原生对象设置状态 originator.setState("Full"); //保存了当前状态 caretaker.add(originator.saveStateMemento()); originator.setState("Empty"); caretaker.add(originator.saveStateMemento()); //当前状态 System.out.println("当前的状态是=" + originator.getState()); //希望恢复到状态一 originator.getStateFromMemento(caretaker.get(0)); System.out.println("恢复后的状态是=" + originator.getState()); } }
21.6、游戏角色恢复状态实例
- 1) 应用实例要求 游戏角色有攻击力和防御力,在大战 Boss 前保存自身的状态(攻击力和防御力),当大战Boss 后攻击力和防御力下降,从备忘录对象恢复到大战前的状态
- 2) 思路分析和图解(类图)
代码实现
/**
* description
* 备忘录对象,负责保存好记录,即 Originator 内部状态
*
* @author xujicheng
* @since 2022年12月09日 16:30
*/
public class Memento {
private int vit; //攻击力
private int def; //防御力
//构造器,用于初始化攻击力和防御力
public Memento(int vit, int def) {
this.vit = vit;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
/**
* description
* 守护者对象,负责保存多个备忘录对象, 使用集合管理,提高效率
*
* @author xujicheng
* @since 2022年12月09日 16:36
*/
public class Caretaker {
//如果只保存一次状态
private Memento memento;
//对GameRole保存多次状态
private ArrayList<Memento> mementos;
//对多个游戏角色保存多个状态
private HashMap<String,ArrayList<Memento>> RolesMementos;
public Caretaker(Memento memento) {
this.memento = memento;
}
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
/**
* description
* 游戏角色——对应的是备忘录模式中的对象角色
*
* @author xujicheng
* @since 2022年12月09日 16:48
*/
public class GameRole {
private int vit; //攻击力
private int def; //防御力
//创建Memento,即根据当前的状态的带Memento
public Memento createMemento() {
return new Memento(vit, def);
}
/**
* 从备忘录对象恢复GameRole的状态
*
* @param memento 备忘录对象
*/
public void recoverGameRoleFromMemento(Memento memento) {
this.vit = memento.getVit();
this.def = memento.getDef();
}
//显示当前游戏角色的状态
public void display() {
System.out.println("游戏角色当前的攻击力:" + this.vit + "游戏角色当前的防御力:" + this.def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月09日 16:53
*/
public class Client {
public static void main(String[] args) {
//创建游戏角色
GameRole gameRole = new GameRole();
gameRole.setVit(100);
gameRole.setDef(100);
System.out.println("和BOSS大战前的状态");
gameRole.display();
//把当前状态保存到守护者对象
Caretaker caretaker = new Caretaker(gameRole.createMemento());
System.out.println("和Boss开始大战");
gameRole.setDef(30);
gameRole.setVit(30);
gameRole.display();
System.out.println("大战后使用备忘录对象恢复角色状态");
gameRole.recoverGameRoleFromMemento(caretaker.getMemento());
System.out.println("恢复后的状态");
gameRole.display();
}
}
21.7、备忘录模式的注意事项和细节
1) 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
2) 实现了信息的封装,使得用户不需要关心状态的保存细节
3) 如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存, 这个需要注意
4) 适用的应用场景:1、后悔药。 2、打游戏时的存档。 3、Windows 里的 ctri + z。4、IE 中的后退。4、数据库的事务管理
5) 为了节约内存,备忘录模式可以和原型模式配合使用
二十二、解释器模式
22.1、四则运算问题
通过解释器模式来实现四则运算,如计算 a+b-c 的值,具体要求
1) 先输入表达式的形式,比如 a+b+c-d+e, 要求表达式的字母不能重复
2) 在分别输入 a ,b, c, d, e 的值 3) 最后求出结果:如图
22.2、传统方案解决四则运算问题分析
1) 编写一个方法,接收表达式的形式,然后根据用户输入的数值进行解析,得到结果
2) 问题分析:如果加入新的运算符,比如 * / ( 等等,不利于扩展,另外让一个方法来解析会造成程序结构混乱,不够清晰.
3) 解决方案:可以考虑使用解释器模式, 即: 表达式 -> 解释器(可以有多种) -> 结果
22.3、 解释器模式基本介绍
基本介绍
- 1) 在编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析树。这里的词法分析器和语法分析器都可以看做是解释器
- 2) 解释器模式(Interpreter Pattern):是指给定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)
- 3) 应用场景
- 应用可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
- 一些重复出现的问题可以用一种简单的语言来表达一个简单语法需要解释的场景
- 4) 这样的例子还有,比如编译器、运算表达式计算、正则表达式、机器人等
22.4、解释器模式的原理类图
对原理类图的说明-即(解释器模式的角色及职责)
- 1) Context: 是环境角色,含有解释器之外的全局信息.
- 2) AbstractExpression: 抽象表达式, 声明一个抽象的解释操作,这个方法为抽象语法树中所有的节点所共享
- 3) TerminalExpression: 为终结符表达式, 实现与文法中的终结符相关的解释操作
- 4) NonTermialExpression: 为非终结符表达式,为文法中的非终结符实现解释操作.
- 5) 说明: 输入 Context he TerminalExpression 信息通过 Client 输入即
22.5、解释器模式来实现四则运算问题
1) 应用实例要求 通过解释器模式来实现四则运算, 如计算 a+b-c 的值
2) 思路分析和图解(类图)
3) 代码实现
/**
* description
* 抽象类表达式,通过HashMap键值对,可以获取到各个变量的值
*
* @author xujicheng
* @since 2022年12月09日 18:26
*/
public abstract class Expression {
//解释公式和数组的关系,key是公式中的参数 ,value就是具体值
public abstract int interpreter(HashMap<String, Integer> var);
}
/**
* description
* 变量的解释器
*
* @author xujicheng
* @since 2022年12月08日 20:59
*/
public class VarExpression extends Expression {
private String key; //字符
//构造器,用于初始化字符的值
public VarExpression(String key) {
this.key = key;
}
//var就是{a = 10,b = 20},根据变量的名称返回对应的值
@Override
public int interpreter(HashMap<String, Integer> var) {
return var.get(this.key);
}
}
/**
* description
* 抽象的运算符号解析器 这里,每个运算符号都之和自己左右的两个数字有更新
* 但左右两个数字有可能也是一个解析的结构,无论何种类型,都是Expression类的实现类
*
* @author xujicheng
* @since 2022年12月08日 20:59
*/
public class SymbolExpression extends Expression {
protected Expression left;
protected Expression right;
//构造器
public SymbolExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
//因为SymbolExpression是让其子类来实现,因此interpreter方法是一个空实现
@Override
public int interpreter(HashMap<String, Integer> var) {
return 0;
}
}
/**
* description
* 减法解析器
*
* @author xujicheng
* @since 2022年12月08日 20:59
*/
public class SubExpression extends SymbolExpression {
public SubExpression(Expression left, Expression right) {
super(left, right);
}
//处理相减的方法
@Override
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) - super.right.interpreter(var);
}
}
/**
* description
* 加法解析器
*
* @author xujicheng
* @since 2022年12月08日 20:59
*/
public class AddExpression extends SymbolExpression {
public AddExpression(Expression left, Expression right) {
super(left, right);
}
//处理相加的方法
@Override
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) + super.right.interpreter(var);
}
}
/**
* description
* 环境角色,含有解释器之外的全局信息
*
* @author xujicheng
* @since 2022年12月08日 20:59
*/
public class Calculator {
//定义表达式
private Expression expression;
// 构造函数传参,并解析
public Calculator(String expStr) {
//安排运算先后顺序
Stack<Expression> stack = new Stack<>();
//将表达式拆分成字符数组
char[] charArray = expStr.toCharArray();
Expression left = null;
Expression right = null;
//遍历我们的字符数组,即遍历[a,+,b],针对不同的情况做相应处理
for (int i = 0; i < charArray.length; i++) {
switch (charArray[i]) {
case '+': //加法
left = stack.pop();// 从stack中取出left
right = new VarExpression(String.valueOf(charArray[++i]));//取出右边的表达式
stack.push(new AddExpression(left, right));//根据得到的left和right构建AddExpression加入stack
break;
case '-': //减法
left = stack.pop();
right = new VarExpression(String.valueOf(charArray[++i]));
stack.push(new SubExpression(left, right));
break;
default:
//如果是一个Var就创建一个VarExpression对象并push到stack
stack.push(new VarExpression(String.valueOf(charArray[i])));
break;
}
}
//当遍历完整个charArray数组后,stack就得到最后的Expression
this.expression = stack.pop();
}
public int run(HashMap<String, Integer> var) {
//最好将表达式和var绑定,然后传递给expression的interpreter进行解释执行
return this.expression.interpreter(var);
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月08日 20:59
*/
public class ClientTest {
public static void main(String[] args) throws IOException {
String expStr = getExpStr();
HashMap<String, Integer> var = getValue(expStr);
Calculator calculator = new Calculator(expStr);
System.out.println("运算结果" + expStr + "=" + calculator.run(var));
}
//获得表达式
public static String getExpStr() throws IOException {
System.out.print("请输入表达式:");
return (new BufferedReader(new InputStreamReader(System.in))).readLine();
}
//获得值映射
public static HashMap<String, Integer> getValue(String expStr) throws IOException {
HashMap<String, Integer> map = new HashMap<>();
for (char ch : expStr.toCharArray()) {
if (ch != '+' && ch != '-') {
if (!map.containsKey(String.valueOf(ch))) {
System.out.print("请输入" + String.valueOf(ch) + "的值");
String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
map.put(String.valueOf(ch), Integer.valueOf(in));
}
}
}
return map;
}
}
22.6、解释器模式在 Spring 框架应用的源码剖析
1) Spring 框架中 SpelExpressionParser 就使用到解释器模式
2) 代码分析+Debug 源码
3) 说明
22.7、解释器模式的注意事项和细节
1) 当有一个语言需要解释执行,可将该语言中的句子表示为一个抽象语法树,就可以考虑使用解释器模式,让程序具有良好的扩展性
2) 应用场景:编译器、运算表达式计算、正则表达式、机器人等
3) 使用解释器可能带来的问题:解释器模式会引起类膨胀、解释器模式采用递归调用方法,将会导致调试非常复杂、效率可能降低
二十三、状态模式
23.1、APP 抽奖活动问题
请编写程序完成 APP 抽奖活动 具体要求如下:
1) 假如每参加一次这个活动要扣除用户 50 积分,中奖概率是 10%
2) 奖品数量固定,抽完就不能抽奖
3) 活动有四个状态: 可以抽奖、不能抽奖、发放奖品和奖品领完
4) 活动的四个状态转换关系图(右图)
23.2、状态模式基本介绍
基本介绍
1) 状态模式(State Pattern):它主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。状态和行为是一一对应的,状态之间可以相互转换
2) 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类
23.3、状态模式的原理类图
对原理类图的说明-即(状态模式的角色及职责)
- 1) Context 类为环境角色, 用于维护 State 实例,这个实例定义当前状态
- 2) State 是抽象状态角色,定义一个接口封装与 Context 的一个特点接口相关行为
- 3) ConcreteState 具体的状态角色,每个子类实现一个与 Context 的一个状态相关行为
23.4、状态模式解决 APP 抽奖问题
1) 应用实例要求 完成 APP 抽奖活动项目,使用状态模式.
2) 思路分析和图解(类图) -定义出一个接口叫状态接口,每个状态都实现它。 -接口有扣除积分方法、抽奖方法、发放奖品方法
3) 代码实现
/**
* description
* 状态抽象类
*
* @author xujicheng
* @since 2022年12月08日 23:24
*/
public abstract class State {
//扣除积分
public abstract void deductMoney();
//是否抽中奖品
public abstract boolean raffle();
//发放奖品
public abstract void dispensePrize();
}
/**
* description
* 不能抽奖的状态
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class NoRaffleState extends State {
//初始化式传入活动引用,扣除积分后改变其状态
RaffleActivity activity;
//构造器,用于初始化状态
public NoRaffleState(RaffleActivity activity) {
this.activity = activity;
}
//当前状态可以扣积分,扣除后将状态设置成可以抽奖状态
@Override
public void deductMoney() {
System.out.println("扣除50积分成功,您可以抽奖了");
activity.setState(activity.getCanRaffleState());
}
//当前状态不能抽奖
@Override
public boolean raffle() {
System.out.println("扣了积分才能抽奖喔");
return false;
}
//当前状态不能发放奖品
@Override
public void dispensePrize() {
System.out.println("不能发放奖品");
}
}
/**
* description
* 可以抽奖的状态
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class CanRaffleState extends State {
//初始化式传入活动引用,抽中奖品后改变其状态
RaffleActivity activity;
//构造器,用于初始化
public CanRaffleState(RaffleActivity activity) {
this.activity = activity;
}
//已经扣除了积分,不能重复扣除
@Override
public void deductMoney() {
System.out.println("已经扣除过积分了");
}
//可以抽奖,抽完奖后,根据实际情况,改变成新的状态
@Override
public boolean raffle() {
System.out.println("正在抽奖,请稍等!");
Random r = new Random();
int num = r.nextInt(10);
// 10%的中奖机会
if(num == 0){
//改变活动状态为发放奖品
activity.setState(activity.getDispenseState());
return true;
}else{
System.out.println("很遗憾没有抽中奖品");
//改变状态为不能抽奖
activity.setState(activity.getNoRafflleState());
return false;
}
}
//不能发放奖品的状态
@Override
public void dispensePrize() {
System.out.println("没中奖,不能发放奖品");
}
}
/**
* description
* 发放奖品的状态
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class DispenseState extends State {
//初始化式传入活动引用,发放奖品后改变其状态
RaffleActivity activity;
//构造器,用于初始化对象
public DispenseState(RaffleActivity activity) {
this.activity = activity;
}
@Override
public void deductMoney() {
System.out.println("不能扣除积分");
}
@Override
public boolean raffle() {
System.out.println("不能抽奖");
return false;
}
//发放奖品的状态
@Override
public void dispensePrize() {
if(activity.getCount() > 0){
System.out.println("恭喜中奖");
// 改变状态为不能抽奖
activity.setState(activity.getNoRafflleState());
}else{
System.out.println("很遗憾,奖品发完了");
//改变状态为奖品发放完毕,后面就不可以再抽奖了
activity.setState(activity.getDispensOutState());
}
}
}
/**
* description
* 奖品发放完毕,活动结束的状态
* 说明:当我们activity 改变成DispenseOutState,即所有的抽奖活动结束
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class DispenseOutState extends State {
//初始化式传入活动引用,奖品发放完毕后改变其状态
RaffleActivity activity;
//构造器,用于初始化对象
public DispenseOutState(RaffleActivity activity) {
this.activity = activity;
}
@Override
public void deductMoney() {
System.out.println("奖品发完了,请下次积极参加~");
}
@Override
public boolean raffle() {
System.out.println("奖品发完了,请下次积极参加~");
return false;
}
@Override
public void dispensePrize() {
System.out.println("奖品发完了,请下次积极参加~");
}
}
/**
* description
* 抽奖活动
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class RaffleActivity {
//state表示活动当前状态,是变化的
State state = null;
//奖品数量
int count = 0;
//四个属性表示四种状态
State noRafflleState = new NoRaffleState(this);
State canRaffleState = new CanRaffleState(this);
State dispenseState = new DispenseState(this);
State dispensOutState = new DispenseOutState(this);
/**
* 构造器,初始化当前状态为noRaffleState,(即不能抽奖状态)
* 初始化奖品数量
*
* @param count 奖品数量
*/
public RaffleActivity(int count) {
this.state = getNoRafflleState();
this.count = count;
}
//扣积分,调用当前状态的deductMoney
public void debutMoney() {
state.deductMoney();
}
//抽奖
public void raffle() {
//如果当前状态是抽奖成功
if (state.raffle()) {
//领取奖品
state.dispensePrize();
}
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
//每领取一次奖品,count要--
public int getCount() {
int curCount = count;
count--;
return curCount;
}
public void setCount(int count) {
this.count = count;
}
public State getNoRafflleState() {
return noRafflleState;
}
public void setNoRaffleState(State noRafflleState) {
this.noRafflleState = noRafflleState;
}
public State getCanRaffleState() {
return canRaffleState;
}
public void setCanRaffleState(State canRaffleState) {
this.canRaffleState = canRaffleState;
}
public State getDispenseState() {
return dispenseState;
}
public void setDispenseState(State dispenseState) {
this.dispenseState = dispenseState;
}
public State getDispensOutState() {
return dispensOutState;
}
public void setDispenseOutState(State dispensOutState) {
this.dispensOutState = dispensOutState;
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class ClientTest {
public static void main(String[] args) {
//创建活动对象,奖品有一个奖品
RaffleActivity activity = new RaffleActivity(1);
//我们连续抽30次奖
for (int i = 0; i < 30; i++) {
System.out.println("--------第" + (i + 1) + "次抽奖----------");
//参加抽奖,第一步点击扣除积分
activity.debutMoney();
// 第二步抽奖
activity.raffle();
}
}
}
23.5、状态模式在实际项目-借贷平台 源码剖析
1) 借贷平台的订单,有审核-发布-抢单 等等 步骤,随着操作的不同,会改变订单的状态, 项目中的这个模块实现就会使用到状态模式
2) 通常通过 if/else 判断订单的状态,从而实现不同的逻辑,伪代码如下
3)借贷平台的类图
4) 使用状态模式完成 借贷平台项目的审核模块 [设计+代码]
/**
* description
* 状态接口
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public interface State {
/**
* 电审
*
* @param context 环境角色
*/
void checkEvent(Context context);
/**
* 电审失败
*
* @param context 环境角色
*/
void checkFailEvent(Context context);
/**
* 定价发布
*
* @param context 环境角色
*/
void makePriceEvent(Context context);
/**
* 接单
*
* @param context 环境角色
*/
void acceptOrderEvent(Context context);
/**
* 无人接单失效
*
* @param context 环境角色
*/
void notPeopleAcceptEvent(Context context);
/**
* 付款
*
* @param context 环境角色
*/
void payOrderEvent(Context context);
/**
* 接单有人支付失败
*
* @param context 环境角色
*/
void orderFailureEvent(Context context);
/**
* 反馈
*
* @param context 环境角色
*/
void feedBackEvent(Context context);
String getCurrentState();
}
/**
* description
* 具体的状态角色
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public abstract class AbstractState implements State {
protected static final RuntimeException EXCEPTION = new RuntimeException("操作流程不允许");
/**
* 抽象类默认实现了State接口的所有方法
* 该类的所有方法,其子类(具体的状态类),可以有选择的进行重写
*
* @param context 环境角色
*/
@Override
public void checkEvent(Context context) {
throw EXCEPTION;
}
@Override
public void checkFailEvent(Context context) {
throw EXCEPTION;
}
@Override
public void makePriceEvent(Context context) {
throw EXCEPTION;
}
@Override
public void acceptOrderEvent(Context context) {
throw EXCEPTION;
}
@Override
public void notPeopleAcceptEvent(Context context) {
throw EXCEPTION;
}
@Override
public void payOrderEvent(Context context) {
throw EXCEPTION;
}
@Override
public void orderFailureEvent(Context context) {
throw EXCEPTION;
}
@Override
public void feedBackEvent(Context context) {
throw EXCEPTION;
}
}
/**
* description
* 各种具体状态类
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
//反馈状态
class FeedBackState extends AbstractState {
@Override
public String getCurrentState() {
return StateEnum.FEED_BACKED.getValue();
}
}
//通用状态
class GenerateState extends AbstractState {
@Override
public void checkEvent(Context context) {
context.setState(new ReviewState());
}
@Override
public void checkFailEvent(Context context) {
context.setState(new FeedBackState());
}
@Override
public String getCurrentState() {
return StateEnum.GENERATE.getValue();
}
}
//没有付款的状态
class NotPayState extends AbstractState {
@Override
public void payOrderEvent(Context context) {
context.setState(new PaidState());
}
@Override
public void feedBackEvent(Context context) {
context.setState(new FeedBackState());
}
@Override
public String getCurrentState() {
return StateEnum.NOT_PAY.getValue();
}
}
//付款的状态
class PaidState extends AbstractState {
@Override
public void feedBackEvent(Context context) {
context.setState(new FeedBackState());
}
@Override
public String getCurrentState() {
return StateEnum.PAID.getValue();
}
}
//发布的状态
class PublishState extends AbstractState {
@Override
public void acceptOrderEvent(Context context) {
//把当前状态设置为NotPayState,至于应该变成哪个状态由流程图决定
context.setState(new NotPayState());
}
@Override
public void notPeopleAcceptEvent(Context context) {
context.setState(new FeedBackState());
}
@Override
public String getCurrentState() {
return StateEnum.PUBLISHED.getValue();
}
}
//回顾的状态
class ReviewState extends AbstractState {
@Override
public void makePriceEvent(Context context) {
context.setState(new PublishState());
}
@Override
public String getCurrentState() {
return StateEnum.REVIEWED.getValue();
}
}
/**
* description
* 环境上下文
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class Context extends AbstractState{
//当前状态state,根据我们的业务流程处理,不停的变化
private State state;
@Override
public void checkEvent(Context context) {
state.checkEvent(this);
getCurrentState();
}
@Override
public void checkFailEvent(Context context) {
state.checkFailEvent(this);
getCurrentState();
}
@Override
public void makePriceEvent(Context context) {
state.makePriceEvent(this);
getCurrentState();
}
@Override
public void acceptOrderEvent(Context context) {
state.acceptOrderEvent(this);
getCurrentState();
}
@Override
public void notPeopleAcceptEvent(Context context) {
state.notPeopleAcceptEvent(this);
getCurrentState();
}
@Override
public void payOrderEvent(Context context) {
state.payOrderEvent(this);
getCurrentState();
}
@Override
public void orderFailureEvent(Context context) {
state.orderFailureEvent(this);
getCurrentState();
}
@Override
public void feedBackEvent(Context context) {
state.feedBackEvent(this);
getCurrentState();
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
@Override
public String getCurrentState() {
System.out.println("当前状态" + state.getCurrentState());
return state.getCurrentState();
}
}
/**
* description
* 客户端——测试类
*
* @author xujicheng
* @since 2022年12月07日 23:09
*/
public class ClientTest {
public static void main(String[] args) {
//创建上下文对象Context
Context context = new Context();
//将当前状态设置为PublishState
context.setState(new PublishState());
System.out.println(context.getCurrentState());
//publish --> not pay
context.acceptOrderEvent(context);
}
}
23.6、状态模式的注意事项和细节
1) 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中
2) 方便维护。将容易产生问题的 if-else 语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产出很多 if-else 语句,而且容易出错
3) 符合“开闭原则”。容易增删状态
4) 会产生很多类。每个状态都要一个对应的类,当状态过多时会产生很多类,加大维护难度
5) 应用场景:当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式
二十四、策略模式
24.1、编写鸭子项目,具体要求如下:
1) 有各种鸭子(比如 野鸭、北京鸭、水鸭等, 鸭子有各种行为,比如 叫、飞行等)
2) 显示鸭子的信息
24.2、传统方案解决鸭子问题的分析和代码实现
1) 传统的设计方案(类图)
2) 代码实现
/**
* description
* 传统方式解决鸭子问题——鸭子的抽象类
*
* @author xujicheng
* @since 2022年12月10日 23:03
*/
public abstract class Duck {
//构造器,用于初始化抽象类对象
public Duck() {
}
public abstract void display(); //显示鸭子的信息
//鸭子叫的方法
public void quack() {
System.out.println("鸭子嘎嘎叫");
}
//鸭子游泳的方法
public void swim() {
System.out.println("鸭子嗷嗷游");
}
//鸭子飞的方法
public void fly() {
System.out.println("鸭子诺诺飞");
}
}
/**
* description
* 鸭子的具体实现子类——野鸭
*
* @author xujicheng
* @since 2022年12月10日 23:07
*/
public class WildDuck extends Duck{
@Override
public void display() {
System.out.println("这是一直野鸭");
}
}
/**
* description
* 鸭子的具体实现子类——北京鸭
*
* @author xujicheng
* @since 2022年12月10日 23:08
*/
public class Peking extends Duck{
@Override
public void display() {
System.out.println("这是一只北京鸭");
}
//由于北京鸭不能飞,因此需要重写fly方法
@Override
public void fly() {
System.out.println("北京鸭不能飞");
}
}
/**
* description
* 鸭子的具体实现子类——玩具鸭
*
* @author xujicheng
* @since 2022年12月10日 23:10
*/
public class ToyDuck extends Duck {
@Override
public void display() {
System.out.println("这是一只玩具鸭");
}
@Override
public void quack() {
System.out.println("玩具鸭不能叫");
}
@Override
public void swim() {
System.out.println("玩具鸭不会游泳");
}
@Override
public void fly() {
System.out.println("玩具鸭不会飞");
}
}
24.3、传统的方式实现的问题分析和解决方案
1) 其它鸭子,都继承了 Duck 类,所以 fly 让所有子类都会飞了,这是不正确的
2) 上面说的 1 的问题,其实是继承带来的问题:对类的局部改动,尤其超类的局部改动,会影响其他部分。会有溢出效应
3) 为了改进 1 问题,我们可以通过覆盖 fly 方法来解决 => 覆盖解决
4) 问题又来了,如果我们有一个玩具鸭子 ToyDuck, 这样就需要 ToyDuck 去覆盖 Duck 的所有实现的方法=>解决思路 -》 策略模式 (strategy pattern)
24.4、策略模式基本介绍
1) 策略模式(Strategy Pattern)中,定义算法族(策略组),分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户
2) 这算法体现了几个设计原则,第一、把变化的代码从不变的代码中分离出来;第二、针对接口编程而不是具体类(定义了策略接口);第三、多用组合/聚合,少用继承(客户通过组合方式使用策略)。
24.5、策略模式的原理类图
策略模式主要由这三个角色组成,环境角色(Context)、抽象策略角色(Strategy)和具体策略角色(ConcreteStrategy)。
- 环境角色(Context):持有一个策略类的引用,提供给客户端使用。
- 抽象策略角色(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略角色(ConcreteStrategy):包装了相关的算法或行为
说明:从上图可以看到,客户 context 有成员变量 strategy 或者其他的策略接口 ,至于需要使用到哪个策略,我们可以在构造器中指定
24.6、策略模式解决鸭子问题
1) 应用实例要求 编写程序完成前面的鸭子项目,要求使用策略模式
2) 思路分析(类图) 策略模式:分别封装行为接口,实现算法族,超类里放行为接口对象,在子类里具体设定行为对象。原则就是:分离变化部分,封装接口,基于接口编程各种功能。此模式让行为的变化独立于算法的使用者
3) 代码实现
/**
* description
* 策略模式中——抽象策略角色
*
* @author xujicheng
* @since 2022年12月10日 23:27
*/
public interface FlyBehavior {
void fly(); //子类具体实现即可
}
/**
* description
* 抽象策略角色的具体实现子类
*
* @author xujicheng
* @since 2022年12月10日 23:29
*/
public class GoodFlyBehavior implements FlyBehavior{
@Override
public void fly() {
System.out.println("飞翔技术高超");
}
}
/**
* description
* 抽象策略角色的具体实现子类
*
* @author xujicheng
* @since 2022年12月10日 23:31
*/
public class NoFlyBehavior implements FlyBehavior{
@Override
public void fly() {
System.out.println("不会飞的鸭子");
}
}
/**
* description
* 抽象策略角色的具体实现子类
*
* @author xujicheng
* @since 2022年12月10日 23:30
*/
public class BadFlyBehavior implements FlyBehavior {
@Override
public void fly() {
System.out.println("飞翔技术拙劣");
}
}
/**
* description
* 鸭子的抽象类
*
* @author xujicheng
* @since 2022年12月10日 23:03
*/
public abstract class Duck {
//属性,聚合策略接口
FlyBehavior flyBehavior;
//构造器,用于初始化抽象类对象
public Duck() {
}
public abstract void display(); //显示鸭子的信息
//鸭子叫的方法
public void quack() {
System.out.println("鸭子嘎嘎叫");
}
//鸭子游泳的方法
public void swim() {
System.out.println("鸭子嗷嗷游");
}
//鸭子飞的方法
public void fly() {
//对抽象类Duck中的抽象方法进行改进,若具体实现子类不为空说明已获取到具体子类
if (flyBehavior != null){
flyBehavior.fly();
}
}
}
/**
* description
* 鸭子的具体实现子类——野鸭
*
* @author xujicheng
* @since 2022年12月10日 23:07
*/
public class WildDuck extends Duck {
//构造器,传入FlyBehavior的对象
public WildDuck() {
flyBehavior = new GoodFlyBehavior();
}
@Override
public void display() {
System.out.println("这是一直野鸭");
}
}
/**
* description
* 鸭子的具体实现子类——北京鸭
*
* @author xujicheng
* @since 2022年12月10日 23:08
*/
public class Peking extends Duck {
//构造器,传入FlyBehavior的对象
public Peking() {
flyBehavior = new NoFlyBehavior();
}
@Override
public void display() {
System.out.println("这是一只北京鸭");
}
}
/**
* description
* 鸭子的具体实现子类——玩具鸭
*
* @author xujicheng
* @since 2022年12月10日 23:10
*/
public class ToyDuck extends Duck {
//构造器,传入FlyBehavior的对象
public ToyDuck() {
flyBehavior = new NoFlyBehavior();
}
@Override
public void display() {
System.out.println("这是一只玩具鸭");
}
@Override
public void quack() {
System.out.println("玩具鸭不能叫");
}
@Override
public void swim() {
System.out.println("玩具鸭不会游泳");
}
@Override
public void fly() {
System.out.println("玩具鸭不会飞");
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月10日 23:54
*/
public class Client {
public static void main(String[] args) {
//创建一个野鸭对象
WildDuck wildDuck = new WildDuck();
wildDuck.fly();
//创建一个玩具鸭对象
ToyDuck toyDuck = new ToyDuck();
toyDuck.fly();
//创建一个北京鸭对象
Peking peking = new Peking();
peking.fly();
}
//也可以在抽象类中给给策略接口定义set方法从而在程序运行时动态改变某个对象的行为
}
24.7、策略模式在 JDK-Arrays 应用的源码分析
1) JDK 的 Arrays 的 Comparator 就使用了策略模式
24.8、策略模式的注意事项和细节
1) 策略模式的关键是:分析项目中变化部分与不变部分
2) 策略模式的核心思想是:多用组合/聚合 少用继承;用行为类组合,而不是行为的继承。更有弹性
3) 体现了“对修改关闭,对扩展开放”原则,客户端增加行为不用修改原有代码,只要添加一种策略(或者行为)即可,避免了使用多重转移语句(if..else if..else)
4) 提供了可以替换继承关系的办法: 策略模式将算法封装在独立的 Strategy 类中使得你可以独立于其Context 改变它,使它易于切换、易于理解、易于扩展
5) 需要注意的是:每添加一个策略就要增加一个类,当策略过多是会导致类数目庞
二十五、职责链模式
25.1、学校 OA 系统的采购审批项目:需求是
采购员采购教学器材
1) 如果金额 小于等于 5000, 由教学主任审批 (0<=x<=5000)
2) 如果金额 小于等于 10000, 由院长审批 (5000<X <= 10000)
3) 如果金额 小于等于 30000, 由副校长审批 (10000<X<=30000)
4) 如果金额 超过 30000 以上,有校长审批 ( 30000<X)
请设计程序完成采购审批项目
25.2、传统方案解决 OA 系统审批,传统的设计方案(类图)
25.3、传统方案解决 OA 系统审批问题分析
1) 传统方式是:接收到一个采购请求后,根据采购金额来调用对应的 Approver (审批人)完成审批。2) 传统方式的问题分析 : 客户端这里会使用到 分支判断(比如 switch) 来对不同的采购请求处理,这样就存在如下问题 (1) 如果各个级别的人员审批金额发生变化,在客户端的也需要变化
2) 客户端必须明确的知道有多少个审批级别和访问
3) 这样 对一个采购请求进行处理 和 Approver (审批人) 就存在强耦合关系,不利于代码的扩展和维护
4) 解决方案 =》 职责链模式
25.4、职责链模式基本介绍
基本介绍
- 1) 职责链模式(Chain of Responsibility Pattern), 又叫 责任链模式,为请求创建了一个接收者对象的链(简单示意图)。这种模式对请求的发送者和接收者进行解耦。
- 2) 职责链模式通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
- 3) 这种类型的设计模式属于行为型模式
25.5、职责链模式的原理类图
对原理类图的说明-即(职责链模式的角色及职责)
- 1) Handler : 抽象的处理者, 定义了一个处理请求的接口, 同时含有另外 Handler
- 2) ConcreteHandlerA , B 是具体的处理者, 处理它自己负责的请求, 可以访问它的后继者(即下一个处理者), 如果可以处理当前请求,则处理,否则就将该请求交个 后继者去处理,从而形成一个职责链
- 3) Request , 含又很多属性,表示一个请求
25.6、职责链模式解决 OA 系统采购审批
1) 应用实例要求
编写程序完成学校 OA 系统的采购审批项目:需求
采购员采购教学器材
如果金额 小于等于 5000, 由教学主任审批
如果金额 小于等于 10000, 由院长审批
如果金额 小于等于 30000, 由副校长审批
如果金额 超过 30000 以上,由校长审批
2) 思路分析和图解(类图)
3) 代码实现
/**
* description
* 责任链模式中——Request 含有很多属性,表示一个请求
*
* @author xujicheng
* @since 2022年12月11日 9:51
*/
public class PurchaseRequest {
private int type = 0; //请求类型
private float price = 0.0f; //请求金额
private int id = 0; //id号,初始化为0
//构造器,用于初始化属性
public PurchaseRequest(int type, float price, int id) {
this.type = type;
this.price = price;
this.id = id;
}
public int getType() {
return type;
}
public float getPrice() {
return price;
}
public int getId() {
return id;
}
}
/**
* description
* 抽象的处理者, 定义了一个处理请求的接口, 同时含有另外的处理者
*
* @author xujicheng
* @since 2022年12月11日 9:59
*/
public abstract class Approve {
Approve approve; //下一个处理者(审批人)
String name; //名字
//构造器,用于初始化对象
public Approve(String name) {
this.name = name;
}
//生成setter方法从而便于获取下一个处理者
public void setApprove(Approve approve) {
this.approve = approve;
}
//处理审批请求的方法,得到一个请求,处理是子类完成的,因此将该方法做成抽象方法
public abstract void processRequest(PurchaseRequest purchaseRequest);
}
/**
* description
* 具体的处理者——院长审批
*
* @author xujicheng
* @since 2022年12月11日 10:24
*/
public class CollegeApprove extends Approve {
//构造器,用于初始化对象
public CollegeApprove(String name) {
super(name);
}
@Override
public void processRequest(PurchaseRequest purchaseRequest) {
if (purchaseRequest.getPrice() < 5000 && purchaseRequest.getPrice() <= 10000) {
System.out.println("请求编号id=" + purchaseRequest.getId() + "被" + this.name + "处理");
} else {
approve.processRequest(purchaseRequest);
}
}
}
/**
* description
* 具体的处理者——副校长审批
*
* @author xujicheng
* @since 2022年12月11日 10:36
*/
public class ViceSchoolMasterApprove extends Approve {
public ViceSchoolMasterApprove(String name) {
super(name);
}
@Override
public void processRequest(PurchaseRequest purchaseRequest) {
if (purchaseRequest.getPrice() < 5000 && purchaseRequest.getPrice() <= 30000) {
System.out.println("请求编号id=" + purchaseRequest.getId() + "被" + this.name + "处理");
} else {
approve.processRequest(purchaseRequest);
}
}
}
/**
* description
* 具体的处理者——校长审批
*
* @author xujicheng
* @since 2022年12月11日 10:41
*/
public class SchoolMasterApprove extends Approve {
public SchoolMasterApprove(String name) {
super(name);
}
@Override
public void processRequest(PurchaseRequest purchaseRequest) {
if (purchaseRequest.getPrice() > 30000) {
System.out.println("请求编号id=" + purchaseRequest.getId() + "被" + this.name + "处理");
}
}
}
/**
* description
* 客户端
*
* @author xujicheng
* @since 2022年12月11日 10:45
*/
public class Client {
public static void main(String[] args) {
//创建一个请求对象
PurchaseRequest purchaseRequest = new PurchaseRequest(1,31000,1);
//创建相关的审批人
DepartmentApprove departmentApprove = new DepartmentApprove("主任");
CollegeApprove collegeApprove = new CollegeApprove("院长");
ViceSchoolMasterApprove viceSchoolMasterApprove = new ViceSchoolMasterApprove("副校长");
SchoolMasterApprove schoolMasterApprove = new SchoolMasterApprove("校长");
//需要将各个审批级别的下一级设置好,设置成环形就不会变成空指针了
departmentApprove.setApprove(collegeApprove);
collegeApprove.setApprove(viceSchoolMasterApprove);
viceSchoolMasterApprove.setApprove(schoolMasterApprove);
schoolMasterApprove.setApprove(departmentApprove);
departmentApprove.processRequest(purchaseRequest);
}
}
25.7、职责链模式在 SpringMVC 框架应用的源码分析
1) SpringMVC-HandlerExecutionChain 类就使用到职责链模式
2) SpringMVC 请求流程简图
3) 代码分析+Debug 源码+说明
5) 对源码总结
- springmvc 请求的流程图中,执行了 拦截器相关方法 interceptor.preHandler 等等
- 在处理 SpringMvc 请求时,使用到职责链模式还使用到适配器模式
- HandlerExecutionChain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程
- HandlerExecutionChain 维护了 HandlerInterceptor 的集合, 可以向其中注册相应的拦截器
25.8、职责链模式的注意事项和细节
1) 将请求和处理分开,实现解耦,提高系统的灵活性
2) 简化了对象,使对象不需要知道链的结构
3) 性能会受到影响,特别是在链比较长的时候,因此需控制链中最大节点数量,一般通过在Handler 中设置一个最大节点数量,在 setNext()方法中判断是否已经超过阀值,超过则不允许该链建立,避免出现超长链无意识地破坏系统性能
4) 调试不方便。采用了类似递归的方式,调试时逻辑可能比较复杂
5) 最佳应用场景:有多个对象可以处理同一个请求时,比如:多级请求、请假/加薪等审批流程、Java Web 中Tomcat 对 Encoding 的处理、拦截器