《图解设计模式》 学习笔记

发布于:2025-03-17 ⋅ 阅读:(14) ⋅ 点赞:(0)

设计模式

本文参考学习了《图解设计模式》中的代码实现和原理解释

迭代器模式

一个个遍历一个存储了大量元素的集合。

简介

Iterator 模式用于在数据集合中按照顺序遍历集合。就类似于我们的循环,一个个去遍历一个集合中的所有元素。

示例代码

首先我们思考一个书本和书架的关系,显然,书架可以存储多本书,而且一本书的不同副本都可以存储在书架上。那么我们未来需要将书上的各个信息都获取到,我们可以设置一个迭代器,去一个个遍历书架上的书籍信息。

  • 代码实现
  1. Book 类
/**
 * Book实体类
 */
public class Book {
    private String name;

    public Book(String name) {
        this.name = name;
    }
    public String getName(){
        return this.name;
    }

}
  1. Aggregate 接口

    • 定义了一个迭代器方法,子类去做对应的实现

    • 用于未来需要拓展实现时,可以复用的接口,未来拓展新的类,比如需要知道大型停车场中所有的车辆信息,那么也只需要实现一个停车场去实现这个接口即可。

/**
 * Aggregate本身是一个存储多个元素的集合
 * 其定义了迭代器的接口
 * 它需要利用这个迭代器去遍历自己存储的元素
 */
public interface Aggregate {
    Iterator iterator();
}

  1. BookShelf 类

    • 表示书架:

      • 定义了书架该有的功能,比如获取书籍,添加书籍

      • 创建迭代器,遍历获取书籍信息

/**
 * 书架,存放着很多书
 * 它的主要功能是获取书
 * 添加书籍
 */
public class BookShelf implements Aggregate{
    private Book[] bookShelf;
    private int lastIndex = 0;
    public BookShelf(int maxSize) {
        this.bookShelf = new Book[maxSize];
    }

    public boolean appendBook(Book book){
        if(book==null){
            return false;
        }
        bookShelf[lastIndex++] = book;
        return true;
    }
    public int getBookCount(){
        return lastIndex;
    }
    public Book getBook(int index){
        if(index >= 0 && index < bookShelf.length){
            return  bookShelf[index];
        }
        return null;
    }

    @Override
    public Iterator iterator() {
        return new BookShelfInterator(this);
    }
}
  1. BookShelfInterator 类
    • 表示书架的迭代器
      • 实现了interator 接口,需要实现其 hasNext() 和 next() 方法实现遍历
public class BookShelfInterator implements Iterator {
    private BookShelf bookShelf;
    private int bookIndex = 0;
    private int  bookCount;
    public BookShelfInterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        bookCount = bookShelf.getBookCount();
    }

    @Override
    public boolean hasNext() {
       return this.bookIndex < this.bookCount;
    }

    @Override
    public Book next() {
        return this.bookShelf.getBook(this.bookIndex++);
    }
}
  1. 测试类
public class TestInterator {
    public static void main(String[] args) {

        BookShelf bookShelf = new BookShelf(10);
        bookShelf.appendBook(new Book("Java教程"));
        bookShelf.appendBook(new Book("C++教程"));
        bookShelf.appendBook(new Book("python入门"));
        Iterator bookShelfIterator = bookShelf.iterator();
        while(bookShelfIterator.hasNext()){
            Book book = (Book) bookShelfIterator.next();
            System.out.println(book.getName());
        }
    }
}

运行结果:

image-20231216110322945

迭代器模式中出现的角色

  • 迭代器 interator,即 iterator 接口。其声明了 hasNext() 和 next() 方法

  • 真正的迭代器 ,BookShelfInterator 类,其实现了具体的迭代方法

  • Aggregate 集合,负责定义创建迭代器角色的接口,这个接口是一个方法,会根据实际的需要创建出一个适合自己类型的迭代器去遍历自己保存的元素。

  • 具体的 Aggregate ,由 BookShelf 承担,其存储着许多 Book 元素。其通过实现 iterator 方法创建适合自己类型的迭代器,也就是上面的真正的迭代器。

迭代器模式的特点

  1. 不管实现如何变化,都可以使用迭代器

让我们思考一下,如果我们没有定义迭代器,我们会如何实现遍历 Aggregate 里的元素呢?

​ 是不是通过一个循环,然后判断一下当前遍历的长度是否超出了 Aggregate 里的元素的长度,如果没有,就获取 Book,如果超出了就结束循环。

这种方式有什么问题呢?

​ 采取这种遍历的方式,会将遍历的行为完全交给 Aggregate 去执行了。当未来我们需要修改一下 Aggregate 的存储类型时,比如从数组改成List。如果我们在程序中调用了很多次循环遍历操作,那么我们就需要逐一去修改原来的循环,而使用迭代器,这些循环操作统一都变为了如下的代码结构:

while(bookShelfIterator.hasNext()){
        Book book = (Book) bookShelfIterator.next();
        System.out.println(book.getName());
    }

那么未来当我们把数组结构改成 List 时,我们只需要去修改对应实体类的方法及其迭代器的实现即可,不需要一个个去修改各个位置上的循环。

例如,我把书籍的数组存储改为 list,我只需要修改下述书架的部分逻辑即可:

/**
 * 书架,存放着很多书
 * 它的主要功能是获取书
 * 添加书籍
 */
public class BookShelf implements Aggregate{
    private List<Book> bookShelf;
    private int lastIndex = 0;
    public BookShelf(int maxSize) {
        this.bookShelf = new ArrayList<>(maxSize);
    }

    public boolean appendBook(Book book){
        if(book==null){
            return false;
        }
        return bookShelf.add(book);
    }
    public int getBookCount(){
        return bookShelf.size();
    }
    public Book getBook(int index){
        if(index >= 0 && index < bookShelf.size()){
            return  bookShelf.get(index);
        }
        return null;
    }

    @Override
    public Iterator iterator() {
        return new BookShelfInterator(this);
    }
}

这里都不用改迭代器的逻辑,就可以正常执行了。

image-20231216113226004

并且不论未来我们定义了什么其他的实体类,无论是车辆还是书籍,要实现对应集合对象中元素的遍历操作,都可以使用 iterator 。我们要做的只是实现 Aggregate 接口,然后定义我们自己的方法就好。

​ 即实现了遍历和实现分开

未来的拓展

​ 迭代器远不止顺序遍历,可以根据需要设计逆向迭代,甚至是跳跃式迭代。或者又能向前又能向后迭代的迭代器。

适配器模式

将本无法使用的接口转换成能够满足需要的接口。

简介

​ Adapter 模式,顾名思义,就是让一个原本无法直接被调用的接口,通过一个 Adapter 来调用。比如我们的电脑接口,有的电脑可能并没有一个 type-c 的接口供我们使用,这时候我们就可以使用一个转接口,将 type-c 口转成电脑上有的 usb 接口。

适配器模式就是这么一个转接口。

基于继承的适配器代码演示

  1. Banner 类,作为我们提供的接口,也就是上面的电脑的USB口。
    • 它提供了两个方法,分别是将字符串加括号和加星号。
public class Banner {
    private String string;

    public Banner(String string) {
        this.string = string;
    }
    public void showWithParen(){
        System.out.println("("+string+")");
    }

    public void showWithAster(){
        System.out.println("*"+string+"*");
    }
}
  1. Print 接口,需要调用接口实现功能。相当于 Type-c 口,但是我们的电脑目前并不支持这个接口。
    • 它定义了两个方法,PringStrong 以星号形式输出字符串,表示字符串强调
    • PrintWeak,以括号形式输出字符串,表示字符串弱化。
public interface Print {
    void PrintStrong();
    void PrintWeak();
}
  1. PrintBanner类,用于实现将 Banner 接口用于我们的 Print 。实现让 Print 调用 Banner 实现功能。这个就是我们的适配器
    • 它的实现很简单,继承 Banner ,实现 Print 。然后通过实现 Print 接口调用 Banner 方法即可。
public class PrintBanner extends Banner implements Print {

    public PrintBanner(String string) {
        super(string);
    }

    @Override
    public void PrintStrong() {
        this.showWithAster();
    }

    @Override
    public void PrintWeak() {
    this.showWithParen();
    }
}
  1. 测试类
public class Test {
    public static void main(String[] args) {
        Print banner = new PrintBanner("Hello World");
        banner.PrintStrong();
        banner.PrintWeak();
    }
}

输出结果:

image-20231216195732611

基于委托的适配器模式代码演示

委托,即交给其他人去处理要求。放到Java语言里,就是将某个方法的实际处理交给其他实例的方法。

Banner 类和 main 类并不需要做任何更改。

将 Print 接口改成类:

  • 将接口方法改为抽象方法
public abstract class Print {
    abstract void PrintStrong();
    abstract void PrintWeak();
}

PrintBanner 类

  • 在 PrintBanner 中,此时只能单继承,那么为了作为适配器,就不能去继承 Banner 类,因为我们是要为 Print 类去服务,因此继承 Print。
  • 在类中加入 Banner 字段,然后在构造器中创建 Banner 对象。
  • 在继承的方法中直接调用 Banner 对象的方法
public class PrintBanner extends Print {
    private Banner banner;//这个banner就是我们委托的对象
    public PrintBanner(String string) {
        this.banner = new Banner(string);
    }


    @Override
    public void PrintStrong() {
        banner.showWithAster();//让委托者执行它的方法
    }

    @Override
    public void PrintWeak() {
        banner.showWithParen();//让委托者执行它的方法
    }
}

适配器模式中出现的角色

  • Target (对象)

​ 负责定义所需的方法,需要使用接口的角色。就是 USB 接口,即我们的 Print 类(接口)。

  • Client(请求者)

​ 负责使用 Target 角色定义的方法进行具体操作。这个由测试类 Test 类负责。

  • Adaptee(被适配者)

​ 就是我们的 Type-c 接口,它要被适配成 USB 才能被 Target 调用。它持有自己的既定方法。也就是 Banner 类。

  • Adapter (适配者)

​ 用来操作 Adaptee 中的方法来让其能够被 Target 使用,也就是我们的转接口。

它是由 PrintBanner 来负责。

适配器的特点

  1. 能够不修改现有代码实现对接口的调用

​ 当我们的编写了一个新的接口,这个接口只符合了部分应用程序的直接调用需求,但是后来一个旧的应用程序也需要调用这个接口,那么我们就可以通过给其编写适配器来实现调用。

  1. 方便排查 Bug

​ 我们的 Target 可能可以直接去调用接口,但是这样就背离了解耦的这一目标。并且当未来我们的程序出现问题的时候,它的报错会出现在 Target 中调用接口的位置。这样的情况我们就无法明确究竟是哪里出错了,因为它既可能是出现在 Target 的错误,也可能是出现在接口的错误。

​ 但是,如果我们设置了适配器,那么错误只会出现在适配器上,因为它才是调用接口的类。使用适配器模式能够方便我们对于 bug 的查错。

两种适配器模式的优缺点

继承的适配器模式

  • 优点:

    • 其继承了 Adaptee ,因此可以根据自己的需要去重写原有 Adaptee 的方法,具有更加好的灵活性。
  • 缺点:

    • 由于 Java 是单继承模式,所以适配器只能继承 Adaptee,不能额外继承其他类,进而导致了 Target 是接口。

委托的适配器模式

  • 优点:
    • 解决单继承的问题,比起继承的模式更加灵活,一个适配器可以适配更多的 Adaptee 。
  • 缺点:
    • 不太好对 Adaptee 的方法做自定义的实现

模版方法模式

简介

定义了具体的流程,但是流程中的方法可以自由发散。

​ 先来解释一下模版,何为模版?我们可以想象一下小时候练字课上写的字帖,我们用半透明的纸贴在印有文字的纸上,这样我们就可以写出一样漂亮的字了。而模版就相当于这个印有文字的纸。

​ 模版方法模式就是指的是带有模版功能的模式,组成模版的方法被定义在父类中,而父类定义的方法是抽象的,需要子类去实现具体方法。但是父类定义了具体的方法执行流程。也就是说,子类去根据自己的需要实现方法,但是各个方法怎么调用,是交给父类去操作的

父类就相当于字帖,子类相当于使用字帖的人,虽然我们可以调整我们笔的轻重缓急,但是我们仍然要按照字帖上文字的形状去描摹。

模版方法代码演示

  1. abstractDisplay 父类,定义了具体的执行流程,和抽象的方法供子类去实现
public abstract class abstractDisplay {
    public abstract void open();
    public abstract  void print();
    public abstract void close();
    public void display(){
        open();
        for(int i = 0; i < 5; i++ ){
            print();
        }
        close();
    }
}
  1. CharDisplay 子类1,定义了对于字符的各种操作
public class CharDisplay extends abstractDisplay{
    private char ch;

    public CharDisplay(char ch) {
        this.ch = ch;
    }

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

    @Override
    public void print() {
        System.out.println(ch);
    }

    @Override
    public void close() {
        System.out.println(">>");
    }
}
  1. StringDisplay 子类2,用于定义字符串的各种操作。
public class StringDisplay extends abstractDisplay{
    private String string;
    private int width;

    public StringDisplay(String string) {
        this.string = string;
        this.width = string.getBytes().length;
    }

    @Override
    public void open() {
        printLine();
    }

