当前位置: 首页 > news >正文

系统架构设计师:设计模式——创建型设计模式

一、创建型设计模式

创建型模式抽象了实例化过程,它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。

随着系统演化得越来越依赖于对象复合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(hard-coding)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类。

在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。整个系统关于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在什么被创建,谁创建它,它是怎样被创建的,以及何时创建这些方面给予了很大的灵活性。它们允许用结构和功能差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。

(一)Abstract Factory 模式

1.模式名称

Abstract Factory,也经常称之为抽象工厂模式。

2.意图解决的问题

在程序中创建一个对象似乎是不能再简单的事情,其实不然。在大型系统开发中存在以下问题:
(1)object new ClassName 是最常见的创建对象方法,但这种方法造成类名的硬编码,需要根据不同的运行环境动态加载相同接口但实现不同的类实例,这样的创建方法就需要配合上复杂的判断,实例化为不同的对象。

(2)为了适用于不同的运行环境,经常使用抽象类定义接口,并在不同的运行环境中实现这个抽象类的子类。普通的创建方式必然造成代码同运行环境的强绑定,软件产品无法移植到其他的运行环境。

抽象工厂模式就可以解决这样的问题,根据不同的配置或上下文环境加载具有相同接口的不同类实例。

3.模式描述

在这里插入图片描述
就如同抽象工厂的名字一样,Abstract Factory 类将接受 Client 的“订单”——Client 发送过来的消息,使用不同的“车间”——不同的 Concrete Factory,根据已有的“产品模型”——Abstract Product,生产出特定的“产品”——Product。

不同的车间生产出不同的产品供客户使用,车间与产品的关系是一一对应的。由于所有的产品都遵循产品模型——Abstract Product,具有相同的接口,所以这些产品都可以直接交付客户使用。

在抽象工厂模式中,Abstract Factory 可以有多个类似于 Create Product()的虚方法,就如同一个工厂中有多条产品线一样。Create Product1()创建产品线 1,Create Product2()创建产品线 2。

事实上,如果仔细观察AbstractFactory 模式就可以发现,对于 Client 来说,最关注的就是在不同条件下获得接口一致但实现不同的对象,只要避免类名的硬编码,采用其他方式也可以实现。

所以也可以采用其他的方式实现。例如在 Java 中就可以采用接口的方式实现。

(1)定义抽象产品接口
// 抽象产品A
public interface AbstractProductA {void methodA();
}// 抽象产品B
public interface AbstractProductB {void methodB();
}
(2) 实现具体产品类
// 具体产品A1
public class ProductA1 implements AbstractProductA {@Overridepublic void methodA() {System.out.println("Product A1");}
}// 具体产品A2
public class ProductA2 implements AbstractProductA {@Overridepublic void methodA() {System.out.println("Product A2");}
}// 具体产品B1
public class ProductB1 implements AbstractProductB {@Overridepublic void methodB() {System.out.println("Product B1");}
}// 具体产品B2
public class ProductB2 implements AbstractProductB {@Overridepublic void methodB() {System.out.println("Product B2");}
}
(3)定义抽象工厂接口
public interface AbstractFactory {AbstractProductA createProductA();AbstractProductB createProductB();
}
(4) 实现具体工厂类
// 具体工厂1
public class ConcreteFactory1 implements AbstractFactory {@Overridepublic AbstractProductA createProductA() {return new ProductA1();}@Overridepublic AbstractProductB createProductB() {return new ProductB1();}
}// 具体工厂2
public class ConcreteFactory2 implements AbstractFactory {@Overridepublic AbstractProductA createProductA() {return new ProductA2();}@Overridepublic AbstractProductB createProductB() {return new ProductB2();}
}
(5)客户端代码
public class Client {public static void main(String[] args) {// 使用具体工厂1创建产品AbstractFactory factory1 = new ConcreteFactory1();AbstractProductA productA1 = factory1.createProductA();AbstractProductB productB1 = factory1.createProductB();productA1.methodA();productB1.methodB();// 使用具体工厂2创建产品AbstractFactory factory2 = new ConcreteFactory2();AbstractProductA productA2 = factory2.createProductA();AbstractProductB productB2 = factory2.createProductB();productA2.methodA();productB2.methodB();}
}

4.场景举例

(1)场景描述

假设你正在开发一个报告生成工具,该工具允许用户输入数据并生成包含图表和表格的报告。用户可以选择输出格式为PDF或者Word文档。每种输出格式都需要特定风格的图表和表格来确保视觉一致性。例如,PDF格式可能要求图表使用矢量图形以保持高质量打印效果,而Word文档可能偏好位图格式以便于编辑。

(2)遇到的困难
  • 多格式支持:为了适应不同的输出需求,你需要分别为PDF和Word文档实现各自的图表和表格生成逻辑。直接针对每个格式编写代码会导致大量的重复工作,并且维护成本高。
  • 切换格式不便:如果想要添加对新格式的支持(比如HTML),你需要修改大量代码来适配新的格式,这增加了出错的可能性以及维护难度。
  • 代码耦合度高:在业务逻辑中直接创建具体图表和表格类的实例会使代码高度依赖于具体的实现细节,降低了代码的灵活性和可复用性。
(3)抽象工厂模式如何解决问题

抽象工厂模式通过定义一组相关对象的接口,但让子类决定究竟要实例化哪些类来解决上述问题。在这个例子中:

