跳过正文
  1. posts/

设计模式 - 应试笔记

·36285 字·73 分钟·
应试笔记 设计模式 面试
陈驰水
作者
陈驰水
感谢您看到这里,祝您生活愉快
目录
本博客为吉林大学设计模式本科课程复习笔记。内容参考了教案和 gonghr 学长的博客。

如果你在准备考试,可以看下面根据历年题总结出的重点排序。

吉林大学软件学院2022级考试几乎完美符合下面总结的重点。

1.重要度从高到低:

​ 组合 or 装饰

​ 状态 or 策略

​ 适配器 + 桥接(或其他)or 外观

​ 观察者 or 中介者

​ 抽象工厂 or 建造者(大概率抽象工厂)

​ 其他小点:原型,代理,单例,命令,职责链,迭代器

2.很少考察的内容:

​ 模板方法不单独考(大概率配合策略模式)

​ 简单工厂和工厂方法大概率被抽象工厂替代考察

​ 迭代器和命令难度较高(没考过)

3.多模式混用:

​ 适配器 + 其他(大概率桥接)

​ 模板方法 + 策略模式

​ 装饰 + 组合

​ 命令模式 + 其他(难度较高)

面向对象七原则
#

1. 开闭原则 (OCP)
#

定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

解释:需要对某个功能进行扩展时,不应该修改已有的代码,而应该通过增加新的代码。此外开闭原则较难实现,尽量接近满足开闭原则即可。

举例:如果要添加一个三角形的面积计算功能,应该通过扩展一个新的类(如 TriangleAreaCalculator),而不是直接修改现有的 Shape 类。

2. 里氏代换原则 ( LSP)
#

定义:子类对象必须能够替换掉父类对象,并且程序的行为不会发生变化。

解释:子类应该能够在任何使用父类的地方使用。即父类不应该有子类没实现的功能。

举例:如果我们有一个父类 Bird,其中有一个方法 fly(),那么如果我们创建了一个 Penguin 子类,它不应该继承 fly() 方法(因为企鹅不能飞)。

image-20241113201938564

3. 迪米特原则 (LoD)
#

定义:也称为“最少知识原则”,一个对象应该尽可能少地了解其他对象。不和陌生人说话,进而降低耦合。

解释:不希望陌生的类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类。

举例:假设有如下代码:a.getB().getC().method()。根据迪米特原则,这种深层次的链式调用是不推荐的。更好的方式是让 a 对象直接提供一个方法调用 method(),而不是通过 BC 对象间接调用。

image-20241027184101131

4. 单一职责原则 (Single Responsibility Principle, SRP)
#

定义:一个类应该只有一个引起变化的原因,或者说一个类应该只负责一件事。

举例:假设有一个类 UserManager,它同时负责用户的管理和日志记录。根据单一职责原则,应该将日志记录的功能提取到一个独立的 Logger 类中,这样 UserManager 只负责用户管理。

5. 接口隔离原则 (Interface Segregation Principle, ISP)
#

定义:客户端不应该被迫依赖它不使用的接口。

解释:不应该强迫子类实现其不需要的父类接口。

举例:假设有一个接口 Worker,其中包含 work()eat() 方法。如果 robot(机器人不需要吃饭)只需要 work() 方法而不需要 eat() 方法,那么应该将接口拆分为 WorkerEater 两个接口。

解决方法

image-20241027184254428

image-20241027184413835

6. 依赖倒置原则 (Dependency Inversion Principle, DIP)
#

定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。此处的依赖不是面向对象关系的依赖,而与语义上的依赖。

解释:这个原则提倡通过依赖接口或抽象类,而不是依赖具体实现类,来降低系统的耦合性和提高模块的可扩展性。高层模块不应该直接依赖于低层模块的具体实现,而应该依赖于抽象。

举例

image-20241027183407270

7. 组合 / 聚合复用原则 (Composition/Aggregation Reuse Principle, CARP)
#

定义:尽可能使用对象的组合/聚合,而不是通过继承来实现复用。

解释:组合和聚合相比继承更加灵活,它们通过将现有类作为新的类成员来实现功能复用,而不是通过继承来扩展类的行为。这样可以避免因为继承而导致的类之间的强耦合和层次结构的僵化。

举例:假设有一个类 Car,它需要具备 Engine 的功能,而不是通过继承 Engine 类来实现 Car 的动力系统功能,应该通过组合的方式,让 Car 包含一个 Engine 对象,这样可以更好地实现代码复用。

通过组合/聚合复用的优缺点 优点:

  1. 新对象存取成员对象的唯一方法是通过成员对象的接口;
  2. 这种复用是黑箱复用,因为成员对象的内部细节是新对象所看不见的;
  3. 这种复用更好地支持封装性;
  4. 这种复用实现上的相互依赖性比较小;
  5. 每一个新的类可以将焦点集中在一个任务上;
  6. 这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。
  7. 作为复用手段可以应用到几乎任何环境中去。 缺点: 就是系统中会有较多的对象需要管理。

通过继承来进行复用的优缺点 优点:

  1. 新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类。
  2. 修改和扩展继承而来的实现较为容易。 缺点:
  3. 继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱”复用。
  4. 如果基类发生改变,那么派生类的实现也不得不发生改变。
  5. 从基类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。

UML图
#

基础画法如下:

image-20241027174800832 image-20241027174804693

注意要区分实现和继承的核心是区分 interface 和 abstract 类。

设计模式定义
#

设计模式的基本要素:

•模式名称 (Pattern name)

•问题 (Problem)

•解决方案 (Solution)

•效果 (Consequences)

三大类模式
#

  1. 创建型模式主要用于创建对象。

  2. 结构型模式主要用于处理类或对象的组合。

  3. 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责。

