23种设计模式

一、创建型模式

  • 创建型模式,共五种
    • 工厂方法模式
    • 抽象工厂模式
    • 单例模式
    • 建造者模式
    • 原型模式

1、工厂方法以及抽象工厂模式

首先任何可以产生对象的类或方法都可以称之为工厂,比如单例也算

  • 目的

工厂模式的目的就是实现创建者和调用者的分离

  • 核心本质

实例化对象不用new,用工厂方法代替,将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦

  • 三种模式

简单工厂:用来生产同一等级结构中的任意产品(对于增加新的产品,需要扩展已有代码)

工厂方法模式:用来生产同一等级结构中的固定产品(支持增加任意产品)

抽象工厂模式:围绕一个超级工厂创建其他工厂。该超级工厂又被称为其他工厂的工厂

①、简单工厂模式

现在又一个汽车接口,他的实现类有三个,奥迪,特斯拉,五菱

public interface Car {
    void go();
}

public class Audi implements Car {
    @Override
    public void go() {
        System.out.println("Audi");
    }
}

public class Tesla implements Car{
    @Override
    public void go() {
        System.out.println("Tesla");
    }
}

public class WuLing implements Car{
    @Override
    public void go() {
        System.out.println("WuLing");
    }
}

对于我们消费者,我们获取汽车不应该直接new,我们应该去卖小汽车的工厂去获取,也就是需要用一个工厂类来帮我们new

public class CarFactory {
    public static Car getCar(String carName) {
        if ("WuLing".equals(carName)) {
            return new WuLing();
        } else if ("Audi".equals(carName)) {
            return new Audi();
        } else if ("Tesla".equals(carName)) {
            return new Tesla();
        } else {
            return null;
        }
    }
}

这就是简单工厂模式,但是有一个弊端,当我们业务上新增了一个大众汽车,那么我们就应该修改CarFactory方法添加支持获取大众汽车的语句,但是这样违背了开闭原则,我们不应该直接去修改以前的代码

②、工厂方法模式

我们可以直接将获取汽车的工厂拆分开,特定工厂生产特定汽车,也就是定义一个汽车工厂接口,具体汽车的工厂类去实现它即可

public interface CarFactory {
    Car getCar();
}

public class WuLingFactory implements CarFactory {
    @Override
    public Car getCar() {
        return new WuLing();
    }
}

public class TeslaFactory implements CarFactory {
    @Override
    public Car getCar() {
        return new Tesla();
    }
}

public class AudiFactory implements CarFactory {

    @Override
    public Car getCar() {
        return new Audi();
    }
}

当我们需要新增大众汽车的时候,只需要再创建一个大众汽车类和他的工厂类,而不需要去修改某一具体的代码,这样可以很好的满足开闭原则,但是可以看到的是,我们创建了太多的类,结构复杂度大大增加

③、抽象工厂模式

  • 定义

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

  • 适用场景

客户端(应用层)不依赖于产品类实例如何被创建、实现等细节

强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码

提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现

  • 概念

产品族:同一个品牌的不同产品

产品等级:同一个产品的不同品牌

image-20210306185241719

首先根据上图描述把产品对象创建出来,先接口

public interface IphoneProduct {
    //开机
    void start();
    //关机
    void shutDown();
    //打电话
    void call();
    //发短信
    void sendMessage();
}

public interface IRouterProduct {
    //开机
    void start();
    //关机
    void shutDown();
    //打开WIFI
    void openWifi();
    //设置
    void setting();
}

对象

public class HuaweiPhone implements IphoneProduct {
    @Override
    public void start() {
        System.out.println("华为手机开机");

    }

    @Override
    public void shutDown() {
        System.out.println("华为手机关机");
    }

    @Override
    public void call() {
        System.out.println("华为手机打电话");
    }

    @Override
    public void sendMessage() {
        System.out.println("华为手机发短信");
    }
}

public class XiaoMiPhone implements IphoneProduct{
    @Override
    public void start() {
        System.out.println("小米手机开机");
    }

    @Override
    public void shutDown() {
        System.out.println("小米手机关机");
    }

    @Override
    public void call() {
        System.out.println("小米手机打电话");
    }

    @Override
    public void sendMessage() {
        System.out.println("小米手机发短信");
    }
}
public class HuaweiRouter implements IRouterProduct {
    @Override
    public void start() {
        System.out.println("华为路由器开机");
    }

    @Override
    public void shutDown() {
        System.out.println("华为路由器关机");
    }

    @Override
    public void openWifi() {
        System.out.println("华为路由器打开Wifi");
    }

    @Override
    public void setting() {
        System.out.println("华为路由器设置");
    }
}

public class XiaoMiRouter implements IRouterProduct{
    @Override
    public void start() {
        System.out.println("小米路由器开机");
    }

    @Override
    public void shutDown() {
        System.out.println("小米路由器关机");
    }

    @Override
    public void openWifi() {
        System.out.println("小米路由器打开Wifi");
    }

    @Override
    public void setting() {
        System.out.println("小米路由器设置");
    }
}

对象完了后就需要创建所有产品的工厂,所以这个超级工厂不会去关心产品如何实现,应该也是一个接口供特定的工厂去实现

public interface IProductFactory {
    //生产手机
    IphoneProduct createPhone();

    //生产路由器
    IRouterProduct createRouter();
}

接下来是华为的工厂和小米的工厂,他们是负责实现他们的产品生产,所以new产品应该交给他们来实现

public class HuaweiFactory implements IProductFactory {
    @Override
    public HuaweiPhone createPhone() {
        return new HuaweiPhone();
    }

    @Override
    public HuaweiRouter createRouter() {
        return new HuaweiRouter();
    }
}

public class XiaoMiFactory implements IProductFactory {
    @Override
    public XiaoMiPhone createPhone() {
        return new XiaoMiPhone();
    }

    @Override
    public XiaoMiRouter createRouter() {
        return new XiaoMiRouter();
    }
}

测试一下

public class Main {
    public static void main(String[] args) {
        System.out.println("================小米手机=================");
        XiaoMiFactory xiaoMiFactory = new XiaoMiFactory();
        XiaoMiPhone xiaoMiPhone = xiaoMiFactory.createPhone();
        xiaoMiPhone.start();
        xiaoMiPhone.call();
        xiaoMiPhone.sendMessage();
        xiaoMiPhone.shutDown();
        XiaoMiRouter xiaoMiRouter = xiaoMiFactory.createRouter();
        xiaoMiRouter.start();
        xiaoMiRouter.setting();
        xiaoMiRouter.openWifi();
        xiaoMiRouter.shutDown();
        System.out.println("================华为手机=================");
        HuaweiFactory huaweiFactory = new HuaweiFactory();
        HuaweiPhone huaweiPhone = huaweiFactory.createPhone();
        huaweiPhone.start();
        huaweiPhone.call();
        huaweiPhone.sendMessage();
        huaweiPhone.shutDown();
        HuaweiRouter huaweiRouter = huaweiFactory.createRouter();
        huaweiRouter.start();
        huaweiRouter.setting();
        huaweiRouter.openWifi();
        huaweiRouter.shutDown();
    }
}

查看一下最终类图

image-20210306200656800

如果我们某天需要添加新的产品如电脑,那么就可以在IProductFactory接口中再新增一个方法即可,但是这不是违反了我们的开闭原则嘛,在实际应用中,我们千万不能“犯强迫症”甚至“有洁癖架构升级是非常正常的一件事情。 只要不频繁升级,根据实际情况可以不遵循开闭原则 。 代码每半年升级一次或者每年升级一次又有何不可呢?

优点

  • 具体产品在应用层的代码隔离,无需关心创建的细节
  • 将一个系列的产品统一到一起创建

缺点

  • 规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难
  • 增加了系统的抽象性和理解难度

2、单例模式

1、饿汉式

类加载到内存后,就实例化一个单例,JVM保证现成安全,简单实用,推荐使用。唯一缺点:不管用到与否,类装载时就完成实例化

public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    //显式私有化构造方法
    private Mgr01() {
    }

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        //true
        System.out.println(m1 == m2);
    }
}

饿汉式单例是线程安全的,因为JVM保证类只会load到内存一次,当类load到内存后,其中的静态变量只会被执行一次,对于多线程访问同样如此

2、懒汉式