  • 定义抽象工厂:首先,你可以定义一个抽象工厂ReportElementFactory,它声明了创建图表(createChart)和表格(createTable)的方法。
  • 具体工厂实现:然后,针对每种输出格式实现具体的工厂类,比如PdfElementFactory和WordElementFactory。这些工厂类实现了抽象工厂中的方法,用于创建符合各自格式规范的图表和表格。
  • 客户端使用:当用户选择输出格式后,系统根据所选格式选择相应的具体工厂实例。之后,所有的图表和表格都通过这个工厂实例来创建,而不需要知道它们具体的类型是什么。

例如,在生成PDF报告时,系统会使用PdfElementFactory来创建适合PDF格式的图表和表格;而在生成Word文档时,则使用WordElementFactory来创建适合Word格式的图表和表格。

这样做的好处是,当你需要添加对一种新格式(如HTML)的支持时,只需创建一个新的具体工厂类并实现相应的图表和表格创建方法即可,无需改动现有的业务逻辑代码。这种方式不仅简化了代码结构,还提高了系统的灵活性和扩展性,同时减少了维护成本。此外,由于业务逻辑与具体元素创建分离,提高了代码的可复用性和灵活性。

为了更直观地展示抽象工厂模式的应用效果,下面将分别给出不使用抽象工厂模式和使用抽象工厂模式的代码对比。我们将以生成报告中的图表和表格为例。

(4)不使用抽象工厂模式

在这个例子中,直接在业务逻辑中创建具体的图表和表格对象,这将导致代码高度依赖于具体实现细节,增加了维护成本。

// PDF格式图表和表格
class PdfChart {public void display() {System.out.println("显示PDF格式图表");}
}class PdfTable {public void display() {System.out.println("显示PDF格式表格");}
}// Word格式图表和表格
class WordChart {public void display() {System.out.println("显示Word格式图表");}
}class WordTable {public void display() {System.out.println("显示Word格式表格");}
}public class ReportGenerator {public static void generatePdfReport() {PdfChart chart = new PdfChart();PdfTable table = new PdfTable();chart.display();table.display();}public static void generateWordReport() {WordChart chart = new WordChart();WordTable table = new WordTable();chart.display();table.display();}public static void main(String[] args) {String reportType = "pdf"; // 假设用户选择了PDF报告if ("pdf".equals(reportType)) {generatePdfReport();} else if ("word".equals(reportType)) {generateWordReport();}}
}
(5)使用抽象工厂模式

通过引入抽象工厂模式,我们可以解耦具体图表和表格的创建过程与业务逻辑,使得添加新格式支持变得更加容易。

首先,定义抽象产品和抽象工厂:

// 抽象产品:图表和表格
interface Chart {void display();
}interface Table {void display();
}// 抽象工厂:用于创建图表和表格
interface ReportElementFactory {Chart createChart();Table createTable();
}// 总结:增加了定义抽象产品的抽象类或接口的代码,增加了定义抽象工程的抽象类或接口的代码

然后,为每种格式实现具体的工厂和产品类:

// PDF格式的具体产品
class PdfChart implements Chart {@Overridepublic void display() {System.out.println("显示PDF格式图表");}
}class PdfTable implements Table {@Overridepublic void display() {System.out.println("显示PDF格式表格");}
}// PDF格式的具体工厂
class PdfElementFactory implements ReportElementFactory {@Overridepublic Chart createChart() {return new PdfChart();}@Overridepublic Table createTable() {return new PdfTable();}
}// Word格式的具体产品
class WordChart implements Chart {@Overridepublic void display() {System.out.println("显示Word格式图表");}
}class WordTable implements Table {@Overridepublic void display() {System.out.println("显示Word格式表格");}
}// Word格式的具体工厂
class WordElementFactory implements ReportElementFactory {@Overridepublic Chart createChart() {return new WordChart();}@Overridepublic Table createTable() {return new WordTable();}
}
// 总结:增加了具体产品要继承或实现抽象产品的代码,增加了具体工厂类的代码

最后,在客户端代码中使用抽象工厂:

public class ReportGeneratorWithAbstractFactory {public static void generateReport(ReportElementFactory factory) {Chart chart = factory.createChart();Table table = factory.createTable();chart.display();table.display();}public static void main(String[] args) {String reportType = "pdf"; // 假设用户选择了PDF报告ReportElementFactory factory;if ("pdf".equals(reportType)) {factory = new PdfElementFactory();} else if ("word".equals(reportType)) {factory = new WordElementFactory();} else {throw new UnsupportedOperationException("不支持的报告类型");}generateReport(factory);}
}
// 总结:减少了各具体产品调用过程的代码

5.效果

对比分析:

(1)代码量

使用抽象工厂模式会引入更多的类和接口,从而增加了一些初始的代码量,所以如果代码不复杂,不要随便引入抽象工厂模式。

使用抽象工厂模式通常意味着引入额外的抽象层(如抽象产品和抽象工厂),这增加了代码的复杂性和理解难度,特别是对于那些不熟悉该模式的人来说。在项目初期或者规模较小的应用中,这种额外的复杂度可能并不值得。

减少的代码为具体调用过程的代码,从代码量来看,如果接口的实现类有不少于3种,使用抽象工厂模式才能从中受益。

(2)一致性保证

使用抽象工厂模式之后,因为要继承抽象工厂类和抽象产品类,可以为扩展编写新的类型的功能提供参考。抽象工厂同时确保了产品族的一致性。比如,在我们的报告生成工具例子中,它确保了为特定输出格式生成的所有图表和表格都属于同一风格。

虽然抽象工厂模式允许轻松地替换整个产品系列,但它也对新产品的添加施加了一定的限制。每当你想要向系统中添加一个新的产品族时,你必须更新抽象工厂接口以支持新的产品类型。如果系统已经非常庞大,这项工作可能会变得相当繁琐。

(3)解耦合

抽象工厂模式通过将对象的创建过程与使用过程分离,减少了业务逻辑对具体实现的依赖。这意味着你可以更容易地替换或更新产品族中的任何一个部分,而不需要修改使用这些产品的业务逻辑代码。

当然,有时开发者可能会倾向于使用抽象工厂模式解决那些实际上不需要这么复杂解决方案的问题。这样做会导致不必要的复杂性和开销。

(4)扩展性

当你需要支持新的产品系列时(例如,在我们的例子中添加HTML格式的支持),你只需要添加新的具体工厂和产品类即可,而无需改动现有的客户端代码。这使得系统更加灵活且易于扩展。

抽象工厂模式假定产品族中的所有产品都是配套使用的,这意味着如果你需要在运行时改变产品组合(例如,在同一个应用中同时使用来自不同产品族的对象),则会比较困难。

(5)滥用抽象工厂模式的例子

假设你正在开发一个简单的命令行工具,这个工具的主要功能是从文本文件读取数据并进行简单处理。在这个场景下,如果你为了支持将来可能的“不同的文件格式”或“不同的输出方式”,就提前设计了一个复杂的抽象工厂模式结构,那么这就属于滥用抽象工厂模式的情况。具体来说:

  • 为每个可能的文件输入格式(比如CSV, JSON等)和输出格式(控制台输出、文件输出等)都设计了相应的具体工厂和产品。
  • 在当前阶段,你的应用程序仅需支持一种文件格式和一种输出方式,因此这些额外的设计和抽象实际上是多余的。
  • 这样的设计不仅增加了项目的复杂度,而且使得维护成本上升,因为每次你想添加一个小的功能改进时,都需要考虑如何适应现有的复杂架构。

在这种情况下,直接实现必要的功能,而不使用任何设计模式可能是更合适的选择。随着项目的发展,如果确实需要支持多种输入输出格式,可以逐步引入更复杂的设计模式。这样不仅可以保持项目的简洁性,也能确保每一部分的设计都是必要的和有效的。总之,选择设计模式应该基于实际需求,而不是预先假设的需求。

(6)总结

应用 Abstract Factory 模式可以实现对象可配置的、动态的创建。灵活运用 Abstract Factory 模式可以提高软件产品的移植性,尤其是当软件产品运行于多个平台,或有不同的功能配置版本时,抽象工厂模式可以减轻移植和发布时的压力,提高软件的复用性。

(二)Builder 模式

1. 模式名称

Builder模式(建造者模式)

2. 意图解决的问题

Builder模式主要用于解决在创建复杂对象时,构造过程需要逐步构建的情况。当一个对象的创建算法应该独立于其组成部分以及它们的装配方式时,使用Builder模式是非常有用的。此外,它还允许对构造过程进行更精细的控制,而不只是简单地调用构造函数。

具体来说,Builder模式可以解决以下问题:

  • 当对象的创建过程非常复杂,包含多个步骤,并且不是所有步骤都是必需的时候。
  • 需要生成不同表示的对象,这些对象之间可能只有细微的差异。
  • 希望避免构造函数参数列表过长的问题,尤其是当许多参数是可选的时候,这可以提高代码的可读性和健壮性。

3. 模式描述

Builder模式是一种创建型设计模式,它提供了一种灵活的方式用于构建复杂的对象。该模式通过将一个复杂对象的构建与其表现分离,使得同样的构建过程可以创建不同的表现形式。

在这里插入图片描述

核心组成

  • Builder(抽象建造者):提供了一个接口或抽象类,定义了创建产品各个部分的方法。这使得你可以轻松地扩展新的具体建造者来支持不同类型的构建逻辑。
  • ConcreteBuilder(具体建造者):实现Builder接口,定义并明确自己所负责创建的部分产品,构造和装配该产品的特定部分。
  • Director(指挥者):类封装了使用建造者对象的构建算法。它允许你改变产品的内部表示而不影响客户端代码。此外,指挥者还提供了一种控制构建过程的方式,确保按照一定的顺序进行构建。
  • Product(产品角色):表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程,包含定义组成部件的类,包括将这些部件装配成最终产品的接口。

工作流程

  • 客户端创建Director对象,并根据需求选择合适的ConcreteBuilder。
  • Director通知ConcreteBuilder开始构建产品。
  • ConcreteBuilder处理产品的构建细节,并返回构建完成的产品给Director。
  • 最终,Director将构建好的产品返回给客户端。
(1) 定义产品类(Product)
public class Product {private List<String> parts = new ArrayList<>();public void add(String part) {parts.add(part);}public void show() {System.out.println("Product parts: " + String.join(", ", parts));}
}
(2)定义抽象建造者(Builder)
public interface Builder {void buildPartA();void buildPartB();Product getResult();
}
(3) 定义具体建造者(ConcreteBuilder)
public class ConcreteBuilder implements Builder {private Product product = new Product();@Overridepublic void buildPartA() {product.add("Part A");}@Overridepublic void buildPartB() {product.add("Part B");}@Overridepublic Product getResult() {return product;}
}
(4) 定义指挥者(Director)
public class Director {private Builder builder;public Director(Builder builder) {this.builder = builder;}public void construct() {builder.buildPartA();builder.buildPartB();}
}
(5) 客户端代码
public class Client {public static void main(String[] args) {// 创建具体建造者和指挥者Builder builder = new ConcreteBuilder();Director director = new Director(builder);// 指挥者指导建造者构建产品director.construct();// 获取最终构建的产品Product product = ((ConcreteBuilder) builder).getResult();product.show();  // 输出:Product parts: Part A, Part B}
}
(6)示例代码

假设我们要构建一辆汽车,汽车的构造涉及多个步骤,如安装引擎、车轮等。我们可以使用Builder模式来简化这个过程:

// Product
class Car {private String engine;private int wheelCount;public void setEngine(String engine) { this.engine = engine; }public void setWheelCount(int wheelCount) { this.wheelCount = wheelCount; }@Overridepublic String toString() {return "Car with engine: " + engine + " and wheels: " + wheelCount;}
}// Builder
abstract class CarBuilder {protected Car car;public Car getCar() { return car; }public void createNewCarProduct() { car = new Car(); }public abstract void buildEngine();public abstract void buildWheels();
}// ConcreteBuilder
class SportsCarBuilder extends CarBuilder {@Overridepublic void buildEngine() { car.setEngine("V8"); }@Overridepublic void buildWheels() { car.setWheelCount(4); }
}// Director
class Director {private CarBuilder builder;public void setBuilder(CarBuilder builder) { this.builder = builder; }public Car constructCar() {builder.createNewCarProduct();builder.buildEngine();builder.buildWheels();return builder.getCar();}
}// 客户端代码
public class Client {public static void main(String[] args) {Director director = new Director();SportsCarBuilder sportsCarBuilder = new SportsCarBuilder();director.setBuilder(sportsCarBuilder);Car car = director.constructCar();System.out.println(car);}
}

4.场景举例

(1)场景描述

假设我们正在开发一个餐厅点餐系统,顾客可以通过该系统选择各种食品和饮料来组成他们的订单。每个订单可以包含多个菜品和饮料,每个项目都有自己的属性(例如,汉堡可能有多种口味、配料可选;饮料有不同的大小和温度选项)。最终,我们需要根据顾客的选择构建一个完整的订单。

(2)遇到的困难:
  • 构造函数参数过多:如果使用传统的构造方法来创建订单对象,由于需要设置很多可选参数(如汉堡的口味、额外添加的配料、饮料的大小等),这将导致构造函数参数列表非常长且难以管理。
  • 代码可读性和维护性差:当有大量参数时,调用构造函数变得复杂,难以理解各个参数的具体含义,尤其是在存在多个可选参数的情况下。此外,随着业务逻辑的变化(比如新增或修改某些选项),构造函数也需要频繁更新,增加了维护成本。
  • 对象状态一致性问题:在某些情况下,某些组合可能是无效的(例如,某款汉堡不支持某种特定的配料)。通过普通的构造方法,很难确保对象被正确地初始化为有效状态。
(3)Builder模式如何解决问题:

Builder模式提供了一种灵活的方法来逐步构建复杂的对象,同时保持代码的清晰度和易维护性。下面是如何应用Builder模式解决上述问题的例子。

定义产品类(Product)

public class Order {private final String burgerType;private final List<String> toppings;private final String drinkType;private final String drinkSize;// 私有构造器private Order(Builder builder) {this.burgerType = builder.burgerType;this.toppings = builder.toppings;this.drinkType = builder.drinkType;this.drinkSize = builder.drinkSize;}public static class Builder {private String burgerType;private final List<String> toppings = new ArrayList<>();private String drinkType;private String drinkSize;public Builder setBurgerType(String burgerType) {this.burgerType = burgerType;return this;}public Builder addTopping(String topping) {this.toppings.add(topping);return this;}public Builder setDrinkType(String drinkType) {this.drinkType = drinkType;return this;}public Builder setDrinkSize(String drinkSize) {this.drinkSize = drinkSize;return this;}public Order build() {// 可以在这里进行有效性检查if (burgerType == null || drinkType == null || drinkSize == null) {throw new IllegalStateException("Order is missing required fields");}return new Order(this);}}
}

使用Builder模式创建对象

public class Main {public static void main(String[] args) {Order order = new Order.Builder().setBurgerType("Cheese Burger").addTopping("Lettuce").addTopping("Tomato").setDrinkType("Coke").setDrinkSize("Large").build();System.out.println(order);}
}

5.效果

在springboot的项目中Lombok的@Builder注解实现了一种简化版的建造者模式(Builder Pattern),它主要用于简化对象的创建过程,特别是对于那些有许多参数或可选参数的对象。

与传统的建造者模式相比,Lombok的@Builder注解提供了更加简洁和方便的使用方式,但它们的核心理念是一致的:都是为了提供一种更灵活、更易读的方式来构造复杂对象。

区别分析

(1)实现细节

传统建造者模式:需要显式地定义一个建造者类,该类包含逐步构建产品所需的方法,并最终返回构建完成的产品实例。这通常涉及更多的样板代码,如定义抽象建造者接口、具体建造者类以及指挥者类等。
Lombok的@Builder注解:通过在类上添加@Builder注解,Lombok会在编译时自动生成必要的建造者逻辑,包括建造者类、建造方法、以及构建完成后的调用方法。开发者不需要手动编写这些样板代码,大大简化了实现过程。

(2)使用场景

传统建造者模式:适用于非常复杂的对象构建过程,尤其是当构建步骤较多且可能变化时。此外,它还允许你为不同的构建过程定义多个具体的建造者,从而支持更多样化的对象配置。
Lombok的@Builder注解:更适合于具有多个属性或有若干可选属性的对象创建。它的主要目的是减少冗长的构造函数和setter方法,使代码更加简洁明了。

(3) 灵活性

传统建造者模式:由于其结构化的设计,可以更容易地扩展和修改构建逻辑。例如,你可以轻松地添加新的构建步骤或者改变现有步骤的顺序。
Lombok的@Builder注解:虽然提供了便捷性,但在灵活性方面略逊一筹。比如,如果你想对构建过程进行一些定制化处理,可能需要额外的工作来覆盖Lombok生成的默认行为。

(4) 可读性和维护性

传统建造者模式:因为所有的构建逻辑都是显式的,所以对于不熟悉该模式的人来说,理解起来可能会稍微困难一点。但是,一旦理解后,它的结构清晰,易于维护。
Lombok的@Builder注解:极大地提高了代码的简洁性和可读性,减少了样板代码的数量。不过,这也意味着你需要了解Lombok的工作原理,否则直接阅读源码时可能不会立即明白背后发生了什么。

(5)总结

Builder模式解决了以下问题:

  • 简化了对象创建过程:通过链式调用的方式设置对象属性,使得代码更加简洁明了。
  • 提高了代码的可读性和维护性:每个属性的设置都非常直观,易于理解和修改。即使将来需要增加新的属性或选项,也只需相应地扩展Builder类即可,而不需要改动已有的客户端代码。
  • 确保对象的一致性:可以在Builder的build()方法中添加必要的验证逻辑,确保生成的对象始终处于有效状态。

总之,Builder模式非常适合用于构建具有多个属性(尤其是那些有很多可选属性)的复杂对象。它不仅简化了对象的创建过程,还增强了代码的灵活性和可维护性,特别是在处理复杂的数据结构时显得尤为重要。

(三)Factory Mehod 工厂方法

1. 模式名称

工厂方法模式(Factory Method Pattern)

2. 意图解决的问题

工厂方法模式意图解决的是对象创建过程中的问题,特别是在需要根据特定条件或环境来决定实例化哪一个类的情况下。它允许一个类的实例化被延迟到子类中进行,从而解决了直接在基类中硬编码具体类名所带来的紧耦合问题。具体来说,它试图解决以下几个方面的问题:

  • 提高代码的可扩展性:当需要添加新的产品类型时,无需修改现有的工厂逻辑,只需增加相应的具体产品和对应的工厂实现。
  • 降低模块间的依赖关系:通过将对象创建的具体细节隐藏在具体的工厂类中,客户端代码不需要知道如何创建具体的产品对象,只需要与抽象接口或抽象类交互。
  • 支持不同的产品等级结构:工厂方法模式可以用于处理具有不同等级结构的产品族。

3. 模式描述

工厂方法模式定义了一个用于创建对象的接口,但让子类决定实例化哪一个类。工厂方法使得一个类的实例化延迟到其子类。这个模式涉及到四个主要角色:

  • 产品(Product):定义工厂方法所创建的对象的接口或基类。
  • 具体产品(Concrete Product):实现了Product接口的具体类,是工厂方法真正创建的对象。
  • 工厂(Creator):声明了工厂方法,该方法返回一个Product类型的对象。Creator也可以定义一个工厂方法的默认实现,该实现返回一个默认的Concrete Product对象。
  • 具体工厂(Concrete Creator):重写工厂方法以返回一个Concrete Product实例。
    在这里插入图片描述
(1) 定义产品接口(Product)
public interface Product {void operation();
}
(2) 实现具体产品类(ConcreteProduct)
public class ConcreteProduct implements Product {@Overridepublic void operation() {System.out.println("ConcreteProduct operation");}
}
(3) 定义创建者接口(Creator)
public abstract class Creator {// 工厂方法,返回一个Product类型的对象public abstract Product factoryMethod();// 其他业务方法,可以使用工厂方法创建的对象public void anOperation() {Product product = factoryMethod();product.operation();}
}
(4) 实现具体创建者类(ConcreteCreator)
public class ConcreteCreator extends Creator {@Overridepublic Product factoryMethod() {return new ConcreteProduct();}
}
(5) 使用示例
public class Main {public static void main(String[] args) {Creator creator = new ConcreteCreator();creator.anOperation(); // 输出: ConcreteProduct operation}
}

4.场景举例

1.场景描述

假设我们正在开发一个文档编辑器,用户可以在其中创建和编辑各种类型的文档。为了满足不同用户的需求,我们需要支持将文档导出为不同的格式,如PDF、Word、HTML等。每种导出格式都有其特定的实现逻辑。

2.遇到的困难:

  • 扩展性差:如果直接在代码中硬编码所有可能的导出格式,每次添加新的导出格式时都需要修改现有的代码,这违反了开闭原则(对扩展开放,对修改关闭),增加了维护成本。
  • 紧耦合:如果客户端代码直接依赖具体的导出类,那么一旦需要替换或增加新的导出格式,就需要改动大量使用这些导出功能的地方,导致代码的可维护性和灵活性降低。
  • 难以测试:由于导出操作可能涉及复杂的文件处理和外部库调用,如果将所有的逻辑集中在一个地方,会使单元测试变得困难。

3.Factory Method模式如何解决问题:

通过应用Factory Method模式,我们可以将导出格式的选择延迟到子类中进行,这样就可以根据不同的需求动态地选择合适的导出方式,而不需要修改已有的代码。

1. 定义产品接口(Product)
public interface DocumentExporter {void export(String content);
}
2. 实现具体产品类(Concrete Product)
public class PdfExporter implements DocumentExporter {@Overridepublic void export(String content) {System.out.println("Exporting to PDF: " + content);// 实现PDF导出逻辑}
}public class WordExporter implements DocumentExporter {@Overridepublic void export(String content) {System.out.println("Exporting to Word: " + content);// 实现Word导出逻辑}
}public class HtmlExporter implements DocumentExporter {@Overridepublic void export(String content) {System.out.println("Exporting to HTML: " + content);// 实现HTML导出逻辑}
}
3. 定义抽象创建者(Creator)
public abstract class ExportManager {// 工厂方法protected abstract DocumentExporter createExporter();public void exportDocument(String content) {DocumentExporter exporter = createExporter();exporter.export(content);}
}
4. 实现具体创建者(Concrete Creator)
public class PdfExportManager extends ExportManager {@Overrideprotected DocumentExporter createExporter() {return new PdfExporter();}
}public class WordExportManager extends ExportManager {@Overrideprotected DocumentExporter createExporter() {return new WordExporter();}
}public class HtmlExportManager extends ExportManager {@Overrideprotected DocumentExporter createExporter() {return new HtmlExporter();}
}
5. 使用示例
public class Main {public static void main(String[] args) {ExportManager pdfManager = new PdfExportManager();pdfManager.exportDocument("This is a test document.");ExportManager wordManager = new WordExportManager();wordManager.exportDocument("This is another test document.");ExportManager htmlManager = new HtmlExportManager();htmlManager.exportDocument("And this one is for the web.");}
}

5.效果

(1)Factory Method模式的效果

Factory Method模式解决了以下问题:

  • 提高了代码的扩展性:每当需要添加新的导出格式时,只需创建一个新的DocumentExporter实现和对应的ExportManager子类即可,无需修改现有的代码。
  • 降低了模块间的依赖关系:客户端代码不再需要知道具体要使用哪个导出类,只需要与抽象接口DocumentExporter以及抽象创建者ExportManager交互,从而实现了松散耦合。
  • 增强了代码的可测试性:由于采用了工厂方法来创建对象,可以轻松地为测试提供模拟对象(Mock Object),从而简化了单元测试的过程。

总之,工厂方法模式非常适合用于解决需要根据运行时条件动态创建对象的情况,它不仅提高了系统的灵活性和可维护性,还遵循了面向对象设计的基本原则,使得代码更加清晰和易于理解。在这个例子中,它帮助我们有效地管理了文档编辑器中不同导出格式的实现,同时也为未来的扩展提供了便利。

(2)工厂方法和抽象工厂的区别

工厂方法模式(Factory Method Pattern)和抽象工厂模式(Abstract Factory Pattern)都是创建型设计模式,它们的主要目的是提供一种机制来创建对象,而无需直接通过具体类实例化。尽管两者有相似之处,但它们解决的问题和使用场景有所不同。

工厂方法模式
定义:定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法使一个类的实例化延迟到其子类。

核心思想:

  • 单个产品等级结构:工厂方法模式通常处理的是单个产品等级结构的情况,即它专注于创建单一类型的产品。
  • 灵活性:允许子类决定如何实例化产品,提供了更大的灵活性。
  • 简单性:相比于抽象工厂模式,工厂方法模式较为简单,因为它只涉及一个产品的创建。

示例:
如果你的应用程序需要根据不同的操作系统显示不同风格的窗口(如Windows风格、Mac风格),你可以为每个操作系统定义一个具体的工厂类(Concrete Creator),这些工厂类实现了创建窗口的方法(工厂方法)。这样,客户端代码只需与抽象的工厂接口交互,而不需要知道具体是哪个平台的窗口被创建了。

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

核心思想:

  • 多个产品族:抽象工厂模式处理的是多个产品等级结构的情况,即它可以创建一组相关的对象,而不是单个对象。
  • 一致性:确保所创建的对象家族之间的一致性,即一起工作的对象应该属于同一个产品族。
  • 复杂性:相比工厂方法模式,抽象工厂模式更加复杂,因为它不仅要创建一个产品,还需要管理一组产品的创建。

示例:
假设你正在开发一个跨平台UI框架,需要同时支持Windows和Mac两种风格的控件(如按钮、文本框等)。这时,你可以定义一个抽象工厂接口,该接口包含创建各种控件的方法。然后,为每种平台实现这个抽象工厂接口的具体工厂类(Concrete Factory),这些具体工厂负责创建符合特定平台风格的所有控件。这样,客户端代码只需要与抽象工厂接口交互,就能获取到一套完整的、风格一致的控件集合。

(四)Prototype模式

1. 模式名称

原型模式(Prototype Pattern)

2. 意图解决的问题

原型模式的主要意图是通过复制现有的实例来创建新的对象,而不是通过实例化类的方式。这种模式特别适用于创建对象的成本较高(如复杂的计算、耗时的数据加载等),或者当对象的创建过程需要隔离时。它允许我们快速地创建对象的一个副本,并且可以根据需要对这个副本进行修改,而不会影响到原始对象。

具体来说,原型模式试图解决以下几个问题:

  • 提高对象创建效率:对于一些复杂对象的创建,可能涉及大量的资源分配和初始化操作,使用原型模式可以通过简单的复制已有的对象来减少这些开销。
  • 避免构造函数的约束:有时候我们需要绕过构造函数的一些限制或复杂性,原型模式提供了一种替代方案。
  • 支持动态创建对象:在运行时根据需要创建对象,而不依赖于具体的类名。

3. 模式描述

原型模式的核心在于使用一个原型实例来指定所要创建的对象类型,并通过复制这个原型来创建新的对象。它涉及到以下几个关键角色:

  • Prototype(原型):声明一个克隆自身的接口。
  • ConcretePrototype(具体原型):实现Prototype接口的类,定义了如何克隆自身的方法。
  • Client(客户端):让一个原型对象克隆自身,从而创建一个新的对象。
    在这里插入图片描述
(1) Prototype 接口

首先定义一个 Prototype 接口,其中包含 clone() 方法。

public interface Prototype {Prototype clone();
}
(2) ConcretePrototype1 类

实现 Prototype 接口的具体类 ConcretePrototype1,并重写 clone() 方法。

public class ConcretePrototype1 implements Prototype {private String name;public ConcretePrototype1(String name) {this.name = name;}@Overridepublic Prototype clone() {return new ConcretePrototype1(this.name);}@Overridepublic String toString() {return "ConcretePrototype1{name='" + name + "'}";}
}
(3) ConcretePrototype2 类

另一个实现 Prototype 接口的具体类 ConcretePrototype2,同样重写 clone() 方法。

public class ConcretePrototype2 implements Prototype {private String name;public ConcretePrototype2(String name) {this.name = name;}@Overridepublic Prototype clone() {return new ConcretePrototype2(this.name);}@Overridepublic String toString() {return "ConcretePrototype2{name='" + name + "'}";}
}
(4) Client 类

客户端代码,用于演示如何使用原型模式创建对象。

public class Client {public static void main(String[] args) {// 创建原型实例Prototype prototype1 = new ConcretePrototype1("Prototype1");Prototype prototype2 = new ConcretePrototype2("Prototype2");// 使用原型实例创建新对象Prototype copy1 = prototype1.clone();Prototype copy2 = prototype2.clone();System.out.println(prototype1); // 输出: ConcretePrototype1{name='Prototype1'}System.out.println(copy1);      // 输出: ConcretePrototype1{name='Prototype1'}System.out.println(prototype2); // 输出: ConcretePrototype2{name='Prototype2'}System.out.println(copy2);      // 输出: ConcretePrototype2{name='Prototype2'}}
}

以上代码实现了原型模式的基本结构。通过定义一个 Prototype 接口,并让具体的 ConcretePrototype1 和 ConcretePrototype2 类实现该接口,我们可以在客户端中通过调用 clone() 方法来快速复制对象,而无需直接调用构造函数。这样不仅提高了对象创建的效率,还使得代码更加灵活和可扩展。

(6)简单示例
// Prototype 接口
public interface Prototype extends Cloneable {Prototype clone() throws CloneNotSupportedException;
}// ConcretePrototype 类实现了 Prototype 接口
public class ConcretePrototype implements Prototype {private String name;public ConcretePrototype(String name) {this.name = name;}// 实现 clone 方法@Overridepublic Prototype clone() throws CloneNotSupportedException {return (Prototype) super.clone();}@Overridepublic String toString() {return "ConcretePrototype{name='" + name + "'}";}
}// 客户端代码
public class Client {public static void main(String[] args) {try {// 创建一个原型实例ConcretePrototype prototype = new ConcretePrototype("Original");// 使用原型实例创建新对象ConcretePrototype copy = (ConcretePrototype) prototype.clone();System.out.println(prototype); // 输出: ConcretePrototype{name='Original'}System.out.println(copy);      // 输出: ConcretePrototype{name='Original'}} catch (CloneNotSupportedException e) {e.printStackTrace();}}
}

在这个例子中:

  • Prototype 是一个标记接口,表明任何实现它的类都应提供一种克隆自己的方法。
  • ConcretePrototype 类实现了 Prototype 接口,并提供了具体的 clone() 方法实现。这里直接调用了 Object 类中的 clone() 方法,该方法执行的是浅拷贝。如果需要深拷贝,则需要手动实现相应的逻辑。
  • Client 类展示了如何使用原型实例来创建新对象。通过调用 clone() 方法,可以从现有的对象快速生成一个新的对象副本。

注意事项

  • 浅拷贝 vs 深拷贝:默认情况下,Java中的clone()方法执行的是浅拷贝,这意味着如果对象包含引用类型的成员变量,那么这些引用指向的对象并不会被复制,而是与原对象共享相同的引用。如果需要完全独立的对象副本,则需要实现深拷贝,这通常涉及到递归地复制所有引用类型的成员变量。
  • Cloneable 接口:虽然Cloneable是一个标记接口,不包含任何方法,但它必须被实现,以便对象可以调用Object类中的clone()方法。如果不实现Cloneable接口,调用clone()会导致CloneNotSupportedException异常。

4.场景举例

(1)场景描述

假设我们正在开发一款在线多人角色扮演游戏(MMORPG),在这个游戏中,玩家可以创建自己的游戏角色。每个角色都有复杂的属性和状态,包括但不限于装备、技能、等级等。为了提高用户体验,我们需要允许玩家快速地创建多个具有相似或相同属性的角色,比如在不同服务器上创建角色,或者在游戏中快速复制一个角色作为副本进行实验。

(2)遇到的困难
  • 对象创建成本高:由于每个角色都包含大量的属性和状态信息,直接通过构造函数创建新的角色实例会涉及到大量重复的初始化工作,这不仅耗时,还可能涉及复杂的计算和数据加载。
  • 代码复杂度增加:如果使用传统的构造函数方式来创建新角色,随着角色属性的增加,构造函数将变得非常庞大且难以维护。此外,当需要修改角色的默认属性时,必须更新所有相关的构造调用,增加了出错的可能性。
  • 性能问题:在一些情况下,可能需要频繁地创建类似的对象(例如,玩家希望在同一时间创建多个具有相同初始配置的角色)。如果每次都从头开始创建这些对象,可能会导致显著的性能瓶颈。
  • Prototype模式如何解决问题:
(3)解决方案

通过应用原型模式,我们可以有效地解决上述问题:

  • 简化对象创建过程:利用现有的对象作为原型,通过复制(克隆)的方式快速生成新的对象实例。这种方式避免了重复的初始化步骤,大大提高了对象创建的效率。
  • 降低代码复杂性:不需要为每个具体的类编写复杂的构造逻辑,只需实现一个简单的clone()方法即可。这样不仅简化了代码结构,也使得维护变得更加容易。
  • 提升性能表现:对于那些需要频繁创建的对象,尤其是那些创建成本较高的对象,原型模式提供了一种高效的解决方案。通过复制已有的对象而不是每次都重新构建,可以显著减少资源消耗和时间开销。
(4)示例代码
// 定义Prototype接口
public interface GameCharacter extends Cloneable {GameCharacter clone();
}// 实现具体的原型类
public class Warrior implements GameCharacter {private String name;private int level;private List<String> equipment;public Warrior(String name, int level) {this.name = name;this.level = level;this.equipment = new ArrayList<>();// 假设这里有一些初始化操作}@Overridepublic GameCharacter clone() {try {// 浅拷贝Warrior copy = (Warrior) super.clone();// 深拷贝列表copy.equipment = new ArrayList<>(this.equipment);return copy;} catch (CloneNotSupportedException e) {return null;}}// Getter and Setter methods...
}// 客户端代码
public class Main {public static void main(String[] args) {// 创建原型角色Warrior prototypeWarrior = new Warrior("Warrior", 1);prototypeWarrior.getEquipment().add("Sword");prototypeWarrior.getEquipment().add("Shield");// 使用原型角色创建新角色Warrior clonedWarrior = (Warrior) prototypeWarrior.clone();System.out.println(clonedWarrior); // 输出: Warrior{name='Warrior', level=1, equipment=[Sword, Shield]}// 修改克隆的角色而不影响原角色clonedWarrior.setName("New Warrior");clonedWarrior.getEquipment().add("Helmet");System.out.println(prototypeWarrior); // 输出: Warrior{name='Warrior', level=1, equipment=[Sword, Shield]}System.out.println(clonedWarrior);    // 输出: Warrior{name='New Warrior', level=1, equipment=[Sword, Shield, Helmet]}}
}

在这个例子中,我们定义了一个GameCharacter接口,并让Warrior类实现了这个接口。通过实现clone()方法,我们可以轻松地复制Warrior对象,而无需重复其复杂的初始化过程。这种方法不仅提高了角色创建的效率,也降低了代码的复杂性和维护难度。

5.效果

(1)prototype模式的适用性

Prototype模式适用于以下情况:

  • 当一个系统应该独立于它的产品创建、构成和表示时。
  • 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
  • 为了避免创建一个与产品类层次平行的工厂类层次时。
  • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
(2)java中的prototype和javascript中的prototype
Java中的原型模式
定义与用途:

在Java中,原型模式是一种设计模式,它通过复制现有的实例来创建新的对象,而不是通过调用构造函数。这种模式通常用于避免复杂对象的重复初始化过程,提高性能。
原型模式的核心在于实现Cloneable接口,并重写clone()方法,从而允许对象自我复制。

特点:
  • 对象复制:主要目的是为了快速生成对象的一个副本,以减少创建新对象时的成本。
  • 深拷贝与浅拷贝:在使用原型模式时,需要特别注意深拷贝和浅拷贝的区别,确保对象及其引用类型的成员变量都被正确复制。
  • 实现方式:通过实现Cloneable接口并重写Object类中的clone()方法来实现对象的克隆。
示例代码:
public class PrototypeExample implements Cloneable {private String name;public PrototypeExample(String name) {this.name = name;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}// Getter and Setter methods...
}
JavaScript中的原型(Prototype)
定义与用途:

在JavaScript中,“原型”是指一个对象用来继承属性和方法的机制。每个JavaScript对象都有一个内部链接指向另一个对象,即它的原型;这个原型对象也有自己的原型,如此层层链接形成所谓的“原型链”。当尝试访问一个对象的属性时,如果该对象本身没有此属性,则会沿着原型链向上查找。
JavaScript中的原型主要用于实现继承,使得对象可以共享属性和方法,减少内存消耗。

特点:
  • 原型链:JavaScript的对象之间通过原型链相连,形成一种继承关系。
  • 动态性:可以通过修改原型对象来动态地为已有的对象添加属性或方法。
  • 效率优化:由于多个对象可以共享同一个原型,因此可以节省内存资源,尤其是在创建大量相似对象的情况下。
示例代码
function Animal(name) {this.name = name;
}Animal.prototype.speak = function() {console.log(this.name + " makes a noise.");
};let dog = new Animal("Dog");
dog.speak();  // 输出: Dog makes a noise.// 动态添加方法到原型
Animal.prototype.walk = function() {console.log(this.name + " is walking.");
};dog.walk();  // 输出: Dog is walking.
主要区别
目的不同:
  • Java中的原型模式主要用于简化对象的创建过程,特别是对于那些创建成本较高的对象。
  • JavaScript中的原型则主要是为了实现对象间的继承,以及属性和方法的共享。
实现机制不同:
  • Java中通过实现Cloneable接口并重写clone()方法来实现对象的克隆。
  • JavaScript则是通过每个对象的__proto__属性或者直接通过构造函数的prototype属性来建立原型链,实现属性和方法的继承。
应用场景不同:
  • Java的原型模式适用于需要高效地创建大量相似对象的场景。
  • JavaScript的原型机制几乎应用于所有的对象创建和继承过程中,因为它构成了JavaScript面向对象编程的基础。

总结来说,尽管Java中的原型模式和JavaScript中的原型都涉及到“复制”或“继承”的概念,但它们的应用场景和技术实现有着本质的不同。Java中的原型模式更侧重于对象的高效创建,而JavaScript中的原型则是其语言特性之一,用于实现基于原型的继承机制。

(五)Singleton 模式

1.模式名称

Singleton,也常称之为单件模式或单根模式。

2.意图解决的问题

在软件开发中,开发人员希望一些服务类有且仅有一个实例供其他程序使用。例如,短消息服务程序或打印机服务程序,甚至对于系统配置环境的控制,为了避免并发访问造成的不一致,也希望仅为其他程序提供一个实例。

对于供整个系统使用的对象可以使用一个全局变量,不过全局变量仅能保证正确编码时使用了唯一的实例。但随着系统不断的扩张,开发队伍的扩大,仍然无法保证这个类在系统中有且仅有一个实例。

3.模式描述

在这里插入图片描述
从结构角度而言,Singleton 是最简单的一个模式,不过其用途很广。

在 Singleton 中,通过将 Singleton 类的构造函数设为 protected 型(或 private)来防止外部对其直接初始化。

需要访问 Singleton 的程序必须通过 getInstance()方法来获得一个 Singleton。

在getInstance() 中仅创建一次 uniqueInstance 就可以保证系统中的唯一实例。

对于 Singleton 中的 uniqueInstance 有两种不同的初始化策略(Lazy Initialization 和Early Initialization),在实现中将分别给出这两种初始化策略的代码。

public class Singleton {// 静态变量保存唯一实例,在类加载时就创建了唯一的实例,保证了线程安全。private static final Singleton uniqueInstance = new Singleton();// 私有构造函数,防止外部实例化private Singleton() {// 初始化操作}// 公共静态方法,返回唯一实例,提供了一个全局访问点,用于获取唯一的实例。public static Singleton getInstance() {return uniqueInstance;}// 单例对象的操作方法public void singletonOperation() {System.out.println("执行单例操作");}// 获取单例数据的方法public String getSingletonData() {return "这是单例数据";}
}// 使用单例的示例代码
public class SingletonDemo {public static void main(String[] args) {// 通过getInstance方法获取单例对象Singleton instance1 = Singleton.getInstance();Singleton instance2 = Singleton.getInstance();// 检查两个实例是否相同System.out.println(instance1 == instance2); // 输出:true// 调用单例对象的方法instance1.singletonOperation(); // 输出:执行单例操作System.out.println(instance1.getSingletonData()); // 输出:这是单例数据}
}
(1) 饿汉式 vs 懒汉式
  • 饿汉式(如上例):在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快,且是线程安全的。
  • 懒汉式:在第一次调用 getInstance() 方法时才初始化实例,这样可以延迟加载,节省内存。但是,懒汉式需要额外的同步机制来保证线程安全,否则可能会导致多个实例被创建。
(2)双重检查锁定(DCL)

如果你希望在保持线程安全的同时,也能够延迟加载,可以使用双重检查锁定的方式实现懒汉式单例:

public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {// 初始化操作}public static Singleton getInstance() {if (uniqueInstance == null) {synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();}}}return uniqueInstance;}public void singletonOperation() {System.out.println("执行单例操作");}public String getSingletonData() {return "这是单例数据";}
}

在这个版本中,volatile 关键字确保了多线程环境下的可见性,而双重检查则避免了每次调用 getInstance() 方法时都进行同步,提高了性能。

4.场景举例

(1)场景描述

假设我们正在开发一个Web应用程序,该应用需要频繁访问某些计算密集型或I/O密集型的数据(例如,复杂的算法计算结果、外部API调用的结果等)。为了提高性能和减少响应时间,我们决定引入一个本地缓存机制来存储这些数据。

这个缓存池并不直接由Spring管理,也不想用Redis作为缓存。

如果应用对延迟极其敏感,那么访问本地内存的速度通常会比通过网络访问Redis更快。

同时,对于一些小型应用或者仅限于单机环境的应用,引入Redis这样的外部依赖可能显得过于复杂且没有必要。

在这种情况下,可以考虑使用简单的本地缓存来加速最频繁访问的数据。

你可能希望该对象池在整个应用中只有一个实例存在,以确保资源共享和状态的一致性。

在这个例子中,我们将创建一个简单的内存缓存池,用于缓存一些计算结果或者频繁访问的数据。为了保证缓存池的唯一性和共享性,我们需要手动实现单例模式。

(2)遇到的困难
  • 重复创建缓存池实例:如果没有适当的控制措施,不同的模块可能会各自创建自己的缓存池实例。这不仅浪费内存资源,还可能导致数据不一致的问题,因为每个缓存池可能持有不同的数据副本。
  • 数据一致性问题:如果允许创建多个缓存池实例,则在更新缓存时可能会遇到数据同步的问题。例如,当一个模块更新了缓存中的某个条目时,其他模块可能仍然使用旧版本的数据,导致数据不一致。
  • 复杂性增加:若没有统一的缓存池管理机制,维护这些分散的缓存池将会变得非常复杂。每当需要调整缓存策略(如缓存过期策略、最大容量限制等)时,都需要逐一修改各个缓存池的配置,增加了维护成本。
  • 潜在的性能瓶颈:如果缓存池被多次实例化,每次都需要重新加载和初始化数据,这对性能是一个极大的损耗,尤其是在高并发环境下,这种影响尤为明显。
(3)单例模式如何解决问题

通过使用单例模式实现缓存池,可以有效地解决上述问题:

  • 唯一实例:采用单例模式确保整个应用程序中只有一个缓存池实例存在。这意味着所有需要访问缓存的地方都将共享同一个缓存池,避免了重复创建实例带来的资源浪费,并且保证了数据的一致性。
  • 数据一致性保障:由于所有的缓存操作都是通过同一个缓存池进行的,因此可以确保任何时候从缓存中获取的数据都是最新和最准确的,解决了不同模块间数据不一致的问题。
  • 简化管理和维护:只需要在一个地方配置和调整缓存池的行为(如设置缓存的最大容量、过期策略等),简化了维护工作,减少了出错的可能性。
  • 性能优化:缓存池只会在首次使用时初始化一次,后续的操作可以直接利用已有的缓存数据,极大地提高了性能,特别是在高并发场景下效果显著。
(4)不使用单例模式
import java.util.concurrent.ConcurrentHashMap;public class CachePool {private final java.util.Map<String, Object> cache;public CachePool() {cache = new java.util.concurrent.ConcurrentHashMap<>();}// 添加缓存项public void put(String key, Object value) {cache.put(key, value);}// 获取缓存项public Object get(String key) {return cache.get(key);}
}

在这个例子中,CachePool 类可以被随意实例化,这意味着每个需要使用缓存的地方都可以创建自己的 CachePool 实例。

public class SomeService {public void processData(String dataKey) {CachePool cachePool1 = new CachePool();CachePool cachePool2 = new CachePool();// 尝试从第一个缓存池获取数据Object cachedData1 = cachePool1.get(dataKey);if (cachedData1 == null) {Object computedData = computeExpensiveOperation(dataKey);cachePool1.put(dataKey, computedData);System.out.println("Computed and cached data for key: " + dataKey);} else {System.out.println("Retrieved from cache 1: " + cachedData1);}// 尝试从第二个缓存池获取数据Object cachedData2 = cachePool2.get(dataKey);if (cachedData2 == null) {System.out.println("Data not found in cache 2");} else {System.out.println("Retrieved from cache 2: " + cachedData2);}}private Object computeExpensiveOperation(String dataKey) {// 模拟耗时操作return "Result of " + dataKey;}
}
(5)使用单例模式

首先,我们创建一个CachePool类,并采用双重检查锁定的方式实现单例模式:

import java.util.concurrent.ConcurrentHashMap;public class CachePool {private static volatile CachePool instance;private final java.util.Map<String, Object> cache;// 私有构造函数,防止外部实例化private CachePool() {cache = new java.util.concurrent.ConcurrentHashMap<>();}// 提供全局访问点,采用双重检查锁定机制public static CachePool getInstance() {if (instance == null) {synchronized (CachePool.class) {if (instance == null) {instance = new CachePool();}}}return instance;}// 添加缓存项public void put(String key, Object value) {cache.put(key, value);}// 获取缓存项public Object get(String key) {return cache.get(key);}
}

接下来,我们在应用程序的某个部分使用这个缓存池。因为CachePool不是由Spring管理的Bean,所以我们不能直接通过依赖注入的方式来获取它的实例,而是需要手动调用其静态方法getInstance()。

public class SomeService {public void processData(String dataKey) {CachePool cachePool1 = CachePool.getInstance();CachePool cachePool2 = CachePool.getInstance();// 尝试从缓存池获取数据Object cachedData = cachePool1.get(dataKey);if (cachedData == null) {Object computedData = computeExpensiveOperation(dataKey);cachePool1.put(dataKey, computedData);System.out.println("Computed and cached data for key: " + dataKey);} else {System.out.println("Retrieved from cache: " + cachedData);}// 再次尝试从同一个缓存池获取数据Object cachedDataAgain = cachePool2.get(dataKey);if (cachedDataAgain == null) {System.out.println("Unexpected: Data not found in cache");} else {System.out.println("Retrieved from cache again: " + cachedDataAgain);}}private Object computeExpensiveOperation(String dataKey) {// 模拟耗时操作return "Result of " + dataKey;}
}

5.效果

使用 Singleton 模式可以保证系统中有且仅有一个实例,这对于很多服务类或者环境配置类来说非常重要。

优点:

  • 资源共享:确保整个应用程序中只有一个缓存池实例存在,所有模块共享相同的缓存数据,提高了资源利用率。
  • 数据一致性:所有缓存操作都是通过同一个实例进行的,保证了数据的一致性和准确性。
  • 简化维护:只需要在一个地方配置和调整缓存池的行为,简化了维护工作,减少了出错的可能性。

缺点:

  • 设计复杂度增加:实现单例模式通常需要更多的代码来处理线程安全等问题,如上述示例中的双重检查锁定。
  • 扩展性限制:在分布式系统或需要跨多个JVM共享数据的情况下,单例模式的本地缓存将不再适用,需要考虑其他解决方案,如Redis等外部缓存服务。
  • Singleton 模式仅适用于系统中至多有一个实例的情况,应避免滥用。很多过度设计的 Singleton 同使用了静态方法的工具类一样,没有任何必要,反而可能降低效率。

单例模式的一个特点是它确保一个类只有一个实例存在。通常,这种控制是通过将构造函数设为私有来实现的,这意味着你不能从外部创建该类的新实例,也不能通过继承来覆盖或扩展其行为。因此,单例类通常不适合用于需要多态性的场景。

在C++中,你可以定义一个通用的单例模板,这个模板可以应用于任何类型,从而减少重复代码。例如,你可以定义一个Singleton<T>模板,然后对于每个想要作为单例使用的类,只需指定相应的类型参数即可,无需为每个类单独编写单例逻辑。

相比之下,Java和C#不支持像C++那样的模板机制(虽然C#有泛型,但它们主要用于类型安全的数据结构和算法,而不是设计模式的具体实现)。因此,在这些语言中,如果要实现单例模式,则必须针对每个具体的类分别编写单例逻辑。然而,这并不是一个问题,因为在一个良好的系统设计中,应该尽量减少对单例模式的依赖,避免出现过多的单例类。

值得注意的是,单例模式并不等同于静态类。虽然两者都可以提供全局访问点,但单例模式允许延迟初始化,并且可以在运行时改变其实现(例如通过子类化),而静态类则不具备这样的灵活性。

理想情况下,一个系统中不应该存在大量的单例类。单例模式应谨慎使用,主要用于那些确实需要在整个应用程序生命周期内保持唯一实例的对象,如配置管理器、日志记录器等。过度使用单例模式可能导致代码难以测试、维护复杂度增加以及潜在的线程安全问题。

相关文章:

  • 微软与Meta大幅增加人工智能基础设施投入
  • 普通 html 项目也可以支持 scss_sass
  • Anaconda中配置Pyspark的Spark开发环境
  • 【中间件】bthread_数据结构_学习笔记
  • terraform 删除资源前先校验资源是否存在关联资源
  • AJAX 实例
  • 【Linux】线程池和线程补充内容
  • Qwen3 正式发布
  • C++——入门基础(2)
  • 工 厂 模 式
  • 游戏引擎学习第252天:允许编辑调试值
  • 企业内训|智能驾驶与智能座舱技术——某汽车厂商
  • 【Qt】网络
  • Python项目源码69:Excel数据筛选器1.0(tkinter+sqlite3+pandas)
  • 《数据结构初阶》【顺序表/链表 精选15道OJ练习】
  • 【数据结构】- 栈
  • 文件操作--文件包含漏洞
  • 如何让Steam下载速度解除封印?!
  • PyTorch线性代数操作详解:点积、矩阵乘法、范数与轴求和
  • 字符串转换整数(atoi)(8)
  • 戴上XR头盔,五一假期在上海也能体验“登陆月球”
  • 辽宁辽阳火灾3名伤者无生命危险
  • 李铁案二审今日宣判
  • 被算法重塑的世界,人与技术如何和谐共处
  • 从咖啡节到话剧、演唱会,上海虹口“文旅商体展”联动促消费
  • 早睡1小时,变化有多惊人?第一个就没想到