设计模式学习笔记

设计模式

七大原则

  • 单一职责:一个类只负责一项业务,一项业务不是一个业务,比如UserService负责用户业务

  • 接口隔离:一个类对另一个类的依赖应该建立在最小的接口上,比如A通过Interface interface来使用其实现类B,如果A只用到b的1,2,3方法,C通过interface使用实现类D,只用到1,4,5方法,则应该设计三个接口,分别包含方法1,23,45

  • 依赖倒置:类A依赖类B,不应该是直接依赖,而是通过依赖B的接口,这样保证了程序的拓展性

    • 依赖有三种方式,举例的接口叫C

      • //new 一个成员变量
        //组合composite
        class A{
            C c=new B();
        }
        
    	//set方法
        //聚合Aggregation
        class A{
            C c;
            private void setC(C c){
                this.c=c;
            }
      }
    
    • //构造器
      //也是composite的一种
      class A{
          C c;
          A(C c){
             this.c=c;
          }
      }
      
  • 里氏替换:父类已经实现好的方法,子类修改会对整个继承体系造成破坏,子类尽量不要重写父类的方法

    • 所有引用基类的地方必须能透明的使用其子类的对象
    • 这个原则告诉我们,继承实际让两个类耦合增强了,适当情况下使用组合,聚合等其他方式实现
  • 开闭原则:编程中最基础,最重要的设计原则,其他原则都是为了满足开闭原则,目的是为了保证软件的拓展性

    • 一个软件实体,像类/模块/方法应该支持扩展,拒绝修改
  • 扩展软件时,主要通过扩展软件实体的行为来实现变化,而不是改变已有的代码

  • 用抽象构建框架,用实现扩展细节

  • 迪米特法则:又成为最少知道原则,一个类对自己依赖的类应该知道的越少越好,不管被依赖的类多么复杂,都应该尽量将逻辑封装在类的内部

    • 直接的朋友:成员变量/方法参数/方法返回值,不是直接朋友的类最好不要以局部变量的形式出现在类的内部
  • 合成复用原则:尽量使用聚合,合成的方式,而不是继承

类图UML

  • 依赖dependency:类中用到对方(方法参数,返回值,方法中使用,成员变量),就存在依赖关系,虚线表示
  • 泛化generation:继承关系,实线,空心箭头
  • 实现implement:实现接口关系,虚线,空心箭头
  • 关联association:表示类之间的联系,可以是双向的,比如都互相用到了对方
  • 聚合aggregation:表示整体和部分的关系,整体和部分可以分开,多个部分组装成一个整体,称为聚合,空心菱形表示
    • 聚合中,整体类和部分类的生命周期不一样,不是一起new出来的
    • 举例大概是人和身份证的关系
  • 组合composition:整体和部分不可以分开,整体类和部分类的生命周期一致,new整体类的时候也new了部分类
    • 举例大概是人和脑袋的关系

单例模式

整个程序中,某个类只存在一个对象

饿汉式

  • 构造器私有化
  • 本类内部创建一个static对象实例instance,作为成员变量
    • 创建可以通过静态代码块也可以直接new
  • 提供一个公有的static方法返回instance

优点:写法简单,装载类的时候就完成了实例化,没有线程同步问题

缺点:可能从未使用过这个实例,有可能内存浪费

懒汉式

在调用getInstance的时候,如果instance==null则创建

这是线程不安全的,但是可以起到lazy loading的效果

lazy loading的意思是,只有当真正需要数据时,才执行数据加载操作

实际开发中不适用这种写法

改进:getInstance函数加一个synchronized修饰符,让线程调用这个函数时进行排队,实现线程同步

​ 问题是,每次getInstance方法都要排队,而只有第一次调用时排队是我们需要的,效率太低,所以实际开发中也不适用这种方法

双重检查

利用双重检查保证了线程同步问题,又实现了lazy loading,同时保证了效率

推荐使用

class Singleton{
    private static volatile Singleton instance;
    private Singleton(){}
    
    public static synchronized Singleton getInstance(){
        if(instance==null){
            //同步代码块
            synchronized(Singleton.class){
                if(instance==null)instance=new instance();
            }
        }
        return instance;
    }
}

volatile是防止指令重排,new不是一个原子操作,分为三步:分配内存空间,执行构造方法初始化对象,把对象指向这个空间

如果指令重排,先分配内存,对象指向这个空间,再调用构造函数初始化对象的话

有可能A进程分配内存,对象指向了这个空间,同时B进程return instance,得到了一个没有初始化的对象,这是不被允许的

静态内部类

JVM在加载外部类的时候并不会加载其静态内部类,在使用到静态内部类的时候才会对静态内部类进行加载

而在JVM装载类的时候是线程安全的,这就解决了线程问题

public class Singleton {
	private static class HolderClass{
    	private static final Singleton instance = new Singleton();
	}
 
	public static Singleton getInstance(){
   	 return HolderClass.instance;
	}
}

枚举

enum Singleton{
   INSTANCE;
    public void sayOk(){
        System.out.println("ok");
    }
}

这种方法也能实现单例模式,能避免多线程同步问题,还能防止反序列化重新创建对象,推荐使用

总结

  1. 饿汉式(静态常量)
  2. 饿汉式(静态代码块)
  3. 饿汉式(线程不安全)
  4. 饿汉式(synchronized方法,线程安全,效率低)
  5. 懒汉式(if null里面做同步代码块,线程不安全,实现不了)
  6. 双重检查
  7. 静态内部类
  8. 枚举