Lazy loading,虽然达到了按需初始化的目的,但在多线程环境下带来了新的问题

public class Mgr02 {
    private static Mgr02 INSTANCE;

    private Mgr02() {
    }

    public static Mgr02 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr02();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr02.getInstance().hashCode())).start();
        }
    }
}

什么时候用什么时候实例化,当一个线程准备获取INSTANCE时,INSTANCE为空,开始实例化,此时另一个线程也来获取,但是上一个线程还没有将INSTANCE实例化,也进入了实例化阶段,到最后造成了INSTANCE实例化了两次

image-20210306103841193

解决方式也很简单,也就是加把锁即可

public static synchronized Mgr02 getInstance() {
    if (INSTANCE == null) {
        try {
            Thread.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        INSTANCE = new Mgr02();
    }
    return INSTANCE;
}

image-20210306104117987

虽然可以通过synchronized解决,但是加锁性能消耗也不小,且到了程序运行后期,INSTANCE早已实例化,已经不需要每次getInstance都耗费额外性能来加锁,所以给业务带来的后果就是效率下降

也可以不给getInstance方法加锁,直接将实例化语句单独打包到加锁语句块中

public static Mgr02 getInstance() {
    if (INSTANCE == null) {
        synchronized (Mgr02.class) {
            try {
                Thread.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr02();
        }
    }
    return INSTANCE;
}

但仔细思考就会发现,这个程序仍然做不到到单例,同样在多线程环境下,如果有多个线程同时成功进入if语句,虽然只会有一个线程能进入加锁语句块,但是当这个线程执行完毕,后面还有若干其他进入if语句的线程将会依次进入

最后,我们可以加双重锁,在加锁语句块中再次执行if检查

public class Mgr02 {
    //如果要进行JIT优化,需要加上volatile,可以解决指令重排的问题
    private static volatile Mgr02 INSTANCE;

    private Mgr02() {
    }

    public static Mgr02 getInstance() {
        if (INSTANCE == null) {
            synchronized (Mgr02.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr02();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr02.getInstance().hashCode())).start();
        }
    }
}

image-20210306105650533

3、静态内部类式

JVM保证单例,加载外部类时不会加载内部类,注意和饿汉式的区别,这样可以实现懒加载

public class Mgr03 {
    private Mgr03() {
    }

    private static class Mgr03Holder {
        private final static Mgr03 INSTANCE = new Mgr03();
    }

    public static Mgr03 getInstance() {
        return Mgr03Holder.INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr03.getInstance().hashCode())).start();
        }
    }
}

4、枚举式

不仅可以解决线程沟通不,还可以防止反序列化

public enum Mgr04 {
    INSTANCE;

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr04.INSTANCE.hashCode())).start();
        }
    }
}

对于普通对象,我们JVM都可以通过对象.class反射实例化对象,对于枚举类,它没有构造方法,如果对他进行反序列化,得到的只能是INSTANCE

3、建造者模式

建造者模式属于创建型模式(创建对象),它提供了一种创建对象的最佳方式。

  • 定义∶指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
  • 主要作用:在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。

用户只需要给出指定复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)

现在需要建造个房子,建造房子需要若干不同的步骤,但是对于客户来讲,他不用关心房屋是怎么创建出来的,他只需要去找包工头,让包工头去指挥工人做那些复杂的事情,最终直接收货即可。那么对于工人来讲,不同的工人能够使用不同的方式建造的房子,所以这里可以把建造者抽象出来

//抽象的建造者,方法
public abstract class Builder {
    abstract void buildA();//地基
    abstract void buildB();//钢筋
    abstract void buildC();//铺电线
    abstract void buildD();//粉刷
    abstract House getHouse();//完工,得到房子
}

具体的工人

public class Worker extends Builder {
    private final House house;

    public Worker() {
        house = new House();
    }

    @Override
    public void buildA() {
        house.setBuildA("地基");
    }

    @Override
    public void buildB() {
        house.setBuildB("钢筋");
    }

    @Override
    public void buildC() {
        house.setBuildC("铺电线");
    }

    @Override
    public void buildD() {
        house.setBuildD("粉刷");
    }

    @Override
    public House getHouse() {
        return house;
    }
}

工人所建造的房子

@Data
public class House {
    private String buildA;
    private String buildB;
    private String buildC;
    private String buildD;
}

指挥工人的包工头

//指挥,核心。负责指挥构建一个工程,工程如何创建由他决定
public class Leader {
    //指挥工人按照顺服建房子
    public House build(Builder builder) {
        builder.buildA();
        builder.buildB();
        builder.buildC();
        builder.buildD();
        return builder.getHouse();
    }
}

模拟客户验收

public static void main(String[] args) {
    Leader leader = new Leader();
    House house = leader.build(new Worker());
    System.out.println(house);
}

也就是房屋的组成是不变的,只是建造的顺序可以修改,也就是在Leader包工头指挥工人,是可以修改builderbuild方法顺序的

  • 建造者与抽象工厂模式的比较:
    • 建造者模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相大的广品,这些产品位于不同的产品等级结构,构成了一个产品族。
    • 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中,客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。
    • 如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车!

4、原型模式

①、概述

  • 定义

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。

②、原型模式的优点:

  • Java]自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

③、原型模式的缺点:

  • 需要为每一个类都配置一个 clone 方法
  • clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
  • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

④、应用

现有一个类Video表示某视频网站上的原创视频《小狐狸学JAVA》,现在作者开放了复制权限,也就是实现了Cloneable接口,然后重写Object类的的clone方法。

@Data
@AllArgsConstructor
public class Video implements Cloneable {
    private String name;
    private Date createTime;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

有一天盗版视频制作者冯某发现这个视频点击率很高,就想把它搬运到自己主页下增加人气。Java中的Object类有一个方法叫clone克隆,也就是冯某自己new不出这个视频,只能靠clone

public static void main(String[] args) throws CloneNotSupportedException {
    Date date = new Date();
    Video video = new Video("小狐狸学JAVA", date);
    Video myVideo = (Video) video.clone();
    System.out.println(video);
    System.out.println(myVideo);
    System.out.println(video == myVideo);
}

image-20210307123705707

接下来我们尝试修改一下克隆出来的对象

public static void main(String[] args) throws CloneNotSupportedException {
    Date date = new Date();
    Video video = new Video("小狐狸学JAVA", date);
    Video myVideo = (Video) video.clone();
    System.out.println(video);
    System.out.println(myVideo);
    System.out.println("===================");
    date.setTime(22222);
    System.out.println(video);
    System.out.println(myVideo);
}

image-20210307123748945

发现克隆出来的对象日期也变了,说明克隆只能且仅能复制对象的值类型,如果是引用类型只能复制其引用,不会复制引用的数据,这就导致复制的源对象上有引用类型的数据,复制的对象和源对象会引用同一个数据。

原型模式是基于方法MemberwiseClone()实现的,MemberwiseClone()C#是一个浅复制的方法。

那么作为抄袭者的冯某,他想把视频搬运过来后,对其内容或者其他信息自行维护怎么办,那就需要改造一下clone方法了

@Override
protected Object clone() throws CloneNotSupportedException {
    Video o = (Video) super.clone();
    o.setCreateTime(new Date(12312412));
    return o;
}

再次测试一下

image-20210307124149388

二、结构型模式

  • 结构型模式,共七种
    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 门面(外观)模式
    • 桥接模式
    • 组合模式
    • 享元模式

1、适配器模式

①、概述

  • 定义

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

  • 优点

    • 客户端通过适配器可以透明地调用目标接口。
    • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
    • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
    • 在很多业务场景中符合开闭原则。
  • 缺点