类模式和对象模式
#

  1. 类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是属于静态的。

    静态:你可以通过 Factory.create() 直接调用该方法,而不需要创建 Factory 的实例

     // 静态工厂方法,通过传入的参数决定创建哪种对象
     public static Animal createAnimal(String type) {
         if ("dog".equalsIgnoreCase(type)) {
             return new Dog();
         } else if ("cat".equalsIgnoreCase(type)) {
             return new Cat();
         }
         return null;
     }
    
  2. 对象模式处理对象间的关系,这些关系在运行时刻变化,更具动态性。

  3. 类模式:主要解决类之间的继承和实现问题,通过继承和接口实现来复用和扩展行为。(extendsimplements

    对象模式:主要处理对象之间的组合、依赖、聚合等关系,通过组合对象来灵活地扩展功能和行为。(依赖、关联、组合、聚合)

GoF设计模式(分类表格)
#

image-20241029165236067

创建型模式
#

创建型模式特点:
#

  1. 客户不知道对象的具体类是什么

    • 客户端代码不需要知道具体创建对象的类是什么,它只依赖于抽象类或接口。这种方式使得客户端代码与具体的实现类解耦,提升了代码的灵活性和可维护性。
    • 好处:如果对象的具体实现发生变化,客户端代码不必修改,因为它依赖的是抽象接口,而不是具体类。
  2. 隐藏了对象实例是如何被创建和组织的

    • 创建型模式将对象创建的逻辑封装在类或方法内部,客户端无需关心具体的创建细节。可以通过工厂方法、建造者模式等方式动态地创建不同类型的对象。
  • 好处:对象的创建逻辑可以根据需求动态变化,并且可以轻松替换或扩展对象的创建方式,而不影响客户端代码。
  1. 使用场景:

    当你在代码中频繁使用 new 运算符来创建对象时,可以考虑使用创建型模式,将对象的创建逻辑抽象出来,从而提升代码的灵活性、可扩展性和可维护性。

简单工厂模式(静态工厂方法)
#

实现简单工厂模式的难点就在于 “如何选择”实现

1. 优点:
#

  • 职责分离:工厂类负责产品对象的创建,客户端只需消费产品,无需关心创建细节。
  • 降低耦合:客户端不需要知道具体产品类名,只需传递参数即可,减少了代码耦合。
  • 提高灵活性:通过配置或参数化创建,可以动态更换或增加产品类,而无需修改客户端代码。
  • 简化客户端代码:创建逻辑集中在工厂类中,简化了客户端的实现。

2. 缺点:
#

  • 单一故障点:工厂类集中所有创建逻辑,若出错会影响整个系统。
  • 增加复杂度:增加了系统中的类,特别是当产品较少时,工厂类显得冗余。
  • 扩展性差:每次新增产品都需修改工厂类,违背“开闭原则”,产品多时工厂逻辑复杂化。
  • 无法继承:使用静态方法创建产品,限制了工厂类形成继承结构,降低了灵活性。

3. 适用场景:
#

  • 对象创建较少,逻辑简单。
  • 客户端不关心创建细节,只需提供参数即可。

image-20241029171042563

image-20241103161902355

完整代码示例:

// 注意静态体现在 public static
// 简单工厂模式和工厂方式模式都是类模式,是静态的
public class TVFactory
{
	public static TV produceTV(String brand) throws Exception
	{
		if(brand.equalsIgnoreCase("Haier"))
		{
			System.out.println("电视机工厂生产海尔电视机!");
			return new HaierTV();
		}
		else if(brand.equalsIgnoreCase("Hisense"))
		{
			System.out.println("电视机工厂生产海信电视机!");
			return new HisenseTV();
		}
		else
		{
			throw new Exception("对不起,暂不能生产该品牌电视机!");
		}
	}
}

public class HaierTV implements TV
{
	public void play()
	{
		System.out.println("海尔电视机播放中......");
	}
}

public class HisenseTV implements TV
{
	public void play()
	{
		System.out.println("海信电视机播放中......");
	}	
}

public interface TV
{
	public void play();
}

// 在另一个样例中,product 可以用 abstract 类,使用实线空箭头
public abstract class User
{
	public void sameOperation()
	{
        System.out.println("修改个人资料!");
	}
	
	public abstract void diffOperation();
}

工厂方法模式(Factory Method)
#

工厂方法模式的本质是:延迟到子类来选择实现

工厂方法模式很好的体现了**“依赖倒置原则”“开闭原则”**。

工厂可以是抽象类,也可以是具体类

image-20241103171344324

代码示例:

// 最上面和最下面的两个接口
public interface TV {
	public void play();
}
public interface TVFactory {
    public TV produceTV();
}

// 具体工厂
public class HisenseTVFactory implements TVFactory {
    public TV produceTV() {
    	System.out.println("海信电视机工厂生产海信电视机。");
    	return new HisenseTV();
    }
}

// 具体产品
public class HisenseTV implements TV {
	public void play() {
		System.out.println("海信电视机播放中......");
	}	
}

抽象工厂模式(Abstract Factory)
#

一个具体工厂可以 creat 多个具体产品,就是抽象工厂模式。

抽象工厂模式的本质是:选择产品簇的实现

1. 模式优点:
#

  • 隔离具体类的生成:客户端无需知道具体的产品类名,只需要通过抽象工厂接口创建产品,降低了系统的耦合性。
  • 易于更换具体工厂:通过更换具体工厂的实例,可以轻松改变整个系统的行为,增强系统的灵活性。
  • 保证产品族一致性:确保客户端始终使用同一产品族中的对象,避免了产品混用带来的兼容性问题。
  • 符合开闭原则:增加新的具体工厂和产品族无需修改已有系统,扩展方便,符合开闭原则(对扩展开放,对修改关闭)。
  • 高内聚低耦合:通过抽象工厂隔离对象创建,提高了系统的内聚性,降低了耦合性,有助于系统的维护和扩展。

2. 模式缺点:
#

  • 扩展困难:抽象工厂规定了所有可能创建的产品集合,增加新的产品种类(产品等级结构)时,需要修改工厂接口和所有子类,扩展较为困难。
  • 开闭原则的倾斜性:虽然增加新的工厂和产品族很方便,但增加新的产品等级结构(新种类产品)较麻烦,需要修改现有的工厂类,违反了开闭原则。

3. 适用环境:
#

  • 系统不应依赖产品实例的创建细节:适用于不关心产品类如何被创建、组合和表达的场景。
  • 存在多个产品族:系统中有多于一个产品族,每次只使用其中一个产品族中的相关对象。
  • 产品族的一致使用:同一产品族中的产品需要一起工作,系统设计时需保证这一约束。
  • 提供统一接口的产品库:系统中所有产品通过同一个接口出现,客户端无需依赖具体的实现类。

image-20241103173243847

只有具体工厂代码有区别:

// 不过是一个工厂可以创造多个产品
public class HaierFactory implements EFactory
{
	public Television produceTelevision()
	{
		return new HaierTelevision();
	}
	
	public AirConditioner produceAirConditioner()
	{
        // 这个是空调
		return new HairAirConditioner();
	}
}

建造者模式(Builder)
#

核心是“组装”,一步一步创建一个复杂的对象,client 和最后的成品交互。

Builder(抽象建造者): 提供一个用于创建产品各部件的抽象类,一般不是接口。

ConcreteBuilder(具体建造者): 实现Builder接口,负责具体产品部件的构造和装配。

Director(指挥者): 负责指挥建造的流程,安排复杂对象的建造顺序。通过调用建造者的部件构造和装配方法,完成产品的构建过程。

Product(产品角色): 是最终被构建的复杂对象,通常由多个组成部件构成。

1. 优点:
#

  • 封装性好:客户端无需知道产品内部细节
  • 解耦性强:产品与创建过程分离
  • 扩展性好:易于添加新的建造者,符合开闭原则
  • 精细控制:创建步骤清晰可控

2. 缺点:(建造者类复杂)
#

  • 适用范围受限:仅适用于相似产品的创建
  • 类数量增加:产品变化复杂时需要更多建造者类

3. 适用场景:
#

  • 产品结构复杂:包含多个组成部分
  • 需要控制生成顺序:产品属性间存在依赖关系
  • 创建过程独立:通过指挥者类统一管理创建过程
  • 复用创建过程:相同流程可创建不同产品

建造者模式与抽象工厂模式的关键区别:

  1. 产品维度不同:
  • 建造者:返回单个完整产品
  • 抽象工厂:返回一系列相关产品(产品族)
  1. 构建过程不同:
  • 建造者:强调步骤化构建过程,通过指挥者协调
  • 抽象工厂:直接通过工厂方法获取产品
  1. 形象比喻:
  • 建造者:像汽车组装厂,组装完整汽车
  • 抽象工厂:像零配件厂,生产相关配件

下图应该为实线继承,因为 builder 是抽象类

image-20241103175924550

image-20241105145106080

// 客户端类
public class Client {
    public static void main(String args[]) {
        // 动态确定套餐种类,使用 XML 配置读取具体的建造者类
        MealBuilder mb = (MealBuilder) XMLUtil.getBean();
        KFCWaiter waiter = new KFCWaiter();   // 服务员是指挥者,负责构建套餐
        waiter.setMealBuilder(mb);        // 服务员设置当前的套餐建造者
        Meal meal = waiter.construct();	 // 用 director 中的 construct 构造 Product
        
        System.out.println(meal.getFood());
        System.out.println(meal.getDrink());
    }
}

// 指挥者类
public class KFCWaiter {
    private MealBuilder mb;
    public void setMealBuilder(MealBuilder mb) {
        this.mb = mb;
    }
    // 构建套餐并返回
    public Meal construct() {
        mb.buildFood();  // 构建主食
        mb.buildDrink(); // 构建饮料
        return mb.getMeal();  // 返回最终构建的套餐
    }
}

// 产品类,表示套餐
public class Meal {
    // 套餐的部件:食物和饮料
    private String food;
    private String drink;
    public void setFood(String food) {
        this.food = food; 
    }
    public void setDrink(String drink) {
        this.drink = drink; 
    }
    public String getFood() {
        return this.food; 
    }
    public String getDrink() {
        return this.drink; 
    }
}

// 抽象建造者类,定义构建流程的接口
public abstract class MealBuilder {
    // 需要构建的套餐对象,因此其和 Meal 是组合关系
    protected Meal meal = new Meal();
    public abstract void buildFood();
    public abstract void buildDrink();
    // 返回构建好的套餐
    public Meal getMeal() {
        return meal;
    }
}

// 具体建造者A,实现套餐A的构建
public class SubMealBuilderA extends MealBuilder {
    public void buildFood() {
        meal.setFood("一个鸡腿堡");
    }
    public void buildDrink() {
        meal.setDrink("一杯可乐");
    }
}

// 具体建造者B,实现套餐B的构建
public class SubMealBuilderB extends MealBuilder {
    public void buildFood() {
        meal.setFood("一个鸡肉卷");
    }
    public void buildDrink() {
        meal.setDrink("一杯果汁"); 
    }
}

原型模式(Prototype)
#

模式动机与定义
#

  • 原型模式通过复制对象自身来创建多个相同对象,适用于复杂且频繁创建的场景,节省资源。
  • 不需关心对象类型,直接复制原型对象。符合里氏替换原则,即子类能实现父类所有的功能。
  • 原型模式:通过原型实例指定对象种类,并复制原型创建新对象,无需了解创建细节。

优点 缺点 适用场景
#

优点:

  • 快速创建对象:能够通过克隆快速创建大量相同或相似的对象,简化了对象的创建过程。
  • 保存对象状态:可以保存对象的中间状态,并基于该状态创建新的对象。

缺点:

  • 需要改造现有类:每个类都需要实现克隆方法,改造现有类时可能需要修改源代码,不满足开闭原则
  • 深克隆复杂:如下代码,确实复杂。

适用场景:

  1. 创建成本较大:当创建新对象的成本较大时,通过克隆现有对象来创建新对象可以节省开销。
  2. 对象状态变化小:系统需要保存对象的状态,且状态变化较小时,可以使用相似对象的复制
  3. 避免复杂的工厂模式:当不希望使用分层次的工厂类来创建对象时,原型模式可以通过复制原型对象来简化对象的创建过程。

分成两种:浅拷贝、深拷贝的区别如下下图所示

核心区别是实现的接口不一样,浅拷贝是Cloneable,深拷贝是Serializable,且深拷贝组合的类也需要实现Serializable

image-20241105165728736

image-20241105200028945

浅拷贝
#

image-20241105183209839

演示代码中还有一个附件类,在 mail(ConcretePrototype) 中组合,用于解释深浅拷贝。

上图的 object 和 Cloneable 二选一即可。一个是自己写的,一个是 java 带的。

// 附件类
public class Attachment {
    // 模拟下载附件的方法
    public void download() {
        System.out.println("下载附件");    
    }
}

// 客户端类,测试克隆功能
public class Client {
    public static void main(String a[]) {
        Email email, copyEmail;
        email = new Email();
        // 克隆原始邮件
        copyEmail = (Email) email.clone()
        // 对象是深拷贝
        System.out.println("email==copyEmail?");
        System.out.println(email == copyEmail); //false
        
        // 附件是同一个对象的浅拷贝
        System.out.println("email.getAttachment==copyEmail.getAttachment?"); 
        System.out.println(email.getAttachment() == copyEmail.getAttachment());//true
    }
}

// 邮件类,实现了 Cloneable 接口,支持克隆
public class Email implements Cloneable {
    // 克隆方法,返回当前对象的浅拷贝
    public Object clone() {
        Email clone = null;
        try {
            // 调用父类的 clone() 方法进行浅拷贝
            clone = (Email) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println("Clone failure!"); // 如果克隆失败,输出错误信息
        }
        return clone;
    }
    
    // 获取附件的方法
    public Attachment getAttachment() {
        return this.attachment;
    }
    
    // 显示邮件内容的方法
    public void display() {
        System.out.println("查看邮件");    
    }
}

深拷贝
#

image-20241105170837384

import java.io.*;

// 附件类,需要实现 Serializable
public class Attachment implements Serializable {
    public void download() {
        System.out.println("下载附件");
    }
}

// 客户端类
public class Client {
    public static void main(String a[]) {
        Email email, copyEmail = null;
        email = new Email();
        try {
            // 深克隆 email 对象,区别就在 deep
            copyEmail = (Email) email.deepClone();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 对象不是一个对象
        System.out.println("email==copyEmail?");
        System.out.println(email == copyEmail); // false,表示是不同的对象

        // 附件对象不是一个附件对象
        System.out.println("email.getAttachment==copyEmail.getAttachment?");
        System.out.println(email.getAttachment() == copyEmail.getAttachment());
    }
}

// 继承的是
public class Email implements Serializable {
    private Attachment attachment = null;
    public Email() {
        this.attachment = new Attachment();
    }

    // 深克隆实现方法
    public Object deepClone() throws IOException, ClassNotFoundException {
        // 将对象写入字节数组输出流
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this); // 序列化当前对象到流中

        // 从字节数组输入流中读取对象
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject(); // 反序列化生成新的对象
    }

    // 获取附件的方法
    public Attachment getAttachment() {
        return this.attachment;
    }

    // 显示邮件内容的方法
    public void display() {
        System.out.println("查看邮件");
    }
}

带原型管理器的原型模式
#

一个维护原型对象集合的专用工厂,通过复制集合中的原型对象来提供克隆服务,并基于抽象原型类实现以支持扩展。本质上 Prototypeable 就是个哈希表

image-20241105203351883

import java.util.*;

// 定义颜色接口,继承Cloneable接口以支持克隆
interface MyColor extends Cloneable {
    public Object clone();    // 克隆方法
    public void display();    // 显示颜色
}

// 实现红色类
class Red implements MyColor {
    // 实现克隆方法
    public Object clone() {
        Red r = null;
        try {
            r = (Red)super.clone();  // 调用Object的clone()方法
        }
        catch(CloneNotSupportedException e) {
        }
        return r;
    }
    
    public void display() {
        System.out.println("This is Red!");
    }
}

// 实现蓝色类,结构同Red类
class Blue implements MyColor {
    public Object clone() {
        Blue b = null;
        try {
            b = (Blue)super.clone();
        }
        catch(CloneNotSupportedException e) {
        }
        return b;
    }
    
    public void display() {
        System.out.println("This is Blue!");
    }
}

// 原型管理器类
class PrototypeManager {
    private Hashtable ht = new Hashtable();    // 存储原型对象的哈希表
    
    // 初始化,加入默认原型
    public PrototypeManager() {
        ht.put("red", new Red());
        ht.put("blue", new Blue());
    }
    
    // 添加新原型
    public void addColor(String key, MyColor obj) {
        ht.put(key, obj);
    }
    
    // 获取原型克隆
    public MyColor getColor(String key) {
        return (MyColor)((MyColor)ht.get(key)).clone();
    }
}

// 客户端测试类
class Client {
    public static void main(String args[]) {
        PrototypeManager pm = new PrototypeManager();
        
        // 获取两个红色对象并显示
        MyColor obj1 = (MyColor)pm.getColor("red");
        obj1.display();
        
        MyColor obj2 = (MyColor)pm.getColor("red");
        obj2.display();
        
        // 判断两个对象是否相同
        System.out.println(obj1 == obj2);  // 输出false,因为是克隆对象
    }
}

相似对象的复制
#

其实就是多个对象类似,一两个值不一样,先复制再改值

public static void main(String args[]) {
    // 创建原型对象
    stu1 = new Student("张无忌","男",24,"软件工程","软件学院","中南大学");
    
    // 克隆两个新对象并修改部分属性
    stu2 = stu1.clone();  // 克隆stu1
    stu2.setStuName("杨过");  // 只修改姓名
    
    stu3 = stu1.clone();  // 再次克隆stu1
    stu3.setStuName("小龙女");  // 修改姓名和性别
    stu3.setStuSex("女");
}

单例模式(Singleton)
#

三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例

image-20241106122327077

// 下面为懒汉式,第一次调用才创建对象
// 需要处理好多线程问题
public class Singleton {
    // 静态变量,保存类的唯一实例
    private static Singleton instance = null;
    // 私有构造函数,防止外部实例化
    private Singleton() {    
    }
    // 静态方法,返回类的唯一实例
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

// 下面是饿汉式,在类加载时,便将单例实例创建好
// 该方式的实现是线程安全的,但稍微浪费资源(实例可能用不到)
public class HungrySingleton {
    // 在类加载时,立即初始化实例
    private static final HungrySingleton instance = new HungrySingleton();
    // 私有构造函数,防止外部实例化
    private HungrySingleton() {}
    // 提供静态方法获取唯一实例
    public static HungrySingleton getInstance() {
        return instance;
    }
}

// 哈希表多例模式,不指定数量,而是每个 key 对应一个多例
public class Multiton {
    // 存储不同实例的Map,按标识符存储
    private static Map<String, Multiton> instances = new HashMap<>();
    // 私有构造函数,防止外部实例化
    private Multiton() {
    }
    // 获取多例实例的方法,根据标识符返回不同的实例
    public static Multiton getInstance(String key) {
        if (!instances.containsKey(key)) {
            instances.put(key, new Multiton());  // 如果不存在,则创建并保存实例
        }
        return instances.get(key);  // 返回已有的实例
    }
}

// 三例模式
public class Multiton {
    // 定义固定数量的实例(如三例)
    private static final int INSTANCE_COUNT = 3;
    // 用于存储三个实例的数组
    private static final Multiton[] instances = new Multiton[INSTANCE_COUNT];
    // 使用一个计数器来轮流分配实例
    private static int currentInstanceIndex = 0;
    // 私有构造函数,防止外部通过 new 创建对象
    private Multiton() {
    }
    // 获取下一个实例的方法
    public static synchronized Multiton getInstance() {
        // 如果实例未被创建,先创建实例
        if (instances[currentInstanceIndex] == null) {
            instances[currentInstanceIndex] = new Multiton();
        }
        // 获取当前实例
        Multiton instance = instances[currentInstanceIndex];
        // 循环更新索引,以便下次调用时返回下一个实例
        currentInstanceIndex = (currentInstanceIndex + 1) % INSTANCE_COUNT;
        return instance;
    }
    
    // 示例方法,模拟实例执行的操作
    public void doSomething() {
        System.out.println("实例 " + this + " 正在执行操作");
    }
}

结构型模式
#

适配器模式
#

转换匹配,复用功能。将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类 可以一起工作。

**类适配器模式:**通过继承来实现适配,能更灵活地置换方法,但受限于多重继承和目标类的局限。 **对象适配器模式:**通过组合来实现适配,能够适配多个类,但置换方法较为复杂。

适合场景:

  1. 系统需要使用现有类,而这些类的接口不符合需求。 适配器模式适用于当系统中需要使用现有类,但它们的接口与系统不兼容的情况。
  2. 希望创建一个可复用的适配器类,用于与多个不相关的类(适配者类和目标类)一起工作。 包括将来可能引入的适配者类的子类也能一起适配。

类适配器
#

image-20241106150822925

// 被适配者类 (旧的接口)
class Adaptee {
    public void specificRequest() {
        System.out.println("Adaptee: Handling specific request");
    }
}

// 目标接口 (客户端期望的接口)
interface Target {
    void request();
}

// 适配器类 (实现了Target接口,适配Adaptee)
public class Adapter extends Adaptee implements Target {
    public void request() {
        specificRequest(); // 把客户端的request方法适配为Adaptee的specificRequest
    }
}

// 客户端类
public class Client {
    public static void main(String[] args) {
        Target target = new Adapter();   // 客户端通过Target接口使用Adapter
        target.request();  // 实际上调用的是Adaptee的specificRequest方法
    }
}

优点

  1. 灵活性更强 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法

缺点

  1. 多重继承限制,对于不支持多重继承的语言(如 Java、C#),类适配器模式一次最多只能适配一个适配者类
  2. 目标类限制,目标抽象类只能是抽象类,不能是具体类,使用上有一定的局限性。
  3. 无法适配子类,不能将一个适配者类及其子类都适配到目标接口。

对象适配器
#

image-20241106215247564

// 目标接口 (客户端期望的接口)
public class Target {
    public void request() {
        System.out.println("Target: Handling request");
    }
}

// 被适配者类 (旧的接口)
public class Adaptee {
    public void specificRequest() {
        System.out.println("Adaptee: Handling specific request");
    }
}

// 适配器类 (通过组合方式实现适配)
public class Adapter extends Target {
    private Adaptee adaptee;  // 适配器持有被适配者的引用

    // 构造函数,传入被适配者的实例
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }
    // 实现Target的request方法,实际上调用Adaptee的specificRequest方法
    @Override
    public void request() {
        adaptee.specificRequest();
    }
}

// 客户端类,和类对象一致
public class Client {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee();
        Target target = new Adapter(adaptee);
        target.request();  // 实际上调用的是Adaptee的specificRequest方法
    }
}

优点

  1. 适配多个类,一个对象适配器可以将多个不同的适配者适配到同一个目标,即同一个适配器可以将适配者类及其子类都适配到目标接口。

缺点

  1. 较难置换方法 与类适配器相比,对象适配器要想置换适配者类的方法较为不便。如果需要置换适配者类的一个或多个方法,必须先创建适配者类的子类,在子类中置换方法,再将该子类作为真正的适配者进行适配。

对象适配器还可能如下形式:

其中 DataOperator 是 target,而两个 adapter 按情况分配适配接口。

image-20241107175723722

模型扩展
#

默认适配器模式

image-20241107181805562

双向适配器

image-20241107181615441

智能适配器

适配多个适配者

桥接模式
#

桥接模式的本质:分离抽象和实现

需要将需求转化为两个维度,固有维度(比如毛笔尺寸)做抽象类,另一个维度(比如毛笔颜色)做接口实现类。

image-20241107182404782

image-20241107194428851

// 实现部分:Color接口
public interface Color {
    void bepaint(String penType, String name);
}

public class Red implements Color {
    @Override
    public void bepaint(String penType, String name) {
        System.out.println(penType + " Red color " + name + ".");
    }
}

public class Green implements Color {
    @Override
    public void bepaint(String penType, String name) {
        System.out.println(penType + " Green color " + name + ".");
    }
}

// 抽象部分:Pen类
public abstract class Pen {
    protected Color color;
    // 设置颜色
    public void setColor(Color color) {
        this.color = color;
    }
    // 抽象绘画方法
    public abstract void draw(String name);
}

public class SmallPen extends Pen {
    @Override
    public void draw(String name) {
        color.bepaint("SmallPen", name);
    }
}

public class BigPen extends Pen {
    @Override
    public void draw(String name) {
        color.bepaint("BigPen", name);
    }
}

// 测试类
public class Client {
    public static void main(String[] args) {
        // 创建一种颜色(红色)
        Color red = new Red();
        // 创建一种笔(大号笔),并设置颜色
        Pen bigPen = new BigPen();
        bigPen.setColor(red);
        bigPen.draw("Flower");
        // 创建另一种颜色(绿色)
        Color green = new Green();
        // 使用小号笔,并设置为绿色
        Pen smallPen = new SmallPen();
        smallPen.setColor(green);
        smallPen.draw("Tree");
    }
}

模式优点:

  1. 桥接模式通过对象间的关联解耦了抽象与实现。符合组合/聚合复用原则
  2. 减少子类数量:桥接模式可以替代多重/多层继承,避免违背单一职责原则
  3. 提高可扩展性:在任意维度上扩展时,不需修改原有系统,符合开闭原则,系统更具扩展性。

模式缺点:

  1. 增加设计难度:桥接模式要求开发者从一开始就设计抽象层。
  2. 需要正确识别系统中的两个独立变化的维度,这需要一定的经验积累,否则使用范围有限。

模式适用环境:

  1. 灵活性需求:当系统需要在抽象化具体化之间增加更多灵活性,避免静态继承关系时,桥接模式可以通过关联关系实现动态耦合。
  2. 独立扩展:当一个类有两个或多个独立变化的维度,并且这些维度需要独立扩展时,桥接模式尤为适用。
  3. 避免多重继承:对于不希望使用多重/多层继承,或者因继承导致类急剧增加的系统,桥接模式是更好的选择。用聚合代替多重继承。

适配器模式与桥接模式的联用
#

image-20241107201111348

// 抽象部分:报表显示
abstract class ReportDisplay {
    protected DataCollector dataCollector;

    public ReportDisplay(DataCollector dataCollector) {
        this.dataCollector = dataCollector;
    }

    public abstract void display();
}

// 具体实现:报表显示方式1
class ReportDisplayStyle1 extends ReportDisplay {
    public ReportDisplayStyle1(DataCollector dataCollector) {
        super(dataCollector);
    }

    @Override
    public void display() {
        System.out.println("Displaying report in style 1:");
        dataCollector.collectData();
    }
}

// 实现部分:数据采集接口
interface DataCollector {
    void collectData();
}

// 适配器类:读取Excel文件的适配器,适配Excel API
class ExcelFileCollector implements DataCollector {
    private ExcelAPI excelAPI;

    public ExcelFileCollector(ExcelAPI excelAPI) {
        this.excelAPI = excelAPI;
    }

    @Override
    public void collectData() {
        excelAPI.readExcel();
    }
}

// Excel API 模拟类
class ExcelAPI {
    public void readExcel() {
        System.out.println("Collecting data from Excel file using Excel API.");
    }
}

组合模式
#

组合模式的本质:**统一叶子对象和组合对象。**处理了树状结构。

image-20241107202918860

image-20241107204657214

// 抽象类 MyElement,定义 eat() 抽象方法
public abstract class MyElement {
    public abstract void eat();
}

// Plate 类,包含 MyElement 对象的集合,并实现 eat() 方法
public class Plate extends MyElement {
    private List<MyElement> list = new ArrayList<>(); // 注意是私有列表
    public void add(MyElement element) { // 组合模式标准的添加元素
        list.add(element);
    }
    public void delete(MyElement element) {    // 删除元素
        list.remove(element);
    }
    // 实现 eat() 方法,调用每个元素的 eat()
    // 可以实现 eat() 盘子中的所有水果
    @Override
    public void eat() {
        for (MyElement element : list) {
            element.eat();
        }
    }
}

// Apple 类,具体实现 eat() 方法
public class Apple extends MyElement {
    @Override
    public void eat() {
        System.out.println("Eating an apple!");
    }
}
// Pear 类,具体实现 eat() 方法
public class Pear extends MyElement {
    @Override
    public void eat() {
        System.out.println("Eating a pear!");
    }
}

// 测试类 Client
public class Client {
    public static void main(String[] args) {
        // 创建 MyElement 对象
        MyElement obj1, obj2, obj3, obj4, obj5;
        Plate plate1, plate2, plate3;
        // 创建水果对象
        obj1 = new Apple();
        obj2 = new Pear();
        // 创建盘子 plate1 并添加水果
        plate1 = new Plate();
        plate1.add(obj1);
        plate1.add(obj2);
        // 创建更多水果对象
        obj3 = new Banana();
        obj4 = new Banana();
        // 创建盘子 plate2 并添加水果
        plate2 = new Plate();
        plate2.add(obj3);
        plate2.add(obj4);
        // 创建 plate3,它可以包含其他盘子和水果
        obj5 = new Apple();
        plate3 = new Plate();
        plate3.add(plate1);
        plate3.add(plate2);
        plate3.add(obj5);
        // 调用 eat() 方法,演示组合模式的效果
        System.out.println("Plate 1 contents:");
        plate1.eat();  // Plate 1 吃掉其包含的水果
        System.out.println("\nPlate 3 contents:");
        plate3.eat();  // Plate 3 吃掉 plate1, plate2 和一个 Apple
    }
}

优点

  1. 统一对象操作:叶子对象与组合对象共享统一的接口,简化了客户端代码,无需区分具体类型。
  2. 递归结构:支持将对象组合成更复杂的结构,可以不断递归组合,形成整体-部分层次结构。
  3. 易于扩展:新增叶子或组合对象时,客户端无需修改,新的子类能够无缝集成。

缺点

  1. 类型限制难:很难在编译时限制组合中只能包含特定类型的组件,必须在运行时动态检查。

适用场景

  1. 表示整体-部分层次结构。
  2. 需要统一操作组合结构中的所有对象。

模型扩展
#

透明组合模式:在抽象类中包含了管理成员对象的方法(如包括add()、remove()以及getChild()等),使客户端可以平等的看待所有的对象,缺点是不够安全

image-20241107210849510

安全组合模式:与透明组合相反,在抽象组件中没有声明任何用于管理成员对象的方法(水果篮例子就是如此),缺点是不够透明

image-20241107211039696

更复杂的组合模式:叶子和 Composite 都可以继续继承,如下图:

image-20241107211340954

装饰模式
#

装饰模式的本质:**动态组合。**动态是手段,组合才是目的。

形态上很像组合模式,在 Composite 中继续继承两个子类。

image-20241107211854322

变形金刚实例如下:

image-20241107212929330

// 定义基础接口 Transform
public interface Transform {
    public void move();
}

// Car 类,实现 Transform 接口,表示一辆车
public final class Car implements Transform {
    public Car() {
        System.out.println("变形金刚是一辆车!");
    }
    @Override
    public void move() {
        System.out.println("在陆地上移动!");
    }
}

// Changer 类,装饰器基类,持有一个 Transform 对象
public class Changer implements Transform {
    private Transform transform; // 私有变量
    public Changer(Transform transform) { // 通过构造函数改变 transform
        this.transform = transform;
    }
    @Override
    public void move() {
        transform.move();
    }
}

// Robot 类,继承 Changer,扩展了说话功能
public class Robot extends Changer {
    public Robot(Transform transform) {
        super(transform); // 调用父类的构造函数,改变父类的 transform,即改变其 move 函数
        System.out.println("变成机器人!");
    }

    public void say() {
        System.out.println("说话!");
    }
}

// Client 测试类
public class Client {
    public static void main(String[] args) {
        // 创建一个基础的 Car 对象
        Transform camaro = new Car();
        camaro.move(); // 在陆地移动
        System.out.println("-----------------------------");

        // 使用 Robot 装饰 Car 对象,添加说话功能
        Robot optimus = new Robot(camaro);
        optimus.move(); //还是 Car 的移动功能
        optimus.say(); // Robot 的说话功能
    }
}

多层装饰
#

当 decorator 有多个子类时,可以进行多重装饰。但装饰到最里面一定是某一个 ConcreteComponent。

历年题中的例子如下:

image-20241113171133367


public class Client {
    public static void main(String[] args) {
        Person p1, p2;
        p1 = new Person("张三", 5000, 1000, 10000);
        p2 = new Person("李四", 10000, 2000, 20000);
        Factory.addPerson("张三", p1);
        Factory.addPerson("李四", p2);
        Component e1, m1, personBusiness, personPayback, teamBusiness;
        String name;

        e1 = new Employee();
        name = p1.getName();
        personBusiness = new PersonBusiness(e1); // 张三是员工,装饰了一层 PersonPayback
        personPayback = new PersonPayback(personBusiness) // 再装饰一层 PersonPayback
        System.out.print(name);
        System.out.println(personPayback.getDes(name));
        System.out.println(personPayback.getBonus(name));

        m1 = new Manager();
        name = p2.getName();
        personBusiness = new PersonBusiness(m1); // 李四是员工,装饰一层 PersonPayback
        personPayback = new PersonPayback(personBusiness); // 再装饰一层
        teamBusiness = new TeamBusiness(personPayback);
        System.out.print(name);
        System.out.println(teamBusiness.getDes(name));
        System.out.println(teamBusiness.getBonus(name));
    }
}
/*
张三普通员工获得个人业务奖金获得个人回款奖金
151.0
李四经理获得个人业务奖金获得个人回款奖金获得团队业务奖金
502.0
*/

模式优点

  1. 灵活性高:相比继承,装饰模式更灵活。继承是静态的,而装饰模式通过将功能分离到不同的装饰器中,可以在运行时动态组合功能,决定最终的行为。
  2. 易于复用:装饰器将每个功能独立实现,简化了功能的复用。多个装饰器可以叠加在同一对象上,或者同一个装饰器可以用于不同对象,提升了功能的复用性。
  3. 简化高层定义:高层定义只需要定义基本功能,通过组合装饰器来动态增加功能,避免在高层设计时考虑所有功能需求。

模式缺点

  1. 对象数量增加:每个装饰器负责不同功能,可能会导致大量细粒度对象的产生,增加系统资源消耗,影响性能。

  2. 调试复杂:由于装饰模式涉及多次功能叠加,调试可能需要逐级排查装饰链。

适用环境

  • 需要动态、透明地为单个对象添加职责,且不影响其他对象。
  • 无法通过继承扩展系统,或继承不利于系统维护时,适合使用装饰模式。特别是当扩展过多或类被定义为不可继承(如 Java 中的 final 类)时,装饰模式是更好的选择。

模式扩展
#

半透明装饰模式:上面变形金刚的例子就是半透明,允许 client 调用具体的实现类(例子中是robot)。好处是可以添加新的功能,比如例子中的说话。

透明装饰模式:所有的功能都在接口或抽象类中表现,client 只调用最高层的接口或抽象类,导致其不能添加新功能,只能在原有的功能上修改。在多重加密系统中使用。

装饰模式简化:删去了最高层的接口或抽象类,转而用具体组件类来代替,要求具体组件功能实现较少:

image-20241108121639403

外观模式
#

外观模式的本质:封装交互,简化调用。

外观模式是迪米特法则的具体体现。

image-20241108122058498

image-20241108122117414

// Fan 类,表示风扇
public class Fan {
    public void on() {
        System.out.println("风扇打开!");
    }
    public void off() {
        System.out.println("风扇关闭!");
    }
}
// Light 类,表示灯光
public class Light {
    private String position;
    public Light(String position) {
        this.position = position;
    }
    public void on() {
        System.out.println(this.position + "灯打开!");
    }
    public void off() {
        System.out.println(this.position + "灯关闭!");
    }
}

// GeneralSwitchFacade 类,作为开关的外观模式
public class GeneralSwitchFacade {
    private Light[] lights = new Light[4]; // 用私有变量关联具体类
    private Fan fan;
    private AirConditioner ac;
    private Television tv;
    
    public GeneralSwitchFacade() { // 组合成一个开关
        lights[0] = new Light("左前");
		...
        fan = new Fan();
        ac = new AirConditioner();
        tv = new Television();
    }
    // 打开所有设备
    public void on() {
        for (Light light : lights) {
            light.on();
        }
        fan.on()...
    }
    // 关闭所有设备
    public void off() {
        for (Light light : lights) {
            light.off();
        }
        fan.off(); ...
    }
}

// Client 测试类
public class Client {
    public static void main(String[] args) {
        GeneralSwitchFacade gsf = new GeneralSwitchFacade();
        gsf.on(); // 打开所有设备
        System.out.println("-----------------------");
        gsf.off(); // 关闭所有设备
    }
}

模式优点

  1. 简化客户端代码:减少了客户端需要处理的对象数量.
  2. 降低耦合性:外观模式实现了客户端与子系统之间的松耦合。
  3. 分层结构清晰 需要暴露给外部的功能集中在外观类中,方便客户端使用,同时隐藏了系统内部的复杂细节。
  4. 不影响直接访问子系统:外观模式提供了访问子系统的统一入口,但并不限制用户直接使用子系统类,保留了系统的灵活性。

模式缺点

  1. 对子系统类限制不严格:外观模式无法严格限制客户端直接访问子系统类,可能导致系统复杂度增加。
  2. 可能违背开闭原则:如果设计不当,增加新的子系统可能需要修改外观类的源代码,从而违背开闭原则。

适用环境

  • 当需要为复杂子系统提供一个简单接口时,可以使用外观模式。
  • 当客户端程序与多个子系统之间存在很大依赖时,外观模式可以通过引入外观类将子系统与客户端解耦。
  • 在层次化结构中,外观模式可以为每一层定义统一的入口。

模型扩展
#

一个系统有多个外观类:在通常情况下,只有一个外观类(使用单例),比如例子中只有一个开关。但实际上也有需要使用多个外观类的情况,每个外观类都负责和一些特定的子系统交互。不要试图通过外观类为子系统增加新行为。

抽象外观类:若有多个外观类,单纯的设计会导致违反开闭原则,因此往往通过抽象外观类进行设计。使得在引入抽象外观类之后,不需要修改原有外观类。

image-20241108123458379

代理模式
#

代理模式的本质:控制对象访问。

远程代理 (Remote Proxy)

  • 功能:为远程对象提供本地代理,简化跨网络的对象调用。
  • 应用场景:分布式系统,如 DCOMWeb ServiceJava RMICORBA
  • 优点:客户端可以像调用本地对象一样访问远程对象,隐藏了网络通信的复杂性。

虚拟代理 (Virtual Proxy)

  • 功能:延迟创建资源消耗大的对象,只有在需要时才真正创建。
  • 应用场景:系统中有高成本对象(如图像、文件等)的按需加载。
  • 优点:优化性能,减少系统初始开销,节省内存和计算资源。

保护代理 (Protect Proxy)

  • 功能:根据用户权限控制对对象的访问。
  • 应用场景:需要基于角色或权限进行访问控制的系统。
  • 优点:增强安全性,确保不同用户只能访问他们有权操作的功能。

缓冲代理 (Cache Proxy)

  • 功能:为目标操作的结果提供缓存,减少重复操作。
  • 应用场景:频繁访问不经常变化的数据,如数据查询缓存。
  • 优点:提高性能,减少计算或数据获取的开销。

智能引用代理 (Smart Reference Proxy)

  • 功能:在对象引用时,增加额外操作,如记录调用次数、管理对象生命周期。
  • 应用场景:需要监控对象使用或增加引用管理的场景。
  • 优点:增强对象管理功能,提供更多控制和监控能力。

防火墙代理 (Firewall Proxy)

  • 功能:控制对网络资源的访问,保护对象免受不可信客户端的侵害。
  • 应用场景:网络安全系统,如公司的防火墙。
  • 优点:提高系统安全性,限制对敏感资源的非法访问。

同步代理 (Synchronization Proxy)

  • 功能:在多线程环境下,确保对对象的安全访问。
  • 应用场景:并发系统,如分布式系统中的同步访问控制。
  • 优点:防止数据竞争,确保多线程的并发安全性。

写入时复制代理 (Copy-On-Write Proxy)

  • 功能:延迟对象的复制操作,直到真正需要时才进行复制。
  • 应用场景:需要优化内存和性能的场景,特别是只读对象的复制。
  • 优点:减少不必要的深拷贝操作,优化性能和资源使用。

image-20241108141016989

// 客户端类
public class Client {
    public static void main(String[] args) {
        // 使用XMLUtil从配置中获取权限对象,假设返回的是代理类
        AbstractPermission permission = (AbstractPermission) XMLUtil.getBean(); 
        // 初始权限级别为0,尝试执行各种操作
        permission.modifyUserInfo();...
        // 设置权限级别为1,再次尝试执行操作
        permission.setLevel(1);
        permission.modifyUserInfo();...
    }
}

// 抽象权限接口,定义所有权限操作
public interface AbstractPermission {
    void modifyUserInfo();  // 修改用户信息权限
    void viewNote();        // 查看帖子权限
    void publishNote();     // 发布帖子权限
    void modifyNote();      // 修改帖子权限
    void setLevel(int level); // 设置权限级别
}

// 真实权限类,实现具体的权限操作
public class RealPermission implements AbstractPermission {
    @Override
    public void modifyUserInfo() { // 需要权限的功能
        System.out.println("修改用户信息!");
    }
    @Override
    public void viewNote() { // 不需要权限的功能
        // 可以一直查看帖子,无需权限
    }
}
// 权限代理类,控制访问权限
public class PermissionProxy implements AbstractPermission {
    // 私有变量,关联关系。虽然有 new,但从生命周期的角度看应该是关联而非组合。
    private RealPermission permission = new RealPermission(); // 持有真实权限对象
    private int level = 0; // 默认权限级别为0   
    @Override
    public void modifyUserInfo() {
        if (level == 0) {
            System.out.println("对不起,你没有修改用户信息的权限!");
        } else if (level == 1) {
            permission.modifyUserInfo();
        }
    }
    @Override
    public void viewNote() { // 可以直接查看不需要权限
        System.out.println("查看帖子!");
    }
}

模式优点

  1. 降低耦合性:代理模式在调用者和被调用者之间引入代理,减少了它们之间的直接依。
  2. 符合开闭原则:客户端针对抽象接口编程。
  3. 提升系统性能:远程代理和虚拟代理可以将资源密集型操作从本地转移或延迟执行。
  4. 权限控制:保护代理可以根据用户权限控制访问,提供不同级别的功能,增强系统的安全性。
  5. 缓存优化:缓冲代理通过缓存操作结果,提高重复操作的效率,缩短执行时间。

模式缺点

  1. 性能开销:代理模式增加了一层间接性,可能导致请求处理变慢,尤其是在频繁调用时。
  2. 实现复杂:某些代理模式(如远程代理、缓存代理)的实现较为复杂,开发和维护成本较高。

适用环境

  • 当客户端需要访问远程对象时,可以使用远程代理,屏蔽网络通信的复杂性。
  • 当需要延迟创建高成本对象时,可以使用虚拟代理,减少系统初始开销。
  • 当需要为频繁访问的结果提供缓存时,适合使用缓冲代理,避免重复计算。
  • 当需要控制权限访问时,适合使用保护代理,基于用户权限提供不同功能。
  • 当需要在对象引用时执行额外操作(如计数、管理)时,可以使用智能引用代理。

模型扩展
#

动态代理: Java 反射机制(主要是 java.lang.reflect.ProxyInvocationHandler)来实现。

// 抽象主题接口,定义了代理类与真实主题类共有的行为
public interface AbstractSubject {
    void request();  // 抽象方法,代理类和真实主题类都会实现
}

// 真实主题类A,实现了AbstractSubject接口
public class RealSubjectA implements AbstractSubject {
    @Override
    public void request() {
        System.out.println("真实主题类A!");
    }
}

// 真实主题类B,实现了AbstractSubject接口
public class RealSubjectB implements AbstractSubject {
    @Override
    public void request() {
        System.out.println("真实主题类B!");
    }
}

// 动态代理类,负责在真实对象方法调用前后执行额外操作
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxy implements InvocationHandler {
    private Object obj;  // 持有被代理的真实对象
    // 无参构造方法
    public DynamicProxy() {}
    // 有参构造方法,传入被代理对象
    public DynamicProxy(Object obj) {
        this.obj = obj;
    }
    // 动态代理的核心方法,拦截对代理对象的所有方法调用
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        preRequest();  // 调用前操作
        method.invoke(obj, args);  // 调用真实对象的方法
        postRequest();  // 调用后操作
        return null;
    }
    public void preRequest() {    // 调用前的操作(如日志记录)
        System.out.println("调用之前!");
    }
    public void postRequest() {    // 调用后的操作(如日志记录)
        System.out.println("调用之后!");
    }
}