实现方式上只有懒汉式不推荐使用

应用场景:

  • Windows的任务管理器,回收站
  • 项目中读取配置文件的类
  • 网站计数器,为了保证同步
  • 数据库连接池
  • Servlet也是单例的
  • Spring中每个bean也是单例的

JDK里的java.lang.Runtime就是一个经典的,饿汉式实现的单例模式

  • 对系统上需要频繁创建和销毁的对象/经常用到的重量级对象/工具类对象/频繁访问数据库或文件的对象(Session工厂,DataSource),使用单例模式
  • 使用单例模式可以提高系统性能
  • 如果想要实例化一个单例,需要使用响应获取对象的方法

通过反射是可以摧毁单例模式的,但是不可以通过反射获得枚举的属性

工厂模式

简单工厂模式

  • 简单工厂模式属于创建型模式,简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例
  • 工厂模式家族最简单使用的模式
  • 概括说就是,定义一个创建对象的类,由这个类来封装实例化对象的代码
  • 当我们需要大量创建某个类的对象时,要用到工厂模式

当用户想要得到一个对象时,不必知道这个对象构造所需要的细节,而是直接从工厂里得到这个对象

这样就实现了当前类与想要得到的类的解耦:原本要依赖所有的Car实现和Car接口,现在只需要依赖CarFactory

比如:车和车工厂的例子,车工厂通过一个String判断生产哪种车,或者为每种车设置一个get方法,返回一个new出来的对象。但是这两种方法都不满足开闭原则,当工厂中横向增加新车的时候,前者需要修改方法的逻辑,后者需要修改类(增加get方法),都是在原有代码的基础上修改的

工厂方法模式

为了实现开闭原则,可以把CarFactory抽象出来,为每种Car都设计一个CarFactory的具体实现类,每个工厂只创建一种车

当横向增加新车的时候,只需要新增车和对应工厂的实现类即可,这样就实现了开闭原则

1653442608410

但是缺点是显而易见的:代码量过大,结构变得复杂

总结

从结构,代码,编程,管理的复杂度上看,都是简单工厂更优秀,但是根据设计原则,应该采用工厂方法模式

而大多数软件都是采用简单工厂模式,为设计原则牺牲太多也是需要考量的

应用场景:

  • JDK中Calendar的getInstance方法
  • JDBC的Connection对象获取
  • Spring的IOC容器创建管理bean对象
  • 反射中Class对象的newInstance方法

抽象工厂模式

简单工厂和工厂方法模式解决的是同一类问题:生产同一等级结构中的产品(比如手机中的华为手机和小米手机)

而抽象工厂模式是解决产品族中的产品:华为产品和小米产品

1653446564021

抽象工厂类似一个共产模板

抽象层面,接口有ProductFactory,PhoneProduct和RouterProduct三个,ProductFactory定义了生产PhoneProduct和RouterProduct的方法

华为产品工厂和小米产品工厂实现了ProductFactory,设计生产华为手机,华为路由器,小米手机,小米路由器的方法

总结

抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定他们具体的类

适用场景:

  • 客户端不依赖于产品类实例如何创建、实现细节等(工厂模式通用)
  • 强调一系列相关产品(产品族)一起使用创建对象需要大量的重复代码
  • 提供一个产品类的库,所有同一等级结构的产品都已同样的接口出现,从而使得客户端不依赖于具体的实现

优点:

  • 具体产品在应用层的代码隔离,无需关心创建的细节(工厂模式通用)
  • 将一个系列的产品统一到一起创建
  • 增加一个产品族是简单的,只需增加一个总工厂接口的实现即可

缺点:

  • 规定了所有可能被创建的集合,产品族中扩展新的产品需要修改原有的代码(比如工厂接口中新增创建笔记本的方法,每个实现类都需要再新增,也可以新创建一个接口,进行接口多实现,但是这样代码臃肿)
  • 增加抽象性和理解难度

建造者模式

封装复杂对象的建造过程,相比于工厂模式,更注重于建造过程的用户可自定义,工厂模式注重的是得到对象

有几个角色

Director指挥者,Builder建造者(抽象类),Worker工人,Product产品

通常是这么用的:

Product product=new Director().build(new Worker());

Worker是Builder的实现类,Director设计一个build方法负责间接调用builder实现类的各种buildA,buildB,buildC细节方法

Director调用不同的Builder实现类可以实现选择不同的建造方法

还有一种用户可以自定义建造顺序的,把用户自己当作Director

public class test {
    public static void main(String[] args) {
        Worker worker = new Worker();
        Product product = worker.buildA("全家桶").buildB("雪碧").getProduct();
        System.out.println(product.toString());
    }
}

总结一下:

应用场景:

  • 需要生成的产品对象有复杂的内部结构,又具备共性
  • 相同的创建过程可以创建不同的产品
  • 适用于具有较多零件(属性)的对象的创建过程

建造者模式与抽象工厂模式的比较:

  • 抽象工厂模式需要实例化工厂类,调用工厂类得到所需对象,而建造者模式可以不直接调用Builder的相关方法,而是通过指挥者类来指导如何生成对象
  • 抽象工厂返回一个产品族中的对象,侧重于生产;建造者模式侧重于一步步构造一个复杂对象,侧重于组装

原型模式

创造一个对象原型,想要获得另一份对象的时候希望直接clone而不是再经过复杂的过程创建

clone也分两种:

  • 浅克隆:对象中的引用类型只clone引用,指向同一份内存空间
  • 深克隆:克隆对象的时候也对其引用类型进行clone;当然如果其引用类型里也有引用类型也需要再clone一层