    • 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
    • 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。

②、类适配器

有一台联想电脑,现在需要插网线上网,但是这款笔记本为了轻薄,没有预装网线接口,所以现在想要上网,就需要用一个到转换器,电脑,网线,适配器如下

public class Computer {
    public void connect(NetToUsb netToUsb) {
        //上网的具体实现,但是没办法连网线,找一个转接头,使用转接器上网
        netToUsb.handleRequest();
    }
}

//网线类,连接电脑
public class NetworkCable
{
    public void request(){
        System.out.println("连接网线上网");
    }
}

同样面向接口编程,适配器可能不止网线一种,所以抽象一个接口

//接口转换器的抽象实现
public interface NetToUsb {
    //作用:处理请求,网线=>usb
    void handleRequest();
}

网线适配器

public class Adapter extends NetworkCable implements NetToUsb{
    @Override
    public void handleRequest() {
        //可以上网了
        super.request();
    }
}

开始上网

public static void main(String[] args) {
    //电脑,网线,适配器
    Computer computer = new Computer();
    NetworkCable networkCable = new NetworkCable();
    Adapter adapter = new Adapter();
    computer.connect(adapter);
}

可以看到,这里电脑上网并没有直接用到NetworkCable网线类,因为Adapter直接就继承了网线类,就好比一个自带网线功能的适配器一样,所以类之间的耦合度比较高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

③、对象适配器

现在修改一下适配器类,不应该让他直接继承网线,应该是以一个参数的形式来连接让电脑上网

public class Adapter extends NetworkCable implements NetToUsb {
    private final NetworkCable networkCable;

    public Adapter(NetworkCable networkCable) {
        this.networkCable = networkCable;
    }

    @Override
    public void handleRequest() {
        //可以上网了
        networkCable.request();
    }
}

现在新的上网方式就变为

public static void main(String[] args) {
    //电脑,网线,适配器
    Computer computer = new Computer();
    NetworkCable networkCable = new NetworkCable();
    Adapter adapter = new Adapter(networkCable);
    computer.connect(adapter);
}

2、装饰器模式

①、概述

定义

  • 指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。

优点

  • 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
  • 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果
  • 装饰器模式完全遵守开闭原则

缺点

  • 装饰器模式会增加许多子类,过度使用会增加程序得复杂性。

②、示例

现在有一个Component接口,里面有一个operation方法,类ConcreteComponent实现此接口,重写operation方法用来拍照。

public class DecoratorTest {
    public static void main(String[] args) {
        Component component = new ConcreteDecorator2(new ConcreteDecorator1(new ConcreteComponent()));
        component.operation();
    }
}

interface Component {
    void operation();
}

class ConcreteComponent implements Component {

    @Override
    public void operation() {
        System.out.println("拍照");
    }
}

abstract class Decorator implements Component {
    Component component;

    public Decorator(Component component) {
        this.component = component;
    }
}

现在需要给拍照功能添加美颜和滤镜效果,定义一个抽象类也去实现Component接口

abstract class Decorator implements Component {
    Component component;

    public Decorator(Component component) {
        this.component = component;
    }
}

可以看到,我们传入了一个Component类型的对象,并没有实现operation方法,目的是为了让子类作为装饰者自己去实现operation方法,并在这个方法中使用Component对象调用原始操作,然后可以做一些其他附加操作。接下来定义Decorator子类

lass ConcreteDecorator1 extends Decorator {
    public ConcreteDecorator1(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        component.operation();
        System.out.println("添加美颜效果");
    }
}

class ConcreteDecorator2 extends Decorator {
    public ConcreteDecorator2(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        component.operation();
        System.out.println("添加滤镜效果");
    }
}

调用

public static void main(String[] args) {
    Component component = new ConcreteDecorator2(new ConcreteDecorator1(new ConcreteComponent()));
    component.operation();
}

image-20210308141920399

3、代理模式

①、概述

定义

  • 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

优点

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性

缺点

  • 代理模式会造成系统设计中类的数量增加
  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
  • 增加了系统的复杂度;

分类

  • 静态代理
  • 动态代理

②、静态代理

Ⅰ、案例A

我们来分析租房,首先房东可能手里有很多的房子可以出租,但是他不想自己亲自打广告,接待访客,他只想给房子然后拿钱,那么他就应该去找房屋中介,对于我们的房客而言,我们将不在直接接触房东,我们只能找到中介,然后中介再找房东

那么结果这么分析,我们可以分析出以下几个角色

  • 抽象角色:一般会使用接口或者抽象类
  • 真实角色(房东):被代理的角色
  • 代理角色(中介):代理真实角色,通常我们会在这个过程中新增一些附属操作,如签合同,看房子,拿提成
  • 客户(房客):寻找代理对象的人

那么我们开始代码实现,房东可以做很多事情,如卖房子,租房子,所以出租这个事情应该定义为接口让房东自己去实现

//租房
public interface Rent {
    void rent();
}

然后定义房东

public class Host implements Rent{
    @Override
    public void rent() {
        System.out.print("房东要出租房子!");
    }
}

然后定义代理对象,首先他应该也有租房这一个方法,而中介的租房能力是来自房东的,且中介在租房这个过程中应该还需要联系房东,他不能直接去继承房东,我们应该用组合的思想,将房东交给中介,而且中介还会做一些其他附属操作

public class Proxy implements Rent {
    private Host host;

    public Proxy() {
    }

    public Proxy(Host host) {
        this.host = host;
    }

    public void seeHouse() {
        System.out.println("中介带你去看房");
    }

    @Override
    public void rent() {
        host.rent();
        System.out.println("来自中介");
    }
	
    public void pare() {
        System.out.println("中介收取租金");
    }
}

模拟房客找中介租房

public static void main(String[] args) {
    Host host = new Host();
    Proxy proxy = new Proxy(host);
    proxy.seeHouse();
    proxy.pare();
    proxy.rent();
}

image-20210307171014863

Ⅱ、案例B

对于我们经常写的Service,其中实现了若干的方法,当我们现在需要增加日志功能,在每一个方法前面打印一行语句,按照以往的经验,我们就需要去手动在每一个方法前面再新增一行日志语句,但是这样很明显违背了我们的开闭原则,更好的解决方案就是使用代理,这也是面向切面编程的Spring AOP的核心思想

代码实现,首先是接口类

public interface UserService {
    void add();
    void delete();
    void update();
    void query();
}

然后是对接口进行实现

public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("add");
    }

    @Override
    public void delete() {
        System.out.println("delete");
    }

    @Override
    public void update() {
        System.out.println("update");
    }

    @Override
    public void query() {
        System.out.println("query");
    }
}

调用

image-20210307172032401

现在需要在每一行语句前面新增一个日志,就不用一行一行去加了,新建一个代理

@Data
public class UserServiceProxy implements UserService {
    private UserServiceImpl userService;

    @Override
    public void add() {
        log("add");
        userService.add();
    }

    @Override
    public void delete() {
        log("delete");
        userService.delete();
    }

    @Override
    public void update() {
        log("update");
        userService.update();
    }

    @Override
    public void query() {
        log("query");
        userService.query();
    }

    private void log(String logName){
        System.out.println("使用了"+logName+"方法");
    }
}

调用

public static void main(String[] args) {
    UserServiceImpl userService = new UserServiceImpl();
    UserServiceProxy userServiceProxy = new UserServiceProxy();
    userServiceProxy.setUserService(userService);
    userServiceProxy.add();
    userServiceProxy.delete();
    userServiceProxy.update();
    userServiceProxy.query();
}

image-20210307172603374

③、动态代理

对于静态代理而言,我们每出现一个被代理对象,都得去新建一个代理,这样代码量就会翻倍,开发效率变低,那么我们就使用动态代理去解决这个问题,我们可以利用反射动态的去管理对象,也就是动态代理类是动态生成的,而不是直接写好的

对于动态代理分为两大类

  • 基于接口:JDK的原生动态代理
  • 基于类:cglib
  • 基于Java字节码:javasist

④、基于接口

需要了解两个类

  • proxy(代理):提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。
  • InvocationHandler(调用处理程序):是由代理实例的调用处理程序实现的接口。每个代理实例都有一个关联的调用处理程序。当在代理实例上调用方法时,方法调用将被编码并分派到其调用处理程序的invoke方法。

新增一个InvocationHandlerProxy对象,我们需要用它来帮我们动态生成代理类

@Data
public class InvocationHandlerProxy implements InvocationHandler {
    //被代理的接口
    private Rent rent;

    //生成得到代理类
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //动态代理的本质就是使用反射机制实现
        return method.invoke(rent, args);
    }
}

调用

public static void main(String[] args) {
    Host host = new Host();
    //代理角色,现在没有
    InvocationHandlerProxy invocationHandlerProxy = new InvocationHandlerProxy();
    //通过调用程序处理角色来处理我们要调用的接口对象
    invocationHandlerProxy.setRent(host);
    //这里就是动态生成的代理
    Rent rent = (Rent) invocationHandlerProxy.getProxy();
    rent.rent();
}