// 客户端类,演示如何使用动态代理
import java.lang.reflect.Proxy;
public class Client {
    public static void main(String[] args) {
        InvocationHandler handler;  // 动态代理的调用处理器
        AbstractSubject subject;  // 抽象主题接口
        // 使用动态代理代理真实主题类A
        handler = new DynamicProxy(new RealSubjectA());
        subject = (AbstractSubject) Proxy.newProxyInstance(
            AbstractSubject.class.getClassLoader(), 
            new Class[]{AbstractSubject.class}, 
            handler
        );
        subject.request();  // 调用代理对象的方法,触发invoke方法
        System.out.println("------------------------------");
        // 使用动态代理代理真实主题类B
        handler = new DynamicProxy(new RealSubjectB());
        subject = (AbstractSubject) Proxy.newProxyInstance(
            AbstractSubject.class.getClassLoader(), 
            new Class[]{AbstractSubject.class}, 
            handler
        );
        subject.request();  // 调用代理对象的方法,触发invoke方法
    }
}

行为型模式
#

行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。

行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用

责任链模式
#

职责链模式的本质:分离职责,动态组合。

职责链可以是直线,树,环等等,常见的是直线。

和组合模式比较像,区别如下:

处理方式不同

  • 职责链模式 中,如果一个对象处理了请求,链条的传递就终止了,处理者唯一。
  • 组合模式 中,操作会应用到所有子对象上,处理是递归的,可能会遍历整个树形结构。