浅克隆的话。只需要调用Object类里定义的native方法clone()

深克隆的话,就需要override这个clone(),对其引用类型也clone()一份

clone()底层是由c++实现的,是在内存层面上进行拷贝,效率高,比在java层面的new和io要快得多

应用实例: Spring 的Bean中有单例模式也有原型模式

原型模式可以和工厂模式结合:如果工厂中创建的对象比较复杂,创建对象的过程就是通过对原型的拷贝

适配器模式

一个A类不能满足B类对接口的需求,就需要用一个适配器来转接

创建一个C接口,D类实现C接口,聚合A类对象,这个D类满足B类对接口的需求,又能调用A类的方法

有几个角色:

  • 目标接口:用户所期待的接口,可以是一个类/抽象类/接口
  • 适配器:适配器继承/实现了目标接口,同时包装了需要适配的对象
  • 需要适配的对象:当前已有的类,但是不符合接口需求

除去聚合的方法,也可以通过继承实现,叫类适配器。

不过对于java,C#这类不支持多继承的语言,类适配器

  • 一次最多只能适配一个类,不够灵活

  • 想要一个适配器多用,目标抽象类只能为接口,有局限性

通过聚合实现的叫对象适配器:

  • 可以一次适配多个目标
  • 可以适配一个目标的子类/实现类

适用场景:

  • 系统中需要使用一些已经定义的类,但这些类的接口(如方法名,方法参数)不符合系统的需要
  • 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,或者一些未来要引进的类一起工作

桥接模式

将抽象部分和实现部分分离,使其可以独立变化

如果一个系统需要在构建的抽象化角色和具体角色之间增加更多的灵活性,避免直接在两个层次之间建立静态的继承关系,可以选择桥接模式

比如电脑和品牌,电脑是抽象化角色,联想台式机是一个具体的角色,通常的想法是用联想台式机继承台式电脑,台式电脑继承电脑,下面指出了这种模式的缺点

而桥接模式则可以使他们在抽象层建立一个关联关系,抽象化角色和实现化角色可以以继承的方式独立拓展而不互相影响,程序运行时可以动态的将一个抽象化子类的对象和一个实现化子类的对象进行组合(注入依赖),实现动态耦合

又叫柄体模式Handle and Body或者接口模式Interface

1653638768703

如果是这种继承结构,每次新增品牌,新增电脑种类,都需要增加一定数量的类才能维持这种结构

且这种设计违背了单一职责原则,一个联想台式既负责了台式的功能又负责了联想的功能

1653641041178

解决办法就是桥接模式。它把品牌和电脑两个元素抽象出来,让电脑负责电脑的功能,品牌负责品牌的功能,由电脑聚合品牌

抽象出接口Brand,抽象类Computer,Computer里内聚合属性Brand,台市电脑,笔记本电脑,平板电脑都可以继承Computer,并注入相应的品牌

这样就实现了电脑和品牌两个维度的拼接

Computer computer=new Laptop(new Apple());

1653641061066

优势:

  • 减少了子类个数,降低管理和维护的成本
  • 实现了抽象化角色和是实现化角色进行动态耦合
  • 可扩充性,符合开闭原则

缺点:

  • 需要针对抽象进行设计,增加系统的设计难度
  • 需要正确识别出系统两个独立变化的未读,使用范围有一定的局限性

跨平台的视频播放软件也是利用的这个设计模式

1653913418132

应用场景:

  • JVM跨平台
    • 应用程序对应品牌,系统对应电脑,抽象出应用程序和系统两个维度
    • 1653640231926
  • AWT的Peer架构(GUI),不同的系统下有不同的界面样式
  • JDBC驱动程序,JDBC可以链接任何数据库

桥接模式+适配器模式的一点思路:

设计之初使用桥接模式构建电脑和品牌的关系,当来了一个新的类型(但不是品牌,没有实现Brand接口),我们就可以使用适配器模式来创建一个适配器实现Brand接口,聚合这个新的类型,再把适配器注入到电脑类型里

装饰者模式

快餐店里有炒面,炒饭,可以选择加鸡蛋,加火腿,加鸡蛋的炒面如果对炒面做继承来实现的话,新增加咸菜/新增新品炒河粉都会在扩展上困难,并且会有类爆炸的问题

装饰者模式:不改变线有对象结构的情况下,动态地给该对象增加一些职责(其实也就是代理模式)

结构

Component抽象构件:定义一个抽象接口/抽象类来规范准备接收附加责任的类

Concrere Component具体构件:实现了抽象构件的类

Decorator抽象装饰:继承或实现抽象构件的抽象类,并包含具体构件的实例,可以通过其子类扩展具体构件的功能

Concrete Decorator具体装饰:实现抽象装饰的相关方法,并给具体构件对象附加额外的责任

应用案例

以快餐为例,

定义接口/抽象类:fastfood

定义实现类firedNoodles和FiredRice继承/实现fastfood

定义装饰类abstract类Garnish继承fastfood,内部聚合一个fastfood引用,通过构造器注入

定义Egg装饰实现类继承Garnish,重写Garnish里Egg特需的方法(得到描述,得到价格)

想要往一个快餐对象里加一个鸡蛋,只需要这么写

food=new Egg(food);

如果是想要加两个鸡蛋的话

food=new Egg(food);
food=new Egg(food);

相当于披了两层装饰