image-20210307175240568

现在中介需要做一些附加操作,就直接放在调用处理程序中即可

@Data
public class InvocationHandlerProxy implements InvocationHandler {
    //被代理的接口
    private Rent rent;

    //生成得到代理类
    public Object getProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        seeHouse();
        //动态代理的本质就是使用反射机制实现
        Object o = method.invoke(rent, args);
        System.out.println("来自中介");
        pare();
        return o;
    }

    public void seeHouse() {
        System.out.println("中介带你看房子");
    }

    public void pare() {
        System.out.println("收取中介费");
    }
}

image-20210307175801948

现在想要给静态代理的案例B也应用这个,我们就还需要修改InvocationHandlerProxy类,这个修改无关紧要,因为他与我们程序本身业务没有关联,可以做到无侵入式编程,为了这个类更加的同样,我们可以把那个代理接口直接编程object类,修改后代码如下

/**
 * @author PengHuAnZhi
 * @createTime 2021/3/7 17:42
 * @projectName DesignPrinciples
 * @className InvocationHandlerProxy.java
 * @description TODO
 */
@Data
public class InvocationHandlerProxy implements InvocationHandler {
    //被代理的接口
    private Object object;

    //生成得到代理类
    public Object getProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log(method.getName());
        //动态代理的本质就是使用反射机制实现
        return method.invoke(object, args);
    }

    public void log(String methodName) {
        System.out.println("调用了" + methodName + "方法");
    }
}

再次测试

public static void main(String[] args) {
    UserServiceImpl userService = new UserServiceImpl();
    InvocationHandlerProxy invocationHandlerProxy = new InvocationHandlerProxy();
    invocationHandlerProxy.setObject(userService);
    UserService userServiceProxy = (UserService) invocationHandlerProxy.getProxy();
    userServiceProxy.add();
    userServiceProxy.delete();
    userServiceProxy.update();
    userServiceProxy.query();
}

image-20210307180808292

动态代理代理的就是一个接口,接口下的一系列业务都会被代理,也就是一个动态代理类可以代理很多的类,可以不用再一对一的手动实现代理类

4、门面(外观)模式

①、概述

定义

  • 又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。

外观(Facade)模式的主要缺点如下。

  • 不能很好地限制客户使用子系统类,很容易带来未知风险。
  • 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

②、案例

有三个系统,每个系统中都有一个方法

class SubSystem1{
    public void method1(){
        System.out.println("SubSystem1.method1");
    }
}
class SubSystem2{
    public void method2(){
        System.out.println("SubSystem2.method2");
    }
}

class SubSystem3 {
    public void method3() {
        System.out.println("SubSystem3.method3");
    }
}

现在来了两个客户端,每个客户端都需要调用三个系统中的方法

class Client1{
    SubSystem1 subSystem1 = new SubSystem1();
    SubSystem2 subSystem2 = new SubSystem2();
    SubSystem3 subSystem3 = new SubSystem3();
    public void doSome1(){
        subSystem1.method1();
        subSystem2.method2();
        subSystem3.method3();
    }
}

class Client2 {
    SubSystem1 subSystem1 = new SubSystem1();
    SubSystem2 subSystem2 = new SubSystem2();
    SubSystem3 subSystem3 = new SubSystem3();

    public void doSome2() {
        subSystem1.method1();
        subSystem2.method2();
        subSystem3.method3();
    }
}

思考这种方式好吗,对于三个系统比较复杂的话,作为客户端就需要分别学期三个系统的方法,然后再逐个new出来调用,这样对于开发很不友好,我们尝试在中间封装一下,让客户端直接调用中间类,就达到以前调用三个系统的目的

class Facade {
    SubSystem1 subSystem1 = new SubSystem1();
    SubSystem2 subSystem2 = new SubSystem2();
    SubSystem3 subSystem3 = new SubSystem3();

    public void doSomeFacade() {
        subSystem1.method1();
        subSystem2.method2();
        subSystem3.method3();
    }
}

新的客户端调用

class Client1 {
    Facade facade = new Facade();

    public void doSome2() {
        facade.doSomeFacade();
    }
}

class Client2 {
    Facade facade = new Facade();

    public void doSome1() {
        facade.doSomeFacade();
    }
}

5、桥接模式

①、概述

  • 定义

将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。

  • 优点
    • 抽象与实现分离,扩展能力强
    • 符合开闭原则
    • 符合合成复用原则
    • 其实现细节对客户透明
  • 缺点
    • 由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。

②、实现

现在市场上的电脑分为很多种,如台式,笔记本,平板,每种产品有不同的品牌,如下

image-20210307150946661

如果我们按照顶级父类为电脑,然后有台式,笔记本,平板子类,然后再是各种品牌的产品,这样三级来分,对于我们产品扩展是很痛苦的,如果新增一个华为电脑,我们必须实现华为台式,华为笔记本,华为平板,如果新增一个手表产品,同样也得创建很多的类,对于我们的最终实现类,他的职责不是唯一的,比如联想台式,他负责联想的品牌,然后种类为台式,他的职责就不唯一了,也就是产品拆分的不彻底

那么分析一下产品的两个维度

image-20210307150832670

对于上面的产品我们可以通过横纵坐标交汇来描述,如果新增一个品牌,只需要在横坐标新增一个品牌即可,如果新增一个类型,就在纵坐标新增即可,最终他们都会与对方交汇组成新的产品

所以首先就是新增品牌,我们仍然对品牌抽象

//品牌
public interface Brand {
    void info();
}

品牌的具体实现,华为,联想

//华为品牌
public class Huawei implements Brand{
    @Override
    public void info() {
        System.out.print("华为");
    }
}

//联想品牌
public class Lenovo implements Brand{
    @Override
    public void info() {
        System.out.print("联想");
    }
}

然后电脑也对其抽象,由于对于电脑来说,品牌是他的属性,我们不能在接口中定义属性,这里可以用抽象类,直接继承

public abstract class Computer {
    protected Brand brand;

    public Computer(Brand brand) {
        this.brand = brand;
    }

    public void info(){
        brand.info();
    }
}

然后就是电脑的实现,台式,笔记本等

public class LaptopComputer extends Computer{

    public LaptopComputer(Brand brand) {
        super(brand);
    }

    @Override
    public void info() {
        super.info();
        System.out.println("笔记本");
    }
}

public class DesktopComputer extends Computer{
    public DesktopComputer(Brand brand) {
        super(brand);
    }

    @Override
    public void info() {
        super.info();
        System.out.println("台式机");
    }
}

调用一下

public static void main(String[] args) {
    //华为笔记本
    Computer huaweiComputer = new LaptopComputer(new Huawei());
    //联想笔记本
    Computer lenovoComputer = new LaptopComputer(new Lenovo());
    huaweiComputer.info();
    lenovoComputer.info();
}

image-20210307162323298

最终类图

image-20210307162626487

现在我们新增一个小米品牌

public class Xiaomi implements Brand{
    @Override
    public void info() {
        System.out.print("小米");
    }
}

调用

public static void main(String[] args) {
    //小米笔记本
    Computer xiaomiComputer = new LaptopComputer(new Xiaomi());
    xiaomiComputer.info();
}

image-20210307163216658

  • 优势分析
    • 桥接模式偶尔类似于多继承方案,但是多继承方案违背了类的单一职责原则,复用性比较差,类的个数也非常多,桥接模式是比多继承方案更好的解决方法。极大的减少了子类的个数,从而降低管理和维护的成本
    • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。符合开闭原则,就像一座桥,可以把两个变化的维度连接起来
  • 劣势分析
    • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
    • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性
  • 最佳实践
    • 如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
    • 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
    • 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
  • 场景
    • Java语言通过Java虚拟机实现了平台的无关性。
    • AWT中的Peer架构
    • JDBC驱动程序也是桥接模式的应用之一。

6、组合模式

①、概述

定义

  • 有时又叫作整体-部分(Part-Whole)模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型设计模式

组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点,树形结构图如下。

组合模式树形结构图

由上图可以看出,其实根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。

这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。

优点

  • 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
  • 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;

缺点

  • 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
  • 不容易限制容器中的构件;
  • 不容易用继承的方法来增加构件的新功能;

②、案例