应用场景不同

  • 职责链模式 适用于多个对象都有机会处理请求的情况,但处理者不明确,希望请求自动沿着链传递直到找到合适的处理者。
  • 组合模式 适用于需要表示部分-整体层次结构的场景,希望客户端无差别地处理单个对象和组合对象。

image-20241108154153067

// 主任类,处理较短时间的请假
public class Director extends Leader {
    public Director(String name) {
        super(name); // 调用父类的构造函数
    }
    // 处理请假请求
    public void handleRequest(LeaveRequest request) {
        if (request.getLeaveDays() < 3) {  // 如果请假天数少于3天,主任处理
            System.out.println("主任" + name + "审批员工" + request.getLeaveName() + "的请假条,请假天数为" + request.getLeaveDays() + "天。");
        } else {
            if (this.successor != null) {  // 如果超过3天,且有上级领导,交给下一个领导处理
                this.successor.handleRequest(request);
            }
        }
    }
}
// 总经理类,处理较大时间范围的请假
...
// 抽象领导类,定义了职责链中的公共行为
public abstract class Leader {
    protected String name;  // 领导的姓名,注意最好用 protected
    protected Leader successor;  // 下一个处理请求的领导,最好用 protected,如果是树,应该用列表表示
    public Leader(String name) {
        this.name = name;
    }
    public void setSuccessor(Leader successor) { // 设置下一个处理者
        this.successor = successor;
    }
    public abstract void handleRequest(LeaveRequest request);// 抽象方法,处理请假请求
}

// 请假请求类,封装请假者的姓名和请假天数
public class LeaveRequest {
    private String leaveName;  // 请假者的姓名
    private int leaveDays;  // 请假天数
    public LeaveRequest(String leaveName, int leaveDays) { // 构造函数
        this.leaveName = leaveName;
        this.leaveDays = leaveDays;
    }

    // 经典 set 与 get
    public void setLeaveName(String leaveName) {
        this.leaveName = leaveName; 
    } 。。。
    public String getLeaveName() {
        return this.leaveName; 
    } 。。。
}
// 客户端类,模拟请假审批流程
public class Client {
    public static void main(String args[]) {
        // 创建各级审批领导
        Leader objDirector, objManager, objGeneralManager, objViceGeneralManager;
        objDirector = new Director("王明");  
        objManager = new Manager("赵强");...

        // 设置职责链,将各级领导串联起来,类似链表
        objDirector.setSuccessor(objManager);
        objManager.setSuccessor(objViceGeneralManager);  
        objViceGeneralManager.setSuccessor(objGeneralManager);  

        LeaveRequest lr4 = new LeaveRequest("赵六", 25);  // 赵六请假25天
        objDirector.handleRequest(lr4);  // 处理请求
    }
}

模式扩展:
#

职责链模式分为纯的职责链模式不纯的职责链模式

  • 纯的职责链模式:每个处理者要么处理请求,要么将请求传递给下一个处理者,且不返回。(比如上面的请假例子)
  • 不纯的职责链模式:每个处理者可以处理请求并将请求继续传递,或者在处理后终止传递。

优点缺点:
#

模式优点

  1. 降低耦合度:职责链模式将请求的发送者与处理者解耦,发送者无需知道具体的处理者是谁,只需将请求提交到链上。
  2. 简化对象连接:每个处理对象只需要保存对其后继者的引用,而不需要保存对所有可能处理者的引用,简化了对象之间的连接关系。
  3. 灵活性高:职责链结构可以在运行时动态修改,处理者可以在链中动态增加、删除或重新排序。
  4. 符合开闭原则:增加新的处理者时不需要修改现有代码,只需在客户端重新构建职责链即可。

模式缺点

  1. 请求可能得不到处理:如果链末尾没有合适的处理者,或职责链配置不当,可能导致请求得不到任何处理。
  2. 性能开销:对于较长的职责链,处理请求需要经过多个对象,可能导致处理效率降低。
  3. 调试困难:链条过长时,调试和跟踪请求的处理过程可能会变得复杂。
  4. 可能产生循环调用:如果存在环形结构,可能会导致循环调用,进而引发系统死循环。

适用环境

  • 当有多个对象可以处理同一请求时,职责链模式适用于在请求发出时不明确指定请求的接收者
  • 当请求的处理对象在运行时动态确定时,职责链模式可以灵活地调整处理者的顺序或增加新的处理者
  • 当需要将请求提交给一组处理者中的某一个或某几个,而不必明确指定具体的处理者时,职责链模式适用。
  • 当希望客户端只需将请求提交给职责链,而不关心具体的处理过程时。

命令模式
#

命令模式的本质:封装请求。

image-20241108171525154

image-20241108172438962

image-20241108172743360

// 命令模式的实现:将请求封装成对象,使得请求的发送者和接收者解耦。
// 这里的例子是电视遥控器控制电视的开、关、换台操作。
// 抽象命令接口,定义了所有具体命令类的统一操作接口
public interface AbstractCommand {
    public void execute();
}
// 具体命令类:打开电视命令
public class TVOpenCommand implements AbstractCommand {
    private Television tv;
    // 构造方法中关联接收者(电视)
    public TVOpenCommand() {
        tv = new Television();
    }
    // 执行命令,调用电视的开机方法
    public void execute() {
        tv.open();
    }
}
// 具体命令类:关闭电视命令
public class TVCloseCommand implements AbstractCommand {
	...
}
// 具体命令类:切换电视频道命令
public class TVChangeCommand implements AbstractCommand {
	...
}
// 接收者类:电视,包含具体的操作方法
public class Television {
    public void open() {
        System.out.println("打开电视机!");
    }
    public void close() {
        System.out.println("关闭电视机!");
    }
    public void changeChannel() {
        System.out.println("切换电视频道!");
    }
}
// 调用者类:遥控器,负责执行命令
public class Controller {
    private AbstractCommand openCommand, closeCommand, changeCommand;
    // 遥控器构造函数,传入具体的命令对象
    public Controller(AbstractCommand openCommand, AbstractCommand closeCommand, AbstractCommand changeCommand) {
        this.openCommand = openCommand;
        this.closeCommand = closeCommand;
        this.changeCommand = changeCommand;
    }
    public void open() {    // 调用打开电视的命令
        openCommand.execute();
    }
    public void change() {    // 调用切换频道的命令
        changeCommand.execute();
    }
    public void close() {    // 调用关闭电视的命令
        closeCommand.execute();
    }
}

// 客户端类:设置命令并触发请求
public class Client {
    public static void main(String[] args) {
        // 创建具体命令对象
        AbstractCommand openCommand = new TVOpenCommand();
        AbstractCommand closeCommand = new TVCloseCommand();
        AbstractCommand changeCommand = new TVChangeCommand();
        // 创建遥控器(调用者),并传入命令对象
        Controller control = new Controller(openCommand, closeCommand, changeCommand);
        // 模拟使用遥控器控制电视操作
        control.open();    // 打开电视
        control.change();  // 切换频道
        control.close();   // 关闭电视
    }
}

image-20241108174446829

模型扩展
#

命令队列:
#

它用于将多个命令对象按顺序存储在一个队列中,并在适当的时机依次执行这些命令。命令队列的主要作用是将命令的请求和执行解耦,允许命令的执行可以被延迟或批量处理。

image-20241108180101218

class CommandQueue { 
    //定义一个ArrayList来存储命令队列  
    private ArrayList<Command> commands = new ArrayList<Command>();  
    public void addCommand(Command command) {  commands.add(command);   }  
    public void removeCommand(Command command) {  commands.remove(command);  }  
    //循环调用每一个命令对象的execute()方法  
    public void execute() {  
        for (Object command : commands) {  
            ((Command)command).execute();  
        }  
    }  
} 
class Invoker {  
    private CommandQueue commandQueue; //维持一个CommandQueue对象的引用  
    //构造注入
    public Invoker(CommandQueue commandQueue) {  this.commandQueue =commandQueue;  } 
    //设值注入  
    public void setCommandQueue(CommandQueue commandQueue) {  
        this.commandQueue = commandQueue;  
    }       
    //调用CommandQueue类的execute()方法  
    public void call() {    commandQueue.execute();     }  
} 