    @Override
    public void print() {
        System.out.println("|" + string + "|");
    }

    @Override
    public void close() {
        printLine();
    }

    public void printLine(){
        System.out.print("+");
        for(int i = 0; i < width ; i++){
            System.out.print("-");
        }
        System.out.println("+");
    }
}
  1. 测试类
public class Test {
    public static void main(String[] args) {
        //创建子类对象
        abstractDisplay d1 = new CharDisplay('H');
        abstractDisplay d2 = new StringDisplay("Hello,World");
        abstractDisplay d3 = new StringDisplay("你好,世界");

        //通通都是调用父类的display方法,子类没有实现
        d1.display();
        d2.display();
        d3.display();

    }
}

测试结果:

<<HHHHH>>
+-----------+
|Hello,World|
|Hello,World|
|Hello,World|
|Hello,World|
|Hello,World|
+-----------+
+---------------+
|你好,世界|
|你好,世界|
|你好,世界|
|你好,世界|
|你好,世界|
+---------------+

进程已结束,退出代码为 0

模板方法出现的角色

  • AbstractClass (抽象类)

​ 其定义了抽象方法,子类可以去实现具体的方法。并且它还需要定义一个具体的流程

  • ConcretClass(具体类)

​ 其负责实现了抽象类定义的抽象方法,在示例中由 CharDisplay 和 StringDisplay 负责。

模板方法的好处

  1. 可以使逻辑处理通用化

​ 已经在父类中编写了算法(固定的方法调用流程),子类就不需要再去定义算法了。

为什么说这个是一个好处呢?感觉其实直接去别的类里复制也行啊??

​ 确实,直接复制粘贴也不是不行,但是我们考虑一个场景,如果说这个固定流程的代码出现了 bug ,如果这个 bug 是当场发现倒是还好,但是如果 bug 是过了很久才发现,那么此时可能你已经将这个流程的代码写到很多地方去了,你就需要一个个去修改。而如果使用了模板方法,出现 bug 了,只需要修改模板的实现就好了。

  1. 满足里氏替换原则

​ 测试类中我们使用了父类变量去指定子类实体,这样即使在没有用 instanceof 等指定子类的种类,程序也能正常工作。

​ 无论父类类型的变量中保存了哪个子类的实例,程序都可以正常工作。

工厂方法模式

将实例的生成交给子类,父类无需知道如何生成

简介

我们之前介绍了模版方法,它是通过父类声明各种操作,交给子类去做具体的实现,但是操作的执行流程是由父类来控制的。这样可以在程序出现 bug 的时候方便我们的修改,只需要修改父类的逻辑流程就可以了。

而接下来的工厂方法,它也是父类和子类之间的联系操作,有点跟模版方法类似,但是它的父类是用来决定实例的生成方式。但是它并不知道生成的子类具体是谁,也就是说,父类进行固定的生成实例操作,但具体这么生成是交给子类去完成。

详细一点的说法就是,父类声明了一些需要子类去做的操作,这些操作是生成实例的一些具体步骤,比如:

  1. 用构造器创建实例
  2. 将实例注册到一个集合中,保存起来

上述的具体操作是交给子类去实现的。

​ 但是,执行这些具体操作的流程是交给父类工厂去执行的。(是不是感觉和模版方法很像?也只不过是工厂方法返回了实例对象,而模版方法返回的可能是一个流程执行结果。)

工厂方法代码演示

这里我们创建了两个包,一个是框架(framework),里面存放了用于进行生产实例的类和进行实例操作的类,比如我们的 Factory 和 Product(抽象的实例,声明抽象方法调用子类去执行实例逻辑) 。

代码结构:

image-20231217103340425

  1. Factory 框架上的工厂
    • 声明了创建产品和注册产品的方法,交给子类去做具体的实现,这两个是生成实例的具体步骤
    • 定义了 create 方法,定义了创建一个实例的具体流程,最后返回实例

这里再看一下声明的函数作用范围细节:

  1. 抽象方法被定义为protected,以便只能在子类中被访问和实现,而不能被客户端直接调用。同时,将具体创建步骤的方法设置为 abstract,要求子类必须去实现。

  2. create方法被定义为final,为了防止子类重写该方法。在工厂方法模式中,create方法通常包含了一些通用的逻辑,如创建产品对象、注册产品对象等,这些逻辑是在父类中已经定义好的,因此不希望子类对该方法进行重写。将create方法定义为final可以确保该方法在子类中不能被修改,从而保持了工厂方法的一致性和稳定性。

package FactoryMethod.framework;

/**
 * 工厂类,用于创建产品
 * 仅仅定义了一个create方法,作为执行创建的逻辑
 */
public abstract class Factory {
    protected abstract Product createProduct(String owner);
    protected abstract void registerProduct(Product product);

    public final Product create(String owner){
        Product product = createProduct(owner);
        registerProduct(product);
        return product;
    }
}
  1. Product
    • 抽象的产品类,声明了产品如何使用
package FactoryMethod.framework;
/**
 * 产品的抽象表示
 * 仅声明了一个use方法,用于产品的使用,交给子类具体实现
 */
public abstract class Product {
    public abstract void use();
}

另一个包是我们具体的实体类工厂和实体类定义,IDCard 和 IDCardFactory

  1. IDCard
    • 具体的产品,可以被 Factory 生产出来,继承了 Product 接口,定义了产品应该如何使用 use
package FactoryMethod.idcard;

import FactoryMethod.framework.Product;

public class IDCard extends Product {
    private String owner;
    public IDCard(String owner) {
        System.out.println("制作 "+owner+" 的IDCard");
        this.owner = owner;
    }

    @Override
    public void use() {
        System.out.println("使用----"+owner+"----的IDCard");
    }
}
  1. IDCardFactory
    • 具体的产品工厂类,其定义了工厂中创建 IDCard 的各个步骤。
    • 但各个步骤的执行过程交给了 Factory 父类去执行
package FactoryMethod.idcard;

import FactoryMethod.framework.Factory;
import FactoryMethod.framework.Product;

import java.util.ArrayList;
import java.util.List;

public class IDCardFactory extends Factory {
    List<Product> list = new ArrayList<>();
    @Override
    public Product createProduct(String owner) {
        return new IDCard(owner);
    }

    @Override
    public void registerProduct(Product product) {
        list.add(product);
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Factory idCardFactory = new IDCardFactory();
        Product p1 = idCardFactory.create("小红");
        Product p2 = idCardFactory.create("小明");
        Product p3 = idCardFactory.create("小刚");
        p1.use();
        p2.use();
        p3.use();
    }
}

执行结果:

制作 小红 的IDCard
制作 小明 的IDCard
制作 小刚 的IDCard
使用----小红----IDCard
使用----小明----IDCard
使用----小刚----IDCard

工厂方法出现的角色

  1. Product (产品)

    该角色属于 framework 一方,是一个抽象类,由示例代码中的 Product 类承担。它定义了由 Factory Method 模式中生成的实例所持有的接口(即产品的功能,比如IDCard的 use 方法)。但是具体的处理是交给子类 ConcretProduct 角色决定。

  2. Creator (创建者)

​ 该角色属于 framework 一方,在示例程序中是由 Factory 类承担, 是一个抽象类,它负责了生成 Product 角色,但是具体的处理是交给子类 ConcretCreator 角色决定。而 Creator 它具体负责了生成实例的执行步骤。各个步骤的定义则全部是由 ConcretCreator 决定。

​ Creator 对于实际负责生成实例的 ConcretCreator 一无所知,它唯一知道的就是只要调用 createProduct 方法就能生成 Product,然后调用 create 方法,能够将实例注册并返回。它并不知道自己创建的是什么产品,它就是一个无情的加工机器 []( ̄▽ ̄)*

不过这样其实是有好处的,它可以将父类和具体子类解耦。

  1. ConcretProduct(具体的产品)

​ 也就是我们的 IDCard 类,继承了 Product ,它定义了具体的 use 方法。

  1. ConcretCreator(具体的创建者)

​ 它是真正负责生产了 ConcretProduct 的角色。其定义了如何生成一个 ConcretProduct 的具体处理。

工厂方法的特点

  1. 实现了父类产品和子类产品的解耦。

​ 让我们回顾一下之前模板方法的测试方法:

public class Test {
    public static void main(String[] args) {
        //创建子类对象
        abstractDisplay d1 = new CharDisplay('H');
        abstractDisplay d2 = new StringDisplay("Hello,World");
        abstractDisplay d3 = new StringDisplay("你好,世界");

        //通通都是调用父类的display方法,子类没有实现
        d1.display();
        d2.display();
        d3.display();

    }
}

这里创建子类对象是通过构造器 new 的子类对象去赋值给父类变量的。

这种操作存在什么问题呢?

​ 可以很明显地看的出来,我们创建子类对象需要指定它的构造器参数,然而,很多时候我们是不知道具体的构造器参数是什么的。而工厂方法模式只需要直接调用 create 方法就能创建出实例对象。这样就实现了客户端( 抽象的Product)和具体产品类的解耦。

​ 具体来说,客户端代码只需要知道工厂方法接口,而不需要知道具体的产品类。当需要创建产品对象时,客户端只需调用工厂方法(create),而不需要关心具体的产品类是如何创建的。这种方式使得产品类的变化不会影响到客户端的代码,从而实现了解耦。

  1. 新增产品更加灵活

​ 未来我们需要新增产品时,也就是添加新的实体类,我们只需要新增对应的产品工厂和产品类就行了。对于生成产品的逻辑都不用做修改。

对于创建实例方法的探讨

在 Factory 中创建实例的方法是:

protected abstract Product createProduct(String owner);
  1. 定义为抽象

它被指定为抽象方法,要求子类必须实现。如果子类不实现,会造成无法正常创建实例对象。因此通过这种方式,来强制子类定义创建对象的方法。

  1. 给与默认实现

当然,我们也可以用默认的实现来解决:

protected createProduct(String owner){
	return new Product(owner);
}

但是这时就不能把 Product 声明成抽象类了,因为我们通过 new 的方式去创建 Product 了。

  1. 让它抛出异常

我们知道,如果子类刻意不去实现该方法,那么我们就可以让该方法抛出一个异常,这样程序就会在运行时出错来告知程序员没有在子类实现 createProduct 方法。

public product createProduct(String owner){
	throw new FactoryMethodRuntimeException();
}

单例模式

类只有一个实例

简介

顾名思义,类只能创建一个唯一的对象的就是单例模式。比如我们希望创建一个地球,但是地球只能有一个,那么我们就不能在程序中出现多个地球对象。

单例模式代码示例

单例模式的实现分为饿汉式和懒汉式。

  • 饿汉式
    • 迫不及待地想创建对象,其会在类加载地时候就把对象创建好,等待用户使用。
    • 不管客户端是否使用这个对象,它都会创建,就意味着饿汉式可能会带来内存空间的浪费
/**
 * 饿汉式单例模式演示
 */
public class Singleton {
    //在类加载时就创建好对象,同时设置为Private,限制外部直接取对象
    private static Singleton singleton = new Singleton();

    //构造器设置为私有,防止外部调用构造器进行创建对象
    private Singleton(){

    }
    //调用开放的 getInstance 方法来获取单例对象
    public static Singleton getInstance(){
        return singleton;
    }
}
  • 懒汉式
    • 正如其名,它很懒,等到客户端要用的时候才开始创建对象。
    • 等必要的时候再创建对象,好处是能够节省内存空间,减少不必要的浪费。
    • 可能会出现一些线程安全问题,比如当两个线程并发地创建对象,可能会创建两个对象。
/**
 * 懒汉式单例模式演示
 */
public class Singleton2 {
    //在类加载时就创建好对象,同时设置为Private,限制外部直接取对象
    private static Singleton2 singleton ;

    //构造器设置为私有,防止外部调用构造器进行创建对象
    private Singleton2(){
        
    }
    //调用开放的 getInstance 方法来获取单例对象
    public static Singleton2 getInstance(){
        //当两个线程同时进来判断,因为此时堆内存中没有单例对象,那么就会同时通过检查进入判断。
        if(singleton == null){
            singleton = new Singleton2();
        }
        return singleton;
    }
}

为了解决线程并发安全问题,我们可以给其加上锁:

 public synchronized static Singleton2 getInstance(){
        //当两个线程同时进来判断,因为此时堆内存中没有单例对象,那么就会同时通过检查进入判断。
        if(singleton == null){
            singleton = new Singleton2();
        }
        return singleton;
    }

加锁势必会带来性能的下降,因此我们可以采用双重检查锁:

public class Singleton2 {
    //在类加载时就创建好对象,同时设置为Private,限制外部直接取对象
    private volatile static Singleton2 singleton ;//注意这里声明了 volatile ,可以确保其他线程对于该对象的可见性。

    //构造器设置为私有,防止外部调用构造器进行创建对象
    private Singleton2(){

    }
    //调用开放的 getInstance 方法来获取单例对象
    public  static Singleton2 getInstance(){
        synchronized(Singleton2.class){
            if(singleton == null){
                singleton = new Singleton2();
            }
        }

        return singleton;
    }
}

当第一个线程调用 getInstance()方法时,第二个线程也可以调用。当第一个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整 个 LazySimpleSingleton 类的阻塞,而是在 getInstance()方法内部的阻塞,只要逻辑不太复杂,对于 调用者而言感知不到。

  • 静态内部类创建单例