去年刚刚结束的全国人口普查,由于我们国家幅员辽阔,人口分布不均,我们需要一个好的统计策略

比如,我国的行政区划分为省级,地级,县级,乡级行政区,根据这个特点,每个夏季行政区可以县负责统计该区域的人口数量,如我们先统计四川省达州市下面所属的各个区的人口数量,这样能得到达州市的人口数量,同样的,成都市,绵阳市等,都可以先统计出他们下级各个行政区的人口数量,最终将这些市的人口汇总,最后得到四川省的人口总数,同样道理,我们也可以得到其他省份的人口数量,最终将这些省的人口数量进行汇总,得到全国人口总数,14333320000

在这个简易的模型中,我们的统计命令由上级像下级进行传达,每一层负责统计当前层次的人口数量,最终得到总人数,能这样做的原因是因为我们的行政区划结构是呈树形结构

首先定义计数接口,让所有容器对象都实现计数

interface Counter {
    //计数
    int count();
}

定义城市类,实现计数

class City implements Counter {
    private int sum = 0;

    public City(int sum) {
        this.sum = sum;
    }

    @Override
    public int count() {
        return sum;
    }
}

定义容器类,容器类也可以拥有容器,容器可以指挥下面的容器计数

class Composite implements Counter {
    private final List<Counter> counters = new ArrayList<>();

    public void add(Counter counter) {
        counters.add(counter);
    }

    public void delete(Counter counter) {
        counters.remove(counter);
    }

    public List<Counter> getChild() {
        return counters;
    }

    @Override
    public int count() {
        int sum = 0;
        for (Counter counter : counters) {
            sum += counter.count();
        }
        return sum;
    }
}

测试

public class PartWholeTest {
    public static void main(String[] args) {
        Composite china = new Composite();
        china.add(new City(1000));//直辖市 —— 北京
        china.add(new City(2000));//直辖市 —— 上海
        Composite sichuan = new Composite();
        sichuan.add(new City(3000));//达州市
        sichuan.add(new City(3000));//成都市
        //...
        china.add(sichuan);
        /*
            China
                |_北京
                |_上海
                |_四川
                    |_达州
                    |_成都
         */
        System.out.println(china.count());
    }
}

7、享元模式

①、概述

定义

  • 享元模式(Flyweight)运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

优点

  • 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

缺点

  • 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  • 读取享元模式的外部状态会使得运行时间稍微变长。

②、案例

有一款游戏,他的一个地图上有很多的树,但是这些树的种类也就那么几种,也就是这片森林是由这有限的几棵树进行排列组合形成的,那么对于游戏开发人员来说,这片森林不可能每一棵树都是new出来的,我们应该new出这有限的几种树,然后共享这些对象组成这片森林,由于我们需要共享这些树,那么其中的属性我们不能随意修改它,应该把他们定义为常量。我们只需要额外对其坐标进行指定

@Data
@AllArgsConstructor
class Tree {
    private final String name;
    private final String data;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class TreeLocation {
    private int x;
    private int y;
    private Tree tree;
}

现在我们定义一个Tree工厂类,专门用来获取Tree,传入树的名称和data,如果没有这个树,就创建然后返回,如果有,直接返回。

class TreeFactory {
    private static final Map<String, Tree> treeMap = new ConcurrentHashMap<>();

    public static Tree getTree(String name, String data) {
        System.out.println("name : " + name + ",data : " + data);
        if (treeMap.containsKey(name)) {
            return treeMap.get(name);
        }
        Tree tree = new Tree(name, data);
        treeMap.put(name, tree);
        return tree;
    }
}

调用

public static void main(String[] args) {
    TreeLocation treeLocation1 = new TreeLocation(0, 4, TreeFactory.getTree("xxx", "xxx"));
    TreeLocation treeLocation2 = new TreeLocation(1, 4, TreeFactory.getTree("xxx", "xxx"));
}

image-20210308153226109

三、行为型模式

行为型模式,共十一种

  • 策略模式
  • 模板方法模式
  • 观察者模式
  • 迭代子模式
  • 责任链模式
  • 命令模式
  • 备忘录模式
  • 状态模式
  • 访问者模式
  • 中介者模式
  • 解释器模式

1、策略模式

①、排序问题

现在有一个冒泡排序方法Sorter

public class Sorter {
    public void sorter(int[] arr) {
        boolean flag;
        int temp;
        for (int i = 1; i < arr.length; i++) {
            flag = true;
            for (int j = 0; j < arr.length - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = false;
                }
            }
            if (flag) {
                break;
            }
        }
    }
}

现在思考一个问题,上面的排序方法可以针对int数组类型进行排序,现在增加业务需求,需要支持double类型数组排序,按照以往经验,将排序方法复制一份即可,参数改为double[]即可,然后再增加一个float类型数组,同样是复制…现在引入新的一个类Cat

@Data
@AllArgsConstructor
public class Cat {
    private float weight;
    private float height;

    public int compareTo(Cat cat) {
        if (this.weight < cat.weight) return -1;
        else if (this.weight > cat.weight) return 1;
        else return 0;
    }
}

我们还想使用Sorter类对Cat数组排序怎么办?我们知道我们不能用Cat[0] > Cat[1]这样的形式来对猫判断大小,所以我们新增了一个compareTo方法对猫进行人为规定的比较,那么此时Sorter方法应该这么写

public class Sorter {
    public void sorter(Cat[] arr) {
        boolean flag;
        Cat temp;
        for (int i = 1; i < arr.length; i++) {
            flag = true;
            for (int j = 0; j < arr.length - i; j++) {
                if (arr[j].compareTo(arr[j + 1]) == 1) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = false;
                }
            }
            if (flag) {
                break;
            }
        }
    }
}

image-20210306121210167

好,猫的问题解决了,那狗呢,兔呢,,,车呢,我们发现我们写的这个排序方法就要被改来改去,那么如何解决这个问题呢?

②、Comparable接口

java.lang包下有一个Comparable接口,其中只有一个方法我们需要实现

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

直接实现Comparable接口

@Data
@AllArgsConstructor
//这里实际上是有一个泛型的,规定传入的参数必须式Cat类型
public class Cat implements Comparable<Cat> {
    private int weight;
    private int height;

    @Override
    public int compareTo(Cat cat) {
        return Integer.compare(this.weight, cat.weight);
    }
}

这样就实现了对所有的类进行排序的支持,但是这样还不够完美,我们实现Comparable接口的compareTo方法只能实现一次,现在我们是对weight进行排序,但是现在需要对height进行排序怎么办呢,那又要修改compareTo方法了,但是如果去修改这个方法的话就违背了我们的开闭原则,即面向扩展开放,面向修改关闭

③、策略模式Comparator接口

实现Comparator接口,然后在其comparaTo方法中添加对应的排序方法

//体重排序
public class CatWeightComparator implements Comparator<Cat> {
    @Override
    public int compare(Cat o1, Cat o2) {
        return Integer.compare(o1.getWeight(), o2.getWeight());
    }
}
//身高排序
public class CatHeightComparator implements Comparator<Cat> {
    @Override
    public int compare(Cat o1, Cat o2) {
        return Integer.compare(o1.getHeight(), o2.getHeight());
    }
}

修改一下Sort方法

public class Sorter<T> {
    public void sorter(T[] arr, Comparator<T> comparator) {
        boolean flag;
        T temp;
        for (int i = 1; i < arr.length; i++) {
            flag = true;
            for (int j = 0; j < arr.length - i; j++) {
                if (comparator.compare(arr[j], arr[j + 1]) == 1) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = false;
                }
            }
            if (flag) {
                break;
            }
        }
    }
}

测试一下

public class Main {
    public static void main(String[] args) {
        Cat[] arr = new Cat[]{new Cat(1, 3), new Cat(2, 2), new Cat(3, 1)};
        Sorter sorter = new Sorter();
        CatWeightComparator catWeightComparator = new CatWeightComparator();
        CatHeightComparator catHeightComparator = new CatHeightComparator();
        sorter.sorter(arr, catHeightComparator);
        System.out.println(Arrays.toString(arr));
        sorter.sorter(arr, catWeightComparator);
        System.out.println(Arrays.toString(arr));
    }
}

image-20210306125457789

④、策略模式

根据上面的例子我们可以很形象的理解什么叫做策略模式,定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)

image-20210306132649731