请求日志:
#

模式通常用于记录系统中执行的命令或操作,以便后续进行操作的回放、撤销(undo)或恢复(redo)。

image-20241108182447913

// 配置文件操作类:请求接收者
// 实现Serializable接口,确保对象可以被序列化并写入文件
class ConfigOperator implements Serializable {  
    // 插入新节点
    public void insert(String args) {  
        System.out.println("增加新节点:" + args);  
    }  
    // 修改节点
    public void modify(String args) {  
        System.out.println("修改节点:" + args);  
    }  
    // 删除节点
    public void delete(String args) {  
        System.out.println("删除节点:" + args);  
    }  
}

// 配置文件设置窗口类:请求发送者
// 负责调用具体的命令对象并保存执行日志
class ConfigSettingWindow {  
    // 定义一个集合来存储每一次操作时的命令对象
    private ArrayList<Command> commands = new ArrayList<Command>();  
    private Command command;   
    // 注入具体命令对象
    public void setCommand(Command command) {  
        this.command = command;  
    }  
    // 执行配置文件修改命令,同时将命令对象添加到命令集合中
    public void call(String args) {  
        command.execute(args);  
        commands.add(command);  
    }  
    // 记录请求日志,生成日志文件,将命令集合写入日志文件
    public void save() {  
        FileUtil.writeCommands(commands);  
    }  
    // 从日志文件中提取命令集合,并循环调用每一个命令对象的execute()方法来实现配置文件的重新设置
    public void recover() {  
        ArrayList list;  
        list = FileUtil.readCommands();  
        for (Object obj : list) {  
            ((Command)obj).execute();  
        }  
    }  
}  

// 工具类:文件操作类
// 负责将命令集合序列化写入日志文件及从日志文件中读取命令集合
class FileUtil {  
    // 将命令集合写入日志文件
    public static void writeCommands(ArrayList commands) {  
        try {  
            FileOutputStream file = new FileOutputStream("config.log");  
            // 创建对象输出流用于将对象写入到文件中
            ObjectOutputStream objout = new ObjectOutputStream(new BufferedOutputStream(file));  
            // 将对象写入文件
            objout.writeObject(commands);  
            objout.close();  
        } catch (Exception e) {  
            System.out.println("命令保存失败!");    
            e.printStackTrace();  
        }  
    }  
    // 从日志文件中提取命令集合
    public static ArrayList readCommands() {  
        try {  
            FileInputStream file = new FileInputStream("config.log");  
            // 创建对象输入流用于从文件中读取对象
            ObjectInputStream objin = new ObjectInputStream(new BufferedInputStream(file));  

            // 将文件中的对象读出并转换为ArrayList类型
            ArrayList commands = (ArrayList)objin.readObject();  
            objin.close();  
            return commands;  
        } catch (Exception e) {  
            System.out.println("命令读取失败!");  
            e.printStackTrace();  
            return null;      
        }         
    }  
}

撤销操作:
#

image-20241108183633362

// 抽象命令类:定义命令的接口,包含执行和撤销操作。
abstract class AbstractCommand {
    // 执行操作,传入参数并返回执行结果
    public abstract int execute(int value);
    // 撤销操作,返回撤销后的结果
    public abstract int undo();
}

// 具体命令类:实现了抽象命令类,负责调用接收者(Adder)的具体操作。该类封装了对加法操作的请求并实现了撤销功能。
class ConcreteCommand extends AbstractCommand {
    private Adder adder = new Adder(); // 接收者(实际执行加法操作的类)
    // 存储上一次操作的值,用于撤销
    private int value;
    // 执行加法操作,并存储当前操作的值
    public int execute(int value) {
        this.value = value;
        return adder.add(value);
    }
    // 撤销上一次加法操作,通过加上一个相反的值
    public int undo() {
        return adder.add(-value);
    }
}

// 调用者类:表示计算器窗口,通过命令对象来执行和撤销操作。
// 该类负责管理命令对象,并通过命令对象执行或撤销操作。
class CalculatorForm {
    private AbstractCommand command;
    // 设置具体的命令对象
    public void setCommand(AbstractCommand command) {
        this.command = command;
    }
    public void compute(int value) {    // 执行运算操作,并输出运算结果
        int result = command.execute(value);
        System.out.println("执行运算,运算结果为:" + result);
    }
    public void undo() {    // 撤销上一次运算操作,并输出撤销后的结果
        int result = command.undo();
        System.out.println("执行撤销,运算结果为:" + result);
    }
}
// 接收者类:实际执行加法操作的类。
class Adder {
    private int num = 0; // 记录当前的累计值
    // 执行加法操作,并返回加法后的结果
    public int add(int value) {
        num += value;
        return num;
    }
}
// 客户端类:负责创建命令对象和调用者,并触发操作。
class Client {
    public static void main(String args[]) {
        // 创建调用者(计算器窗口)
        CalculatorForm form = new CalculatorForm();
        // 创建具体命令对象(加法命令)
        ConcreteCommand command = new ConcreteCommand();
        // 将命令对象设置到调用者
        form.setCommand(command);
        form.compute(10); // 输出:执行运算,运算结果为:10
        form.undo();      // 输出:执行撤销,运算结果为:15
    }
}

宏命令:
#

递归调用它所包含的每个成员命令的execute()方法。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理。

image-20241108184354984

优点缺点
#

模式优点

  1. 更松散的耦合:命令模式将请求的发送者与接收者完全解耦,发送者只需要调用命令,而不需要知道具体的接收者是谁。相同的请求者可以使用不同的接收者,相同的接收者也可以为不同的请求者服务。
  2. 更动态的控制:命令模式可以将请求封装为对象,并动态进行参数化、队列化和日志化操作,使系统更加灵活。请求可以在不修改调用者的情况下被延迟执行或加入队列。
  3. 更容易的组合命令:命令模式允许你轻松实现命令队列或宏命令(组合多个命令的命令),使得系统可以执行复杂的操作序列。
  4. 更好的扩展性:命令模式符合“开闭原则”,可以轻松添加新的命令对象而无需修改现有代码。

模式缺点

  1. 类的膨胀:命令模式可能会引入大量的具体命令类,每个操作对应一个具体的命令类,在一些复杂系统中可能导致类的数量急剧增加。
  2. 增大系统开销:由于命令对象的引入,系统需要为每个请求生成额外的命令对象,可能会导致一定的内存开销和处理开销,特别是当命令数量较多时。
  3. 请求排队的复杂性:如果命令模式与请求队列等复杂处理机制结合,可能会增加系统的复杂性,尤其是在管理命令的执行顺序和撤销操作时。

适用环境

  • 需要抽象出执行的动作:当需要将执行的动作抽象出来,并且将其参数化时,命令模式能够很好地将这些动作封装成命令对象,然后实现参数化配置。
  • 需要在不同时间指定、排列和执行请求:如果系统要求在不同时间点指定请求并按照一定的顺序执行,命令模式可以将请求封装为命令对象,并通过队列或日志机制来管理它们的执行顺序。
  • 需要支持撤销和重做操作:命令模式可以方便地实现撤销(Undo)和重做(Redo)操作。通过管理已执行的命令对象,可以支持对操作进行回滚或重做。
  • 系统崩溃后的操作恢复:如果系统崩溃后需要恢复之前的操作,命令模式可以将操作请求封装成命令对象,并结合日志来记录命令列表,在系统恢复时重新执行这些命令。
  • 事务性系统:在事务性系统中,命令模式能够将事务的操作封装为命令对象,正向遍历执行事务操作,出现异常时反向遍历进行回滚操作,实现事务管理。

迭代器模式
#

迭代器模式的本质:控制访问聚合对象中的元素。

image-20241109141620591

下图中使用内部类来实现上面 Concretelterator 关联 ConcreteAggregate

而 createlterator 函数体现了依赖关系

image-20241109141558708

// 迭代器接口:定义用于遍历电视机频道的方法
public interface TVIterator {
    void setChannel(int i);  // 设置当前频道
    void next();             // 移动到下一个频道
    void previous();         // 移动到上一个频道
    boolean isLast();        // 判断是否为最后一个频道
    Object currentChannel(); // 获取当前频道
    boolean isFirst();       // 判断是否为第一个频道
}

// 电视接口:提供创建迭代器的工厂方法
// 这是工厂方法模式的一部分,具体的电视机类将实现该接口,提供相应的迭代器
public interface Television {
    TVIterator createIterator();
}

// TCL电视机类:实现Television接口并提供具体的频道及迭代器实现
public class TCLTelevision implements Television {
    private Object[] obj = { "湖南卫视", "北京卫视", "上海卫视", "湖北卫视", "黑龙江卫视" };
    // 创建并返回具体的迭代器对象
    public TVIterator createIterator() {
        return new TCLIterator();
    }
    // TCL电视的迭代器实现:实现TVIterator接口,能够遍历TCL电视机的频道
    class TCLIterator implements TVIterator {
        private int currentIndex = 0;
        // 移动到下一个频道
        public void next() {
            if (currentIndex < obj.length) {
                currentIndex++;
            }
        }
        public void previous() {
		...
        }
    }
}
// Skyworth电视机类:实现Television接口并提供具体的频道及迭代器实现
public class SkyworthTelevision implements Television {
    private Object[] obj = { "CCTV-1", "CCTV-2", "CCTV-3", "CCTV-4", "CCTV-5", "CCTV-6", "CCTV-7", "CCTV-8" };
    // 创建并返回具体的迭代器对象
    public TVIterator createIterator() {
        return new SkyworthIterator();
    }
    // Skyworth电视的迭代器实现:实现TVIterator接口,能够遍历Skyworth电视机的频道
    private class SkyworthIterator implements TVIterator {
        private int currentIndex = 0;
		...
    }
}

// 客户端类:模拟电视机操作,通过遍历器显示频道
public class Client 
    // 正向遍历并显示所有频道
    public static void display(Television tv) {
        TVIterator i = tv.createIterator();
        System.out.println("电视机频道:");
        while (!i.isLast()) {
            System.out.println(i.currentChannel().toString());
            i.next();
        }
    }
    // 逆向遍历并显示频道
    public static void reverseDisplay(Television tv) {
        TVIterator i = tv.createIterator();
        i.setChannel(5);  // 设置当前频道为第5个
        System.out.println("逆向遍历电视机频道:");
        while (!i.isFirst()) {
            i.previous();
            System.out.println(i.currentChannel().toString());
        }
    }
    // 主程序入口,模拟从XML文件中获取具体的电视机对象并进行频道遍历
    public static void main(String a[]) {
        Television tv;
        tv = (Television) XMLUtil.getBean();  // 动态获取具体的电视机对象
        display(tv);
        System.out.println("--------------------------");
        reverseDisplay(tv);
    }
}

模式扩展
#

内部迭代器和外部迭代器

  • 内部迭代器指的是由迭代器自己来控制迭代下一个元素的步骤,客户端无法干预,因此,如果想要在迭代的过程中完成工作的话,客户端就需要把操作传给迭代器,迭代器在迭代的时候会在每个元素上执行这个操作。

  • 外部迭代器指的是由客户端来控制迭代下一个元素的步骤,像前面的示例一样,客户端必须显示的调用next来迭代下一个元素。常见的实现多属于外部迭代器。

带迭代策略的迭代器:

  • 实现的一个基本思路,就是先把聚合对象的聚合数据获取到,并存储到迭代器里面来,这样迭代器就可以按照不同的策略来迭代数据了。

双向迭代器: 可以同时向前或向后

优点缺点:
#

优点

  1. 支持多种遍历方式:通过替换不同的迭代器,可以在同一个聚合对象上使用不同的遍历算法。。
  2. 简化聚合类设计:迭代器模式将遍历逻辑从聚合类中分离,使得聚合类不再负责数据的遍历,简化了聚合类的设计。
  3. 符合开闭原则:引入抽象层后,新增聚合类和迭代器类时无需修改原有代码。

缺点

  1. 类的数量增加:由于职责分离,每增加一个新的聚合类通常需要对应增加一个迭代器类,导致类的数量成对增加,增加了系统的复杂性。
  2. 抽象迭代器设计难度高:设计一个通用的抽象迭代器并不容易,必须充分考虑未来的扩展需求,且这些子类不能用于所有类型的聚合结构。

适用环境

  1. 无须暴露内部结构:当需要访问聚合对象的内容,但不希望暴露其内部表示时,可以使用迭代器模式。
  2. 支持多种遍历方式:当需要为聚合对象提供多种遍历方式。
  3. 统一接口:为不同的聚合结构提供统一的遍历接口,客户端可以通过一致的方式操作不同的聚合结构。

中介者模式
#

中介者模式的本质:封装交互。

image-20241109151200935

image-20241109151221160

// 中介者模式:抽象中介者
public abstract class AbstractChatroom {
    public abstract void register(Member member);  // 注册成员
    public abstract void sendText(String from, String to, String message);  // 发送文本
    public abstract void sendImage(String from, String to, String image);  // 发送图片
}

// 具体中介者:处理成员之间的消息传递
import java.util.*;
public class ChatGroup extends AbstractChatroom {
    private Hashtable<String, Member> members = new Hashtable<>(); // 用哈希表存储对象
    public void register(Member member) {    // 注册成员,将成员加入聊天室
        if (!members.contains(member)) {
            members.put(member.getName(), member);
            member.setChatroom(this);  // 设置成员所属的聊天室
        }
    }
    // 发送文本信息,并进行敏感词过滤
    public void sendText(String from, String to, String message) {
        Member member = members.get(to);
        String newMessage = message.replaceAll("C", "*");  // 替换敏感词
        member.receiveText(from, newMessage);  // 接收文本
    }
    // 发送图片,如果图片内容过长则发送失败
    public void sendImage(String from, String to, String image) {
        Member member = members.get(to);
        if (image.length() > 5) {
            System.out.println("图片太大,发送失败!");
        } else {
            member.receiveImage(from, image);  // 接收图片
        }
    }
}

// 抽象同事类:成员基类
public abstract class Member {
    protected AbstractChatroom chatroom;  // 中介者引用,用 protected
    protected String name;
    public Member(String name) {
        this.name = name;
    }
	...// 经典 get 和 set
    public abstract void sendText(String to, String message);
    public abstract void sendImage(String to, String image);
    // 接收文本信息
    public void receiveText(String from, String message) {
        System.out.println(from + "发送文本给" + this.name + ",内容为:" + message);
    }
    // 接收图片信息
    public void receiveImage(String from, String image) {
        System.out.println(from + "发送图片给" + this.name + ",内容为:" + image);
    }
}

// 具体同事类:钻石会员
public class DiamondMember extends Member {
    public DiamondMember(String name) {
        super(name);
    }

    // 钻石会员可以发送文本
    public void sendText(String to, String message) {
        System.out.println("钻石会员发送信息:");
        chatroom.sendText(name, to, message);  // 通过中介者发送
    }