但是到头来还是要降低性能,因为还是使用了锁。那么我们可以通过什么方式解决呢?

可以通过静态内部类的形式来实现:

/**
 * 懒汉式单例模式演示
 */
public class Singleton2 {
    //在类加载时就创建好对象,同时设置为Private,限制外部直接取对象
    private volatile static Singleton2 singleton ;

    //构造器设置为私有,防止外部调用构造器进行创建对象
    private Singleton2(){

    }
    //调用开放的 getInstance 方法来获取单例对象
    public  static Singleton2 getInstance(){
        if(singleton == null){
           singleton = SingletonHolder.LAZY;
        }
        return singleton;
    }
    //设置静态内部类,该类只会在调用它时被创建
    private static class SingletonHolder{
        //使用了静态内部类,定义其静态对象,同时还设定了final,即使是多个进程进入,最后也只能有一个对象,解决并发问题。
        private static final Singleton2 LAZY = new Singleton2();
    }
}

解决反射问题

我们仍然可以用反射来实现创建多个实例,对于这种情况,由于反射的底层仍然是调用构造器,因此我们可以通过在构造器中做判断,如果对象已经被创建,那么就直接抛出一个异常。

/**
 * 懒汉式单例模式演示
 */
public class Singleton2 {
    //在类加载时就创建好对象,同时设置为Private,限制外部直接取对象
    private volatile static Singleton2 singleton ;

    //构造器设置为私有,防止外部调用构造器进行创建对象
    private Singleton2(){
        if(singleton!=null){
            throw new RuntimeException("单例已经被创建,请勿重复创建对象");
        }
    }
    //调用开放的 getInstance 方法来获取单例对象
    public  static Singleton2 getInstance(){
        if(singleton == null){
           singleton = SingletonHolder.LAZY;
        }
        return singleton;
    }
    //设置静态内部类,该类只会在调用它时被创建
    private static class SingletonHolder{
        //使用了静态内部类,定义其静态对象,同时还设定了final,即使是多个进程进入,最后也只能有一个对象,解决并发问题。
        private static final Singleton2 LAZY = new Singleton2();
    }
}

设置好了之后,咋们来玩一玩测试代码试试:

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //懒汉式测试
        Singleton2 singleton1 = Singleton2.getInstance();
        Singleton2 singleton2 = Singleton2.getInstance();
        if (singleton1 == singleton2) {
            System.out.println("两个对象是同一个对象");
        } else {
            System.out.println("两个对象是不同对象");
        }
        //测试反射创建对象
        Constructor<?> LazyConstructor = Class.forName("Singleton.Singleton2").getDeclaredConstructor();
        //设置构造器可以被访问,否则直接newInstance()无法调用
        LazyConstructor.setAccessible(true);
        Object LazySingleton = LazyConstructor.newInstance();
    }
}

测试结果:

成功报错!!

image-20231217153904883

这样就能解决上述的问题了。

单例模式的特点

  • 仅能返回一个对象,我们知道,多个对象管理起来可能会很困难,那么对于业务中只需要一个对象就能完成的工作,用一个对象进行操作和管理会很方便,出现 bug 了也只需要针对该对象进行处理。

原型模式

用一个原型去复制一个新的对象。

简介

原型模式,正如上面的解释一样,就是我们创建一个对象,然后去复制它。实现的具体操作就是实现 Cloneable 接口,调用其 clone() 方法。通过这种方式,我们能实现对对象的浅拷贝

  • 浅拷贝:只能拷贝对象,对对象中的基本数据类型和对象本身进行拷贝。但如果对象内有引用数据类型字段,则无法拷贝,该引用对象变量仍然引用了原来的地址。因为clone仅仅复制了这个变量,但是变量指向的地址还是原来的。

原型模式代码演示

  1. Product 接口
    • 作为产品的抽象类,实现了 Cloneable 接口,方便子类调用 clone 拷贝
    • 同时声明了use方法,让子类产品去实现功能。
/**
 * Product接口,声明了克隆方法createClone
 * 继承 Cloneable 类,便于后续使用 clone 方法
 * 由于是产品,所以需要定义use方法,并交给子类去实现
 */
public interface Product extends Cloneable{
    Product createClone();
    void use();
}
  1. Manager 类
    • 作为功能的使用者,其调用自己的函数去进行拷贝(可有可无),为了解耦,最好编写该类
/**
 * 作为对拷贝对象的使用者
 * 调用其方法可以进行对象的注册和拷贝
 * 当然,也可以直接调用 Product 的拷贝方法
 */
public class Manager {
    private HashMap<String,Product> showCase = new HashMap<>();
    //传入的原型,注册到哈希表中
    public void register(String name, Product proto){
        if(name.equals("") || proto == null){
           throw new RuntimeException("name or proto is null");
        }
        showCase.put(name,proto);
    }
    public Product create(String protoName){
        Product product = showCase.get(protoName);
        Product productClone = product.createClone();
        return productClone;
    }
}
  1. MessageBox 具体的产品类
    • 定义了具体的克隆方法
    • 定义了use方法
/**
 * 具体的产品。
 * 定义了克隆自己的方法
 * 还定义了use方法,输出当前对象名称,确定是否正确拷贝
 */
public class MessageBox implements Product {
    public MessageBox() {
    }
    @Override
    public Product createClone() {
        Product product = null;
        try {
            Object o = super.clone();
            if(o!=null){
                product = (Product) o;
            }
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return product;
    }
    @Override
    public void use() {
        //输出当前的对象名称。如果是复制的对象,use方法输出的结果和原型是不一样的。
        String str = "MessageBox"+this;
        System.out.println(str);
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        MessageBox messageBox = new MessageBox();
        Manager manager = new Manager();
        messageBox.use();
        manager.register("strongMessage",messageBox);
        Product product = manager.create("strongMessage");
        product.use();
    }
}

输出结果:

image-20231217225006871

浅拷贝的问题

上面那样去执行代码是没问题的,但是谈若我们把 str 放到成员变量中去:

public class MessageBox implements Product {
    //输出当前的对象名称。如果是复制的对象,use方法输出的结果和原型是不一样的。
    String str = "MessageBox"+this;
    public MessageBox() {
    }
    @Override
    public Product createClone() {
        Product product = null;
        try {
            Object o = super.clone();
            if(o!=null){
                product = (Product) o;
            }
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return product;
    }
    @Override
    public void use() {
        System.out.println(str);
    }
}

再来执行同样的测试代码,得到的结果:

image-20231217225456758

String 是引用类型,str在成员变量中只是被复制了一个变量,引用的地址没有变化。

浅拷贝问题的解决

为了解决原型模式浅拷贝的问题,我们可以通过重写 clone 方法,让它根据我们子类的具体情况去复制对象。

比如:

  @Override
    protected MessageBox clone() throws CloneNotSupportedException {
        String cloneStr = "clone";
        Object o = super.clone();
        MessageBox cloneMessageBox = (MessageBox) o;
        cloneMessageBox.str= cloneStr+cloneMessageBox;
        return cloneMessageBox;
    }

拷贝的时候再创建一个 String,然后赋值给新对象。

输出结果:

image-20231217231703160

可以看得出来,两个 String 对象不同了。

原型模式的角色

  1. Prototype(原型)

​ 负责定义了用于复制现有实例来生成新实例的方法。由 Product 接口承担。其需要实现了 Cloneable 接口。

  1. ConcretPrototype (具体的原型)

​ 负责实现了复制现有实例并生成新实例的方法。由 MessageBox 承当。

  1. Client (使用者)

​ 负责使用实例的方法生成新的实例。

原型模式的特点

  • 对于某些通过构造器创建对象不太方便的时候,可以通过使用原型方法

(当然,也可以用咋们的工厂方法···,不过工厂方法底层也是去调用子类的构造器,那么这里做一个限定:这个构造器调用可能会带来巨大的资源消耗或者时间消耗的情况去使用原型模式)。

​ 比如一个构造器里每执行一次就休眠10s,那么为了提高效率,我们可以使用原型模式直接调用 clone 复制对象。

建造者模式

简介

将复杂实例的构建转为多个部件的组装

​ 当我们试图去创建一个复杂的对象的时候,比如这个对象是一个计算机,它需要由主板、处理器、内存、显卡、硬盘、机箱等组件组成。也就是说,这个对象的属性非常多,很难一气呵成,我们就需要一个采购员,我们把配置告诉他,然后他来给我们组装计算机。

构造者模式代码示例

我们通过一个创造一篇文章并写入文本的例子来说明这个模式。

​ 文章中需要有标题 title、内容 string 、条目 item 等内容。我们要想完成一篇文章,就需要构造这上面三个部分。

  1. Builder 抽象类
    • 定义了一系列部件的创建方法
/**
 * 抽象建造者
 * 定义了一系列部件构造方法
 */
public abstract class Builder {
    public abstract void makeTitle(String title);
    public abstract void makeString(String str);
    public abstract void makeItems(String[] item);
    public abstract void close();
}
  1. Director 类,监工,指导建造者如何去建造产品
    • 其依赖了抽象的 Builder
/**
 * 监工
 * 指导建造者如何一步步建造产品
 */
public class Director {
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public void construct(){
        builder.makeTitle("Greeting");
        builder.makeString("从早上至下午");
        builder.makeItems(new String[]{"早上好","下午好"});
        builder.makeString("晚上");
        builder.makeItems(new String[]{"晚上好","晚安","再见"});
        builder.close();
    }
}
  1. TxtBuilder 具体的建造者,实现了抽象 Builder 定义的对于部件建造的具体实现
public class TxtBuilder extends Builder{
    private PrintWriter writer;
    private String filename;
    @Override
    public void makeTitle(String title) {
        filename = title + ".txt";
        try {
            writer = new PrintWriter(filename);
        } catch (IOException e) {
            e.printStackTrace();
        }
        writer.println("欢迎来到"+title);
    }

    @Override
    public void makeString(String str) {
        writer.println(str);
    }

    @Override
    public void makeItems(String[] item) {
        for(String i : item){
            writer.println(i);
        }
    }

    @Override
    public void close() {
        writer.println("要完事了");
        writer.close();
    }
    public String getResult(){
        return filename;
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        TxtBuilder builder = new TxtBuilder();
        Director director = new Director(builder);
        director.construct();
        String result = builder.getResult();
        System.out.println(result);
    }
}

测试结果:

image-20231218190956253

image-20231218191001858

建造者模式的角色

  1. Builder (建造者)

​ 负责定义生成实例(文章)的接口,本例中由 Builder 承担。

  1. Director (监工)

​ 负责使用 Builder 角色的接口 API 生成实例。它不依赖与具体的建造者,它只调用了 Builder 角色中的方法。示例中由 Director 担任

  1. ConcretBuilder (具体的建造者)

​ ConcretBuilder 实现了 Builder 的接口方法,它定义了生成实例时各个部件的建造方法。此外,它也能有自己的实现,它能够根据自己的需要,显示构造后的最终生成结果的方法。

  1. Client(使用者)

​ 由 Test 类承担,它使用了建造者模式。

建造者模式的特点

好处

  • 方便对于复杂对象的创建,类如果不好直接在构造器中完成全部的属性赋值,可以通过构造器模式一一将其创建。

  • 封装性好,将对象的构造和表示分离开,不需要通过构造器去实现构造。而通过调用多个方法来将多个属性赋值。

  • 扩展性好,各个具体的建造者之间相互独立,未来可以拓展更多的建造者,利于系统的解耦。

  • 各个角色之间的依赖减少,客户端只选择了具体要创建的对象和让 Director 去调用 Construct 创建对象而已。就像我们买电脑,我们指定了具体的配置,然后交给采购员帮我们购买。而 Director 也不需要知道具体的 Builder,它只不过使用了抽象 Builder 的几个方法去固定建造了一个对象。

不足

  • 产品的组成部分被固定死了,因为监工也只能去创建抽象 Builder 里面指定的构造部件。这使得部分有着特殊属性的对象难以用一套模版去构建。

  • 如果产品的内部变化复杂,如果产品内部属性发生了变化,则建造者也要同步修改。后期维护成本大。

抽象工厂模式

不仅可以抽象化建造者,还能将工厂和零部件也抽象化。

简介

​ 上一节我们介绍了建造者模式,这个模式对于我们创建一个复杂对象非常有帮助,因为它可以指导建造者去一步步创建复杂对象中的零部件,然后再将这些组装成一个完整的对象。而抽象工厂,它用工厂去替代建造者,这个工厂是抽象的,当然,它有自己的子类。也就意味着,我们可以创建不同实体的工厂。去创建更多不同的复杂对象了。同时,原本在建造者模式中是实体的各个零部件,在抽象工厂中也有自己的抽象父类。 这下子,我们就可以有更多不同种类的零部件啦。

抽象工厂代码演示

抽象部分

​ 我们这边用生成电脑作为示例,假设一个电脑需要 CPU,显卡 Graphics ,硬盘 HardDisk 三个零部件组成。

//抽象显卡
public abstract class ComputerGraphics {
    private String GraphicsName;

    public ComputerGraphics(String graphicsName) {
        GraphicsName = graphicsName;
    }

}
//抽象硬盘
public abstract class ComputerHardDisk {
    private String HardDiskName;