不仅仅是Comparator接口使用了策略模式,比如ThreadPoolExecutor类在实例化的时候需要传入一个RejectedExecutionHandler接口类型的参数,作为拒绝的执行策略

package java.util.concurrent;

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

这个执行策略有四个实现类,以实现不同的逻辑

image-20210306133542809

2、模板方法模式

①、概述

定义

  • 模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。

优点

  • 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  • 它在父类中提取了公共的部分代码,便于代码复用。
  • 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。

缺点

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
  • 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。

②、案例

在进行某一复杂操作的时候,某个类中可能会执行一系列初始化方法,执行完毕后才能执行剩余的方法,他就提供一个模板,供用户去实现,现在定义一个抽象类,供某一具体的类实现,也就是不改变算法的结构,可重新定义该算法的某些特定步骤

abstract class AbstractClass {
    public void operation() {
        System.out.println("step1");
        System.out.println("step2");
        System.out.println("step3");
        //此方法需要用户自己实现
        templateMethod();
    }

    abstract protected void templateMethod();
}

具体类,也就是初始化结束,用户需要实现的方法

class SubClass extends AbstractClass {

    @Override
    protected void templateMethod() {
        System.out.println("SubClass");
    }
}

调用

public static void main(String[] args) {
    AbstractClass abstractClass = new SubClass();
    abstractClass.operation();
}

3、观察者模式

①、概述

观察者(Observer)模式的定义:

  • 指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

优点

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
  • 目标与观察者之间建立了一套触发机制。

缺点

  • 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
  • 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

②、案例

张三因为经商失败欠了一屁股债,债主非常多,但是他非常讲信用,所以他的债主们并不担心他会跑路,对他很放心,但是债主们都想知道张三什么时候才能还钱,所以现在有两个方案,其一是债主们每天都去找张三,问他有没有钱,有钱就还债,其二就是张三主动告诉寨主们,可以把欠条复印件放在他这儿,如果自己有钱了,就马上按复印件来找寨主们还钱。

第一种方式对于债主和张三都不友好,既然大家都相信张三,张三也讲信用,第二种方式才更加合适,这种方式在设计模式中就称为观察者模式,也是我们熟悉的发布订阅模式,下面给出代码实现

借款方的接口

interface Credit {
    //收钱
    void takeMoney();
}

创建两个借款方

class WangWu implements Credit {

    @Override
    public void takeMoney() {
        System.out.println("王五收到钱");
    }
}

class LiSi implements Credit {

    @Override
    public void takeMoney() {
        System.out.println("李四收到钱");
    }
}

创建还款方的接口

interface Debit {
    //借钱
    void borrow(Credit credit);

    //通知还钱
    void notifyCredit();
}

张三

class ZhangSan implements Debit {
    private final List<Credit> credits = new ArrayList<>();
    //1标识有钱
    private final Integer status = 0;

    @Override
    public void borrow(Credit credit) {
        credits.add(credit);
    }

    @Override
    public void notifyCredit() {
        credits.forEach(Credit::takeMoney);
    }
}

模拟调用

public static void main(String[] args) {
    Debit zhangSan = new ZhangSan();
    zhangSan.borrow(new WangWu());
    zhangSan.borrow(new LiSi());
    zhangSan.notifyCredit();
}

最终类图

image-20210308182134165

类图主要有两大块,Debit还款方,他被称为主题对象,Credit贷款方,称为观察者,主题对象负责添加观察者,也就是放入了List集合内,等到通知刷新,调用观察者的某些方法

4、迭代子模式

①、概述

定义

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

优点

  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 遍历任务交由迭代器完成,这简化了聚合类。
  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

缺点

  • 增加了类的个数,这在一定程度上增加了系统的复杂性。

在日常开发中,我们几乎不会自己写迭代器。除非需要定制一个自己实现的数据结构对应的迭代器,否则,开源框架提供的 API 完全够用。

②、案例

如果不使用迭代器模式,也就是不使用java内置迭代器,我们对于一个集合遍历就需要自己了解数据容器的内部结构,然后根据此结构实现遍历

class IterateWithoutIterator {

    private List<Object> list;

    public void setContainer(List<Object> list) {
        this.list = list;
    }

    // 访问并且处理容器数据的方法
    public void printElements() {
        // 访问list容器内的数据
        if (list == null) throw new NullPointerException();
        for (Object o : list) {
            System.out.println(o);
        }
    }
}

但是这仅仅是一个List集合,数据容器的实现机制不止这一种,如何让访问数据的方法对于每个容器都通用,也就是想要仅仅修改容器源而不修改遍历逻辑,这里定义访问容器数据的迭代器接口

interface Iterator<E> {

    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException();
    }
}

可以想到,只要让容器自己去实现这个接口,并实现针对它自身的访问数据的方法,我们作为调用者便不必去了解容器本身的实现机制,但是我们不能直接让用户通过容器本身来操作容器,那么我们所有直接通过容器进行的数据访问修改操作都会直接影响容器内的数据,因为我们访问的数据和容器维护的数据是同一份数据,这样做是不安全的,我们可以让用户访问的是容器的副本,定义一个Iterable接口,用于创建一个新的Iterator

interface Iterable<E> {

    Iterator<E> createIterator();

}

容器就不用实现Interator接口了,而是interable,当用户需要访问容器数据的时候,调用createInterator方法即可创建一个针对本容器的迭代器。在定义容器的时候,这里手动初始化了容器内的数据

class MyContainer<E> implements Iterable<Object> {

    Object[] elements;

    //初始化容器中的数据
    public MyContainer() {
        elements = new Byte[10];
        for (int i = 0; i < 10; i++)
            elements[i] = (byte) i;
    }

    private class T<E> implements Iterator<E> {
        private int position = -1;
        private final Object[] data = elements;

        @Override
        public boolean hasNext() {
            return ++position < data.length;
        }

        @Override
        public E next() {
            return (E) data[position];
        }
    }

    //创建迭代器
    @Override
    public Iterator<Object> createIterator() {
        return new T<>();
    }
}

定义一个这个迭代器对象

class IterateWithIterator {
    //将容器数据拷贝进来
    private Iterator<Object> elements;

    public void setContainer(Iterator<Object> newElements) {
        this.elements = newElements;
    }

    // 访问并且处理容器数据的方法
    public void printElements() {
        if (elements == null) throw new NullPointerException();
        // 访问list容器内的数据
        while (elements.hasNext()) {
            System.out.println(elements.next());
        }
    }
}

可以看到的是,我们迭代器对象并没有直接操作容器数据,而是通过固定的hasNextnext这两个容器本身提供的方法来操作数据,对于用户来讲,容器内部结构便透明了,更加利于开发。

由于访问数据的方法是容器自身提供的,所以容器本身也可以指定数据访问的顺序以及方法

5、责任链模式

①、概述

定义

  • 为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

责任链模式也叫职责链模式。在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。

优点

  • 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  • 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  • 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  • 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  • 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

缺点

  • 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  • 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  • 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

②、案例

现在有一个场景,当我们在CSDN上面发布博客的时候,点击发布,后台要经过信息处理,如敏感词汇过滤,一些恶意内容等,如果通过才允许放入数据库,先模拟以下通常情况下处理字符替换

public class ChainOfResponsibilityTest {
    public static void main(String[] args) {
        Msg msg = new Msg();
        msg.setName("两米以下皆凡人");
        msg.setMsg("大家好:),sudo rm -rf /*,systemctl stop firewall.service");

        //进行过滤操作
        String m = msg.getMsg();
        msg.setMsg(m.replaceAll("rm", "**").replaceAll("systemctl", "*********"));
        System.out.println(msg.getMsg());
    }
}

@Data
@ToString
class Msg {
    String name;
    String msg;
}

可以看到,我们文章传到后台后,我们对他执行了过滤"rm“和”systemctl"两个关键字

image-20210310094458400

但是这仅仅是针对这两个关键字的处理,现在想要增加新的过滤功能,最简单的办法就是在这段代码后面加就行,但是这很明显不符合开闭原则,这样的程序可扩展性很低,而且真正做过滤的时候,往往过滤规则是很繁杂的,如果全部写进一个地方,就会很混乱,有没有其他更好的方式呢

抽象出过滤规则接口

interface Filter {
    void doFilter();
}

定义过滤节点