好处

  • 比继承更灵活,易于扩展,比如想增加炒河粉/加培根
  • 继承是静态的附加责任,装饰者模式则是动态的附加责任,装饰者是继承的一个替代模式,可以动态扩展被装饰类
  • 装饰类和被装饰类可以独立发展,不会相互耦合
  • 遵循开闭原则,可以独立扩展

使用场景

  • 不能采用继承或采用继承不利于程序扩展和维护时
    • 程序中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得类爆炸
    • 类定义不能被继承(final类)
  • 希望只对单个对象动态且透明地添加职责
  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时

JDK应用案例

装饰者模式在JDK的应用案例:BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter

FileWriter fw=new FileWirter("a.txt");
BufferedWriter bw =new BufferdWriter(fw);

1653910649666

使用装饰者模式对Writer子类进行了增强,增加了缓冲区,提高了效率

静态代理和装饰者的比较

相同点:

  • 都要实现与目标类相同的业务接口(Fastfood)
  • 都需要在类里声明目标对象
  • 都可以在不修改目标类的前提下增强目标方法

不同点:

  • 目的不同
    • 装饰者模式是为了增强目标对象
    • 静态代理是为了保护和隐藏目标对象
  • 获取目标对象构件的地方不同
    • 装饰者模式是通过聚合注入
    • 静态代理是组合,在内部创建,以此来隐藏目标对象

外观模式

概述

类似于基金,投资人把资金托管于经理人,经理人投资于多个领域,收益归投资人所有

外观模式又称门面模式,通过多个复杂的子系统提供一个一致的接口,使之更容易被访问

这些子系统对外有一个统一的接口,外部应用不用关心内部子系统具体的细节

外观模式是迪米特法则(最少知道原则)的典型应用

结构

Facade外观角色:为多个子系统提供一个共同的接口

SubSystem子系统角色:实现系统的部分功能,客户可以通过外观访问它

用户只需要注重外观角色,不了解子系统角色

应用案例

智能家电控制,用户只需要操控一个智能家电对象即可,不需要详细操作每个家电对象

1653914110388

好处

  • 降低了子系统(家电)和客户端(main方法等)之间的耦合度,使得子系统的变化不影响调用它的客户类
  • 对系统屏蔽了子系统组件,减少了处理的复杂度

缺点

不符合开闭原则,修改麻烦,外观类内部还需要进一步地设计

使用场景

  • 分层系统构件时,使用外观模式定义子系统中每层地入口点可以简化系统之间的依赖关系(高层依赖低层的接口即可,不需要依赖低层具体的实现,降低了耦合度)
  • 当一个复杂系统的子系统很多时,可以为系统模式设计一个简单的接口供外界访问,方便外界访问(作为一个软件来说,这是必要的)
  • 当客户端和多个子系统之间联系紧密,可以引入外观模式来将其分离

源码分析

1653916416104

1653916425859

组合模式

又名部分整体模式,是用于把一组相似的对象当作一个单一的对象,组合模式依据树形结构来组合对象,用来表示部分以及整体的层次

举个例子就是文件树,文件和文件夹是两种相似的对象,用户希望对它们操作一致,需要开发者用到组合模式

结构

  • 抽象根结点(Component):定义系统各个层次对象的公有方法和属性,可以预先定义一些默认行为和属性
    • 用户希望一致对待文件夹和文件,所以需要设计抽象根结点
  • 树枝结点(Composite):定义树枝结点的行为,存储子节点,组合树枝结点和叶子节点
  • 叶子结点(Leaf):叶子结点对象,再无分支,最小对象

应用案例

软件菜单,一个菜单可以包含其他菜单,也可以包含菜单项

1653964848954

组合模式的分类

  • 透明组合模式
    • 是组合模式的标准实现
    • 抽象根节点中声明了所有用于管理成员对象的方法,比如MenuComponent中声明了add和remove方法(父类中默认实现就是抛出类型不支持的Exception),这样做的好处是确保所有的构件类中都有相同的接口
    • 缺点是不够安全,因为叶子结点和容器对象在本质上是有区别的,叶子结点不能有add和remove操作,出错阶段在运行阶段(如果有相应的错误处理代码,抛出Exception)
  • 安全组合模式
    • 抽象根节点中不声明任何用于管理成员对象的方法,交给树枝结点Menu声明实现
    • 缺点是不够透明,叶子构件和容器构件具有不同的方法,用户必须有区别地对待两种构件
    • 抽象根节点中没有定义那些方法,不能完全针对抽象编程

优点

  • 清楚定义分层次地复杂对象,方便对整个结构进行控制
  • 客户端可以一致使用一个组合结构或者单个对象
  • 在组合模式中增加新的树枝结点和叶子结点都很方便,无需修改类库,符合开闭原则,如果不使用组合模式,可能要分别定义一级,二级,三级结点,新使用四级结点还要重新定义
  • 为树形的面向对象实现提供了一种灵活的解决方案

使用场景

应树形结构而生,使用场景就是出现树形结构的地方

文件目录显示,多级目录呈现等树形结构的操作

享元模式

类似于共享单车思想,使用时共享,用完归还,提高大量细粒度对象的复用

举个例子就是俄罗斯方块中不同形状的方块,每个方块都是一个对象,这会占用大量的内存,享元模式中,同一形状的方块(可以不同颜色)使用同一个对象,颜色是外部状态

1654002939446

利用一个工厂模式,每次通过传入的key值取到hashmap中的对象,对对象注入一些信息(颜色)来执行一些操作,这样就实现了对象的复用

优缺点