    // 钻石会员可以发送图片
    public void sendImage(String to, String image) {
        System.out.println("钻石会员发送图片:");
        chatroom.sendImage(name, to, image);  // 通过中介者发送
    }
}

// 具体同事类:普通会员
public class CommonMember extends Member {
...
}

// 客户端:模拟聊天室中的操作
public class Client {
    public static void main(String[] args) {
        // 创建具体中介者(聊天室)
        AbstractChatroom happyChat = new ChatGroup();
        Member member1 = new DiamondMember("张三"); // 创建多个会员(同事)
        Member member2 = new DiamondMember("李四");
        happyChat.register(member1);   // 将会员注册到聊天室
        happyChat.register(member2);
        // 模拟会员之间的消息传递
        member1.sendText("李四", "李四,你好!");
        member2.sendText("张三", "张三,你好!");
    }
}

优点缺点:
#

优点

  1. 松散耦合:中介者模式通过将多个同事对象之间的交互逻辑封装到中介者对象中,减少了同事对象之间的直接依赖。各个同事对象可以独立变化或复用,避免了“牵一发而动全身”的问题。
  2. 集中控制交互:所有同事对象之间的交互逻辑由中介者集中管理,交互行为发生变化时,只需修改中介者对象,而无需修改同事对象,增强了系统的可扩展性和维护性。
  3. 多对多变成一对多:引入中介者后,同事对象之间的多对多关系变为中介者和同事对象之间的一对多关系,简化了对象间的关系,使系统结构更加清晰且易于理解和实现。

缺点

  1. 过度集中化:随着同事对象的交互复杂度增加,中介者对象可能变得非常复杂,承担过多的逻辑,导致中介者难以管理和维护,甚至出现“上帝对象”的问题。

适用环境

  1. 复杂的对象交互:当一组对象之间的通信方式复杂且相互依赖,导致系统难以维护时,可以采用中介者模式将交互逻辑集中管理,减少对象之间的直接依赖。
  2. 减少对象直接依赖:当一个对象需要与多个对象交互并导致耦合度过高时,可以使用中介者模式,将其与其他对象的交互封装到中介者中,降低耦合度并提高对象的复用性。

中介者模式和外观模式的区别

image-20241109203528218

备忘录模式
#

备忘录模式的本质:保存和恢复内部状态。

image-20241109203715200

image-20241109203746429

// 备忘录模式:发起人类,负责创建和恢复备忘录
public class UserInfoDTO {
    private String account;
    private String password;
    private String telNo;
	...//经典set get
    // 创建备忘录,保存当前状态
    public Memento saveMemento() {
        return new Memento(account, password, telNo);
    }
    // 从备忘录中恢复状态
    public void restoreMemento(Memento memento) {
        this.account = memento.getAccount();
        this.password = memento.getPassword();
        this.telNo = memento.getTelNo();
    }
    // 显示当前状态
    public void show() {
        System.out.println("Account: " + this.account);
        System.out.println("Password: " + this.password);
        System.out.println("TelNo: " + this.telNo);
    }
}

// 备忘录模式:备忘录类,负责存储发起人的状态
class Memento {
    private String account;
    private String password;
    private String telNo;
    // 构造方法,保存状态
    public Memento(String account, String password, String telNo) {
        this.account = account;
        this.password = password;
        this.telNo = telNo;
    }
    ...// 经典set get
}

// 备忘录模式:管理者类,负责保存和恢复备忘录
public class Caretaker {
    private Memento memento;
    // 获取备忘录
    public Memento getMemento() {
        return memento;
    }
    // 保存备忘录
    public void setMemento(Memento memento) {
        this.memento = memento;
    }
}


public class Client {
    public static void main(String[] args) {
        // 创建发起人(UserInfoDTO)和管理者(Caretaker)
        UserInfoDTO user = new UserInfoDTO();
        Caretaker caretaker = new Caretaker();
        // 设置初始状态
        user.setAccount("zhangsan");
        user.setPassword("123456");
        user.setTelNo("13000000000");
        System.out.println("状态一:");
        user.show();
        // 保存状态一到备忘录
        // 把 save 里面 new 出来的 set 到caretaker 中
        caretaker.setMemento(user.saveMemento()); 
        user.setPassword("111111");
        user.setTelNo("13100001111");
        System.out.println("状态二:");
        user.show();
        // 从备忘录中恢复到状态一
        user.restoreMemento(caretaker.getMemento());
        System.out.println("回到状态一:");
        user.show();
    }
}

优点缺点
#

优点

  1. 提供状态恢复机制:备忘录模式允许用户在状态无效或出错时,方便地回到某个特定的历史步骤,恢复之前的状态。
  2. 封装状态信息:备忘录对象对外部封装了原发器的状态,其他代码无法直接修改备忘录中的数据,从而保护了原发器的状态不被外界破坏。
  3. 支持多次撤销:通过使用列表、堆栈等集合来存储多个备忘录对象,可以实现多次的撤销操作,允许用户恢复到更早的状态。

缺点

  1. 资源消耗较大:如果原发器类的成员变量过多,每次保存状态都会占用大量的存储空间和系统资源,尤其是在需要频繁保存状态的情况下,资源消耗问题会更加突出。

适用环境

  1. 需要保存并恢复对象状态时:当需要保存对象的全部或部分状态以便日后恢复,或者实现撤销功能时,备忘录模式是一个合适的选择。
  2. 保护对象历史状态的封装性:当不希望对象的历史状态被外界对象破坏或暴露实现细节时,可以使用备忘录模式来保证对象状态的封装性和安全性。

备忘录模式和命令模式的组合使用
#

两者都提供了撤销或回到某个节点的功能,在命令模式执行撤回或重做的命令时,使用备忘录模式。

备忘录模式和原型模式的组合使用
#

在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接用原型模式克隆一个原发器对象。

观察者模式
#

观察者模式的本质:触发联动。

image-20241109211913856

image-20241109211848484

// 观察者接口:定义了观察者的接口,所有观察者都必须实现 response() 方法
public interface MyObserver {
    void response();  // 抽象响应方法
}
// 具体观察者类:Dog 实现了 MyObserver 接口,定义了具体的响应行为
public class Dog implements MyObserver {
    @Override
    public void response() {
        System.out.println("狗跟着叫!");
    }
}
// 具体观察者类:Mouse 实现了 MyObserver 接口,定义了具体的响应行为
public class Mouse implements MyObserver {
    @Override
    public void response() {
        System.out.println("老鼠吓得四处逃窜!");
    }
}

public abstract class MySubject {
    // 用于存储观察者的列表,可以有多个观察者
    protected ArrayList<MyObserver> observers = new ArrayList<>();
    public void attach(MyObserver observer) {    // 注册观察者方法
        observers.add(observer);
    }
    public void detach(MyObserver observer) {    // 注销观察者方法
        observers.remove(observer);
    }
    // 抽象方法,具体的子类需要实现该方法来通知所有观察者
    public abstract void cry();
}
// 具体主体类:Cat 继承 MySubject,并实现 cry() 方法
public class Cat extends MySubject {
    @Override
    public void cry() {
        System.out.println("猫叫了!");
        // 通知所有注册的观察者,在父类中
        for (MyObserver observer : observers) { // 在这里控制唤醒哪些观察者
            observer.response();
        }
    }
}

// 客户端代码:测试观察者模式
public class Client {
    public static void main(String[] args) {
        MySubject subject = new Cat();
        // 创建多个观察者对象
        MyObserver obs1 = new Mouse();
        MyObserver obs3 = new Dog();
        subject.attach(obs1);        // 注册观察者到主体
        subject.attach(obs3);
        // 触发主体行为(猫叫),并通知所有观察者
        subject.cry();
    }
}

image-20241113153942559

学习通例题:
#

image-20241113164031921

image-20241113164005767

// 回调函数接口
interface Runnable {
    void run();
}
// 事件触发器类
class Trigger {
    // 事件名称与回调函数列表的映射
    private Map<String, List<Runnable>> eventMap = new HashMap<>();

    // 注册事件
    public void on(String eventName, Runnable callback) {
        if (callback != null) {
            eventMap.computeIfAbsent(eventName, k -> new ArrayList<>()).add(callback);
        }
    }
    // 触发事件
    public void raiseEvent(String eventName) {
        System.out.println("Process " + eventName + " start");
        List<Runnable> callbacks = eventMap.get(eventName);
        if (callbacks != null) {
            for (Runnable callback : callbacks) {
                callback.run(); // 执行回调
            }
        }
        System.out.println("Process " + eventName + " end");
    }
}

// 示例回调函数
class Func01 implements Runnable {
    public void run() {
        System.out.println("func01()");
    }
}

class Func02 implements Runnable {
    public void run() {
        System.out.println("func02()");
    }
}

class Func03 implements Runnable {
    public void run() {
        System.out.println("func03()");
    }
}

// 测试代码
public class Main {
    public static void main(String[] args) {
        // 创建事件触发器
        Trigger trigger = new Trigger();

        // 注册事件
        trigger.on("event01", new Func01());
        trigger.on("event01", new Func02());
        trigger.on("event02", new Func02());
        trigger.on("event03", new Func03());

        // 触发事件
        trigger.raiseEvent("event01");
        trigger.raiseEvent("event02");
        trigger.raiseEvent("event03");
        trigger.raiseEvent("event01"); // 再次触发 event01
        trigger.raiseEvent("event02"); // 再次触发 event02
    }
}

优点缺点:
#

优点

  1. 解耦目标与观察者:目标对象只依赖于抽象的观察者接口,不需要知道具体的观察者类,降低了耦合度。
  2. 动态联动:通过动态注册和管理观察者,允许在运行时控制通知范围,实现灵活的联动效果。
  3. 支持广播通信:目标对象可向所有注册的观察者广播通知,适应一对多的通信需求,同时也可扩展限制广播的范围。
  4. 符合开闭原则:增加新的观察者无需修改现有代码,系统扩展性强。

缺点

  1. 性能问题:每次通知都会广播给所有观察者,可能导致性能浪费。应及时注销不需要的观察者。
  2. 避免死循环:若两个对象互为观察者和目标,可能产生相互通知的死循环,需要特别注意设计。

适用环境

  1. 对象间存在依赖关系:一个对象的改变需要通知其他对象,但不需知道具体有哪些对象会受到影响。
  2. 触发链场景:当需要创建链式触发机制时,使用观察者模式可以让一个对象的行为依次影响其他对象。

状态模式
#

状态模式的本质:根据状态来分离和选择行为。

状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化。

关键是状态转移以及 state 中的 statecheck 函数

image-20241110123023899

image-20241110123618844

// 抽象状态类:定义了用户行为的抽象接口
public abstract class AbstractState {
    protected ForumAccount acc;  // 用户账户
    protected int point;         // 用户积分
    protected String stateName;  // 状态名称
    // 状态检查方法,子类具体实现
    public abstract void checkState(int score);
    // 文件下载行为,默认实现
    public void downloadFile(int score) {
        System.out.println(acc.getName() + "下载文件,扣除" + score + "积分。");
        this.point -= score;
        checkState(score);
        System.out.println("剩余积分为:" + this.point + ",当前级别为:" + acc.getState().stateName);
    }
    ...
}
// 具体状态类:新手状态
public class PrimaryState extends AbstractState {
    public PrimaryState(AbstractState state) {
        this.acc = state.acc;
        this.point = state.getPoint();
        this.stateName = "新手";
    }
    public PrimaryState(ForumAccount acc) {
        this.point = 0;
        this.acc = acc;
        this.stateName = "新手";
    }
    @Override
    public void downloadFile(int score) {
        System.out.println("对不起," + acc.getName() + ",您没有下载文件的权限!");
    }
    @Override
    public void checkState(int score) {
        if (point >= 1000) {
            acc.setState(new HighState(this));
        } else if (point >= 100) {
            acc.setState(new MiddleState(this));
        }
    }
}
// 具体状态类:高手状态
public class MiddleState extends AbstractState {
	...
}
// 具体状态类:专家状态
public class HighState extends AbstractState {
 ...
}

// 用户账户类:负责管理用户的状态
public class ForumAccount {
    private AbstractState state;
    private String name;
    public ForumAccount(String name) { // 改变状态
        this.name = name;
        this.state = new PrimaryState(this); // ForumAccount 和 PrimaryState 相互关联

    }
    public void setState(AbstractState state) { // 经典 set 与 get
        this.state = state;
    }
    public AbstractState getState() {
        return this.state;
    }
    public String getName() {
        return this.name;
    }
    public void downloadFile(int score) {
        state.downloadFile(score);
    }
    public void writeNote(int score) {
        state.writeNote(score);
    }
    public void replyNote(int score) {
        state.replyNote(score);
    }
}

// 客户端代码:用于测试状态模式的实现
public class Client {
    public static void main(String[] args) {
        ForumAccount account = new ForumAccount("张三");
        account.writeNote(20);
        account.downloadFile(20);
        account.replyNote(100);
		...
    }
}

实例2
#

image-20241110125857986

画出状态转移图:

image-20241110125925679

image-20241110130223701

实例代码(工程性质很强,不是传统模板):

// 抽象状态类:定义账户状态的抽象行为
abstract class AccountState {
    protected Account acc;  // 关联的账户对象
    public abstract void deposit(double amount);    // 存款操作
    public abstract void withdraw(double amount);    // 取款操作
    public abstract void computeInterest();    // 计算利息
    // 最重要的:状态检查与转换
    public abstract void stateCheck();
}

// 正常状态:具体状态类
class NormalState extends AccountState {
    // 通过账户对象创建正常状态
    public NormalState(Account acc) {
        this.acc = acc;
    }
    // 通过状态对象创建正常状态
    public NormalState(AccountState state) {
        this.acc = state.acc;
    }
    // 存款操作并检查状态转换
    public void deposit(double amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }
    // 取款操作并检查状态转换
    public void withdraw(double amount) {
        acc.setBalance(acc.getBalance() - amount);
        stateCheck();
    }
    // 正常状态下无需支付利息
    public void computeInterest() {
        System.out.println("正常状态,无须支付利息!");
    }
    // 状态转换逻辑
    public void stateCheck() {
        if (acc.getBalance() > -2000 && acc.getBalance() <= 0) {
            acc.setState(new OverdraftState(this));
        } else if (acc.getBalance() == -2000) {
            acc.setState(new RestrictedState(this));
        } else if (acc.getBalance() < -2000) {
            System.out.println("操作受限!");
        }
    }
}
// 透支状态:具体状态类
class OverdraftState extends AccountState {
    // 通过状态对象创建透支状态
    public OverdraftState(AccountState state) {
        this.acc = state.acc;
    }
    // 存款操作并检查状态
    public void deposit(double amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }
    // 取款操作并检查状态
    public void withdraw(double amount) {
        acc.setBalance(acc.getBalance() - amount);
        stateCheck();
    }
    // 透支状态下计算利息
    public void computeInterest() {
        System.out.println("计算利息!");
    }
    // 状态转换逻辑
    public void stateCheck() {
        if (acc.getBalance() > 0) {
            acc.setState(new NormalState(this));
        } else if (acc.getBalance() == -2000) {
            acc.setState(new RestrictedState(this));
        } else if (acc.getBalance() < -2000) {
            System.out.println("操作受限!");
        }
    }
}