    public ComputerHardDisk(String hardDiskName) {
        HardDiskName = hardDiskName;
    }
}
//抽象处理器
public abstract class ComputerProcessor {
    private String ProcessorName;

    public ComputerProcessor(String processorName) {
        ProcessorName = processorName;
    }
}

这个虽然和零部件们放一块,但是实际上它应该算作产品。但是都是要在工厂生产的,所以没啥太大区别,只是它用了下各个零部件。

//抽象电脑
public abstract class Computer {
    private String ComputerName;
    private ComputerProcessor processor;
    private ComputerGraphics graphics;
    private ComputerHardDisk hardDisk;
    public Computer(String computerName, ComputerProcessor processor, ComputerGraphics graphics, ComputerHardDisk hardDisk) {
        ComputerName = computerName;
        this.processor = processor;
        this.graphics = graphics;
        this.hardDisk = hardDisk;
    }
    public abstract void open();
}

​ 然后我们还需要一个电脑工厂,用来生成各种品牌的电脑。

public  abstract class ComputerFactory {
        //作为工厂,你总得返回给客户端一个工厂对象吧,不然人家咋生成电脑呢?所以定义 getComputerFactory 方法
    public static ComputerFactory getComputerFactory(String factoryName) {
        ComputerFactory factory = null;
        try {
            factory = ( ComputerFactory) Class.forName(factoryName).getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return factory;
    }
    //定义创建处理器、显卡、硬盘以及电脑的方法
    public abstract ComputerProcessor createProcessor(String ProcessorName);
    public abstract ComputerGraphics createGraphics (String GraphicsName);
    public abstract ComputerHardDisk createHardDisk(String HardDiskName);
    public abstract Computer createComputer(String ComputerName, ComputerProcessor processor, ComputerGraphics graphics, ComputerHardDisk hardDisk);

}

实体部分

分为联想电脑和戴尔电脑,及其各自的显卡处理器等。(当然,我这指的是电脑上装的,不代表这些显卡、CPU 是他们家的( 。。。 诶嘿

public class DellComputer extends Computer {

    public DellComputer(String computerName,
                        ComputerProcessor processor,
                        ComputerGraphics graphics,
                        ComputerHardDisk hardDisk) {
        super(computerName, processor, graphics, hardDisk);
    }
    public void open(){
        System.out.println("Dell Computer is opening...");
    }
}
public class DellComputerGraphics extends ComputerGraphics {
    public DellComputerGraphics(String graphicsName) {
        super(graphicsName);
    }
}
public class DellComputerHardDisk extends ComputerHardDisk {
    public DellComputerHardDisk(String hardDiskName) {
        super(hardDiskName);
    }
}
public class DellProcessor extends ComputerProcessor {
    public DellProcessor(String processorName) {
        super(processorName);
    }
}

Dell的工厂类:

public class DellComputerFactory extends ComputerFactory {

    @Override
    public ComputerProcessor createProcessor(String ProcessorName) {
        return new DellProcessor(ProcessorName);
    }

    @Override
    public ComputerGraphics createGraphics(String GraphicsName) {
        return new DellComputerGraphics(GraphicsName);
    }

    @Override
    public ComputerHardDisk createHardDisk(String HardDiskName) {
        return new DellComputerHardDisk(HardDiskName);
    }

    @Override
    public Computer createComputer(String ComputerName, ComputerProcessor processor, ComputerGraphics graphics, ComputerHardDisk hardDisk) {
        return new DellComputer(ComputerName,processor,graphics,hardDisk);
    }
}

联想的同理,这里就不展示代码了。

测试类:

public class Test {
    public static void main(String[] args) {
        ComputerFactory dellFactory = ComputerFactory.getComputerFactory("AbstractFactory.DellFactory.DellComputerFactory");
        ComputerProcessor IntelProcessor = dellFactory.createProcessor("英特尔处理器");
        ComputerGraphics NvdiaGraphics = dellFactory.createGraphics("英伟达显卡");
        ComputerHardDisk SunHardDisk = dellFactory.createHardDisk("三星硬盘");
        Computer dellComputer = dellFactory.createComputer("戴尔电脑",
                IntelProcessor, NvdiaGraphics, SunHardDisk);
        dellComputer.open();

    }
}

运行结果:

image-20231218211147017

抽象工厂模式的角色

  1. AbstractProduct (抽象产品)

    • 它负责定义了抽象工厂生成的角色的抽象零件和抽象产品的接口。
  2. AbstarctFactory (抽象工厂)

    • 它负责定义了用于生成抽象产品的接口。
  3. ConcreteProduct (具体产品)

    • 实现了抽象产品角色的接口。
  4. ConcreteFactory(具体工厂)

    • 负责实现了抽象工厂角色的接口。它是真正负责创建各个零部件生成产品的工厂

抽象工厂的特点

优点

  • 提供了一种封装对象创建过程的方式:抽象工厂模式将对象的创建过程封装在了具体的工厂类中,客户端只需要关注工厂接口和产品接口,无需关心具体的实现细节。

  • 实现了产品族的切换:通过使用不同的具体工厂类,可以方便地切换整个产品族的产品,从而实现了接口与实现的分离。

  • 保持了代码的一致性:抽象工厂模式保持了一致性,即所有由同一工厂创建的产品都相互关联,使得产品之间可以很方便地进行配合使用。

缺点

  • 不易扩展新的产品:当需要给产品家族新增一个产品时,需要修改抽象工厂的接口及所有的具体工厂类,这就违反了开闭原则,增加了代码的维护成本。

  • 增加了系统的复杂度:抽象工厂模式引入了多个抽象和具体类,增加了系统的复杂度和理解难度。

桥接模式

将一个类的功能层次结构和实现层次结构连接起来

层次结构介绍

通过继承的方式有两种选择,一种是拓展类的功能,子类新增方法,父类和子类这个叫做类的功能层次结构。

另一种情况是子类去实现父类的方法,但是并没有新增自己独立的方法,这种父子类结构关系称为实现层次结构。

桥接模式的目标就是将他俩连接起来。

桥接模式代码示例

  1. 被实现和被添加方法的原始类,最高父类:Display。这里采用委托的方式来操作实现层次结构类
public class Display {
    private DisplayImpl displayImpl;

    public Display(DisplayImpl displayImpl) {
        this.displayImpl = displayImpl;
    }

    public void open(){
        this.displayImpl.rawOpen();
    }
    public void print(){
        this.displayImpl.rawOpen();
    }
    public void close(){
        this.displayImpl.rawOpen();
    }


    public void display(){
        open();
        print();
        close();
    }
}
  1. 实现功能层次结构的子类 CountDisplay
public class CountDisplay extends Display{

    public CountDisplay(DisplayImpl displayImpl) {
        super(displayImpl);
    }

    public void MultiDisplay(int time){
        open();
        for(int i = 0 ;i < time; i++){
            print();
        }
        close();
    }
}
  1. 实现层次结构的抽象子类 DisplayImpl
//这里不继承Display,
// 这样防止继承带来的强关联。而使用委托,可以减少这个关联。因为继承只能单继承,以后想修改就很麻烦
//这个设计抽象类,让子类做具体实现
//未来还可以设置成抽象工厂来做更多的拓展
public abstract class DisplayImpl {
    public abstract void rawOpen();
    public abstract void rawPrint();
    public abstract void rawClose();
}
  1. 具体的实现层次结构实现类
public class StringDisplayImpl extends DisplayImpl{
    @Override
    public void rawOpen() {
        System.out.println("打开");
    }

    @Override
    public void rawPrint() {
        System.out.println("输出一行文字····");
    }

    @Override
    public void rawClose() {
        System.out.println("关闭");
    }
}

测试代码:

public class Test {
    public static void main(String[] args) {
        DisplayImpl String1 = new StringDisplayImpl();
        DisplayImpl String2 = new StringDisplayImpl();
        Display display = new Display(String1);
        CountDisplay display1 = new CountDisplay(String2);
        display.display();
        System.out.println("分割线----------------");
        display1.MultiDisplay(10);
    }
}

输出结果:

打开
输出一行文字····
关闭
分割线----------------
打开
输出一行文字····
输出一行文字····
输出一行文字····
输出一行文字····
输出一行文字····
输出一行文字····
输出一行文字····
输出一行文字····
输出一行文字····
输出一行文字····
关闭

可以看的出来,我们的最高类中利用委托实现层次结构类,实现了两个结构的连接。

桥接模式的角色

  1. Abstraction (抽象化)

​ 类的功能层次最上层,即 Display 类。它定义了最基本的方法,交给实现层次结构的类去做具体处理。

  1. RefinedAbstraction (改善后的抽象化)

​ 就是位于功能层次结构上的子类。对抽象化角色做了功能上的拓展。

  1. Implementor (实现者)

​ 位于类的实现层次结构的最上层,它是对抽象化角色的接口的具体实现。

  1. ConcretImplementor(具体实现者)

​ 该角色负责对实现者定义功能的具体处理。

桥接模式的特点

  • 桥接模式的目的就是为了让两个层次分开,这样可以更好地管理。减少一个类同时具有两个层次。但是要确保增加后的功能能够被所有的实现使用,不能让分开的层次毫无联系,因此需要进行桥接。

  • 上面使用了委托来实现实现层次结构的调用,是因为委托相较继承,是弱关联。如果我们采取了继承,会造成父子类形成强耦合,如果父类做了一些修改,就可能会影响子类。最后可能会修改大量的代码。

策略模式

一个类的行为可以在其运行时改变

​ 顾名思义,我们解决一个问题得考虑使用的策略,有时候策略可以有很多种。但是它们归根结底都是为了解决一个问题。针对一个问题,我们需要编写特定的算法,但是这个算法不唯一,而策略模式能够用相同的调用模式做出不同的解决方案。我们在解决问题时,可能需要根据不同的时机去选择不同的策略,我们希望可以通过调用统一的接口去实现不同的方法。这就是策略模式。

策略模式代码示例

​ 假设马上开学了,小方又要为返校的交通工具而苦恼了,它考虑究竟是要做飞机到达学校,还是做高铁到达学校,或者是先坐高铁,再坐飞机。他十分纠结,他现在是又想快点到学校,又想在床上上睡大觉,于是他写了一个策略模式帮他选择。

  1. Fang,我们的决策调用者
    • Context 上下文类,作为决策API的使用者,通过委托的方式保存了决策接口。
/**
 * 上下文类,Context。作为策略的调用者
 */
public class Fang {
    private Strategy strategy;
    private int money;//小方拥有的钱
    private int time;//小方拥有的时间
    public Fang(Strategy strategy,int money, int time) {
        this.strategy = strategy;
        this.money = money;
        this.time = time;
    }
    public void takeTransportation(){
        strategy.takeTransportation();
    }

    public Strategy getStrategy() {
        return strategy;
    }

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public int getTime() {
        return time;
    }

    public void setTime(int time) {
        this.time = time;
    }
}
  1. Strategy 接口,定义了决策的API,但具体实现交给子类:
/**
 *  策略模式
 *  策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
 *  策略模式让算法独立于使用它的客户而变化,也称为政策模式。
 *  策略模式是一种对象行为型模式。
 *  策略模式包含三个角色:
 *  1、抽象策略(Strategy)角色:这是一个抽象角色,通常是一个接口,定义了一个操作的接口,
 *  不同的策略实现类都实现了该接口。
 */
public interface Strategy {
    void takeTransportation();
}
  1. PlaneStrategy 坐飞机决策,具体的决策
    • 仅简单定义一个方法,表示坐飞机
public class PlaneStrategy implements Strategy{
    @Override
    public void takeTransportation() {
        System.out.println("小方选择坐飞机去学校");
    }
}
  1. TrainStrategy 同上,也是具体的决策
public class TrainStrategy implements Strategy{

    @Override
    public void takeTransportation() {
        System.out.println("小方选择坐火车去学校");
    }
}

测试类:

public class Test {
    public static void main(String[] args) throws InterruptedException {
             //小方目前时间很紧,但是巨有钱。于是他二话不说就买飞机冲!
        Fang fang1 = new Fang(new PlaneStrategy(),200000,5);
        fang1.takeTransportation();
            //这个小方目前没啥钱,但是他很早就开始准备买票了,嘿嘿,他决定快乐选择了一张高铁二等座,时间长,还能呼呼大睡
        Fang fang2 = new Fang(new TrainStrategy(), 2000, 100);
        fang2.takeTransportation();
        /*
         * 这个小方啊,即想要呼呼大睡,又想快点到。他决定整点不一样的,先做会高铁,等时间快不够的时候转去做飞机,
         * 两全其美,诶嘿,爽到。
         */
        Fang fang3 = new Fang(new TrainStrategy(), 20000, 100);
        fang3.takeTransportation();
        while(fang3.getTime()>=20){
            fang3.setTime(fang3.getTime()-20);
            //让小方睡会
            System.out.println("阿巴阿巴,小方在睡大觉······");
            Thread.sleep(1000);
        }
        System.out.println("我靠,要迟到了,赶紧转机,冲冲冲!!!");
        fang3.setStrategy(new PlaneStrategy());
        fang3.takeTransportation();
    }
}

输出结果:

小方选择坐飞机去学校
小方选择坐火车去学校
小方选择坐火车去学校
阿巴阿巴,小方在睡大觉······
阿巴阿巴,小方在睡大觉······
阿巴阿巴,小方在睡大觉······
阿巴阿巴,小方在睡大觉······
阿巴阿巴,小方在睡大觉······
我靠,要迟到了,赶紧转机,冲冲冲!!!
小方选择坐飞机去学校

策略模式出现的角色

  1. Context 上下文

    ​ 使用策略的角色。其保存了 Strategy 角色的具体实现实例。并使用 ConcretStrategy 角色去实现需求。即调用 Strategy 的 API。

​ 本例中由 Fang 类承担。

  1. Strategy 策略

    ​ 定义了实现策略所必须的接口。本例中由 Strategy 承担。

  2. ConcretStrate 具体策略

    ​ Strategy 接口的实现类,实现了 Strategy 定义的 API ,其定义了具体的策略,被 Context 真正调用的角色。

策略模式的特点

优点:

  • 算法可以自由切换。

  • 避免使用多重条件判断。

  • 扩展性良好。

  • 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。

缺点:

  • 策略类会增多,可以通过使用享元模式在一定程度上减少对象的数量。

  • 所有策略类都需要对外暴露。即客户端必须知道所有的策略类,并自行决定使用哪一个策略类。

使用场景:

  1. 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  2. 一个系统需要动态地在几种算法中选择一种。
  3. 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

组合模式

​ 计算机文件系统中,由文件和目录组成,一个目录中可以有文件也可以有子目录。目录相当于一个容器,我们可以往里面放小容器,也可以放内容。因此这么一个递归的结构就是组合模式。

组合模式代码示例

  1. Entry

    是文件和目录的父类,便于一致管理文件和目录两个类。、

    定义了toString 方法,同一输出结果

    其定义了获取大小和名称的方法,交给子类去是实现。

    同时设置了 protected 的 printList 方法,只允许子类去调用,防止外部调用该方法,胡乱传参,可能导致输出错误

    定义了一个 add 方法,交给目录类去实现,其默认实现是如果调用的不是目录类的 add 方法,就会抛出异常。

public  abstract class Entry {
    public abstract String getName();
    public abstract int getSize();
    public abstract void printList(String prefix);
    public void printList(){
        //传入空字符,调用重写方法
        printList("");
    }
    public void add(Entry director){
        throw new FileTreatMentException();
    }

    @Override
    public String toString() {
        return getName() + "(" + getSize() + ")";
    }
}

FileTreatMentException

一个简单的异常类:

public class FileTreatMentException  extends RuntimeException{
    public FileTreatMentException() {

    }
    public FileTreatMentException(String Msg){
        super(Msg);
    }
}
  1. File 类,文件的类,没啥好说的,就是简单地设置字段。
public class File extends Entry{
    private String name;
    private int size;

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int getSize() {
        return this.size;
    }

    @Override
    protected void printList(String prefix) {
        System.out.println(prefix + "/" + this);
    }
}
  1. Director 类

    有一个 ArrayList 负责存储该目录下的文件和目录

    getSize 方法调用时,会遍历自己存储的文件和目录,如果是文件,则直接求和文件大小,如果是目录,则递归调用,直到最底层,将所有的子目录的大小全部加一块。

    调用 printList 方法时,会递归调用自己存储的目录下的目录和文件同名方法,保持了一致性。

public class Director extends Entry{
    private String name;
    private ArrayList<Entry>  director = new ArrayList<>();
    @Override
    public String getName() {
        return this.name;
    }

    public Director(String name) {
        this.name = name;
    }

    @Override
    public int getSize() {
        int size = 0;
        for(Entry entry : director){
           size += entry.getSize();
        }
        return size;
    }

    @Override
    protected void printList(String prefix) {
        System.out.println(prefix + "/" + this);
        for(Entry entry : director){
            entry.printList(prefix + "/" + name);
        }
    }
    @Override
    public void add(Entry director){
        this.director.add(director);
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        try {
            System.out.println("Making root entries...");
            Director rootDir = new Director("root");
            Director binDir = new Director("bin");
            Director tmpDir = new Director("tmp");
            Director usrDir = new Director("usr");
            rootDir.add(binDir);
            rootDir.add(tmpDir);
            rootDir.add(usrDir);
            binDir.add(new File("vi",10000));
            binDir.add(new File("latex",20000));
            rootDir.printList();

        }catch (FileTreatMentException e){
            e.printStackTrace();
        }
    }
}

输出结果:

Making root entries...
/root(30000)
/root/bin(30000)
/root/bin/vi(10000)
/root/bin/latex(20000)
/root/tmp(0)
/root/usr(0)

进程已结束,退出代码为 0

组合模式出现的角色

  1. Leaf (树叶)

​ 表示 内容 的东西,即我们示例代码中的 File 类,其内部不能再放入其他对象

  1. Composite (复合物)

​ 表示容器的角色。其内部可以放入 Leaf 和 Composite 角色。本例中由 Director 角色担任

  1. Component

​ 让 Leaf 和 Composite 角色具有一致性的角色,是它们的父类。 由 Entry 担任。

  1. Client

​ 使用 Composite 模式的角色。由 Test 类担任。

组合模式的特点

​ 让我们考虑一个场景,就是我们现在有一个很混乱的文件夹,里面有音乐文件、视频文件、文本文件。我们希望将它们分类放好。对于未使用组合模式的情况,我们就需要在类中创建一个音乐集合、视频集合、文本集合。然后根据文件类型将它们放到不同的集合中去。这中间就需要判断一下这个文件属于哪种类型,然后再去找对应的类型的集合,将其放进去,就很麻烦。

​ 而且,如果未来我们又多了一个图片文件,就又得添加一个图片集合,然后把图片文件放进去,拓展起来非常麻烦。

然而使用了组合模式,我们就可以把音乐目录、视频目录、文本目录全部放到根目录下,然后根据我们的需要,将各种文件放入到各个子目录下。这样,就不需要创建多个集合了。全部都是目录类,不需要大量的 ArrayList 去存储这些文件和目录。

未来需要拓展新的文件类型,就只需要创建对应的文件类,然后继承 Component 接口,实现一致性方法,然后将该对象添加到根目录下的图片目录即可。

优点

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。

  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。

  • 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。

  • 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

装饰模式

给原本空空的对象加上更多的功能

简介

​ 假设我们有一个房子,但是此时这个房子还只是个胚子,也就是空有壳,没有家具,也没有各种装饰品。现在,我们需要给它加上一些装饰,让它更像一个温馨的小家。

​ 装饰模式就是类似于这种思路,它可以对一个对象不断地添加功能,让其变成一个功能强大的对象。

它由一个超类 Component 作为最顶层,它有两种子类,一个是具体的被装饰者ConcretComponent。另一个是 装饰者 Decorator,它内部聚合了 Component 对象,可以调用其方法。然后 Decorator 也有自己的

装饰模式 UML 类图

image-20231220102220882

装饰模式代码示例

  1. Component

    房子

/**
 * 最顶级的组件
 * 仅定义了一个操作,交给子类去实现
 */
public abstract class Component {
    public abstract void operation();
}
  1. 具体的 Component
    1. 厨房
public  class Kitchen extends Component{

    @Override
    public void operation() {
        System.out.println("这是厨房");
    }

    @Override
    public String toString() {
        return "厨房";
    }
}

​ 2. 书房

public class StudyRoom extends Component{
    @Override
    public void operation() {
        System.out.println("这是书房");
    }

    @Override
    public String toString() {
        return "书房";
    }
}
  1. 抽象的装饰者
public abstract class Decorator extends Component{
    private Component component;

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

    @Override
    public void operation() {
        component.operation();
    }

    public Component getComponent() {
        return component;
    }
}

4。 具体的装饰者

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

    @Override
    public void operation() {
        super.operation();
        setTable();
    }
    private  void setTable(){
        System.out.println("给这个"+super.getComponent().toString()+"加个桌子");
    }

    @Override
    public String toString() {
        return "桌子";
    }
}
public class ChairDecorator extends Decorator{
    public ChairDecorator(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        setChair();
    }
    private void setChair(){
        System.out.println("给这个"+super.getComponent().toString()+"加个椅子");
    }

    @Override
    public String toString() {
        return "椅子";
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Component kitchen = new Kitchen();//创建一个厨房
        Decorator tableDec = new TableDecorator(kitchen);//给厨房加个桌子
        Decorator chairDecorator = new ChairDecorator(tableDec);//厨房怎么只能有桌子没有椅子,给它加一个
        chairDecorator.operation();
        System.out.println("------------------------------------");
        Component studyRoom = new StudyRoom();
    //给咋们的书房加个椅子再加个桌子再加个椅子再加个桌子
        ChairDecorator dec = new ChairDecorator
                (new TableDecorator
                        (new ChairDecorator
                                (new TableDecorator(studyRoom))));
        dec.operation();
    }
}

测试结果:

这是厨房
给这个厨房加个桌子
给这个桌子加个椅子
------------------------------------
这是书房
给这个书房加个桌子
给这个桌子加个椅子
给这个椅子加个桌子
给这个桌子加个椅子

进程已结束,退出代码为 0

可以很显然地看出来,它进行了递归调用。

装饰模式出现的角色

  1. Component

    增加功能的核心角色,它是最原始的一个胚子,也就是我们的房子。

  2. ConcretComponent

    该角色实现了 Component 定义的接口,其是房子胚子中的一些具体内容,比如厨房,书房。

  3. Decorator

    抽象装饰者,它聚合了 Component 对象。 Decorator 知道自己要装饰的对象。

  4. ConcretDecorator

    具体的装饰者。它负责真正给 ConcretComponet 加上装饰

装饰者模式的特点

优点

  • 可以动态的给对象添加新功能,通过运行时选择不同的具体装饰类给对象添加不同的新功能

  • 使用对象关联方式替代继承复用方式,符合合成复用原则,大大减少了类爆炸

  • 可以进行多次装饰,通过对具体装饰类进行排列组合,可以创造出很多种不同的新行为

  • 具体构件类和具体装饰类可以独立变化,扩展新功能时,不需要修改系统现有代码,符合开闭原则。

缺点

  • 装饰模式会生成大量的功能类似的小对象(具体装饰类)

  • 装饰模式可以进行多次装饰达到对原有对象功能的增强,但在排查、定位问题时也会特别麻烦,需要逐级排查,提高问题排查成本。

访问者模式

将数据处理和数据结构分离开

我们编写类时,会在类中编写很多方法,部分方法会对类做一些结构的修改,对于一些类,我们可能不希望在类中去修改,于是我们就需要**数据结构与处理分离开。**而访问者模式可以满足这个需要,只需要编写一个 访问者 去访问结构中的元素,在访问者中去做相应的处理即可。

访问者模式代码示例

先来介绍被访问者这边:

我们用的还是之前组合模式的代码:

不过不再是在 Director 里调用 PrintList 方法了。而是让我们的 Visitor 来实现这个功能。

public interface Element {
    //定义接收访问函数
    void accept( Visitor visitor);
}
public abstract class Entry implements Element{

    public boolean add( Entry entry){
        throw new FileTreatmentException();
    }

    public Iterator iterator(){
        throw new FileTreatmentException();
    }

    public abstract String getName();


    public abstract int getSize();

    @Override
    public String toString() {
        return  this.getName()+this.getSize();
    }
}
public class Director extends Entry{
    private String name;
    private ArrayList<Entry> dir = new ArrayList<>();
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public Director(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int getSize() {
        Iterator<Entry> iterator = dir.iterator();
        int size = 0;
        while(iterator.hasNext()){
            size+=iterator.next().getSize();
        }
        return size;
    }

    @Override
    public boolean add(Entry entry) {
        if (entry!=null){
            dir.add(entry);
            return true;
        }
        return false;
    }

    @Override
    public Iterator iterator() {
       return dir.iterator();
    }
}
public class File extends Entry{
    private String name;
    private int size;
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int getSize() {
        return this.size;
    }
}

异常

public class FileTreatmentException  extends RuntimeException{
    public FileTreatmentException() {

    }
    public FileTreatmentException(String Msg){
        super(Msg);
    }
}

访问者:

抽象类,简单定义一下访问的函数,针对不同的类用不同的访问:

public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Director director);

}

具体的访问者,定义了具体的 visit 方法。

public class ListVisitor extends Visitor{
    private String currentDir = "";
    @Override
    public void visit(File file) {
        System.out.println(currentDir + "/" + file);
    }