优点:

  • 极大减少内存中相似/相同的对象,节省资源
  • 外部状态(颜色)相对独立,且不影响内部状态(形状)

缺点:

  • 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态,使程序逻辑复杂

使用场景

  • 一个系统有大量相同或者相似的对象,造成内存的大量耗费
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象
  • 使用享元模式时需要维护一个存储享元对象的享元池(该例子中的hashmap),而这需要耗费一定的系统资源,因此要在需要多次用到享元对象的情况下使用享元模式

JDK源码分析

Integer使用了享元模式

Integer默认先创建并缓存-128~127之间数的Integer对象在一个数组中,当new一个Interger时,计算下标,如果位于数组中则从缓存中返回,否则创建一个新的Integer对象

模板方法模式

面向对象程序设计过程中,设计一个系统时知道了算法所需要的所有流程步骤和这些步骤的执行顺序,但是某些步骤的具体实现还未知(与具体的环境相关)

例如去银行办理业务,取号,排队,办理具体业务,评分

这个具体业务就是未知的具体实现

模板方法模式就是将这种实现延时到子类中去实现

结构

  • 抽象类:负责给出一个算法的轮廓和骨架,由一个模板方法和若干个基本方法组成
    • 模板方法:顺序调用其基本方法
    • 基本方法:实现算法的各个步骤
      • 抽象方法:留给子类去实现
      • 由抽象类给出声明和实现,子类可以进行覆盖也可以直接继承
      • 钩子方法(Hook Method):抽象类中已经实现的,包括用于判断的逻辑方法和需要子类重写的空方法两种,一般是用于判断的逻辑方法,isXXX,返回一个boolean类型
  • 具体子类:实现抽象类中定义的抽象方法和钩子方法,它们时一个顶级逻辑的组成步骤

案例实现

做饭需要倒油,热油,放蔬菜,放调料,翻炒这几个步骤

抽象类中定义模板方法cookProcess来设置对外的接口

pourVegetable()和pourSuace()定义为abstract方法,交给子类去实现

1654007466018

优缺点

优点:

  • 提高代码的复用性
    • 将相同部分的代码放在抽象的父类中,不同的代码放入不同的子类
  • 实现了反向控制
    • 通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制

这里解释一下反向控制

父类中的cookProcess方法调用了放蔬菜和放调料的函数,这两个函数在父类中是没有定义的,所以说是子类的实现决定了父类的行为

缺点:

  • 每个不同的实现都需要定义一个子类,类的个数增加(但是不是爆炸式),系统庞大,设计抽象
  • 反向控制提高了代码的阅读难度

适用场景

  • 整体步骤固定,个别部分易变,可以用模板方法模式将易变的部分抽象出来
  • 需要通过子类来决定父类算法某个步骤是否执行,实现子类对父类的反向控制

JDK源码分析

InputStream类就用了模板方法模式

InputStream是一个抽象类

read()是一个抽象方法,作用是读取一个字节数据

带三个参数的read是一个模板方法,把每次读取的字节放入buffer数组中,这个模板方法多次调用子类中定义的read()

1654008333426

策略模式

出行可以选择多种交通工具(算法),开发工作也可以选择多种开发工具

该模式定义了一系列算法,并将每个算法封装起来,使之可以相互替换,算法的变化不会影响使用算法的客户

策略模式是一种行为模式,通过对算法进行封装,把使用算法的责任(SalesMan)和算法的实现(StrategyABC)分割开来,委派给不同的对象

结构

  • 抽象策略类:接口/抽象类,给出具体策略类所需要的所有接口
  • 具体策略类:实现了抽象策略类,提供了算法的具体实现
  • Context类:持有一个策略类的引用,交由客户端调用

案例实现

市场促销,要求不同的节日举办不同的促销活动(不同的促销活动可以进行替换)

每次使用不同的策略时就把对应的对象注入到SalesMan中

1654047598015

优缺点

优点:

  • 策略类之间可以自由切换
  • 易于扩展,增加一个新的策略只需要创建一个具体的策略类,满足开闭原则
  • 避免使用多重条件选择语句if else

缺点:

  • 客户端必须知道所有的策略类,自行决定选择使用哪一个
  • 会产生很多策略类,可以通过享元模式来进行对象的复用

使用场景

  • 一个系统需要动态地在几种算法中选择一种时,可以将每个算法封装到策略类中
  • 一个类定义了多种行为,以多个条件语句的形式出现,可以用策略类替换这些条件语句
  • 各算法彼此独立,要求对客户隐藏实现细节

JDK源码分析

Comparator类就使用了策略模式

Arrays的sort有的重载要求传入一个Comparator对象,Comparator是一个接口,传入的是实现了其比较方法的实现类对象,这就是一种策略模式的体现

这个模式中,Arrays是Context类(环境类),抽象策略类是Comparator,具体策略类是我们传入的Comparator实现类

命令模式

将一个请求封装为一个对象,将发出请求和执行请求的责任分隔开,两者之间通过命令对象进行沟通,这样方便存储,传递,调用,增加和管理命令对象

结构

抽象命令类:定义命令的接口

具体命令类:实现命令的接口,通常会持有接收者,可以调用接收者的方法来执行操作

接收者(Receiver):真正执行命令的对象

请求者(Invoker):要求命令对象执行请求,通常持有命令对象,这是客户端真正触发命令的地方,命令对象的入口

案例实现

以服务员,大厨,客户点餐为例子