// 受限状态:具体状态类
class RestrictedState extends AccountState {
    // 通过状态对象创建受限状态
    public RestrictedState(AccountState state) {
        this.acc = state.acc;
    }
    // 存款操作并检查状态
    public void deposit(double amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }
    // 受限状态下取款失败
    public void withdraw(double amount) {
        System.out.println("帐号受限,取款失败");
    }
    // 受限状态下计算利息
    public void computeInterest() {
        System.out.println("计算利息!");
    }
    // 状态转换逻辑
    public void stateCheck() {
        if (acc.getBalance() > 0) {
            acc.setState(new NormalState(this));
        } else if (acc.getBalance() > -2000) {
            acc.setState(new OverdraftState(this));
        }
    }
}

// 账户类:负责管理账户的当前状态
class Account {
    private AccountState state;  // 账户当前状态
    private String owner;        // 账户持有者
    private double balance;      // 账户余额
    // 构造函数,初始化账户并设置初始状态为正常状态
    public Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
        this.state = new NormalState(this);  // 初始状态为正常状态
        System.out.println(this.owner + "注册成功,初始余额为:" + this.balance);
    }
    // 存款操作,将操作委托给当前状态
    public void deposit(double amount) {
        state.deposit(amount);
        System.out.println("存款 " + amount + " 元,当前余额:" + this.balance);
        System.out.println("当前账户状态:" + state.getClass().getSimpleName());
    }
    // 取款操作,将操作委托给当前状态
    public void withdraw(double amount) {
        state.withdraw(amount);
        System.out.println("取款 " + amount + " 元,当前余额:" + this.balance);
        System.out.println("当前账户状态:" + state.getClass().getSimpleName());
    }
    // 计算利息,将操作委托给当前状态
    public void computeInterest() {
        state.computeInterest();
    }
    // 设置账户状态
    public void setState(AccountState state) {
        this.state = state;
    }
    // 获取账户当前状态
    public AccountState getState() {
        return this.state;
    }
    // 设置账户余额
    public void setBalance(double balance) {
        this.balance = balance;
    }
    // 获取账户余额
    public double getBalance() {
        return this.balance;
    }
}

// 客户端测试代码
class Client {
    public static void main(String args[]) {
        Account acc = new Account("张三", 0.0);  // 创建账户并初始化
        acc.deposit(1000);      // 存款 1000
        acc.withdraw(2000);     // 取款 2000,进入透支状态
        acc.deposit(3000);      // 存款 3000,返回正常状态
        acc.withdraw(4000);     // 取款 4000,再次进入透支状态
        acc.withdraw(1000);     // 取款 1000,进入受限状态
        acc.computeInterest();  // 计算利息
    }
}

优点缺点
#

优点

  1. 简化逻辑控制 将状态处理封装到独立类中,分散复杂的控制逻辑,使得代码结构清晰,便于维护。
  2. 状态与行为分离 通过公共接口分离状态和行为,应用程序只需关注状态切换,而无需关心具体行为处理。
  3. 扩展性强 新增状态只需增加实现类,并在状态维护中加入转换逻辑,扩展方便。
  4. 显式状态转换 独立的状态对象使转换明确,保证状态一致性,避免内部状态不一致问题。

缺点

  1. 类和对象增多 引入多个状态类,增加系统的复杂性和运行开销。
  2. 实现复杂 不当使用可能导致结构复杂,增加设计和维护难度。
  3. 对开闭原则支持有限 新增状态时需修改状态转换逻辑,影响开闭原则。

适用场景

  1. 对象行为依赖其内部状态,且状态变化会引起行为变化。
  2. 代码中存在大量与状态相关的条件判断,影响代码的灵活性和可维护性。

模型扩展
#

共享状态:如果希望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象

// 开关类:管理开关状态
class Switch {
    private State state;  // 当前状态
    private static State onState = new OnState();   // 打开状态, 静态
    private static State offState = new OffState(); // 关闭状态, 静态
    private String name; // 开关名称
    // 构造函数,初始化为打开状态
    public Switch(String name) {
        this.name = name;
        this.state = onState; // 默认状态为打开
    }
    public void setState(State state) {
        this.state = state;
    }
    public void on() {    // 开关打开操作
        System.out.print(name);
        state.on(this);
    }
    public void off() {    // 开关关闭操作
        System.out.print(name);
        state.off(this);
    }
    // 根据类型获取状态
    public static State getState(String type) {
        if (type.equalsIgnoreCase("on")) {
            return onState;
        } else {
            return offState;
        }
    }
}
// 抽象状态类:定义开关状态的行为接口
abstract class State {
    public abstract void on(Switch s);
    public abstract void off(Switch s);
}
class OnState extends State {// 打开状态类
    public void on(Switch s) {
        System.out.println("已经打开!");
    }
    public void off(Switch s) {
        System.out.println("关闭!");
        s.setState(Switch.getState("off"));  // 转换到关闭状态
    }
}
class OffState extends State {// 关闭状态类
    public void on(Switch s) {
        System.out.println("打开!");
        s.setState(Switch.getState("on"));  // 转换到打开状态
    }
    public void off(Switch s) {
        System.out.println("已经关闭!");
    }
}

// 客户端测试代码
class Client {
    public static void main(String[] args) {
        Switch s1 = new Switch("开关1");
        Switch s2 = new Switch("开关2");
        s1.on();  // 开关1 打开
        s2.on();  // 开关2 打开
        s1.off(); // 开关1 关闭
        s2.off(); // 开关2 关闭
        s2.on();  // 开关2 打开
        s1.on();  // 开关1 已经打开
    }
}

简单状态模式:简单状态模式是指状态都相互独立,没有状态转化,遵循“开闭原则”。

可切换状态的状态模式:也就是上面实例中的模式,在具体状态类内部需要调用环境类Context的setState()方法进行状态的转换操作,在具体状态类中可以调用到环境类的方法。

状态模式和观察者模式的区别:这两个模式都是在状态发生改变的时候触发行为,只不过观察者模式的行为是固定的,那就是通知所有的观察者,而状态模式是根据状态来选择不同的处理,当状态发生改变的时候,动态改变行为。

策略模式
#

策略模式的本质:分离算法,选择实现。

结构上和简单状态模式非常相似,策略模式更强调策略的选择,而状态模式强调状态的多变。

image-20241110143340976

// 定义排序策略接口
public interface Sort {
    int[] sort(int[] arr);
}
// ArrayHandler 类:持有 Sort 策略对象
public class ArrayHandler {
    private Sort sortObj;
    // 设置具体的排序策略
    public void setSortObj(Sort sortObj) {
        this.sortObj = sortObj;
    }
    // 调用策略进行排序
    public int[] sort(int[] arr) {
        return sortObj.sort(arr);
    }
}
// 冒泡排序实现类
public class BubbleSort implements Sort {
    public int[] sort(int[] arr) {
        // 省略排序具体实现
        return arr;
    }
}
// 插入排序实现类
public class InsertionSort implements Sort {
    public int[] sort(int[] arr) {
        // 省略排序具体实现
        return arr;
    }
}
// Client 类:客户端代码
public class Client {
    public static void main(String[] args) {
        int[] arr = {1, 4, 6, 2, 5, 3, 7, 10, 9};
        ArrayHandler arrayHandler = new ArrayHandler();
        // 获取具体的排序策略对象
        Sort sort = (Sort) XMLUtil.getBean();
        arrayHandler.setSortObj(sort); // 设置策略
        // 执行排序
        int[] result = arrayHandler.sort(arr);
        // 输出排序结果
        for (int i : result) {
            System.out.print(i + ", ");
        }
    }
}

优点缺点
#

优点

  1. 算法复用:将算法封装在策略类中,多个环境可复用,避免重复代码。
  2. 管理算法族:通过继承共享公共代码,简化算法管理。
  3. 减少条件语句:避免大量条件判断,将算法选择与实现分离。
  4. 替代继承:支持运行时动态切换算法,遵循单一职责原则。
  5. 符合开闭原则:便于在不修改现有代码的情况下扩展新算法。

缺点

  1. 增加复杂度:客户端需了解所有策略并自行选择,增加系统复杂性。
  2. 类数量多:每个策略都需要独立类,类数量随策略增多而增加。
  3. 仅适用于平等算法:不适合嵌套或组合算法的复杂场景。

适用场景

  1. 动态选择算法:系统需在多种算法间切换时。
  2. 避免复杂条件判断:通过策略模式简化多重选择语句。
  3. 隐藏算法细节:封装复杂算法,提升安全性和可维护性。

算法扩展
#

策略模式与状态模式的比较

模板方法模式
#

模板方法模式的本质:固定算法骨架。

模板模式的结构非常简单,如下:

image-20241110153922592

image-20241110154815416

public abstract class BankTemplateMethod {
    public void takeNumber() {
		System.out.println("取号排队。");
	}
	public abstract void transact();	
	public void evaluate() {
		System.out.println("反馈评分。");
	}
	// 模板模式的核心是改变一个过程中的一部分
    public void process() {
        this.takeNumber();
        this.transact();
        this.evaluate();
    }
} 

上面例子就是继承,下面可以看看钩子方法。让子类能够插入或跳过算法中的某些步骤

image-20241114171123735

// 核心在下面的 isValid 中体现钩子方法,这样跳过了父类的 convertData() 
public abstract class HookDemo {
    public abstract void getData();

    public void convertData() {
        System.out.println("通用的数据转换操作。");
    }
    public abstract void displayData();
    public void process() {
        getData();
        if (isValid()) {
            convertData();
        }
        displayData();
    }

    public boolean isValid() {
        return true;
    }
}
class SubHookDemo extends HookDemo {
    public void getData() {
        System.out.println("从XML配置文件中获取数据。");
    }

    public void displayData() {
        System.out.println("以柱状图显示数据。");
    }

    public boolean isValid() {
        return false;
    }
}
class Client {
    public static void main(String a[]) {
        HookDemo hd;

        hd = new SubHookDemo();
        hd.process();
    }
}

作业多的一道题!
#

image-20241113162843197

image-20241114172206398

import java.util.*;

// 回调函数接口
interface Runnable {
    void run();
}

// 事件触发器类
class Trigger {
    // 事件名称与回调函数列表的映射
    private Map<String, List<Runnable>> eventMap = new HashMap<>();
    // 注册事件
    public void on(String eventName, Runnable callback) {
        if (callback != null) {
            eventMap.computeIfAbsent(eventName, k -> new ArrayList<>()).add(callback);
        }
    }
    // 触发事件
    public void raiseEvent(String eventName) {
        System.out.println("Process " + eventName + " start");
        List<Runnable> callbacks = eventMap.get(eventName);
        if (callbacks != null) {
            for (Runnable callback : callbacks) {
                callback.run(); // 执行回调
            }
        }
        System.out.println("Process " + eventName + " end");
    }
}

// 示例回调函数
class Func01 implements Runnable {
    public void run() {
        System.out.println("func01()");
    }
}

class Func02 implements Runnable {
    public void run() {
        System.out.println("func02()");
    }
}

class Func03 implements Runnable {
    public void run() {
        System.out.println("func03()");
    }
}

// 测试代码
public class Main {
    public static void main(String[] args) {
        // 创建事件触发器
        Trigger trigger = new Trigger();
        // 注册事件
        trigger.on("event01", new Func01());
        trigger.on("event01", new Func02());
        trigger.on("event02", new Func02());
        trigger.on("event03", new Func03());
        // 触发事件
        trigger.raiseEvent("event01");
        trigger.raiseEvent("event02");
        trigger.raiseEvent("event03");
        trigger.raiseEvent("event01"); // 再次触发 event01
        trigger.raiseEvent("event02"); // 再次触发 event02
    }
}

Java 语法:
#

1. 深拷贝与浅拷贝
#

浅拷贝
#

浅拷贝是指创建一个新对象,但其中的字段(特别是引用类型的字段)仍然指向原始对象中相同的引用。

  • 特点

    • 基本类型的字段会被复制。
    • 引用类型的字段不会被复制,只是复制引用,两个对象共享同一个引用类型的实例。
  • 实现方式

    1. 实现 Cloneable 接口并重写 clone() 方法。
    2. 使用 super.clone() 来进行浅拷贝。
class Person implements Cloneable {
    String name;
    int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 浅拷贝
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();  // 这是浅拷贝
    }
}
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person("Alice", 25);
        Person p2 = (Person) p1.clone();  // 浅拷贝

        System.out.println(p1.name == p2.name);  // true,引用相同
    }
}

深拷贝
#

深拷贝是指创建一个新对象,并且递归地复制所有字段,包括引用类型字段所引用的对象,使得新对象与原对象完全独立。

  • 实现方式
    1. 手动克隆每一个引用类型的字段。
    2. 使用序列化进行深拷贝(通过将对象序列化再反序列化来实现深拷贝)。
class Address {
    String city;
    
    public Address(String city) {
        this.city = city;
    }
}

class Person implements Cloneable {
    String name;
    int age;
    Address address;
    
    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    
    // 深拷贝
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        cloned.address = new Address(this.address.city);  // 深拷贝,创建新的Address
        return cloned;
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Address address = new Address("New York");
        Person p1 = new Person("Alice", 25, address);
        Person p2 = (Person) p1.clone();  // 深拷贝

        System.out.println(p1.address.city == p2.address.city);  // false,引用不同
    }
}

2. 列表(List)
#

列表(List) 是 Java 中常用的数据结构之一,用于存储有序的元素集合。List 接口有多个实现类,如 ArrayListLinkedList

ArrayList
#

  • 基于动态数组实现,支持快速的随机访问。
  • 插入和删除操作代价较高,尤其是在列表中间进行操作时,因为需要移动元素。
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // 创建一个ArrayList
        List<String> list = new ArrayList<>();
        
        // 添加元素
        list.add("Alice");
        list.add("Bob");
        list.add("Charlie");
        
        // 获取元素
        System.out.println(list.get(1));  // 输出:Bob
        
        // 删除元素
        list.remove(0);
        
        // 遍历列表
        for (String name : list) {
            System.out.println(name);
        }
    }
}

3. 哈希表(HashMap)
#

哈希表(HashMap) 是 Java 中用于存储键值对的常用数据结构之一。它基于哈希函数实现,能够在常数时间内完成插入、删除和查找操作。

HashMap 的常用操作
#

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        // 创建一个HashMap
        Map<String, Integer> map = new HashMap<>();
        
        // 添加键值对
        map.put("Alice", 25);
        map.put("Bob", 30);
        map.put("Charlie", 35);
        
        // 获取值
        System.out.println(map.get("Alice"));  // 输出:25
        
        // 删除键值对
        map.remove("Bob");
        
        // 遍历HashMap
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

4. 关键字:
#

abstract:用于声明抽象类或抽象方法,不能直接实例化。

abstract class Animal { public abstract void sound(); }

interface:用于声明接口,类通过 implements 实现接口中的方法。

interface Animal { void sound(); }

extends:用于声明类的继承关系,子类继承父类的属性和方法。

class Dog extends Animal { void sound() { System.out.println("Bark"); } }