class FilterA implements Filter {
    @Override
    public void doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll("rm", "**"));
    }
}

class FilterB implements Filter {
    @Override
    public void doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll("systemctl", "*********"));
    }
}

调用过滤链

public class ChainOfResponsibilityTest {
    public static void main(String[] args) {
        Msg msg = new Msg();
        msg.setName("两米以下皆凡人");
        msg.setMsg("大家好:),sudo rm -rf /*,systemctl stop firewall.service");

        //进行过滤链操作
        List<Filter> filters = new ArrayList<>();
        filters.add(new FilterA());
        filters.add(new FilterB());
        for (Filter filter : filters) {
            filter.doFilter(msg);
        }
        System.out.println(msg.getMsg());
    }
}

这样简单放到List中也是一种简单实现,我们再对其封装

class FilterChain {
    //进行过滤操作
    List<Filter> filters = new ArrayList<>();

    FilterChain add(Filter filter) {
        filters.add(filter);
        return this;
    }

    void doFilter(Msg msg) {
        for (Filter filter : filters) {
            filter.doFilter(msg);
        }
    }
}

应用

public class ChainOfResponsibilityTest {
    public static void main(String[] args) {
        Msg msg = new Msg();
        msg.setName("两米以下皆凡人");
        msg.setMsg("大家好:),sudo rm -rf /*,systemctl stop firewall.service");

        //进行过滤操作
        FilterChain filterChainA = new FilterChain();
        filterChainA.add(new FilterA()).add(new FilterB());
        filterChainA.doFilter(msg);
        System.out.println(msg.getMsg());
    }
}

这样对于新增过滤规则便会变得更加易用,当然我们也可以定义新的FilterChain,让两条责任链一起过滤

class FilterC implements Filter {
    @Override
    public void doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll(":)", "^_^"));
    }
}

class FilterD implements Filter {
    @Override
    public void doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll("service", "*******"));
    }
}

串联责任链

//进行过滤操作
FilterChain filterChainA = new FilterChain();
filterChainA.add(new FilterA()).add(new FilterB());
filterChainA.doFilter(msg);
FilterChain filterChainB = new FilterChain();
filterChainB.add(new FilterC()).add(new FilterD());
filterChainB.doFilter(msg);

如果连续调用两个doFilter感觉很不爽,还可以让FilterChain自己也实现一个Filter接口,然后用FilterChain直接add一个新的FilterChain,然后调用前面这个FilterChain

class FilterChain implements Filter{

FilterChainA直接怼到FilterChainB上面

//进行过滤操作
FilterChain filterChainA = new FilterChain();
filterChainA.add(new FilterA()).add(new FilterB());
FilterChain filterChainB = new FilterChain();
filterChainB.add(new FilterC()).add(new FilterD());
filterChainA.add(filterChainB);
filterChainA.doFilter(msg);

现在有了新的需求,如果要在执行过滤连的过程中,由Filter自身决定要不要继续往下执行,如何实现这个需求,可以修改Filter接口,让每次执行Filter返回一个状态

interface Filter {
    //每执行一次返回一个布尔类型,true标识继续执行,false为终止
    boolean doFilter(Msg msg);
}

修改后,Filter跟着修改

class FilterA implements Filter {
    @Override
    public boolean doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll("rm", "**"));
        return true;
    }
}

class FilterB implements Filter {
    @Override
    public boolean doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll("systemctl", "*********"));
        return true;
    }
}

class FilterC implements Filter {
    @Override
    public boolean doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll(":>", "^_^"));
        return false;
    }
}

class FilterD implements Filter {
    @Override
    public boolean doFilter(Msg m) {
        String msg = m.getMsg();
        m.setMsg(msg.replaceAll("service", "*******"));
        return true;
    }
}

class FilterChain implements Filter {
    //进行过滤操作
    List<Filter> filters = new ArrayList<>();

    FilterChain add(Filter filter) {
        filters.add(filter);
        return this;
    }

    public boolean doFilter(Msg msg) {
        for (Filter filter : filters) {
            if (!filter.doFilter(msg)) return false;
        }
        return true;
    }
}

可以看到达到了目的

image-20210310102300788

6、命令模式

①、概述

定义

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

优点

  • 通过引入中间件(抽象接口)降低系统的耦合度。
  • 扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 UndoRedo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
  • 可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。

缺点

  • 可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
  • 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。

②、案例

我们开发一款文字编辑器,当前任务是创建工具任务栏,工具栏包含很多按钮,包含新增,删除,打开,保存,打印等等功能,我们可以定义一个普通的Button按钮,我们可以新增各种功能的按钮继承它,然后在具体的按钮下面新增自己的业务逻辑,这种方式非常简单,但是有缺陷,当我们创建大量的子类的时候,如果我们修改基类,就需要修改所有子类的代码,而且Button本身是作为图形按钮,但是我们却把业务逻辑写在了按钮中,图形渲染和业务逻辑放在一个类中,两个业务逻辑便无法复用,比如一个复制操作,我们可以点击按钮,也可以使用键盘快捷键,也可以右键菜单复制,这都需要用到复制操作,但是我们把它写进了Button中,所以这种方式不可取

更好的方式就是我们将业务逻辑分层,按钮只负责接收用户请求,传递参数,回显数据,将真正的业务计算放到后台的业务逻辑上面

首先把命令接口定义出来

interface Command {
    void execute();
}

表现层

class SaveButton {
    private Command command;

    /*
     * 次数省略一堆渲染操作
     */
    public void bindCommand(Command command) {
        this.command = command;
    }

    public void doPrint() {
        if (command == null) {
            throw new RuntimeException("设备初始化失败");
        }
        command.execute();
    }
}

逻辑层

class PrintService{
    public void print(String text){
        System.out.println(text);
    }
}

定义一个显示的界面

@Data
class TextBox {
    private String context;

}

定义打印命令

class PrintCommand implements Command {

    private final PrintService service = new PrintService();
    private final TextBox box;

    public PrintCommand(TextBox textBox) {
        this.box = textBox;
    }

    @Override
    public void execute() {
        service.print(box.getContext());
    }
}

测试

public class CommandTest {
    public static void main(String[] args) {
        SaveButton saveButton = new SaveButton();
        TextBox box = new TextBox();

        PrintCommand printCommand = new PrintCommand(box);
        saveButton.bindCommand(printCommand);

        box.setContext("ABCDEFG");
        saveButton.doPrint();

        box.setContext("ABCDEFGHI");
        saveButton.doPrint();
    }
}

image-20210310111332837

7、备忘录模式

①、概述

定义

  • 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。

优点

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

缺点

  • 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

结构

  • 源发器类:也就是原始对象
  • 备忘录类:存储原始对象的拷贝
  • 负责人类:存储备忘录类

②、案例

这个设计模式很简单,直接上代码吧

/*
 * 源发器类,员工
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
class Employee {
    private String name;
    private Integer age;
    private double salary;

    //进行备忘操作,并返回备忘录对象
    public EmployeeMemento employeeMemento() {
        return new EmployeeMemento(this);
    }

    //进行数据恢复,恢复成指定备忘录对象状态
    public void recovery(EmployeeMemento employeeMemento) {
        this.name = employeeMemento.getName();
        this.age = employeeMemento.getAge();
        this.salary = employeeMemento.getSalary();
    }
}

/*
 * 备忘录类
 */
@Data
@NoArgsConstructor
@ToString
class EmployeeMemento {
    private String name;
    private Integer age;
    private double salary;

    public EmployeeMemento(Employee employee) {
        this.name = employee.getName();
        this.age = employee.getAge();
        this.salary = employee.getSalary();
    }
}