1654059375397

  • 订单类 Order,一个结构类,内置一个hashMap来保存餐品和餐品含有的数量,一个int表示桌号,这个模式中Order代表要处理的数据
  • 接收者Chef,设计execute()方法,这是真正执行操作的方法
  • 抽象接口Command,抽象命令类,
  • OrderCommand,是一个关于点餐的Command具体实现类,内置Order和Chef,可以调用chef的方法来处理Order中的数据
  • 请求者Waiter,内置一个List,负责添加命令,和执行list中的所有命令
  • 客户端操作:

1654059197379

优缺点

优点:

  • 降低调用操作的对象和实现该操作的对象解耦
  • 增加或者删除命令非常方便,满足开闭原则
  • 可以实现宏命令,与组合模式结合,多个命令装配成一个组合命令,即宏命令
  • 和备忘录模式结合可以实现撤销和恢复操作

缺点:

  • 可能会产生过多的具体命令类
  • 系统结构复杂

使用场景

  • 系统需要将请求者和调用者解耦,使之不直接交互
  • 需要在不同的时间指定请求,实现请求排队
  • 需要支持命令的撤销与恢复

JDK源码分析

Runnable使用了命令模式

想要开启一个线程时,用户需要自定义一个具体命令类实现抽象命令类接口Runnable,实现run方法

同时这个命令类中还应该有一个用户自定义的接收者来封装用户想要实现的操作,在run方法中调用接收者的方法

最后把具体命令类的对象注入到Thread类(请求者)中,由Thread类对象来调用start方法来开启线程

1654060454177

责任链模式

一个请求有多个对象可以处理,但是每个对象的处理条件和权限不同

比如请假可以找部门负责人,副总经理,总经理,但是每个人能批的天数不同,需要根据不同的天数找不同的领导请假,就需要记住每个领导的各种信息,增加了难度

定义

为了避免发送者和多个请求处理者耦合在一起,将所有请求的可处理者通过前一对象记住下一对象来链接成一条链,当有请求发生时,可将请求沿着这条链传递,直到可以处理它

结构

  • 抽象处理者(Handler):包含抽象处理方法和一个后继链接
  • 具体处理者(Concrete Handler):实现抽象处理者的方法,判断是否可以处理当前请求,不可以则转发给后继,可以则处理
  • 客户类(Client):创建处理链,并向链头提交请求,不需要关注处理的传递和细节

案例实现

1654074575140

这里抽象类Handler里定义了一个abstract方法HandleLeaveRequest()交给子类去实现,submit中判断当前类是否满足处理的条件,如果满足就调用HandleLeaveRequest(),否则就交由其后继调用submit()

优缺点

优点:

  • 降低了请求者和多个处理者之间的耦合度
  • 增强了系统的可扩展性,只需要新增一个处理者类并在适当的位置注入,满足开闭原则
  • 增强了给对象委派职责的灵活性,可以动态地改变链内的成员次序
  • 简化了对象之间的连接,取代了众多的if else(针对于客户端)
  • 责任分担

缺点:

  • 不能保证每个请求一定被处理,也许链条直到最后都没有符合条件对象来处理请求
  • 对于较长的责任链,涉及多个处理对象,影响性能
  • 责任链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能由于错误设置而系统出错,比如循环调用

源码解析

javaweb中的FilterChain类就利用了命令模式

通过添加用户自定义的Filter接口实现类到链中,从链表头开始调用doFilter方法,同时传入下一个FilterChai后继,完成前置操作设置之后,调用后继对象的doFilter操作,再设置后继操作

1654078537092

状态模式

电梯有多个状态,关门,开门,运行,停止

常规实现

可以在接口中定义四个常数来表示四种状态,四个方法代表四个动作

抽象出一个接口是为了方便扩展

子类四个方法实现中,需要用switch进行当前状态判断

客户端注入状态就可以调用四个方法

1654080371977

这种方法的缺点很明显:

  • 难以扩展,新的状态比如断电,就需要对每个方法中的switch进行修改
  • 使用了大量的switch case,可读性差

定义

对有状态的对象,把复杂的判断逻辑提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为

结构

  • 环境角色(Context):也叫上下文,定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作交给相对应的状态对象来处理
  • 抽象状态角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为
  • 具体状态角色:实现抽象状态所对应的行为

案例实现

1654401050626

定义abstract类LiftState,声明四个子类继承LiftState表示当前已有的四种状态

声明一个Context类表示环境,内置四个静态的状态类对象和一个父类的引用,给这个父类的引用设置get,set方法,这个父类引用就可以通过setLiftState(Context.OPENINGSTATE)来设置当前状态

比较绕的地方是,Context维护一个LiftState父类的引用,LiftState也维护一个Context类的引用

当各个状态类调用符合当前状态的方法时,直接操作;如果调用不符合当前状态的方法时,调用

super.context.setLiftState(Context.CLOSINGSTATE);

LiftState里维护Context的引用,这样做的目的是为了能够在子类里进行状态切换,把状态判断和切换的逻辑封装到子类中,而不是Context类中

主方法中使用电梯的功能需要new出Context对象并调用setLiftState

优缺点

优点:

  • 将所有与某个状态相关的行为放到一个类中,可以方便地增加新状态,只需要改变当前的状态对象即可
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块

缺点:

  • 增加了系统类和对象的个数
  • 结构和实现复杂,使用不当容易造成混乱
  • 对开闭原则的支持并不好,新增一个状态及其功能的时候需要修改Context类和LiftState和其子类

使用场景

  • 一个对象的行为取决于状态,必须在运行时根据状态改变它的行为时
  • 含有庞大的分支结构,分支取决于对象的状态