implements:用于声明类实现接口,一个类可以实现一个或多个接口。

class Dog implements Animal { public void sound() { System.out.println("Bark"); } }

public、protected、private:用于声明类、方法或变量的访问权限。

public class Animal { protected String name; private void sound() { } }

static:用于声明类变量或类方法,不依赖对象实例访问。

public static int count = 0;  // 静态变量

final:用于声明常量、不可继承的类、不可重写的方法。

public final class Constants { public static final double PI = 3.14; }

super:用于引用父类的构造方法或方法。

class Dog extends Animal { Dog() { super.sound(); } }

this:用于引用当前对象,通常用于区分类的成员变量与局部变量。

class Animal { private String name; Animal(String name) { this.name = name; } }

Serializable:原型模式深拷贝,序列化基类。

public class Attachment implements Serializable

Cloneable:原型模式浅拷贝,可以克隆基类。

public class Email implements Cloneable

5. 输入输出
#

        // 创建 Scanner 对象,用于从控制台读取输入
        Scanner scanner = new Scanner(System.in);
        // 读取字符串
        System.out.print("Enter your name: ");
        String name = scanner.nextLine();

简答题总结
#

2018
#

为什么优先使用对象组合而不是类继承 因为继承会使类之间高度耦合,组合确实比继承更加灵活,也更有助于代码维护。

请举例说明你对创建型设计模式的理解 创建型模式分为5种:简单工厂模式、工厂模式、抽象工厂模式、原型模式、单例模式。它重点在于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。

请举例说明哪些情况下可以使用组合模式 宏命令,MVC中的View图,文件夹的管理

抽象工厂模式和原型模式有哪些相同和不同,可以联合使用吗? 抽象工厂模式:通常由工厂方法模式来实现。但一个工厂中往往含有多个工厂方法生成一系列的产品。这个模式强调的是客户代码一次保证只使用一个系列的产品。当要切换为另一个系列的产品,换一个工厂类即可。 原型模式:工厂方法的最大缺点就是,对应一个继承体系的产品类,要有一个同样复杂的工厂类的继承体系。原型模式中的工厂方法为clone,它会返回一个拷贝(可以是浅拷贝,也可以是深拷贝,由设计者决定)。为了保证用户代码中到时可以通过指针调用clone来动态绑定地生成所需的具体的类。这些原型对象必须事先构造好。 原型模式相对工厂方法模式的另一个好处是,拷贝的效率一般对构造的效率要高。 二者可以联合成为原型工厂模式

请给出一个适合使用策略模式的示例场景,并说明与不使用策略模式相比,使用策略模式有哪些优点 超市的各种折扣策略。 策略模式满足“开闭原则”,超市可以在不修改原有系统的基础上选择算法和行为,对现有优惠策略的变更非常方便。策略模式可以避免使用多重条件转移语句

2017
#

请举例说明你对白箱复用和黑箱复用的理解 继承就是白箱复用,对于父类的封装性不好;组合时黑箱复用,调用者并不清楚依赖对象的实现细节。

GOF设计模式有几种类型,分别包括那些模式 23种,分别是创建型模式(5种)、结构型模式(7种)、行为型模式(11种)

请举例说明桥接模式如何将抽象部分与它的实现部分分离,使它们可以独立地变化 假如你有一个几何形状Shape类,从它能扩展出两个子类: 圆形Circle和 方形Square 。 你希望对这样的类层次结构进行扩展以使其包含颜色,所以你打算创建名为红色Red和蓝色Blue的形状子类。 如图所示的桥接模式uml图实现了在颜色和形状两个维度上的划分:

简述组合模式和装饰模式的基本思路,并说明两者有何异同 组合模式是组合多个对象形成树形结构以表示’整体-部分’的关系层次,组合模式对单个对象和组合对象的使用具有一致性。 装饰模式则强调能给一个对象增加一些额外的职责和功能。 二者都属于结构型模式,意在将类或者对象结合在一起形成更大的结构。 组合模式相较于装饰模式,实现对象往往含有树形结构,并且引入了抽象容器构建类使得客户端能以一致的方式处理树形结构中的叶子节点和容器节点。

2016
#

简述什么是硬编码,并说明硬编码有什么缺点? 硬编码是将数据直接嵌入到程序或其他可执行对象的源代码中的软件开发实践。其特点有: ①硬编码数据通常只能通过编辑源代码和重新编译可执行文件来修改 ②硬编码的数据通常表示不变的信息,例如物理常量,版本号和静态文本元素。

简述类适配器模式和对象适配器模式的基本思想,并说明两者有和异同 二者的基本思想都是:将一个类的接口变成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。 不同在于对象适配器——将Adapter类作修改,不是继承Adaptee类,而是持有Adaptee的实例,以解决兼容性的问题。对象适配器更符合“合成复用原则”(系统中尽量使用关联关系来替代继承关系)。

简述工厂方法模式和抽象工厂模式的基本思想,并说明两者有何异同 工厂模式:定义一个用于创建对象的借口,让子类决定实例化哪一个类。 抽象工厂模式:为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。 基本思想都是将对象的创建与使用分离。 区别在于:产品是否单一,如果产品单一,最合适用工厂模式,但是如果有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。再通俗深化理解下:工厂模式针对的是一个产品等级结构 ,抽象工厂模式针对的是面向多个产品等级结构的。

简述状态模式和策略模式的基本思想,并说明两者有何异同? 策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。 状态模式策略模式很相似,都将类的"状态"封装了起来,在执行动作时进行自动的转换,从而实现,类在不同状态下的同一动作显示出不同结果。 但状态模式与策略模式的区别在于,这种转换是"自动",“无意识"的。

现在大多数软件都有撤销和重做的功能,请问这种功能一般是草用什么设计模式实现的,并举例说明是如何实现的。 主要使用的是命令模式 。

2015
#

面向对象软件设计中,给一个类或对象增加行为(新功能)的方式有哪些?请举例说明 继承机制:使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法,但是这种方法是静态的,用户不能控制增加行为的方式和时机。

关联机制:将一个类的对象嵌入另一个对象中,由另一个对象来调用嵌入对象的行为同时扩展其行为,我们称另一个对象为装饰器。

装饰模式:装饰者模式允许动态地为对象增加新的功能,而无需修改现有类的代码。这种模式通过将对象封装到另一个对象中来实现扩展。

简述外观模式和中介者模式的基本思想,并说明两者有何异同? 外观模式:为子系统的一组接口提供一个统一的入口。 中介者模式:用一个中介对象来封装一系列的对象交互,使原有个对象之间不需要显示地相互引用,从而使其耦合松散。 外观模式封装的是子系统外部和子系统内部模块间的交互;而中介者模式是提供多个平等的同事对象之间交互关系的封装,一般是用在内部实现上。也就是说外观模式是外部到系统内部的单向的交互,中介者模式是内部多个模块的多向的交互。

请给出一个适合使用状态模式的示例场景,并说明与不适用状态模式相比,有哪些优点? 在标准大气压条件下,水在温度低于0℃为固态,0~100℃为液态,大于100℃为气态。 用状态模式的优点: ①封装了状态的转换规则,可以对相应代码进行集中管理 ②将与状态相关的行为放在一个类,只需要注入不同状态即可让环境对象拥有不同的行为 ③状态模式避免了使用庞大的条件语句与转换代码

你认为设计模式能否增强软件复用性?请列举两个模式并结合实例进行说明。 能。1. 工厂模式(Factory Pattern)

工厂模式将对象的创建逻辑集中在工厂类中,客户端只需通过工厂获取对象,而无需直接实例化。这样,创建逻辑可以复用,且扩展时只需修改工厂类,不影响客户端代码。

示例

class ShapeFactory:
    def create_shape(self, shape_type):
        if shape_type == "circle":
            return Circle()
        if shape_type == "rectangle":
            return Rectangle()

通过工厂模式,不同的客户端都可以复用同一工厂创建对象,扩展时无需修改客户端代码。

策略模式(Strategy Pattern)

策略模式将算法封装在独立的类中,允许在运行时动态替换算法。不同的策略可被不同客户端复用,同时新策略添加时无需修改现有代码。

示例

class ShoppingCart:
    def __init__(self, strategy):
        self.strategy = strategy
    def checkout(self, amount):
        self.strategy.pay(amount)

策略模式使得不同的支付方式可灵活替换和复用,扩展性强。

这两种模式均通过解耦和模块化设计提高了代码的维护性和复用性。

2014.2
#

在面向对象设计时,采用水平关联(含依赖、组合、聚合、普通关联等)的方法或者继承都可以适应变化。请说明两种方式在适应变化上各自的优缺点。 组合关系—— 优点①不破环封装性,类之间松耦合,彼此相对独立 ②具有较好的可扩展性 ③在运行时,整体对象可以选择不同类型的局部对象 ④整体可以对局部进行包装,封装局部类的接口提供新的接口 缺点①整体类不能自动获得和局部类同样的接口 ②创建整体类的对象时需要创建所有局部类的对象

继承关系—— 优点①子类能自动继承父类的接口 ②子类对象创建时,无需创建父类对象 缺点①破坏封装性,父类子类之间紧耦合,子类依赖父类实现,缺乏独立性 ②支持扩展,但往往以增加系统结构为代价 ③不支持动态继承,在运行时子类无法选择不同的父类 ④子类不能改变父类的接口

变化整体上分为接口变化和实现变化。若这两类变化同时存在,通常应首先采用那种设计模式分离稳定与变化部分?举例说明如何设计。 工厂方法、桥接模式、策略模式都可以。 所谓的变化部分我们可以理解为接口或者类的扩展,而呈现给客户的只是一个统一的接口,用户不必关心内部如何实现,只要每次调用同一个方法,只传不同即可,而针对接口的变化,采用以上结合适配器模式即可转换关键代码部分。

请举例说明什么是简单工厂方法?什么是单件(单例)模式?分别是如何实现的? 简单工厂模式可以根据参数的不同返回不同类的实例。 单例模式是确保莫一个类只有一个实例,并且能自行实例化并向这个系统提供这个实例。

线性聚集关系是一种现实中常见的逻辑关系,如一个A聚集多个B,一个B聚集多个C,…….。使用合成模式可以统一的接口访问各个部分。请给出本例应用合成模式的设计类图。子类聚集父类时该模式的特点之一,请再说出也具有该特点的其它几个模式的名称。 装饰模式、观察者模式

为达到某些设计目的,设计时可以同时组合使用多个设计模式。请举出一个同时组合使用策略模式和对象适配器模式。要求给出例子的应用场景,设计方案。 应用场景:User中提供给用户的接口是一个普通的按钮,通过点击按钮实现排序算法,该排序算法可能在不同的时间发生改变(用户可以实现其他算法,按钮也可能变成倒计时等形式触发算法行为)。

2014.3
#

软件公司要开发一个图形界面组件库,界面组件分为两大类,一类是单元控件(例如:按钮、文本框等),一类是容器控件,例如对话框、表单等,请问采用何种模式设计比较好?请画出UML设计类图 组合模式,

化妆品公司的报表系统可将不同月份的销售数据以柱状图、曲线图和饼状图等多种形式展示出来。各个月份的销售数据可以从文本文件中获取,也可以从数据库中获取,还可以从Excel文件中获取,如果需要从Excel文件中获取数据,则需要调用与Excel相关的API,而这个API是现有系统所不具备的,该API由厂商提供。请问采用何种模式设计比较好?请画出UML设计类图。 桥接模式+适配器模式

你认为面向对象方法中类设计的难点是什么,如何应对? 难点:设计可复用的类要求必须找到相关的对象,以适当的粒度归类,在定义类的接口和继承层次,建立对象间的基本关系。你的设计应该对手头的问题有针对性,同时对将来的问题和需求也要有足够的通用性。你也希望避免重复设计或尽可能少做重复设计。 应对方法:学习设计模式,用更加简单方便的设计和体系结构。

请阐述你对开闭原则的理解,以及如何设计能达到开闭原则的要求? 理解:软件实体(模块、类、方法)应对扩展开放,对修改关闭,可以让软件系统复用,并易于维护。 设计方法: ①对软件系统中不变的部分加以抽象成不变的接口,以应对未来的扩展。 ②接口最小功能设计原则:原有的接口要么可以应对未来的扩展,不足的部分可以通过定义新的接口来实现。 ③模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。 综上,软件系统是否具有良好的接口(抽象)设计是判断软件系统是否满足开闭原则的一种重要的判断基准。

请举例说明在应用标准外观模式时可能产生的问题,以及对应的解决方案。 存在的问题是:可能通过继承一个外观类在子系统中加入了新的行为。

银行信用卡中心的电子账单系统包括了发送器、账单、广告信和发送队列等对象,其中广告信的业务逻辑是:先到数据库中把客户信息一个一个的取出,每个客户都生成一份个性化的邮件,然后交给发送机进行发送处理。请问使用哪种模式设计广告信较好?如何设计? 每个客户都要发送一个个性化邮件,所以使用原型模式

开发一个用于数值计算的大型程序库,他的功能是强大的,但需要使用者具备较高的数学专业知识。为方便菜鸟级用户的使用,需要提供一个简化版的,但同时又不希望影响专业人员的使用。使用那种设计模式较好?如何设计? 外观模式

在2D游戏的开发中,需要绘制地图,地图由大量的“一样的图块”拼接而成,而“图块”的种类有限,目前有草地、海洋、沙漠、山地等,未来肯定要增加种类,但不会太多太频繁。采用何种模式设计“图块”类比较好?如何设计? 人物构成要素复杂,应该用建造者模式,地图块只有有限的几种,一整个地图上大面积重复,应该用享元


其他
#

当一个系统要由多个产品系列中的一个来配置时,我们选择抽象工厂模式

当创建复杂对象的算法应该独立于该对象的组成部分以及他们的装配方式时,我们选择建造者模式

当一个类希望由它的子类来指定它所创建的对象的时候,我们选择工厂模式

当一个系统应该独立于它的产品创建、构成和表示时,要使用抽象工厂模式

当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时,要使用单例模式

当你想使用一个已经存在的类,而他的接口不符合你的需求时,要使用适配器模式

当你想表示对象的部分-整体层次结构,要使用组合模式

当不能采用生成子类的方法进行扩充时,要使用装饰模式

当你要为一个复杂子系统提供一个简单接口时,要使用外观模式

一个应用程序使用了大量的对象,要使用享元模式

在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用代理模式

你想在不明确指定接收者的情况下,向对各对象中的一个提交一个请求时,要使用职责链模式

访问一个聚合对象的内容而无需暴露它的内部表示,要使用迭代器模式

一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解,要使用中介者模式

当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变,要使用观察者模式

一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为,要使用状态模式

相关文章

机器学习 - 应试笔记
·4954 字·10 分钟
应试笔记 机器学习
操作系统 - 应试笔记
·6271 字·13 分钟
应试笔记 操作系统
从记忆化搜索到动态规划
·645 字·2 分钟
算法与数据结构 记忆化搜索 动态规划
“二分查找” 总结与例题
·3279 字·7 分钟
算法与数据结构 二分 模板