/*
 * 负责人类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
class CareTaker {
    private EmployeeMemento employeeMemento;
}

测试

public class MementoTest {
    public static void main(String[] args) {
        CareTaker careTaker = new CareTaker();
        Employee employee = new Employee();
        employee.setName("彭焕智");
        employee.setAge(22);
        employee.setSalary(0);
        System.out.println(employee);
        careTaker.setEmployeeMemento(employee.employeeMemento());//备份
        employee.setAge(21);
        System.out.println(employee);
        employee.recovery(careTaker.getEmployeeMemento());
        System.out.println(employee);
    }
}

image-20210310113924212

当然这样只可以备忘一次,如果想要备忘多次也很简单,就是负责人类中将备忘录对象设计为一个栈就可以了

8、状态模式

①、概述

定义

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

优点

  • 结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
  • 将状态转换显示化,减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  • 状态类职责明确,有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。

缺点

  • 状态模式的使用必然会增加系统的类与对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
  • 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。

②、案例

阿里巴巴Java开发公约手册有提到少用if else语句

image-20210310132259992

这里举个例子,张三第一天去电子厂上班的时候,心情非常开心,在doWork的时候,非常积极主动,不久就把活干完了,第二天上班的时候,在路上碰到赵四和王五,然后张三被他们打了一顿,打完之后赵四王五就跑了,他瞬间就非常生气,但是还是去上班了,只不过这一天都无精打采的,第三天去上班,却发现老板赵六带着小姨子跑了,张三就非常难过,因为他的工钱还没有被支付,他就啥也没做,卷了铺盖就走了,这个例子可以说明,同一个人,在做同一件事情的时候,状态不同,表现是不一样的,通常我们这样做的时候都是用的if else做判断

状态模式就是把不同状态下的不同表现抽象出来,这样我们把状态传递给他的时候,他就会有不同的表现。

首先定义状态抽象类

abstract class State {
    abstract void doWork();
}

然后实现几个状态

class Happy extends State {

    @Override
    void doWork() {
        System.out.println("积极主动");
    }
}

class Angry extends State {

    @Override
    void doWork() {
        System.out.println("无精打采");
    }
}

class Sad extends State {
    @Override
    void doWork() {
        System.out.println("啥也不干");
    }
}

定义环境

class Context {
    protected State state;

    public void changeState(State state) {
        this.state = state;
    }

    public void doSomething() {
        state.doWork();
    }
}

调用

public class StateTest {
    public static void main(String[] args) {
        Context zhangSan = new Context();
        zhangSan.changeState(new Happy());
        zhangSan.doSomething();
        zhangSan.changeState(new Angry());
        zhangSan.doSomething();
    }
}

image-20210310133916674

9、访问者模式

①、概述

定义

  • 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。

优点

  • 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  • 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  • 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  • 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

缺点

  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  • 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  • 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

②、案例

现在有一个智能机器人,由于系统版本太低,只会算1+1,现在我们需要新的功能,但是我们不会去买个新的吧,太费钱了,那么我们就通过现有的资源,直接推送软件升级包到机器人里,这个软件包包含了新的操作和行为,它可以不改变系统硬件的前提下升级一些新的功能,能这么做的原因就是它可以访问这些硬件资源,使这些硬件资源执行新的指令,可以想到的是,硬件设计之初就留了入口,方便扩展。

接下来用代码演示上面的功能,定义机器人

@Data
@AllArgsConstructor
class Robot {
    private HardDisk hardDisk;
    private Cpu cpu;

    public Robot() {
        hardDisk = new HardDisk();
        cpu = new Cpu();
        cpu.command = "计算 : 1 + 1 = 2";
        hardDisk.command = "记住 : 1 + 1 = 2";
    }

    public void calc() {
        cpu.run();
        hardDisk.run();
    }

    public void accept(Visitor visitor) {
        cpu.accept(visitor);
        hardDisk.accept(visitor);
    }
}

我们假设机器人中只有两个硬件,一个硬盘一个Cpu,然后有一个对外提供的访问入口visitor,先定义硬件接口

@Data
@NoArgsConstructor
@AllArgsConstructor
abstract class Hardware {
    String command;

    public void run() {
        System.out.println(command);
    }

    /**
     * 提供软件包的入口
     */
    public abstract void accept(Visitor visitor);
}

然后是实现

class Cpu extends Hardware {

    @Override
    public void accept(Visitor visitor) {
        visitor.visitCpu(this);
    }
}

class HardDisk extends Hardware {

    @Override
    public void accept(Visitor visitor) {
        visitor.visitHardDisk(this);
    }
}

然后定义访问接口

interface Visitor {
    /**
     * 表示访问CPU
     *
     * @param cpu Cpu
     */
    void visitCpu(Cpu cpu);

    /**
     * 表示访问CPU
     *
     * @param hardDisk HardDisk
     */
    void visitHardDisk(HardDisk hardDisk);
}

实现一个更新访问

class UpdateVisitor implements Visitor {

    @Override
    public void visitCpu(Cpu cpu) {
        cpu.command += " 计算 : 1 + 2 = 3";
    }

    @Override
    public void visitHardDisk(HardDisk hardDisk) {
        hardDisk.command += " 记住 : 1 + 2 = 3";
    }
}

测试

public class VisitorTest {
    public static void main(String[] args) {
        Robot robot = new Robot();
        robot.calc();
        UpdateVisitor updateVisitor = new UpdateVisitor();
        robot.accept(updateVisitor);
        robot.calc();
    }
}

image-20210310140804413

访问者模式,用于封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作

看一下最终类图

image-20210310141034222

10、中介者模式

①、概述

定义

  • 定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

优点

  • 类之间各司其职,符合迪米特法则。
  • 降低了对象之间的耦合性,使得对象易于独立地被复用。
  • 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

缺点

  • 中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。

②、案例

机场中,同一时间有很多飞机要降落,如何确定降落次序,第一种方式就是,飞行员们互相沟通来决定谁是下一架降落的飞机,这时候飞行员就需要留意机场附近所有的飞机,并且分别相互沟通,确定降落次序,当然这种方式必然不可取,直接来到第二种,机场中都会有一个降落塔台,所有飞行员都直接与塔台中的空管进行沟通,他不关心你从哪儿飞来的,有多少乘客,他只负责怎么协调降落,这样使得飞行员之间不需要相互依赖。

又比如,我们想要娶媳妇,但是我们结识女方的机会很少,我们就需要找到红娘给我们牵线搭桥,从而原地结婚,这里代码实现就实现这个例子

定义中介者类,也就是婚姻中介所

interface MarriageAgency {
    /**
     * 为Person配对
     *
     * @param person
     */
    void pair(Person person);

    /**
     * 注册
     *
     * @param person
     */
    void register(Person person);
}

实现类

/**
 * 中介者实现类
 */
class MarriageAgencyImpl implements MarriageAgency {
    List<Person> personList = new ArrayList<>();

    @Override
    public void register(Person person) {
        personList.add(person);
    }

    @Override
    public void pair(Person person) {
        for (Person p : personList) {
            if (p.age == person.requestAge && p.sex != person.sex) {
                System.out.println(" 将 " + person.name + " 与 " + p.name + " 送入洞房");
            }
        }
    }
}

定义人

@Data
@NoArgsConstructor
@AllArgsConstructor
class Person {
    /**
     * 姓名
     */
    String name;
    /**
     * 年龄
     */
    int age;
    /**
     * 性别
     */
    Sex sex;
    /**
     * 姓名
     */
    int requestAge;
    /**
     * 婚姻中介
     */
    MarriageAgency marriageAgency;

    /**
     * 寻找对象
     */
    public void findPartner() {
        marriageAgency.pair(this);
    }
}

enum Sex {
    /**
     * MALE 男士
     * FEMALE 女士
     */
    MALE, FEMALE;
}

测试

public class MediatorTest {
    public static void main(String[] args) {
        //婚介所
        MarriageAgency marriageAgency = new MarriageAgencyImpl();

        //第一位男嘉宾
        Person 彭焕智 = new Person("彭焕智", 18, Sex.MALE, 18, marriageAgency);

        //四位女嘉宾
        Person 郭泫雅 = new Person("郭泫雅", 25, Sex.FEMALE, 18, marriageAgency);
        Person 贾玲 = new Person("贾玲", 25, Sex.FEMALE, 18, marriageAgency);
        Person 李沁 = new Person("李沁", 18, Sex.FEMALE, 18, marriageAgency);
        Person 高圆圆 = new Person("高圆圆", 25, Sex.FEMALE, 18, marriageAgency);

        marriageAgency.register(Giao桑);
        marriageAgency.register(郭泫雅);
        marriageAgency.register(贾玲);
        marriageAgency.register(尚格格);
        marriageAgency.register(高圆圆);

        //缘分一线牵
        marriageAgency.pair(Giao桑);
    }
}

测试

image-20210310151532716

11、解释器模式

用的实在太少了,也多少有点难度,不记录了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值