观察者模式

又被成为publish-Subscribe,发布-订阅模式

定义了一种一对多的依赖关系,多个观察者监听某一主题(Subject)对象,主题对象在状态变化时,通知所有的观察者对象,使之能够自动更新自己

结构

  • Subject:抽象主题,把所有观察者保存在一个集合里,每个主题可以有任意数量的观察者,提供一个接口,可以增加和删除观察者
  • 具体主题:将有关状态存入具体观察者,主题内部状态发生改变时,给所有注册过的观察者发送通知
  • 抽象观察者:观察者的抽象类,定义一个更新接口,得到主题更改通知时更新自己
  • 具体观察者:实现更新接口

案例实现

微信公众号

公众号有内容更新时,就会推送给关注的用户客户端

1654411928164

具体主题对象里有一个保存Observer父类引用的list,当有新的消息时,遍历list提示每个具体观察者更新

优缺点

优点:

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合
  • 可以实现一对多广播机制

缺点:

  • 如果观察者非常多,通知观察者会耗时多
  • 如果观察者之间有循环依赖,那么会导致观察值循环调用,系统崩溃

使用场景

  • 对象间存在一对多,一个对象的状态发生改变影响其他对象
  • 一个抽象模型有两个方面,一个方面依赖于另一方面时

JDK中的实现

1654490204926

JDK中为观察者模式内置了观察者和被观察者的接口

中介者模式

在多个直接耦合的类之间设立一个中介类来管理那些不属于自己的行为

中介角色封装了一系列对象之间的交互,且可以独立改变他们之间的交互,实现了解耦

结构

抽象中介者:接口,提供了注册和转发同事对象信息的抽象方法

具体中介者:实现接口,List管理同时对象,协调各个同事者之间的交互关系,同时依赖于同事角色

抽象同事类:定义同事类的接口,保存中介者角色,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能

具体同事类:实现接口,当需要于其他同事类进行交互时,交由中介者负责

案例实现

1654491964214

租客和房主都通过contact与绑定的中介对象交互

优缺点

优点:

  • 松散耦合,同事对象可以独立地变化和复用,不需要一改而改全身
  • 集中控制交互,交互行为变化时,只需要修改中介者,扩展系统也只需要扩展中介者的内容
  • 一对多关联转为双向的一对一,对象的关系更容易实现和理解

缺点:

  • 同事类太多时,中介者职责过大,复杂而庞大,系统难以维护

使用场景

  • 对象之间存在复杂的引用关系
  • 想要创建一个运行于多个类之间的对象,又不想继承生成新的子类

迭代器模式

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示

结构

  • 抽象聚合:容器的抽象接口,提供CRUD和创建迭代器的方法
  • 具体聚合:实现接口,返回一个迭代器实例
  • 抽象迭代器:定义访问和遍历聚合元素的接口,通常有hasNext()和next()方法
  • 具体迭代器:实现接口

案例实现

1654511027895

Student容器中getStudentIterator方法只需要将list作为参数传给new出的StudentIterator即可

迭代器即可通过next和hasNext方法遍历容器里的数据

优缺点

优点:

  • 支持以不同的方式遍历一个聚合对象,同一个聚合对象上可以定义多种遍历方式,迭代器模式中只需要用一个不同的迭代器来替换即可实现遍历算法的改变,可以通过新增迭代器类来支持新的遍历方式
  • 简化了聚合类,由于引入了迭代器,聚合类内不必自行提供数据遍历等方法
  • 由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无需修改原有代码

缺点:

  • 增加了类的个数,变复杂了,但无伤大雅

使用场景

  • 需要为聚合对象提供多种遍历方式
  • 需要为遍历不同聚合结构提供一个统一的接口
  • 当访问一个聚合对象的内容而不希望暴露其内部细节

JDK源码分析

java开发中如果想使用迭代器模式,只需要让自定义的容器实现java.util.iterable接口并实现其中的iterator()方法返回一个java.util.Iterator实例即可

1654514697137

访问者模式

封装一些作用域某种数据结构中的各元素的操作

将数据结构和数据结构的操作分离

结构

  • 抽象访问者:定义了对每一个元素访问的行为
  • 具体访问者:实现上者,给出具体行为
  • 抽象元素:定义了一个接收访问者的方法,其意义是,每一个元素都要可以被访问者访问
  • 具体元素:提供接受访问方法的具体实现,通常是使用访问者提供的访问该元素类的方法
  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,可以理解为一各具有容器性质或者复合对象特性的类,含有一组元素,并且可以迭代这些元素

案例实现

这里访问者的方法不应该是feed这种具体的名称,应该是operate这种通用的名字,便于扩展其他的操作

不同的具体访问者给出不同的具体实现,元素角色通过接受不同的具体访问者来实现不同的操作

1654517508775

1654517538089

1654517942451

1654571041388

客户端

1654518285258

优缺点

优点:

  • 拓展性好
    • 不修改对象结构中元素的情况下,为对象结构中的元素添加新的功能(继承定义新的访问者类来新增溜宠物功能)
  • 复用性好
    • 通过访问者来定义整个对象结构通用的功能,提高了复用程度
  • 分离无关行为
    • 通过访问者来分离无关的行为,把相关行为封装在一起,构成一个访问者,使每一个访问者功能单一