    //具体处理,遍历输出文件层次
    @Override
    public void visit(Director director) {
        System.out.println(currentDir+"/"+ director);
        String savedir = currentDir;
        currentDir = currentDir + "/" + director.getName();
        Iterator iterator = director.iterator();
        while (iterator.hasNext()){
            Entry entry =(Entry) iterator.next();
            entry.accept(this);
        }
        currentDir = savedir;
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Entry rootDir = new Director("root");
        Entry binDir  = new Director("bin");
        Entry srcDir  = new Director("src");
        Entry f1 = new File("f1.xml",1000);
        Entry f2 = new File("f2.xml",1000);
        rootDir.add(binDir);
        rootDir.add(srcDir);
        binDir.add(f1);
        binDir.add(f2);
        rootDir.accept(new ListVisitor());
    }
}

运行结果:

/root2000
/root/bin2000
/root/bin/f1.xml1000
/root/bin/f2.xml1000
/root/src0

进程已结束,退出代码为 0

访问者模式出现的角色

  1. 抽象访问者 Visitor

​ 定义了访问的抽象方法

  1. 具体访问者 ConcretVisitor

​ 实现了 Visitor 的方法,具体的访问操作

  1. Element

    表示访问者访问的对象。它声明了接收访问者的 accept 方法

  2. ConcretElement

​ 具体的访问对象。其负责实现了 accpet 方法。