缺点:

  • 对象结构变化困难
    • 新增一个具体元素类,都要在每一个具体访问者类中新增一个对应的具体操作,违反了开闭原则
  • 违反了依赖倒置原则
    • 访问者模式中访问者类依赖了具体类,而没有依赖抽象类
    • 如果依赖了抽象类,方法的数量可能少了,但是需要在方法中封装各种判断逻辑来决定针对哪种数据结构使用哪种操作逻辑。而且传入的时候是具体类,参数中转成抽象类,最后在方法中又转成具体类,这非常不合理

使用场景

  • 对象结构相对稳定,其操作算法经常变化的程序
  • 对象结构中的对象需要提供多种不同且不相关的操作,且要避免这些操作影响对象的结构

扩展

分派

  • 变量被声明时的类型叫做变量的静态类型,又叫明显类型
  • 变量所引用的对象的真是类型又叫做变量的实际类型
  • 比如Map map=new HashMap();
  • 根据对象的类型而对方法进行的选择,就是分派(Dispatch),分为静态分派和动态分派

Java编译器在编译时期只知道对象的静态类型,而不知道对象的实际类型,但是调用方法则是根据对象的真实类型,而不是静态类型

动态分派:发生在运行时期,通过子类重写父类的方法来实现(其实就是多态)

静态分派:发生在编译时期,通过函数的重载来实现,但是由于编译器只知道对象的静态类型,所以在多个重载方法之间都是根据静态类型来选择的

1654573009955

双分派

所谓双分派技术,就是在选择一个方法的时候,不仅要根据消息接收者的运行时区别,还要根据参数的运行时区别

1654573928107

1654573946534

  • 上面代码中,客户端将Execute对象作为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,由于这里是依据的方法重写,所以是动态分派,执行的是实际类型中的方法(但其实都是父类Animal中的方法)
  • 在Animal声明的方法中,将this作为参数传递给Execute的执行方法,这里完成了第二次调用,由于是根据方法的重载来进行的,所以是静态分派,但是this是具体的实际类型的对象,所以调用的给每个类设计的逻辑

1654574460025

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的(在运行时期决定),所以重载就是动态的了(所以重载也在运行时期决定了)

备忘录模式

概述

1654574613151

又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态

结构

  • 发起人(Originator):记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务,可以访问备忘录里所有信息
  • 备忘录(Menento):负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人
  • 管理者(Caretaker):对备忘录进行管理,提供保存与获取备忘录的功能,但不能对备忘录的内容进行访问与修改

备忘录有两个等效的接口

  • 窄接口:管理者对象和发起人之外的任何对象看到的都是备忘录的宅接口,这个宅接口只允许其把备忘录对象传给其他的对象
  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口,这个宽接口允许其读取数据,以便根据这些数据恢复这个发起人对象的内部状态

案例实现

实现游戏存档回溯功能

白箱备忘录

备忘录角色对任何对象都提供一个宽接口,内部存储的状态对所有角色公开

1654597865921

游戏角色是发起人角色,有三个int属性,具体的操作(fight方法)会改变其属性

备忘录角色也有与发起人角色相同的属性

RoleStateMemento方法可以保存对象信息,返回一个备忘录对象,备忘录对象交由管理者管理

recoverState方法接收一个备忘录对象,将其中的属性赋值给调用的发起人对象

白象模式是破坏封装性的,但是通过程序员自律(只在发起人对象中对备忘录内容进行访问),同样在一定程度上是可以实现用意的

但是如果我们的程序是给其他人用,就有可能被其他对象访问到备忘录内容

黑箱备忘录

备忘录对发起人提供宽接口,其他对象提供窄接口

Java语言中,实现双重接口的办法就是将备忘录类设置为发起人类的内部成员类

1654657064636

Memento是一个标识接口,对外提供窄接口,只有一些允许外界调用的方法

RoleStateMemento实现了Memento接口,同时定义了发起人的属性和一些方法,是发起人类里的内部宽接口

优缺点

优点:

  • 提供了一种恢复状态的机制
  • 实现了内部状态的封装(黑箱)
  • 简化了发起人类,发起人不需要管理和保存其内部的各个部分,而是保存在备忘录里,交由管理者进行管理,符合单一职责原则

缺点:

  • 如果保存的内部信息过多或者频率过高,占用内存资源大

使用场景

  • 需要保存与恢复数据的场景,如游戏中间结果存档
  • 需要提供一个可回滚操作的场景,word,IDEA编辑的Ctrl+z和事务回滚

解释器模式

结构

1654658474706

案例实现

1654661657419

context类里定义了一个map为每个Variable存储一个映射值

抽象表达式类中的InterPret表示解析,如果是终结符,就解析出其值;如果是+这种非终结符,就解析成left.Interpret+right.Interpret

传入context类就是为了获取终结符和值的映射

优缺点

优点:

  • 易于改变和扩展文法
    • 解释器模式中使用类来表示文法规则,可以通过继承等机制改变/扩展文法
  • 实现文法较为容易
    • 抽象语法树中每一个表达式结点的实现方式都是相似的,不复杂
  • 增加新的解释表达式方便
    • 如果要新增一个终结符表达式/非终结符表达式,不需要修改原有代码,符合开闭原则

缺点:

  • 对于复杂文法难以维护
    • 每一个终结符/非终结符就是一个类,太多文法规则会导致类的个数急剧增加,难以管理和维护
  • 执行效率低
    • 使用了大量的循环和递归

使用场景

  • 语言的语言比较简单,不在乎执行效率
  • 重复问题出现,且可以用一种简单的语言来表达
  • 一个语言需要解释执行,且句子可以表示为一个抽象语法树
posted @   木马伊人  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示