  1. ObjectStructure

负责处理 Element 角色的集合。 方便 ConcretVisitor 角色对每个 Element 角色进行处理。它由 Director 类担任,这个类在这个模式中有两个责任。由它的 iterator 方法来实现这个功能。

访问者模式的特点

  1. 双重分发,由 Entry 的 accpet 开始,参数传递了 Visitor,然后 Visitor 调用 visit 又传递了 Entry 。即:两个角色共同回顾决定了实际进行的处理。

  2. 实现了处理从数据结构中分离开。

  3. 满足开闭原则,对拓展是开放的,对修改是封闭的。我们可以很轻松地新增各种访问者来实现对 Element 的处理。但是不能对 Element 去做修改,若是修改了,就需要更改大量的 visitor。

  4. 易于增加 ConcreteVisitor

  5. 不好增加 ConcreteElement

责任链模式

将任务一个个传递下去,直到有人去处理它或者无人处理

简介

​ 将多个对象组成一条责任链,对于一个任务,将根据这条责任链,一个个地让每个对象都判断是否执行,直到有对象执行任务或者直到最后也没有对象执行任务。

责任链模式代码示例

Trouble 类:

public class Trouble {
    private int number;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public Trouble(int number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return "Trouble{" +
                "number=" + number +
                '}';
    }
}

Support 类:

public abstract class Support {
    private String name;
    private Support next;

    public Support(String name) {
        this.name = name;
    }

    public Support setNext(Support next) {
        this.next = next;
        return next;
    }
    public void support(Trouble trouble){
        if(resolve(trouble)){
            done(trouble);
        }else if(this.next!=null){
            next.support(trouble);
        }else{
            fail(trouble);
        }
    }

    @Override
    public String toString() {
        return " " + name ;

    }
    public abstract boolean resolve(Trouble trouble);
    public  void done(Trouble trouble) {
        System.out.println(trouble + " is resolved by" + this + ".");
    }
    public  void fail(Trouble trouble){
        System.out.println(trouble + "cannot be resolved");
    }

}
public class NoSupport extends Support{
    public NoSupport(String name) {
        super(name);
    }

    @Override
    public boolean resolve(Trouble trouble) {
        return false;
    }
}
public class LimitSupport extends Support{
    private int limit;

    public LimitSupport(String name, int limit) {
        super(name);
        this.limit = limit;
    }

    @Override
    public boolean resolve(Trouble trouble) {
        if(trouble.getNumber()<=limit){
            return true;
        }
        return false;
    }
}
public class OddSupport extends Support{
    public OddSupport(String name) {
        super(name);
    }

    @Override
    public boolean resolve(Trouble trouble) {
        if(trouble.getNumber()%2==1){
            return true;
        }
        return false;
    }
}
public class SpecialSupport extends Support{
    private int number;

    public SpecialSupport(String name, int number) {
        super(name);
        this.number = number;
    }

    @Override
    public boolean resolve(Trouble trouble) {
        if (trouble.getNumber()==number){
            return true;
        }
        return false;
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Support bob = new NoSupport("bob");
        Support sam = new LimitSupport("Sam",100);
        Support pic = new LimitSupport("Pic",200);
        Support smith = new LimitSupport("Smith",300);
        OddSupport cuiXi = new OddSupport("CuiXi");
        SpecialSupport amy = new SpecialSupport("Amy", 423);

        bob.setNext(sam).setNext(pic).setNext(smith).setNext(cuiXi).setNext(amy);

        for(int i = 0 ; i< 500 ; i+=33){
            bob.support(new Trouble(i));
        }
    }
}

结果:

Trouble{number=0} is resolved by Sam.
Trouble{number=33} is resolved by Sam.
Trouble{number=66} is resolved by Sam.
Trouble{number=99} is resolved by Sam.
Trouble{number=132} is resolved by Pic.
Trouble{number=165} is resolved by Pic.
Trouble{number=198} is resolved by Pic.
Trouble{number=231} is resolved by Smith.
Trouble{number=264} is resolved by Smith.
Trouble{number=297} is resolved by Smith.
Trouble{number=330}cannot be resolved
Trouble{number=363} is resolved by CuiXi.
Trouble{number=396}cannot be resolved
Trouble{number=429} is resolved by CuiXi.
Trouble{number=462}cannot be resolved
Trouble{number=495} is resolved by CuiXi.

进程已结束,退出代码为 0

责任链模式出现的角色

  1. Handler 处理者

    定义了处理请求的接口,它知道下一个处理者是谁,如果有自己无法处理的请求,就交给下一个处理者去处理。

  2. ConcreteHandler 具体处理者

    实现了处理者处理请求的接口。

  3. Client

    向责任链中第一个处理者发送请求的角色。

责任链模式的特点

  1. 弱化了发送请求和处理请求对象之间的关系。请求发送者并不清楚具体是谁处理了请求。

  2. 可以根据实际需要,动态地在程序执行时更改责任链。对于未使用责任链,明确指定请求处理对象的代码,它就很难在程序运行过程中去更改请求的处理者。因为请求可能会被处理的呈现随机结果,对于明确请求处理者的情况,这就很难确定哪个处理者能处理这个请求。

    而采用责任链,则可以通过迭代很轻松地找到能够处理请求的处理者。

  3. 可以让每个处理者专注于自己的工作

  4. 会带来请求处理的延迟。虽然责任链提高了程序的灵活性,但通过迭代的方式推卸责任,也带来了性能下降。

外观模式

给复杂应用程序提供一个高层接口

简介

当我们编写了一个大型应用程序,它有非常多的类,功能非常复杂难以理解。这时我们可以通过编写一个高层类,将程序中的部分功能(多个类结合实现的功能)结合到该类的一个方法中。这就是我们的外观模式。

​ 相当于一个门面,一个简单易理解,好看的门面总是能吸引更多的顾客。而这个高层类就是这样的设计,它能够让本来复杂,类之间功能繁多的程序api调用变得简单。

​ 为了避免出现第二个复杂类,也就是防止我们的高层类随着实现越来越多的功能,也变得非常复杂,因此我们可以根据情况创建新的高层类去作为调用其他功能的接口。

外观模式代码示例

这里复杂类,我们用两个简单的类来替代,实际情况里其实也就是让我们的外观类去执行各个复杂类的API,然后避免外部直接调用复杂类就好:

这里把类的范围设置为无限定符,这样可以避免包外类访问

/**
 * 调节照相机的各种配置
 */
 class Adjust {

    public Adjust() {
    }

    public void doAdjust() throws InterruptedException {
        System.out.println("正在完成调节配置");
        Thread.sleep(1000);
        System.out.println("调节完成");
    }
}
/**
 * 对焦操作
 * 类定义去掉了public,避免包外的类访问
 */
class Focus {
    public Focus() {
    }

    public void doFocus() throws InterruptedException {
        System.out.println("正在进行对焦");
        Thread.sleep(1000);
        System.out.println("对焦完成");
    }
}

高层类:

public class PhotoItem {
    private PhotoItem() {//禁止外部调用构造器获取当前对象,外部只能使用静态方法
    }

    public static void takePhoto() throws InterruptedException {
        Adjust adjust = new Adjust();
        Focus focus = new Focus();
        adjust.doAdjust();
        focus.doFocus();
        System.out.println("设备调试完毕,拍摄成功");
    }
}

测试类:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        PhotoItem.takePhoto();
    }
}
正在完成调节配置
调节完成
正在进行对焦
对焦完成
设备调试完毕,拍摄成功

进程已结束,退出代码为 0

外观模式的角色

  1. Facade 外观

    代表构成系统的去多其他角色的简单窗口

  2. 构成系统的许多其他角色

  3. Client 请求者

外观模式的特点

  1. 可以很明显地看出来,调用接口少了,可以更简单地去实现原本复杂类的功能,更好维护。

  2. 可以添加很多 Facade ,这样避免一个 Facade 过大导致成为新的复杂类。

  3. 外观可能会成为与所有类都耦合的高层类

仲裁者模式

将类之间的调用交给一个仲裁者去执行,并且它们之间的影响是双向的

简介

和之前的外观模式一样,仲裁者模式也是通过设置一个外部的接口,然后去调用各个类之间的方法来实现原本类中的功能。但是仲裁者和外观模式不同之处在于外观模式是单一地去调用各个类的功能,而仲裁者模式是双向的,它会受到程序执行过程中类的改变而改变它的行为。

仲裁者模式代码示例

.

public interface Mediator {
    void createColleague();
    void colleagueChanged();
}
public interface Colleague {
    void setMediator(Mediator mediator);
    void setColleagueEnable(boolean enable);

}
public class StringColleague implements Colleague{
    private Mediator mediator;
    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnable(boolean enable) {
        if(enable){
            System.out.println("允许进行字符串操作");
        }else{
            System.out.println("不允许进行字符串操作");
        }

    }
    //设置一个能够实现变动的操作
    public int ItemStateChange(int input){
        if(input%3!=0){
            setColleagueEnable(false);
            return 1;
        }else{
            setColleagueEnable(true);
            return 0;
        }
    }
}
public class CharColleague implements Colleague{
    private Mediator mediator;
    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnable(boolean enable) {
        if(enable){
            System.out.println("允许进行字符操作");
        }else{
            System.out.println("不允许进行字符操作");
        }

    }

    public int ItemStateChange(int input){
        //字符无论执行结果如何都返回1
        if(input%2!=0){
            setColleagueEnable(false);
        }else{
            setColleagueEnable(true);
        }
        return 1;
    }
}
public class ConcreteMediator implements Mediator{
    private StringColleague stringColleague;
    private CharColleague charColleague;
    @Override
    public void createColleague() {
        this.stringColleague = new StringColleague();
        this.charColleague = new CharColleague();
        stringColleague.setMediator(this);
        charColleague.setMediator(this);
    }

    @Override
    public void colleagueChanged() {
        //如果字符串不能执行就让字符去执行,实现了让 Mediator 受到 Colleague 情况的影响,形成双向通信。
        for(int i = 0 ; i<500 ; i+=33){
            int i1 = stringColleague.ItemStateChange(i);
            if(i1 != 1) {
                charColleague.ItemStateChange(i);
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Mediator concreteMediator = new ConcreteMediator();
        concreteMediator.createColleague();
        concreteMediator.colleagueChanged();
    }
}
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作
允许进行字符串操作
允许进行字符操作
允许进行字符串操作
不允许进行字符操作

进程已结束,退出代码为 0

仲裁者模式出现的角色

  1. Mediator 仲裁者

    负责定义与 Colleague 角色进行通信和做出决定的接口。

  2. ConcreteMediator 具体的仲裁者

    负责实现 Mediator 定义的接口。负责做出实际的决定。

  3. Colleague 同事

    负责定义了与 Mediator 进行通信的接口。

  4. ConcreteColleague 具体的同事

    负责实现了 Colleague 定义的接口,同时可以有自己的拓展。

仲裁者模式的特点

和外观模式比起来,它类之间是双向通信的,会互相影响,但是依然是通过一个高层类,来维护一系列复杂类。

优势

  • 符合单一职责原则,您可以将各种组件之间的通信提取到一个地方,使其更易于理解和维护

  • 符合开闭原则,可以在不改变实际成员的情况下引入新的 mediator

  • 很明显的,则能够减少程序之间的耦合

  • 可以更轻松的重用单个组件

劣势

  • 随着组件的增多,mediator 对象会变成一个 God Object

观察者模式

将变化通知给观察者

简介

​ 我们之前介绍了中介者模式,它会接收来自各个同事的变动而改变自己的决定。而观察者也和中介者模式类似。观察者内部会存储观察对象的一些数据,它会根据观察对象的变化,同步更新自己的数据。

观察者模式代码示例

public interface Observer {
    void update(NumberGenerator numberGenerator);
}
blic abstract class NumberGenerator {
    private ArrayList<Observer> observers = new ArrayList<Observer>();
    public void addObserver(Observer observer){
        observers.add(observer);
    }
    public void deleteObserver(Observer observer){
        observers.remove(observer);
    }
    public void notifyObservers(){
        Iterator iterator = observers.iterator();
        while(iterator.hasNext()){
            Observer next = (Observer) iterator.next();
            next.update(this);
        }
    }
    public abstract int getNumber();
    public abstract void execute();
}
public class RandomNumberGenerator extends NumberGenerator{
    private int number;
    private Random random = new Random();

    @Override
    public int getNumber() {
        return this.number;
    }

    @Override
    public void execute() {
        for(int i = 0 ; i < 20 ; i++){
            this.number = random.nextInt(50);
            notifyObservers();
        }
    }
}
public class DigitObserver implements Observer {
    @Override
    public void update(NumberGenerator numberGenerator) {
        int number = numberGenerator.getNumber();
        System.out.println("DigitObserver:"+ number );
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class GraphObserver implements Observer{
    @Override
    public void update(NumberGenerator numberGenerator) {
        int number = numberGenerator.getNumber();

        for(int i = 0 ; i< number ; i++){
            System.out.print("*");
        }
        System.out.println("");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Observer digitObserver = new DigitObserver();
        Observer graphObserver = new GraphObserver();
        RandomNumberGenerator r = new RandomNumberGenerator();
        r.addObserver(digitObserver);
        r.addObserver(graphObserver);
        r.execute();
    }
}
DigitObserver:28
****************************
DigitObserver:19
*******************
DigitObserver:3
***
DigitObserver:2
**
DigitObserver:44
********************************************
DigitObserver:7
*******
DigitObserver:10
**********
DigitObserver:15
***************
DigitObserver:41
*****************************************
DigitObserver:20
********************
DigitObserver:36
************************************
DigitObserver:48
************************************************
DigitObserver:9
*********
DigitObserver:48
************************************************
DigitObserver:48
************************************************
DigitObserver:48
************************************************
DigitObserver:0

DigitObserver:46
**********************************************
DigitObserver:15
***************
DigitObserver:29
*****************************

观察者模式的角色

  1. 抽象观察者

    定义了如果被观察对象发生了变化,它同步更新的方法。它负责接收来自观察对象的状态变化的通知

  2. 具体观察者

    实现了具体接收被观察对象状态变化的通知。它会去获取观察对象的最新装填

  3. 观察对象

    定义实现了添加观察者和删除观察者的方法,它声明了获取现在状态的方法,该方法交给子类去实现

  4. 具体的观察对象

    实现了通知观察者的当前观察对象状态情况的方法。它可以获取到自身的状态,并通过父类方法通知给观察者。

观察者模式特点

  • 具体观察者和具体观察对象并不知道对方是谁,它们都是通过自己的超类去实现通信。可以很轻松地增加具体观察者和具体观察对象。

  • 要注意观察者更新数据方法的执行顺序,如果观察者的方法调用会影响被观察对象,那么一定要注意多个观察者方法的调用顺序。避免带来问题。也可以确保各个观察者之间的独立性,就不会出现上述问题了

  • 传递消息的方式不只可以直接传递整个对象,还可以传递更多的细节,帮助观察者做更多的功能。

  • 观察者模式也被称为发布订阅模式,观察者是被动地接收来自观察对象的变化。

  • MVC 模型中, Model 就是一个观察对象,View 是观察者。因为一般来说, 一份 Model 可以应用在多个 View 上, View 利用 Model 提供的模型数据,去构建自己的显示。

备忘录模式

给对象的状态做一个保存,并且可以支持回溯

当我们编辑一个文本时,有时候觉得这一步不满意,会做一个撤回的操作,让它恢复到上一个动作的状态。同时,有的文本编辑器可以支持多次撤回,还能往前恢复,这就相当于一个保存功能,可以让我们自由地恢复到需要的位置。

备忘录模式代码示例

一起来玩个抽奖游戏:

这里注意一下细节:这里的函数和字段,之对外暴露的 getMoney 方法。其他方法都是只能类自己或者同包下的类可以访问。为了避免外部直接获取其中保存的字段,进而修改字段保存的数据导致数据不匹配。

public class Memento {
    private int money;
    private int level;

    Memento(int money, int level) {
        this.money = money;
        this.level = level;
    }
    public int getMoney(){
        return this.money;
    }
    int getLevel(){
        return this.level;
    }

}
public class Gamer {
    private int level;
    private int money;
    private HashMap<String,Memento> mementos = new HashMap<>();
    private ArrayList<String> timeStamp = new ArrayList<>();
    public Gamer(int level, int money) {
        this.level = level;
        this.money = money;
    }

    public int getLevel() {
        return level;
    }

    public int getMoney() {
        return money;
    }

    public ArrayList<String> getTimeStamp() {
        return timeStamp;
    }

    /*
            玩家可以进行游戏,游戏的规则是随机投骰子,投到1可以得到100金币并提升等级1级
            2会被扣除50金币,3-6无事发生
             */
    public void PlayGame(){
        Random random = new Random();
        int i = random.nextInt(6);
        if( i == 1){
            this.money += 100;
            this.level += 1;
            System.out.println("抽到了1,金币+100,等级+1");
        }else if(i == 2){
            this.money -= 50;
            System.out.println("抽到了2,金币-50");
        }else{
            System.out.println("无事发生");
        }
    }

    public Memento createMemento(){
        Memento memento = new Memento(this.money,this.level);
        //创建时间戳,用来存储不同的记录
        LocalDateTime now = LocalDateTime.now();
        String timeStamp = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd hh mm ss"));
        //将该状态保存
        this.timeStamp.add(timeStamp);
        this.mementos.put(timeStamp,memento);
        System.out.println("创建游戏备忘录成功");
        return memento;
    }

    public void restoreMemento(Memento memento){
        this.money = memento.getMoney();
        this.level = memento.getLevel();
    }
    //玩家可以根据自己的需要自定义恢复存档。
    public void restoreMementoByGamer(String timeStamp){
        Memento memento = mementos.get(timeStamp);
        this.money =memento.getMoney();
        this.level = memento.getLevel();
    }
}

public class Test {
    public static void main(String[] args) {
        Gamer gamer = new Gamer(1, 100);
        //先保存一下初始状态
        Memento memento = gamer.createMemento();
        int idx = 1;
        System.out.println("开始玩游戏");
        while(gamer.getMoney() > 0 && idx<=50){
            idx++;
            System.out.println("第"+idx+"次游戏");
            //开始玩游戏
            gamer.PlayGame();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(gamer.getMoney()>memento.getMoney()){
                //只要钱多了,就保存一下
               memento = gamer.createMemento();
                System.out.println("玩家所持的金钱和等级增加了,并保存了当前状态。\n玩家金钱:"+ gamer.getMoney()+"\n玩家等级:"+ gamer.getLevel());
            }else if(gamer.getMoney()<memento.getMoney()/2){
                //钱变少了就回档,欸嘿,真爽,老子就是不亏,单机游戏爽歪歪
                gamer.restoreMemento(memento);
                System.out.println("玩家金钱减少了,选择回档");
            }
        }
        System.out.println("玩家结束了游戏,结算玩家的获得:\n玩家金钱:"+ gamer.getMoney() +"\n玩家等级"+gamer.getLevel());
    }
}

image-20231223144320331

我们可以根据我们的需要,随时跳转到之前保存的状态。

备忘录模式出现的角色

  1. 生成者 Originator

    该角色会保存自己的最新状态并生成 Memento 角色。当把以前保存的 Memento 角色传递给 生成者 时,生成者可以恢复到 Memento 角色时的状态。

  2. Memento 备忘录

​ 该角色会将 Originator 角色的内部信息整合在一起。 在 Memeto 角色中虽然保存了 Originator 角色的信息,但它不会向外公开这些信息。

​ 它分为了窄接口和宽接口。

  • 窄接口:外部可以通过该接口获得信息,但是窄接口仅仅提供了部分允许公开的信息,类中的 getMoney 就是一个窄接口

  • 宽接口,所有用于获取恢复对象状态信息的方法的集合。由于宽接口会暴露 Memento 的所有内部信息,所以只能使用宽接口的只有 Originator 角色。

  1. Caretaker 负责人

​ 当 Caretaker 角色想要保存当前的 Originator 角色的状态时,它会通知 Originator 角色。 Originator 角色收到通知后会生成 Memento 角色的实例并返回给 Caretaker 角色。Caretaker 还需要一直保存着该实例。

  • Caretaker 角色只能使用 Memento 的窄接口。

备忘录模式的特点

  1. 可以有多个 Memento

  2. Memento 可以被永久保存在磁盘中

作用:

  • 当需要保存一个对象的内部状态,以方便后续在用户出错时进行恢复。
  • 如果一个接口让其它对象直接获取内部状态,又要不破坏对象的封装性。

状态模式

将状态定义成类

State模式,回想一下之前写策略模式的情况,我们会根据实际情况的变化,修改我们调用的策略方法。而这个实际情况可以根据 if 来判断的。若满足某一个if,就执行对应的语句

状态模式就是将这个 if 所指的状态封装为一个类,然后在其中定义实现了该 if 下的语句。

比如我们在中午十二点,吃午饭,在下午两点,喝下午茶。那么我们就可以定义两个类,一个叫做中午十二点,一个叫做下午两点。然后在其中定义各自的方法。

状态模式代码示例

现在有一个人,他有着非常规律的作息和生活习惯,他早上8点起床开始写学习写代码,一直持续到中午12点,然后他去吃午饭,吃到下午1点,他继续写代码。在下午两点的时候,他要喝下午茶,然后一小时后,他继续写代码,直到晚上10点。

类:

  1. 人,其内部存储一个 state 字段,用来存储此时的状态。同时它实现了超类的方法。

  2. 抽象的 state,用来定义一些基本的状态方法,比如此时要做的事情以及转换状态的函数。

  3. 抽象的实体,被人给继承,它定义了一系列人要做的操作。

Context,抽象实体,定义一些实体要做的事情,然后它定义了一个转换状态的方法

public interface  Context {
    void changeState(State state);
    void doWork();
    void setState(int time);
}
public class Human implements Context{
    private State state;
    @Override
    public void changeState(State state) {
        this.state = state;
    }

    @Override
    public void doWork() {
        //根据状态做工作
        state.doWork();
    }

    @Override
    public void setState(int time) {
        state.setState(this,time);
    }
}

抽象状态类:

public interface State {
   static State getInstance(){
       throw new RuntimeException();
   };
    void setState(Context context,int time);
    void doWork();
}
public class Morning implements State{
    private static State morning = new Morning();

    public static State getInstance() {
        return morning;
    }

    @Override
    public void setState(Context context ,int time) {
        if(time>=8 && time <12){
            context.changeState(morning);
        }else if(time == 12){
            context.changeState(NoonDay.getInstance());
        }else{
            context.changeState(Afternoon.getInstance());
        }
    }

    @Override
    public void doWork() {
        System.out.println("早起就该敲代码");
    }
}
/**
 * 中午的状态类
 */
public class NoonDay implements State{

    private static State noonDay = new NoonDay();
    public static State getInstance() {
        return noonDay;
    }

    @Override
    public void setState(Context context, int time) {
        if(time>=8 && time <12){
            context.changeState(Morning.getInstance());
        }else if(time == 12){
            context.changeState(NoonDay.getInstance());
        }else{
            context.changeState(Afternoon.getInstance());
        }
    }

    @Override
    public void doWork() {
        System.out.println("我中午吃饭");
    }
}
public class Afternoon implements State{
    private static Afternoon afternoon = new Afternoon();

    public static Afternoon getInstance() {
        return afternoon;
    }

    @Override
    public void setState(Context context, int time) {
        if(time>=8 && time <12){
            context.changeState(Morning.getInstance());
        }else if(time == 12){
            context.changeState(NoonDay.getInstance());
        }else{
            context.changeState(Afternoon.getInstance());
        }
    }

    @Override
    public void doWork() {
        System.out.println("我下午到晚上要一直敲代码");
    }
}
public class Test {
    public static void main(String[] args) {
        Context human = new Human();
        human.changeState(Morning.getInstance());
        for(int i = 8 ; i < 23 ;i++){
            human.doWork();
            human.setState(i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

状态模式的特点

将状态定义成类

  1. 具体状态之间需要知道互相的作用范围,防止具体状态直接出现状态覆盖的情况。

  2. 具体状态和具体的对象(人)都不知道互相是谁,它们之间是通过两个抽象接口连通的。

  3. 未来添加更多的具体对象非常简单,只需要实现接口,并定义方法即可。而添加具体状态需要确定其他的具体状态的执行范围。

享元模式

将已经创建好的对象保存起来重复利用,避免创建大量相同状态的对象

​ 享元模式(Flyweight Pattern)是一种结构型设计模式,它主要解决的问题是创建大量相似对象时的内存开销问题。该模式通过共享具有相同状态的对象来减少内存使用量。

​ 享元模式的思想是:当需要创建一个新对象时,首先检查是否已经存在具有相同状态的对象。如果存在,则返回已经存在的对象,否则创建一个新的对象。因此,如果要创建多个具有相同状态的对象,可以重复使用相同的对象,从而减少内存开销。

享元模式代码示例

这里设想一个水果的情况,水果店里有一大堆的水果,我们希望能将它们作为对象创建出来,但是不能都创建一个对象,不然内存要爆了。因此我们对每种水果都只创建一个对象。

抽象水果:

public abstract class AbstractFruit {
    public abstract String getFruit();
    public void showFruit(){
        System.out.println(getFruit());
    }
}
public class Apple extends AbstractFruit{
    private String name;

    public Apple(String name) {
        this.name = name;
    }

    @Override
    public String getFruit() {
        return name;
    }
}
public class Banana extends AbstractFruit{
    private String name;

    public Banana(String name) {
        this.name = name;
    }

    @Override
    public String getFruit() {
        return name;
    }
}

单例的水果工厂:

避免外部调用者创建了很多水果工厂,由于每个水果工厂维护自己的水果映射表,因此它无法检测出其他水果工厂里存储的水果。也就无法判断是否重复创建了相同水果了。因此用单例模式。

public class FruitFactory {
    private static FruitFactory fruitFactory = new FruitFactory();
    private HashMap<String,AbstractFruit> fruits = new HashMap<>();
    private FruitFactory() {
    }
    public static FruitFactory getInstance() {
        return fruitFactory;
    }
    public AbstractFruit getFruit(String name,int fruitId){
        AbstractFruit fruit;
        if(fruits.get(name)!=null){
            return fruits.get(name);
        }else{
            if(fruitId == 1){
                fruit = new Apple(name);
            }else{
                fruit = new Banana(name);
            }
            fruits.put(name,fruit);
        }
        return fruit;
    }

}

测试:

public class Test {
    public static void main(String[] args) {
        FruitFactory r = FruitFactory.getInstance();
        AbstractFruit apple = r.getFruit("apple", 1);
        apple.showFruit();
        System.out.println(apple);
        //测试是不是拿到的都是一样的
        for(int i = 0 ; i < 100; i++){
            AbstractFruit apple1 = r.getFruit("apple", 1);
            System.out.println(apple1);
        }
    }
}

image-20231223212058513

享元模式的角色

抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公 共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法 来设置外部数据(外部状态)。

具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元 类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享 元类提供唯一的享元对象。

非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不 能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。

享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元 对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在 的话,则创建一个新的享元对象。

享元模式的特点

优点

  • 减少内存使用:由于享元模式共享对象,因此可以减少内存使用。

  • 提高性能:创建和销毁对象会占用大量的CPU时间和内存空间。因此,使用享元模式可以提高性能。

  • 代码简洁:享元模式可以使代码更简洁,因为该模式使用相同的对象来处理多个请求,而不需要创建大量的对象。

缺点

  • 共享对象可能会对线程安全造成问题。如果不正确地实现共享机制,则可能导致多个线程对同一对象进行更改,从而导致竞争条件。

  • 需要牺牲一定的时间和空间,来实现对象共享和控制机制。这意味着,当对象之间没有重复性时,使用享元模式可能会导致额外的开销。

  • 共享对象的改变可能会导致多个地方都发生改变,因此对共享对象的修改需要格外注意其带来的影响。

代理模式

暂时先代替主人完成工作,直到自己无法执行时,再让主人工作

简介

​ 代理模式属于结构型设计模式。为其他对象提供一种代理以控制对这个对象的访问。

​ 在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

代理模式代码示例

  1. 共同的抽象接口

  2. 代理类

  3. 必要时执行真正工作的对象

一个场景,Boss 最近工作很忙,让秘书先帮他去处理一些简单的杂活,直到上台演讲的工作他才亲自出马。

public interface ProxyTable {
    //做演讲的方法
    void doSpeaking();
    void doSomething();
}
public class Boss implements ProxyTable{
    @Override
    public void doSpeaking() {
        System.out.println("Boss亲自演讲");
    }

    @Override
    public void doSomething() {
        //啥也不做,交给秘书
    }
}
public class Secretary implements ProxyTable{
    private Boss boss;


    @Override
    public void doSpeaking() {
        //到要演讲了,再创建Boss,让Boss工作
        if(boss == null){
            boss = new Boss();
            boss.doSpeaking();
        }
    }
    public void doSomething(){
        System.out.println("秘书正在做杂货");
    }
}
public class Test {
    public static void main(String[] args) {
        Secretary secretary = new Secretary();
        secretary.doSomething();
        secretary.doSomething();
        secretary.doSpeaking();
    }
}

可以从测试代码中看出来,所有的动作都是代理去做的,但是doSpeaking底层是调用了Boss 的方法,而调用者无需在意其中的细节。

image-20231223214113018

代理模式的角色

  1. 抽象接口

    定义了基本的方法,要求代理和真正的对象都要实现该方法

  2. 代理对象

    对于对象不执行的方法,交给代理对象去执行。等到需要真正对象执行方法时再创建对象。

  3. 真正对象

    执行必须执行的方法的对象。平时的工作交给代理对象。

代理模式的特点

优点

  • 代理模式可以隐藏真实对象的实现细节,使客户端无需知晓真实对象的工作方式和结构。

  • 通过代理类来间接访问真实类,可以在不修改真实类的情况下,对其进行扩展、优化或添加安全措施。

  • 代理模式实现起来简单,易于扩展和维护,符合面向对象设计原则中的开闭原则。

缺点

  • 代理模式可能会引入额外的复杂性和间接性,增加程序设计和维护的难度。

  • 对象代理可能会降低系统性能,特别是在处理大数据量或频繁调用的情况下,因为代理需要额外的计算和网络通信开销。

命令模式

在真正方法执行和调用方法的对象之间,包裹一层命令

简介

命令模式,指的是将原本执行一个逻辑命令执行请求(比如一个方法调用的请求封装成类),使得发出请求,即调用方法的一方和执行请求的一方分离开。让两者通过命令对象来进行通信。

解耦了请求的发送者和接受者。

命令模式代码示例

抽象命令者

public abstract class AbstractCommander {
    public abstract void execute(); // 定义执行方法
}

具体命令者:其会去调用需要被执行方法的对象 receiver

public class ConcreteCommander extends AbstractCommander{
    private Receiver receiver;

    public ConcreteCommander(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.doThing();
    }
}

接收请求的对象

public class Receiver {
    public void doThing(){
        System.out.println("接受者执行事情");
    }
}

调用者

public class Invoker {
    private AbstractCommander commander;
    public void SetCommand(AbstractCommander commander){
        this.commander = commander;
    }
    public void doThing(){
        System.out.println("调用者执行命令");
        commander.execute();
    }
}

测试类:

public class Main {
    public static void main(String[] args) {
        Invoker invoker = new Invoker();
        AbstractCommander concreteCommander = new ConcreteCommander(new Receiver());
        invoker.SetCommand(concreteCommander);
        invoker.doThing();
    }
}

image-20231224220148047

命令模式出现的角色

  1. 抽象命令类(Command):命令的抽象类,类中对需要执行的命令进行声明,一般来说要对外公布一个execute方法用来执行命令。

  2. 具体命令类(Concrete Command):抽象命令类的具体实现子类,对抽象类中声明的方法进行实现。

  3. 调用类(Invoker):调用者,负责调用命令,执行命令功能的相关操作,是具体命令对象业务的真正实现者。

  4. 接受类(Receiver):接收者,负责接收命令并且执行命令。

命令模式的特点

  1. 将调用者和接收者分离开,它们之间的通信是通过命令来实现的。

  2. 命令模式将命令都封装起来,所以封装性好

  3. 一般如果拓展性好的同时会带来显著的缺点就是复杂,代码多,会有大量具体的命令类。命令模式同样的,如果命令很多的话,代码量就比较多。使用命令模式,不用管命令多简单,都需要写一个命令类来封装。

  4. 命令可以被撤销

    //测试是不是拿到的都是一样的
    for(int i = 0 ; i < 100; i++){
    AbstractFruit apple1 = r.getFruit(“apple”, 1);
    System.out.println(apple1);
    }
    }
    }




[外链图片转存中...(img-Kht5Ymf0-1742013315470)]





## 享元模式的角色

**抽象享元角色(Flyweight)**:通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公 共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法 来设置外部数据(外部状态)。



**具体享元(Concrete Flyweight)角色** :它实现了抽象享元类,称为享元对象;在具体享元 类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享 元类提供唯一的享元对象。



**非享元(Unsharable Flyweight)角色** :并不是所有的抽象享元类的子类都需要被共享,不 能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。



**享元工厂(Flyweight Factory)角色** :负责创建和管理享元角色。当客户对象请求一个享元 对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在 的话,则创建一个新的享元对象。



## 享元模式的特点



**优点**



+ 减少内存使用:由于享元模式共享对象,因此可以减少内存使用。



+ 提高性能:创建和销毁对象会占用大量的CPU时间和内存空间。因此,使用享元模式可以提高性能。



+ 代码简洁:享元模式可以使代码更简洁,因为该模式使用相同的对象来处理多个请求,而不需要创建大量的对象。



**缺点**



+ 共享对象可能会对线程安全造成问题。如果不正确地实现共享机制,则可能导致多个线程对同一对象进行更改,从而导致竞争条件。



+ 需要牺牲一定的时间和空间,来实现对象共享和控制机制。这意味着,当对象之间没有重复性时,使用享元模式可能会导致额外的开销。
  
+ 共享对象的改变可能会导致多个地方都发生改变,因此对共享对象的修改需要格外注意其带来的影响。





# 代理模式

> **暂时先代替主人完成工作,直到自己无法执行时,再让主人工作**



##  简介

​	代理模式属于结构型设计模式。为其他对象提供一种代理以控制对这个对象的访问。

​	在某些情况下,一个对象**不适合或者不能直接引用**另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。



## 代理模式代码示例

1. 共同的抽象接口



2. 代理类



3. 必要时执行真正工作的对象



一个场景,Boss 最近工作很忙,让秘书先帮他去处理一些简单的杂活,直到上台演讲的工作他才亲自出马。



```java
public interface ProxyTable {
    //做演讲的方法
    void doSpeaking();
    void doSomething();
}
public class Boss implements ProxyTable{
    @Override
    public void doSpeaking() {
        System.out.println("Boss亲自演讲");
    }

    @Override
    public void doSomething() {
        //啥也不做,交给秘书
    }
}
public class Secretary implements ProxyTable{
    private Boss boss;


    @Override
    public void doSpeaking() {
        //到要演讲了,再创建Boss,让Boss工作
        if(boss == null){
            boss = new Boss();
            boss.doSpeaking();
        }
    }
    public void doSomething(){
        System.out.println("秘书正在做杂货");
    }
}
public class Test {
    public static void main(String[] args) {
        Secretary secretary = new Secretary();
        secretary.doSomething();
        secretary.doSomething();
        secretary.doSpeaking();
    }
}

可以从测试代码中看出来,所有的动作都是代理去做的,但是doSpeaking底层是调用了Boss 的方法,而调用者无需在意其中的细节。

[外链图片转存中…(img-LGD1bcKk-1742013315470)]

代理模式的角色

  1. 抽象接口

    定义了基本的方法,要求代理和真正的对象都要实现该方法

  2. 代理对象

    对于对象不执行的方法,交给代理对象去执行。等到需要真正对象执行方法时再创建对象。

  3. 真正对象

    执行必须执行的方法的对象。平时的工作交给代理对象。

代理模式的特点

优点

  • 代理模式可以隐藏真实对象的实现细节,使客户端无需知晓真实对象的工作方式和结构。

  • 通过代理类来间接访问真实类,可以在不修改真实类的情况下,对其进行扩展、优化或添加安全措施。

  • 代理模式实现起来简单,易于扩展和维护,符合面向对象设计原则中的开闭原则。

缺点

  • 代理模式可能会引入额外的复杂性和间接性,增加程序设计和维护的难度。

  • 对象代理可能会降低系统性能,特别是在处理大数据量或频繁调用的情况下,因为代理需要额外的计算和网络通信开销。

命令模式

在真正方法执行和调用方法的对象之间,包裹一层命令

简介

命令模式,指的是将原本执行一个逻辑命令执行请求(比如一个方法调用的请求封装成类),使得发出请求,即调用方法的一方和执行请求的一方分离开。让两者通过命令对象来进行通信。

解耦了请求的发送者和接受者。

命令模式代码示例

抽象命令者

public abstract class AbstractCommander {
    public abstract void execute(); // 定义执行方法
}

具体命令者:其会去调用需要被执行方法的对象 receiver

public class ConcreteCommander extends AbstractCommander{
    private Receiver receiver;

    public ConcreteCommander(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.doThing();
    }
}

接收请求的对象

public class Receiver {
    public void doThing(){
        System.out.println("接受者执行事情");
    }
}

调用者

public class Invoker {
    private AbstractCommander commander;
    public void SetCommand(AbstractCommander commander){
        this.commander = commander;
    }
    public void doThing(){
        System.out.println("调用者执行命令");
        commander.execute();
    }
}

测试类:

public class Main {
    public static void main(String[] args) {
        Invoker invoker = new Invoker();
        AbstractCommander concreteCommander = new ConcreteCommander(new Receiver());
        invoker.SetCommand(concreteCommander);
        invoker.doThing();
    }
}

[外链图片转存中…(img-zXZxgu0L-1742013315470)]

命令模式出现的角色

  1. 抽象命令类(Command):命令的抽象类,类中对需要执行的命令进行声明,一般来说要对外公布一个execute方法用来执行命令。

  2. 具体命令类(Concrete Command):抽象命令类的具体实现子类,对抽象类中声明的方法进行实现。

  3. 调用类(Invoker):调用者,负责调用命令,执行命令功能的相关操作,是具体命令对象业务的真正实现者。

  4. 接受类(Receiver):接收者,负责接收命令并且执行命令。

命令模式的特点

  1. 将调用者和接收者分离开,它们之间的通信是通过命令来实现的。

  2. 命令模式将命令都封装起来,所以封装性好

  3. 一般如果拓展性好的同时会带来显著的缺点就是复杂,代码多,会有大量具体的命令类。命令模式同样的,如果命令很多的话,代码量就比较多。使用命令模式,不用管命令多简单,都需要写一个命令类来封装。

  4. 命令可以被撤销