各类设计模式大盘点
在开发中,常常发现自己的代码结构紊乱,方法含糊不清,没有一种清晰明显的设计模式及方法,这是在代码编写中是极为不提倡的,为了提高自身的代码素质,提高项目整体的结构设计,在此将系统的总结复习对应的23种Java设计模式,及Golang, Rust等常用的设计模式,在总结的同时,给出在业务项目中最为常见的用法及建议
Java设计模式
在常见的23种设计模式中,我们通常将其分为3大类,分别是:
- 创建型模式
- 结构型模式
- 行为型模式
那我们分别来介绍这三种设计模式:
创建型模式
这类模式主要关注对象的创建过程
单例模式(Singleton)
确保一个类只有一个实例,并提供全局访问点。这可以用于数据库连接、日志记录等需要全局访问的情况。
简要实现:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
// Private constructor prevents instantiation from other classes
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
常用应用场景:
- 全局配置:当你需要在应用程序中使用某些全局设置或参数时,单例模式非常有用。例如,你可以创建一个包含所有配置设置的单例类,然后在应用程序的任何地方都可以访问这些设置,而无需将设置作为参数传递给每个方法和类。
- 日志记录:单例模式常用于创建日志记录器。这样可以确保所有的日志条目都会进入同一个位置,而不是被分散到不同的日志记录器中。
- 数据库连接池:在许多web应用程序中,数据库连接是通过单例模式进行管理的。这是因为建立数据库连接是一项开销很大的操作,所以通常会预先创建一定数量的连接,并在需要时复用这些连接,而不是每次需要时都新建连接。
- 缓存:单例模式也常用于实现缓存。这是因为你通常只需要一个缓存对象,而且这个对象需要在应用程序的多个地方被访问。如果你创建多个缓存对象,那么每个对象都会占用额外的内存,而且可能会导致缓存数据的不一致。
- 硬件接口访问:对于访问打印机、图形卡等硬件设备的类,通常使用单例模式,因为这些硬件设备通常只能被一个进程或线程访问。
原型模式(Prototype)
通过复制现有对象来创建新对象,适用于对象创建成本高或者类初始化需要非常多的资源和时间的情况。
public class Prototype implements Cloneable {
private String name;
public Prototype(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Prototype clone() {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
常用应用场景:
- 大对象的复制:当需要复制一个大对象时,直接创建一个新的实例并设置其属性可能会非常耗时。此时,可以使用原型模式来复制这个对象,这通常比创建一个新的实例更快。
- 性能关键应用:在某些性能要求非常高的应用中,原型模式可以减少数据库操作,因为可以直接复制已经从数据库中获取的对象,而不是每次都去查询数据库。
- 独立于具体类的设计:当一个系统需要独立于其具体类的设计时,原型模式可以用来隐藏这些具体类的信息。这样,系统只需要知道如何处理原型接口或抽象类,而不需要知道具体类。
- 动态添加或删除产品:原型模式允许在运行时动态添加或删除产品。这是通过复制现有的原型实例来创建新的产品实例,而不是通过实例化一个类。
- 在分布式系统中复制数据:在分布式系统中,原型模式可以用来复制数据,以便在不同的系统节点之间共享这些数据。
- 复杂对象的构建:有些对象的构建过程可能非常复杂,此时可以使用原型模式来简化对象的构建过程。首先创建一个原型对象,然后复制这个原型对象来创建新的对象。
工厂方法模式(Factory Method)
提供一个创建对象的接口,但由子类决定实例化的类。这可以用于创建具有共同接口的一系列产品,而不需要知道具体类。
产品接口及对应产品实现类:
public interface Product {
void use();
}
public class ConcreteProduct1 implements Product {
public void use() {
System.out.println("Using ConcreteProduct1");
}
}
public class ConcreteProduct2 implements Product {
public void use() {
System.out.println("Using ConcreteProduct2");
}
}
工厂接口及对应实现类
public interface Factory {
Product createProduct();
}
public class ConcreteFactory1 implements Factory {
public Product createProduct() {
return new ConcreteProduct1();
}
}
public class ConcreteFactory2 implements Factory {
public Product createProduct() {
return new ConcreteProduct2();
}
}
常用应用场景:
- 日志记录器工厂:一个应用程序可能需要在多个输出媒介上记录日志。应用程序可以在运行时选择从文本文件、系统控制台、HTML文件等来源读取日志。在这种情况下,可以使用工厂方法模式来设计一个日志记录器工厂,根据需要产生不同类型的日志记录器。
- 数据库访问查询语句:在许多应用程序中,用户可能需要对不同类型的数据库进行操作。在这种情况下,可以使用工厂方法模式来设计一个查询语句工厂,根据不同的数据库类型生成对应的查询语句。
- 支付方式选择:在电子商务或其他类型的应用中,用户可能需要使用不同的支付方式(如信用卡、电子钱包、银行转账等)。在这种情况下,可以使用工厂方法模式来设计一个支付方式工厂,根据用户的选择生成对应的支付方式。
- 图形界面工具的创建:在图形用户界面(GUI)应用中,可能需要创建不同类型的元素,如按钮、文本框、复选框等。在这种情况下,可以使用工厂方法模式来设计一个元素工厂,根据需要生成不同类型的元素。
- 不同操作系统下的任务处理:在跨平台应用中,可能需要根据不同的操作系统执行不同的任务。在这种情况下,可以使用工厂方法模式来设计一个任务处理工厂,根据操作系统的类型生成对应的任务处理器。
实际上,工厂方法模式的应用场景非常广泛,只要是需要创建对象,而且希望系统独立于其具体实现的场合,都可以考虑使用工厂方法模式。
抽象工厂模式(Abstract Factory)
提供一个接口以创建一系列相关或相互依赖的对象,而无需指定它们具体的类。适用于需要创建产品族的情况,保证产品之间的兼容性。
这个例子中,我们将创建一个抽象工厂类PaymentFactory,以及两个具体工厂类CreditCardFactory和EwalletFactory。每个具体工厂都会生产一个具体产品,即CreditCard和Ewallet。
抽象产品类Payment和两个具体产品类CreditCard和Ewallet:
public interface Payment {
void pay();
}
public class CreditCard implements Payment {
@Override
public void pay() {
System.out.println("Pay with credit card");
}
}
public class Ewallet implements Payment {
@Override
public void pay() {
System.out.println("Pay with e-wallet");
}
}
抽象工厂类PaymentFactory和两个具体工厂类:
public abstract class PaymentFactory {
public abstract Payment createPayment();
}
public class CreditCardFactory extends PaymentFactory {
@Override
public Payment createPayment() {
return new CreditCard();
}
}
public class EwalletFactory extends PaymentFactory {
@Override
public Payment createPayment() {
return new Ewallet();
}
}
常见应用场景:
- 跨平台应用开发:在开发跨平台应用时,可以使用抽象工厂模式来创建在不同操作系统下具有不同实现的组件。例如,在开发跨平台的图形用户界面(GUI)时,可以创建一个抽象工厂,该工厂可以生成在Windows、MacOS和Linux下具有不同实现的按钮、菜单和窗口等组件。
- 数据库迁移:在进行数据库迁移时,可以使用抽象工厂模式来创建在不同数据库(如MySQL、Oracle、SQL Server等)下具有不同实现的数据访问对象。
- 支持多种支付方式:在开发电商系统时,可以使用抽象工厂模式来创建支持不同支付方式(如信用卡、PayPal、比特币等)的支付处理对象。
Q: 工厂方法与抽象工厂方法之间有什么区别和联系呢
A: 如果产品单一,最合适用工厂模式,但是如果有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。 再通俗深化理解下:工厂模式针对的是一个产品等级结构 ,抽象工厂模式针对的是面向多个产品等级结构的。
建造者模式(Builder)
将一个复杂对象的构造与其表示分离,使得同样的构造过程可以创建不同的表示。适用于需要生成复杂对象的情况,可以更好地控制对象的创建过程。
以最好理解的订单创建为例,使用建造者模式来设计,即订单为创建的对象,订单创建者为建造者。
public class Order {
private String productId;
private String userId;
private String addressId;
private int quantity;
public Order(String productId, String userId, String addressId, int quantity) {
this.productId = productId;
this.userId = userId;
this.addressId = addressId;
this.quantity = quantity;
}
// getters and setters...
}
public class OrderBuilder {
private String productId;
private String userId;
private String addressId;
private int quantity;
public OrderBuilder setProductId(String productId) {
this.productId = productId;
return this;
}
public OrderBuilder setUserId(String userId) {
this.userId = userId;
return this;
}
public OrderBuilder setAddressId(String addressId) {
this.addressId = addressId;
return this;
}
public OrderBuilder setQuantity(int quantity) {
this.quantity = quantity;
return this;
}
public Order build() {
return new Order(productId, userId, addressId, quantity);
}
}
常见应用场景
- 复杂对象的构建:当一个对象由多个部分构成,并且对象的创建过程比较复杂时,可以使用建造者模式。例如,一个复杂的报告或文档,可能包含头部、主体、脚注等部分,每部分的创建过程可能都很复杂,这时就可以使用建造者模式。
- 参数过多的构造函数:当一个类的构造函数参数过多,且很多参数都是可选的,使用建造者模式可以提高代码的可读性和易用性。例如,一个用户的类,可能包含姓名、年龄、性别、地址、电话、邮箱等多个属性,使用建造者模式可以方便地创建用户对象。
- 需要生成不同种类的产品:当需要生成的产品有多种类别,且每种类别的产品都有不同的创建过程,可以使用建造者模式。例如,快餐店可能需要制作各种不同的套餐,每种套餐都包含不同的食物和饮料,这时就可以使用建造者模式。
- 对象的创建和表示分离:当对象的创建过程和对象的表示过程需要分离时,可以使用建造者模式。例如,一个游戏中的角色创建过程可能包含选择角色类型、设置角色属性、选择装备等步骤,而角色的表示过程可能包含显示角色的图像、显示角色的属性等步骤,这时就可以使用建造者模式。****
结构型模式
这类模式主要关注类和对象的组合
适配器模式(Adapter)
将一个类的接口转换成客户希望的另外一个接口。适用于需要将不兼容接口的类一起工作的情况。
该例子使用适配器模式来适配不同的支付方式
统一支付接口:
public interface Payment {
void pay(double amount);
}
然后,我们为每种支付方式创建适配器:
public class CreditCardPaymentAdapter implements Payment {
private CreditCardPayment creditCardPayment;
public CreditCardPaymentAdapter(CreditCardPayment creditCardPayment) {
this.creditCardPayment = creditCardPayment;
}
@Override
public void pay(double amount) {
creditCardPayment.makePayment(amount);
}
}
public class PayPalPaymentAdapter implements Payment {
private PayPalPayment payPalPayment;
public PayPalPaymentAdapter(PayPalPayment payPalPayment) {
this.payPalPayment = payPalPayment;
}
@Override
public void pay(double amount) {
payPalPayment.processPayment(amount);
}
}
应用场景:
- 系统集成:当两个系统需要进行集成,但是它们的接口并不兼容时,可以使用适配器模式进行转换,使得两个系统可以正常交互。
- 第三方库的使用:在使用第三方库时,如果它的接口和我们的系统不兼容,可以使用适配器模式进行转换。
- 旧系统升级或重构:在进行系统升级或重构时,新系统和旧系统的接口可能不兼容,可以使用适配器模式进行转换,使得新旧系统可以正常交互。
- 多种类型的对象需要进行同样的操作:如果有多种类型的对象,它们都需要进行同样的操作,但是它们的接口不同,可以使用适配器模式进行转换,使得我们可以用同一种方式对它们进行操作。
桥接模式(Bridge)
将抽象部分与实现部分分离,使它们可以独立变化。适用于抽象和实现可以有多种方式存在的情况。
根据支付场景中使用的桥接模式的例子
支付方法接口
public interface PayMethod {
void processPayment();
}
支付方式的抽象类
public abstract class Payment {
protected PayMethod payMethod;
protected Payment(PayMethod payMethod) {
this.payMethod = payMethod;
}
public abstract void pay();
}
一些具体实现的支付方式
public class OnlinePayment extends Payment {
public OnlinePayment(PayMethod payMethod) {
super(payMethod);
}
@Override
public void pay() {
payMethod.processPayment();
}
}
public class OfflinePayment extends Payment {
public OfflinePayment(PayMethod payMethod) {
super(payMethod);
}
@Override
public void pay() {
payMethod.processPayment();
}
}
一些具体的支付方法
public class Alipay implements PayMethod {
@Override
public void processPayment() {
System.out.println("Processing payment with Alipay...");
}
}
public class WechatPay implements PayMethod {
@Override
public void processPayment() {
System.out.println("Processing payment with WechatPay...");
}
}
测试方法
public class BridgeModeTest {
@Test
public void testPaymentBridge() {
Payment onlinePayment = new OnlinePayment(new Alipay());
onlinePayment.pay();
Payment offlinePayment = new OfflinePayment(new WechatPay());
offlinePayment.pay();
}
}
应用场景
该模式对于处理复杂对象的变化非常有用,特别是当对象有多个维度的变化时
- 跨平台应用开发:例如,一个软件可能需要在不同的操作系统(如Windows,Linux,MacOS等)上运行,而且每个操作系统可能有多种版本。在这种情况下,我们可以将软件的核心功能(抽象部分)与其在各个操作系统上的实现(实现部分)分离,使得核心功能和操作系统的实现都可以独立地变化和扩展。
- UI设计:在设计用户界面时,可能需要处理各种不同的颜色主题、字体和布局等。我们可以将这些属性看作是用户界面的“实现部分”,而用户界面的核心功能则是“抽象部分”。通过使用桥接模式,我们可以独立地改变和扩展用户界面的颜色主题、字体和布局,而不影响其核心功能。
- 数据库驱动:大多数应用程序需要与数据库进行交互,而且可能需要支持多种不同的数据库(如MySQL,PostgreSQL,Oracle等)。在这种情况下,我们可以将应用程序的业务逻辑(抽象部分)与其数据库驱动(实现部分)分离,使得业务逻辑和数据库驱动都可以独立地变化和扩展。
- 网络协议:在网络编程中,我们经常需要处理各种不同的网络协议(如HTTP,FTP,SMTP等)。我们可以将网络通信的核心功能(抽象部分)与其协议实现(实现部分)分离,使得核心功能和协议实现都可以独立地变化和扩展。
组合模式(Composite)
将对象组合成树形结构以表示"部分-整体"的层次结构。适用于需要表示对象的部分-整体层次结构的情况。
我们使用组合模式设计一个简单的文件系统
抽象文件组件
public abstract class FileComponent {
public void add(FileComponent fileComponent) {
throw new UnsupportedOperationException();
}
public void remove(FileComponent fileComponent) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public abstract void display();
}
文件类和文件夹类
// 文件类
public class File extends FileComponent {
private String name;
public File(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void display() {
System.out.println(getName());
}
}
// 文件夹类
public class Directory extends FileComponent {
private String name;
private List<FileComponent> components = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
@Override
public void add(FileComponent fileComponent) {
components.add(fileComponent);
}
@Override
public void remove(FileComponent fileComponent) {
components.remove(fileComponent);
}
@Override
public String getName() {
return name;
}
@Override
public void display() {
System.out.println(getName());
for (FileComponent component : components) {
component.display();
}
}
}
应用场景
- 文件系统:这是组合模式最经典的应用场景。在文件系统中,我们有文件和文件夹,文件夹可以包含文件或者其他文件夹。这种结构就是典型的组合模式。
- GUI控件:在图形用户界面(GUI)中,我们可以将复杂的控件看作是简单控件的组合。例如,一个窗口可以包含面板,面板可以包含按钮,文本框等。这种结构也是典型的组合模式。
- 组织结构:在一个企业中,我们有CEO,下面有各个部门的经理,经理下面有员工。这种层次结构也可以使用组合模式来表示。
- HTML DOM结构:在HTML中,我们有元素和元素的容器,容器可以包含元素或者其他的容器。这种结构也是典型的组合模式。
- 语法树:在编译原理中,源代码经过词法分析和语法分析后,会生成一个语法树,这个语法树的结构就是典型的组合模式。
装饰模式(Decorator)
动态地给一个对象添加一些额外的职责。适用于需要扩展一个类的功能,或给一个类添加附加职责的情况。
使用装饰器模式实现一个简单的前端GUI页面的例子
抽象组件接口及对应的几个装饰类:
public interface Component {
String draw();
}
public class Window implements Component {
@Override
public String draw() {
return "Drawing a window";
}
}
public class ScrollBarDecorator implements Component {
private final Component component;
public ScrollBarDecorator(Component component) {
this.component = component;
}
@Override
public String draw() {
return component.draw() + " with a scrollbar";
}
}
public class BorderDecorator implements Component {
private final Component component;
public BorderDecorator(Component component) {
this.component = component;
}
@Override
public String draw() {
return component.draw() + " with a border";
}
}
测试
public class DecoratorTest {
@Test
public void testDecorator(){
Component window = new Window();
Component windowWithScrollbar = new ScrollBarDecorator(window);
Component windowWithScrollbarAndBorder = new BorderDecorator(windowWithScrollbar);
System.out.println(window.draw()); // Output: Drawing a window
System.out.println(windowWithScrollbar.draw()); // Output: Drawing a window with a scrollbar
System.out.println(windowWithScrollbarAndBorder.draw()); // Output: Drawing a window with a scrollbar with a border
}
}
常见业务应用
- 图形用户界面工具包(GUI Toolkit):图形用户界面(GUI)工具包是装饰模式的一个经典应用。用户可以动态地添加或删除元素的某些特性,比如滚动条、边框等。这种模式允许开发者在运行时添加新的行为到现有对象,而无需修改其底层代码。
- Java I/O:Java的I/O类库就广泛应用了装饰模式。例如,
BufferedReader
、InputStreamReader
等都是装饰器,它们都可以包装一个InputStream
对象,为其添加新的行为,如缓冲读取、字符编码转换等。 - Web开发中的中间件:在Web开发中,装饰模式也经常被用作中间件的实现。例如,一个HTTP请求可能需要通过身份认证、日志记录、数据压缩等一系列的中间件处理。每个中间件都可以看作是一个装饰器,它们可以在不改变其他中间件的情况下,灵活地添加或删除。
- 金融系统:在金融系统中,装饰模式也有广泛的应用。例如,一个投资组合可能包含多种不同类型的投资,如股票、债券、期权等。每种投资可能有不同的计费策略,如基础费用、交易费用、管理费用等。可以使用装饰模式动态地为投资组合添加各种费用。
- 权限控制系统:在权限控制系统中,装饰模式可以用于动态地添加或删除用户的权限。例如,一个用户可能有基本的浏览权限,然后可以动态地添加编辑权限、删除权限等。
外观模式(Facade)
提供了一个统一的接口,用来访问子系统中的一群接口。适用于需要简化和统一大型代码库的访问接口的情况。
我们使用外观模式来建议的设计一个计算机系统,它包含了CPU,内存,硬盘三个部分。
// 子系统类
public class CPU {
public void start() {
System.out.println("CPU is starting...");
}
}
public class Memory {
public void start() {
System.out.println("Memory is starting...");
}
}
public class HardDrive {
public void start() {
System.out.println("Hard Drive is starting...");
}
}
// 外观类
public class Computer {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public Computer() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void start() {
cpu.start();
memory.start();
hardDrive.start();
}
}
业务场景应用
- API封装:在开发大型应用时,通常会有许多复杂的子系统,如数据库、文件系统、网络通信等。为了简化这些子系统的使用,可以使用外观模式创建一个统一的API接口。
- 库和框架:很多库和框架都使用外观模式来提供简洁的接口。例如,jQuery库提供了一个简单的API来处理复杂的JavaScript任务。
- 微服务架构:在微服务架构中,每个服务都是一个独立的子系统。为了简化服务之间的交互,可以使用外观模式创建一个API网关,该网关封装了所有微服务的接口。
- 操作系统:操作系统提供了许多底层的API,如文件操作、网络通信、内存管理等。为了简化这些API的使用,操作系统通常会提供一个更高级别的API,这就是使用了外观模式。
- 电商平台:在电商平台中,购物车、支付、库存管理、物流等都是独立的子系统。为了简化用户的购物行为,可以使用外观模式创建一个购物接口,该接口封装了所有子系统的操作。
Q: 装饰模式和外观模式感觉好像啊?它们有什么区别?
A:
- 目的:装饰模式的目的是给一个对象动态地添加职责,而外观模式的目的是简化一个接口。
- 使用场景:装饰模式用于在运行时给对象动态地添加新的行为,而外观模式用于简化复杂系统的接口,使得客户端更容易地使用这个系统。
- 设计:装饰模式通常有一个抽象组件类和一些具体组件类,装饰器类继承自抽象组件类,可以包装任何具体组件对象。而外观模式只有一个外观类,它包含了对子系统的引用,提供了一个简化的接口。
享元模式(Flyweight)
使用共享技术有效地支持大量细粒度的对象。适用于需要大量对象,且对象之间有很多重复状态的情况。
假设我们有一个绘制不同颜色的圆的系统。如果每次绘制圆时都创建一个新的对象,那么系统可能会因为创建太多的对象而变慢或者耗尽内存。为了避免这种情况,我们可以使用享元模式来重用已经创建的圆对象。
创建一个Circle
接口:
public interface Circle {
void draw();
}
我们创建一个实现了Circle
接口的ConcreteCircle
类:
public class ConcreteCircle implements Circle {
private String color;
public ConcreteCircle(String color) {
this.color = color;
}
@Override
public void draw() {
System.out.println("Drawing circle of color : " + color);
}
}
创建一个CircleFactory
类来管理ConcreteCircle
对象:
import java.util.HashMap;
import java.util.Map;
public class CircleFactory {
private static final Map<String, Circle> circleMap = new HashMap<>();
public static Circle getCircle(String color) {
Circle circle = circleMap.get(color);
if (circle == null) {
circle = new ConcreteCircle(color);
circleMap.put(color, circle);
System.out.println("Creating circle of color : " + color);
}
return circle;
}
}
测试
public class FlyweightPatternTest {
private static final String[] colors = {"Red", "Green", "Blue", "White", "Black"};
@Test
public void testFlyweightPattern() {
for (int i = 0; i < 10; ++i) {
Circle circle = CircleFactory.getCircle(getRandomColor());
circle.draw();
}
}
private String getRandomColor() {
return colors[(int) (Math.random() * colors.length)];
}
}
在这个测试方法中,我们尝试创建10个圆,但实际上只有5种颜色的圆对象被创建。当我们尝试获取一个已经存在的圆对象时,CircleFactory
会直接返回该对象,而不是创建一个新的对象。这就是享元模式的作用。
场景应用
享元模式在工作业务中的应用场景可以非常广泛,主要是在需要大量重复对象,且这些对象的大部分状态都可以外部化的情况下,可以使用享元模式来减少系统的内存消耗。以下是一些常见的应用场景:
- 数据库连接池:数据库连接是一种资源消耗较大的操作,如果每次需要访问数据库都新建一个连接,会造成不必要的资源浪费。通过使用享元模式,可以创建一个连接池,当需要访问数据库时,先从连接池中取出一个已经创建的连接,使用完毕后再放回连接池,这样就可以复用已经创建的连接,减少资源消耗。
- 字符串常量池:在Java等编程语言中,为了减少相同字符串的重复创建,通常会使用字符串常量池来存储已经创建的字符串。当需要创建新的字符串时,先检查字符串常量池中是否已经存在该字符串,如果已经存在,则直接返回该字符串,否则创建新的字符串。
- GUI开发:在图形用户界面(GUI)开发中,有许多组件是可以复用的,比如按钮、文本框等。通过使用享元模式,可以复用这些组件,减少系统的内存消耗。
- 游戏开发:在游戏开发中,经常需要创建大量的游戏对象,比如子弹、敌人等。如果每个对象都占用一定的内存,那么在大量对象的情况下,会消耗大量的内存。通过使用享元模式,可以复用游戏对象,减少内存消耗。
- 文档编辑器:在文档编辑器中,可能需要处理大量的字符。通过使用享元模式,可以对字符进行复用,减少内存消耗。
注:
上述示例是由HashMap来规避对象的重复创建,当然我们可以用其他工具类来实现享元模式,如:
- List或Set: 可以将已经创建的对象存储在List或Set中。当需要创建新的对象时,遍历这个List或Set,检查是否已经存在相同的对象。这种方法的缺点是,如果存储的对象很多,那么检索效率会比较低。
- 数组: 如果可创建的对象数量有限且已知,比如棋盘游戏中的棋子类型,可以使用数组来存储已经创建的对象。数组的索引可以对应到具体的对象类型,这样检索效率会比较高。
- 缓存工具: 可以使用一些缓存工具,比如Redis、Memcached等,来存储已经创建的对象。这种方法的优点是可以处理大量的对象,且检索效率较高。但是,需要额外的缓存服务器,且实现复杂度较高。
- 对象池: 对象池是一种特殊的数据结构,用于存储和管理对象。对象池在创建时会预先创建一定数量的对象,当需要对象时,从池中取出一个,使用完毕后再放回池中。这种方法适用于对象创建成本较高的情况,比如数据库连接、线程等。
- 软引用或弱引用: 在Java中,可以使用软引用(SoftReference)或弱引用(WeakReference)来存储对象。软引用和弱引用指向的对象在内存不足时会被垃圾回收器回收,这样可以防止内存泄漏。这种方法适用于大量的对象,且这些对象的生命周期不需要精确控制的情况。
以上就是一些常见的方法,具体使用哪种方法,需要根据实际的业务需求和系统资源来决定。
代理模式(Proxy)
为其他对象提供一种代理以控制对这个对象的访问。适用于需要控制对原始对象的访问,或需要在访问对象时附加其他操作的情况。
在这个例子中,我们将创建一个数据库服务接口,一个真实的数据库服务,以及一个代理数据库服务。代理服务将在真实服务之前添加一些额外的逻辑,例如日志记录和访问控制。
接口及实现类:
public interface DatabaseService {
void query(String sql);
}
public class RealDatabaseService implements DatabaseService {
@Override
public void query(String sql) {
System.out.println("Executing query: " + sql);
}
}
public class ProxyDatabaseService implements DatabaseService {
private DatabaseService realDatabaseService;
public ProxyDatabaseService(DatabaseService realDatabaseService) {
this.realDatabaseService = realDatabaseService;
}
@Override
public void query(String sql) {
System.out.println("Log: " + sql);
if (checkAccess()) {
realDatabaseService.query(sql);
} else {
System.out.println("Access denied for query: " + sql);
}
}
private boolean checkAccess() {
// Access control logic here
return true;
}
}
测试
public class ProxyTest {
@Test
public void testQuery() {
DatabaseService realDatabaseService = new RealDatabaseService();
DatabaseService proxyDatabaseService = new ProxyDatabaseService(realDatabaseService);
// Capture the standard output
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
String testSql = "SELECT * FROM users";
proxyDatabaseService.query(testSql);
String expectedOutput = "Log: " + testSql + "\r\n" + "Executing query: " + testSql + "\r\n";
assertEquals(expectedOutput, outContent.toString());
}
}
常见应用场景
- 远程代理:远程代理可以隐藏一个对象存在于不同地址空间的事实。例如,当你在浏览一个存在于远程服务器上的网页时,你实际上是通过浏览器(作为代理)在进行交互。
- 虚拟代理:如果一个对象需要很大的计算资源来创建,而创建过程中又不需要用户交互,那么可以使用虚拟代理。代理先为这个大对象提供一个替身,然后在后台悄悄创建这个大对象。当对象准备就绪后,代理再将请求转发给这个对象。
- 保护代理:如果需要对原对象的访问进行权限控制,可以使用保护代理。例如,一些系统可能需要对其用户的访问权限进行控制,只有满足特定条件的用户才能访问某些资源。
- 智能引用代理:当一个对象被引用时,提供一些额外的操作,比如计算对象的引用次数,这样就可以在适当的时候释放对象。
- 缓冲代理:在网络应用中,尤其是web应用中,缓存代理(Caching Proxy)起着重要的作用,它可以暂时存储(缓存)客户端请求的结果。当其他客户端请求同样的内容时,可以直接从缓存中获取,而不必再向服务器请求,这样可以减少网络带宽的使用和服务器的负载。
- 防火墙(Firewall)代理:控制网络资源的访问,保护主机不会被恶意用户接触。
- 同步代理:在多线程环境中,它可以在工作线程完成一定任务之前,暂时代替其完成一些工作。
行为型模式
这类模式主要关注对象之间的通信
责任链模式(Chain of Responsibility)
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。适用于有多个对象可以处理同一请求,但具体由哪个对象处理未定的情况。
这种模式通常用于构建一个对象链,每个对象都有机会处理请求,或者将请求传递给链中的下一个对象
假设我们有一个日志系统,它可以记录不同级别的日志:错误(Error)、警告(Warning)和信息(Info)。我们可以创建一个日志抽象类和几个具体的日志记录器,每个记录器知道其下一个级别的记录器是谁,以此形成一个责任链。
//日志的级别
public class Level {
public static final int INFO = 1;
public static final int WARNING = 2;
public static final int ERROR = 3;
}
//抽象的记录器类
public abstract class Logger {
protected int level;
protected Logger nextLogger;
public void setNextLogger(Logger nextLogger){
this.nextLogger = nextLogger;
}
public void logMessage(int level, String message){
if(this.level <= level){
write(message);
}
if(nextLogger !=null){
nextLogger.logMessage(level, message);
}
}
abstract protected void write(String message);
}
//具体的记录器类
public class ConsoleLogger extends Logger {
public ConsoleLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Standard Console::Logger: " + message);
}
}
public class ErrorLogger extends Logger {
public ErrorLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Error Console::Logger: " + message);
}
}
public class FileLogger extends Logger {
public FileLogger(int level){
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("File::Logger: " + message);
}
}
测试
public class ResponsibilityTest {
@Test
public void testResponsibility(){
Logger loggerChain = getChainOfLoggers();
loggerChain.logMessage(Level.INFO, "This is an information.");
loggerChain.logMessage(Level.WARNING, "This is a warning level message.");
loggerChain.logMessage(Level.ERROR, "This is an error information.");
}
@Test
public void testChainOfResponsibility() {
Logger loggerChain = ResponsibilityTest.getChainOfLoggers();
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
loggerChain.logMessage(Level.INFO, "This is an information.");
assertTrue(outContent.toString().contains("Standard Console::Logger: This is an information."));
outContent.reset();
loggerChain.logMessage(Level.WARNING, "This is a warning level message.");
assertTrue(outContent.toString().contains("File::Logger: This is a warning level message."));
assertTrue(outContent.toString().contains("Standard Console::Logger: This is a warning level message."));
outContent.reset();
loggerChain.logMessage(Level.ERROR, "This is an error information.");
assertTrue(outContent.toString().contains("Error Console::Logger: This is an error information."));
assertTrue(outContent.toString().contains("File::Logger: This is an error information."));
assertTrue(outContent.toString().contains("Standard Console::Logger: This is an error information."));
}
/*
* 按照日志级别的顺序(Info < Warning < Error)将这些记录器链接起来
*/
private static Logger getChainOfLoggers(){
Logger errorLogger = new ErrorLogger(Level.ERROR);
Logger fileLogger = new FileLogger(Level.WARNING);
Logger consoleLogger = new ConsoleLogger(Level.INFO);
errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);
return errorLogger;
}
}
常见应用场景
- 审批流程:在许多企业中,审批流程都是一个多级的过程,比如请假、报销等,需要通过多个级别的审批。这种情况下就可以使用责任链模式,每个审批级别是链上的一个节点,请求在链上传递,直到被处理。
- 日志记录:在日志记录系统中,根据日志的级别(ERROR、WARNING、INFO、DEBUG等),可能需要不同的处理方式,例如只将ERROR级别的日志写入文件,而将所有级别的日志输出到控制台。这种情况下,可以使用责任链模式,将不同的日志处理器链接起来,每个处理器处理各自负责的日志级别。
- GUI事件处理:在图形用户界面的事件处理中,事件可能需要被多个对象处理,例如一个鼠标点击事件,可能需要被按钮对象、窗口对象、应用对象等依次处理。这种情况下,可以使用责任链模式,将这些对象链接起来,形成一个事件处理的责任链。
- Java的异常处理:在Java的异常处理机制中,当一个方法抛出一个异常时,JVM会将这个异常传递给调用者处理,如果调用者不能处理(没有对应的catch块),则继续抛给调用者的调用者,直到被处理。这其实就是一个责任链模式。
- 过滤器:在Web开发中,请求经常需要经过一系列的过滤器进行处理,例如字符编码过滤器、登录状态检查过滤器、权限检查过滤器等。这种情况下,可以使用责任链模式,将这些过滤器链接起来,形成一个处理请求的责任链。
命令模式(Command)
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。适用于需要将请求调用者和请求接收者解耦的情况。
我们可以创建一个打开灯的命令和一个关闭灯的命令来实现模拟命令模式
定义一个命令接口,所有的命令都需要实现这个接口:
public interface Command {
void execute();
}
一些具体的实现命令
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
light实现
public class Light {
public void on() {
System.out.println("Light on");
}
public void off() {
System.out.println("Light off");
}
}
命令的调用者
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
测试
public class CommandPatternTest {
@Test
public void testCommandPattern() {
Light light = new Light();
Command lightOnCommand = new LightOnCommand(light);
Command lightOffCommand = new LightOffCommand(light);
RemoteControl remoteControl = new RemoteControl();
// Turn on the light
remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton();
// Turn off the light
remoteControl.setCommand(lightOffCommand);
remoteControl.pressButton();
}
}
常用场景
- 操作队列:命令模式可以用于实现操作队列。你可以按顺序将命令对象放入队列中,并在需要的时候依次执行它们。例如,在电商平台中,用户的购买操作、退货操作等都可以看作是一种命令,系统通过命令模式将这些操作排队处理。
- 撤销/重做:命令模式是实现撤销和重做操作的一种常用方式。每个命令都有一个执行和撤销的方法,命令的执行历史保存在一个列表中。通过这个列表和这些方法,你可以在任何时候将命令的执行撤销或者重做。例如,在文本编辑器(如Word,记事本等)中,我们所做的修改、删除、添加操作都可以通过撤销或重做来实现。
- 异步操作:命令模式可以将一组操作的调用、请求和执行分离,使得发送者和接收者不直接交互。这非常适用于异步和后台操作。例如,你可能需要在后台线程中执行一些操作,而主线程则可以通过命令模式将这些操作封装为命令,然后将它们发送到后台线程中执行。
- 宏命令:命令模式可以用于实现宏,即一组命令的集合。当执行一个宏命令时,一系列的命令将按照特定的顺序执行。例如,在游戏中,玩家可以创建一些宏命令来实现一系列的动作。
- GUI按钮与菜单项:在图形用户界面中,按钮和菜单项的行为可以使用命令模式来实现。每一个按钮或菜单项都是一个具体的命令。
- 网络请求:在网络编程中,每一个网络请求都可以看作是一个命令。客户端发送的请求被封装为命令对象,然后在服务器端执行。
解释器模式(Interpreter)
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。适用于需要解析语言,并且语言中的句子可以表示为抽象语法树的情况。
为了简化问题,我们可以设计一个简单的“加减法解释器”,它能解析包含加法和减法的表达式。
定义一个表达式接口
public interface Expression {
int interpret();
}
具体的加法和减法表达式
public class AddExpression implements Expression {
private final Expression leftExpression;
private final Expression rightExpression;
public AddExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret() {
return leftExpression.interpret() + rightExpression.interpret();
}
}
public class SubtractExpression implements Expression {
private final Expression leftExpression;
private final Expression rightExpression;
public SubtractExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret() {
return leftExpression.interpret() - rightExpression.interpret();
}
}
创建一个数字表达式,它是一个终结符表达式
public class NumberExpression implements Expression {
private final int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() {
return this.number;
}
}
创建一个解析器来解析表达式
public class Parser {
public Expression parse(String str) {
// 这里简化了解析过程,只处理形如 "2 + 3 - 4" 的表达式
String[] elements = str.split(" ");
Expression left = new NumberExpression(Integer.parseInt(elements[0]));
for (int i = 1; i < elements.length; i += 2) {
String operator = elements[i];
int number = Integer.parseInt(elements[i + 1]);
switch (operator) {
case "+":
left = new AddExpression(left, new NumberExpression(number));
break;
case "-":
left = new SubtractExpression(left, new NumberExpression(number));
break;
default:
throw new IllegalArgumentException("Unsupported operator: " + operator);
}
}
return left;
}
}
测试
public class ParserPatternTest {
@Test
public void testParser() {
Parser parser = new Parser();
String expression = "2 + 3 - 4";
Expression expr = parser.parse(expression);
System.out.println(expression + " = " + expr.interpret());
}
}
常用范围
- 编程语言解释器:这是解释器模式最直接的应用场景。例如,Python、JavaScript、Ruby等都是解释型语言,它们的执行环境就是一个按照解释器模式设计的系统。
- SQL解析:数据库查询语言SQL就是一个典型的领域特定语言,数据库系统需要解析SQL语句并执行相应的操作,这个过程就可以用解释器模式来实现。
- 配置文件解析:很多软件系统都会使用配置文件来设置一些参数,例如XML、JSON、YAML等格式的文件,解析这些配置文件的过程也可以看作是解释器模式的应用。
- 表达式计算:在很多软件系统中,都需要对一些表达式进行计算,例如Excel中的公式计算,或者一些规则引擎中的规则解析,这些都是解释器模式的应用场景。
- 协议解析:在网络通信中,各种各样的协议都需要进行解析,例如HTTP协议、FTP协议等,这些协议的解析过程也可以用解释器模式来实现。
- 模板引擎:模板引擎需要解析模板语言并生成相应的文档,这个过程也可以用解释器模式来实现,例如Jinja2、Thymeleaf等。
迭代器模式(Iterator)
提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。适用于需要提供一种统一的遍历对象的方法的情况。
这里在Java中可以使用Iterator接口偷懒实现迭代器模式
一个简单的书籍对象
@Data
public class Book {
private final String name;
public Book(String name) {
this.name = name;
}
}
书架对象及书架迭代器对象
public class BookShelf implements Iterable<Book> {
private final Book[] books;
private int last = 0;
public BookShelf(int maxsize) {
this.books = new Book[maxsize];
}
public Book getBookAt(int index) {
return books[index];
}
public void appendBook(Book book) {
this.books[last] = book;
last++;
}
public int getLength() {
return last;
}
@Override
public Iterator<Book> iterator() {
return new BookShelfIterator(this);
}
}
public class BookShelfIterator implements Iterator<Book> {
private final BookShelf bookShelf;
private int index;
public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < bookShelf.getLength();
}
@Override
public Book next() {
Book book = bookShelf.getBookAt(index);
index++;
return book;
}
}
测试
public class IteratorTest {
@Test
public void testIterator() {
BookShelf bookShelf = new BookShelf(4);
bookShelf.appendBook(new Book("Around the World in 80 Days"));
bookShelf.appendBook(new Book("Bible"));
bookShelf.appendBook(new Book("Cinderella"));
bookShelf.appendBook(new Book("Daddy-Long-Legs"));
for (Book book : bookShelf) {
System.out.println(book.getName());
}
}
}
迭代器么,大家都懂就不介绍了
中介者模式(Mediator)
用一个中介对象来封装一系列的对象交互。适用于一组对象以定义良好但复杂的方式进行通信的情况。
中介者模式主要用于减少对象之间的直接交互,使得对象之间的依赖关系更加清晰,避免出现网状结构。下面将创建一个简单的聊天室例子来展示中介者模式
创建一个Mediator
接口和一个实现类ChatRoom
public interface Mediator {
void sendMessage(String msg, User user);
}
public class ChatRoom implements Mediator {
@Override
public void sendMessage(String msg, User user) {
System.out.println(user.getName() + " says: " + msg);
}
}
创建一个User
类,它将使用Mediator
来发送消息:
public class User {
private String name;
private Mediator chatRoom;
public User(String name, Mediator chatRoom) {
this.name = name;
this.chatRoom = chatRoom;
}
public String getName() {
return this.name;
}
public void send(String msg) {
chatRoom.sendMessage(msg, this);
}
}
测试
public class MediatorPatternTest {
@Test
public void testMediator() {
Mediator chatRoom = new ChatRoom();
User john = new User("John", chatRoom);
User jane = new User("Jane", chatRoom);
john.send("Hi, Jane!");
jane.send("Hello, John!");
}
}
减少对象之间的直接交互,使得对象之间的依赖关系更加清晰。
业务中常见使用场景
- 聊天室:聊天室是中介者模式的一个经典例子。聊天室充当中介者的角色,处理所有的消息发送和接收。每个用户(或者称为参与者)不直接与其他用户通信,而是通过聊天室来发送和接收消息。
- 飞机调度系统:在一个机场,飞机调度系统可以用作中介者,管理所有的飞机起飞和降落。飞机不需要直接与其他飞机交流,而是通过调度系统来知道何时起飞和降落。
- MVC框架:在MVC(Model-View-Controller)框架中,控制器(Controller)经常扮演中介者的角色。它协调模型(Model)和视图(View)之间的交互。
- 图形用户界面:在复杂的图形用户界面(GUI)中,中介者模式可以帮助减少组件之间的直接交互,使得组件更加独立,代码更易于维护和扩展。
- 工作流引擎:在工作流引擎中,中介者模式可以用来协调各个步骤或过程之间的交互,使得整个流程可以灵活地进行修改和扩展。
备忘录模式(Memento)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。适用于需要保存一个对象在某一个时刻的状态或部分状态的情况。
设计一个简单的备忘录模式。这个模式通常用于实现撤销操作。
创建备忘录类
@Getter
public class Memento {
private final String state;
public Memento(String state) {
this.state = state;
}
}
创建发起人
@Getter
public class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
public Memento saveStateToMemento() {
return new Memento(state);
}
public void getStateFromMemento(Memento memento) {
state = memento.getState();
}
}
创建负责人
public class CareTaker {
private final List<Memento> mementoList = new ArrayList<>();
public void add(Memento state) {
mementoList.add(state);
}
public Memento get(int index) {
return mementoList.get(index);
}
}
测试
public class MementoPatternTest {
/*
* 在这个测试方法中,我们首先创建了一个发起人和一个负责人。
* 然后,我们改变了发起人的状态,并保存了每个状态。
* 最后,我们从负责人中恢复了每个状态,并打印出来。
*/
@Test
public void testMemento() {
Originator originator = new Originator();
CareTaker careTaker = new CareTaker();
originator.setState("State #1");
originator.setState("State #2");
careTaker.add(originator.saveStateToMemento());
originator.setState("State #3");
careTaker.add(originator.saveStateToMemento());
originator.setState("State #4");
System.out.println("Current State: " + originator.getState());
originator.getStateFromMemento(careTaker.get(0));
System.out.println("First saved State: " + originator.getState());
originator.getStateFromMemento(careTaker.get(1));
System.out.println("Second saved State: " + originator.getState());
}
}
常用场景
- 撤销操作:这可能是备忘录模式最常见的用例。许多应用程序(如文本编辑器、图像编辑器、数据库事务管理等)都需要提供撤销操作的功能。备忘录模式可以在执行操作前保存状态,如果用户选择撤销操作,就可以恢复到之前的状态。
- 游戏存档:在许多游戏中,玩家可以在任意时间点保存游戏状态,然后在以后的任意时间恢复到这个状态。这就是备忘录模式的应用。
- 软件版本控制:在软件开发中,版本控制系统(如Git)可以保存项目在特定时间点的状态,然后允许开发者在任意时间切换到任意版本。这也是备忘录模式的一个实际应用。
- 数据库事务管理:在数据库事务中,如果事务失败或者被用户中止,系统需要能够回滚到事务开始前的状态。备忘录模式就可以用来实现这个功能。
- 快照功能:在一些系统中,可能需要定期保存系统状态的快照,以便在系统出现问题时可以恢复到正常状态。这也是备忘录模式的一个应用场景。
观察者模式(Observer)
当对象间存在一对多关系时,使用这种模式。当一个对象被修改时,会自动通知它的依赖对象。适用于一个对象状态的改变需要同时改变其他对象的状态,而且实际的对象是动态的情况。
这个例子中,我们有一个新闻发布系统,有多个订阅者关注这个系统。当有新的新闻发布时,所有的订阅者都应该得到通知。
public class News {
private final List<Consumer<String>> listeners = new ArrayList<>();
public void registerListener(Consumer<String> listener) {
listeners.add(listener);
}
public void unregisterListener(Consumer<String> listener) {
listeners.remove(listener);
}
public void publishNews(String news) {
listeners.forEach(listener -> listener.accept(news));
}
}
测试
public class ObserverTest {
@Test
public void testObserver() {
News news = new News();
Consumer<String> listener1 = (newsItem) -> System.out.println("Listener 1 received news: " + newsItem);
Consumer<String> listener2 = (newsItem) -> System.out.println("Listener 2 received news: " + newsItem);
news.registerListener(listener1);
news.registerListener(listener2);
news.publishNews("Big news!");
}
}
观察者模式在工作业务方面的常见应用场景包括:
- 实时通知:例如股票价格的实时更新,或者新闻网站的实时新闻更新,都可以使用观察者模式来实现。
- 事件驱动程序:例如GUI中的按钮点击事件,或者键盘按键事件,都可以使用观察者模式来实现。
- 数据绑定:例如在前端开发中,当数据模型发生改变时,视图也需要随之更新,这可以使用观察者模式来实现。
- 系统间的消息传递:例如微服务架构中,一个服务的状态变化需要通知其他服务,也可以使用观察者模式。
- 社交网络:例如用户发布状态,所有关注该用户的人都会收到通知,这就是观察者模式的应用。
状态模式(State)
允许一个对象在其内部状态改变时改变它的行为。适用于一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为的情况。
假设我们有一个文档,它可以有三种状态:草稿,审核和发布
public interface DocumentState {
void handlePublish(Document document);
void handleReview(Document document);
}
public class Draft implements DocumentState {
@Override
public void handlePublish(Document document) {
document.setState(new Published());
}
@Override
public void handleReview(Document document) {
document.setState(new Review());
}
}
public class Review implements DocumentState {
@Override
public void handlePublish(Document document) {
document.setState(new Published());
}
@Override
public void handleReview(Document document) {
// do nothing
}
}
public class Published implements DocumentState {
@Override
public void handlePublish(Document document) {
// do nothing
}
@Override
public void handleReview(Document document) {
// do nothing
}
}
public class Document {
private DocumentState state;
public Document() {
this.state = new Draft();
}
public void setState(DocumentState state) {
this.state = state;
}
public void publish() {
state.handlePublish(this);
}
public void review() {
state.handleReview(this);
}
}
测试
public class StatePatternTest {
@Test
public void testDocumentState() {
Document document = new Document();
document.publish();
assertTrue(document.getState() instanceof Published);
document = new Document();
document.review();
assertTrue(document.getState() instanceof Review);
}
}
业务应用场景
- 订单处理:订单在创建、支付、发货、完成等过程中的状态转换。
- 工作流引擎:工作流引擎中经常需要根据当前状态来决定下一步的操作。
- 游戏编程:游戏角色常常需要根据当前状态(如正常、攻击、防御、逃跑等)来决定行为。
- 网络协议:TCP协议中的连接状态,如LISTEN、SYN-SENT、ESTABLISHED等。
策略模式(Strategy)
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。适用于有多种算法可以解决一个问题的时候,可以根据情况动态选择算法。
我们以一个简单的排序策略来实现策略模式
public interface SortStrategy {
void sort(int[] numbers);
}
public class BubbleSortStrategy implements SortStrategy {
public void sort(int[] numbers) {
// 实现冒泡排序
}
}
public class QuickSortStrategy implements SortStrategy {
public void sort(int[] numbers) {
// 实现快速排序
}
}
public class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] numbers) {
strategy.sort(numbers);
}
}
测试
public class SortStrategyTest {
@Test
public void testSortStrategy() {
int[] numbers = {5, 2, 4, 6, 1, 3};
Sorter sorter = new Sorter(new BubbleSortStrategy());
sorter.sort(numbers);
// 断言排序结果
sorter = new Sorter(new QuickSortStrategy());
sorter.sort(numbers);
// 断言排序结果
}
}
策略模式在现实业务中的应用场景
- 报价系统:根据不同的客户和销售策略,报价系统需要提供多种报价策略。例如,新客户大单报价、老客户小批量报价等。
- 促销策略:电商平台经常会有各种各样的促销活动,如满减、打折、赠品等。这些不同的促销策略可以使用策略模式来实现。
- 路由策略:在微服务架构中,服务之间的调用可能有多种路由策略,如随机路由、轮询、根据响应时间选择最快的服务等。
- 资源清理策略:在系统运行过程中,可能需要对某些资源进行清理,如内存、磁盘空间、数据库连接等。不同的资源可能需要不同的清理策略。
模板方法模式(Template Method)
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。适用于一些通用的处理需要在不同的子类中有不同的实现,或者部分处理由子类实现的情况。
这里我们使用模板方法模式实现一个简单的数据挖掘例子
定义一个抽象类,该类包含一个模板方法,该方法定义了数据挖掘任务的步骤
public abstract class DataMiningTask {
// Template method
public final void executeTask() {
preprocessData();
runDataMiningAlgorithm();
postprocessResults();
}
// Steps defined in the template method
public abstract void preprocessData();
public abstract void runDataMiningAlgorithm();
public abstract void postprocessResults();
}
定义一些具体的数据挖掘任务,这些任务将实现上述抽象类中定义的方法
public class ClusteringTask extends DataMiningTask {
@Override
public void preprocessData() {
// Implementation of data preprocessing for clustering
}
@Override
public void runDataMiningAlgorithm() {
// Implementation of a clustering algorithm
}
@Override
public void postprocessResults() {
// Implementation of results postprocessing for clustering
}
}
public class ClassificationTask extends DataMiningTask {
@Override
public void preprocessData() {
// Implementation of data preprocessing for classification
}
@Override
public void runDataMiningAlgorithm() {
// Implementation of a classification algorithm
}
@Override
public void postprocessResults() {
// Implementation of results postprocessing for classification
}
}
测试
public class TemplatePattenTest {
@Test
public void testClusteringTask() {
DataMiningTask task = new ClusteringTask();
task.executeTask();
task = new ClassificationTask();
task.executeTask();
}
}
在工作业务中,模板方法模式的应用场景主要包括:
- 数据挖掘任务:数据预处理、数据挖掘和结果后处理这三个步骤是一致的
- Web开发框架:例如Spring MVC,在处理HTTP请求时,都需要进行请求解析、处理和响应
- 测试框架:例如JUnit,每个测试用例的执行流程包括设置环境、执行测试和清理环境
- 数据库访问:例如Hibernate,在进行数据库访问时,都需要进行连接、执行和关闭连接
访问者模式(Visitor)
在不改变数据结构的前提下,增加作用于整个结构上的操作。适用于数据结构相对稳定,但操作又需要经常变化的情况。
我们使用访问者模式来创建一个电子交易产品的应用,例如书籍和电子产品
public interface Product {
void accept(Visitor visitor);
}
定义两种类型的产品:书籍和电子产品:
public class Book implements Product {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
public class Electronic implements Product {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
定义访问者接口:
public interface Visitor {
void visit(Book book);
void visit(Electronic electronic);
}
创建一个具体的访问者,比如,一个计算折扣的访问者
public class DiscountVisitor implements Visitor {
@Override
public void visit(Book book) {
System.out.println("Apply 10% discount on books.");
}
@Override
public void visit(Electronic electronic) {
System.out.println("Apply 20% discount on electronics.");
}
}
创建一个产品列表,并让折扣访问者访问每个产品:
public class DiscountVisitor implements Visitor {
@Override
public void visit(Book book) {
System.out.println("Apply 10% discount on books.");
}
@Override
public void visit(Electronic electronic) {
System.out.println("Apply 20% discount on electronics.");
}
}
测试方法
public class VisitorPatternTest {
@Test
public void testVisitor() {
List<Product> productList = new ArrayList<>();
productList.add(new Book());
productList.add(new Electronic());
Visitor visitor = new DiscountVisitor();
for (Product product : productList) {
product.accept(visitor);
}
}
}
在工作业务中,访问者模式常见的应用场景包括
- 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
- 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。
Golang常用设计模式
虽然说凤姐穿比基尼语言核心的理念是简洁为上,但根据业务场景,还需要一些相对简洁的设计模式来更合理的处理业务,下面列举一些常用的设计模式在Golang中实现的方式
- 工厂模式:在Golang中,我们通常使用构造函数来实现工厂模式。
- 单例模式:Golang中的单例模式通常通过sync.Once或者init()函数来实现。
- 策略模式:Golang的接口特性使得策略模式的实现变得非常直观和简单。
- 观察者模式:Golang可以通过channel和goroutine来实现观察者模式,这是一种非常具有Golang特色的实现方式。
- 装饰器模式:Golang的装饰器模式通常通过在函数中嵌套函数来实现。
- 适配器模式:Golang的接口可以很好地支持适配器模式。
- 命令模式:Golang的函数类型和接口可以很好地实现命令模式。
- 中介者模式:Golang中的channel就是一种很好的中介者模式实现。
- 原型模式:Golang可以通过结构体的深拷贝来实现原型模式。
- 组合模式:Golang的嵌入特性可以很好地实现组合模式。
TODO:完善Golang的常用设计模式案例及测试
Rust常用设计模式
比起设计模式,我相信我最讨厌的还是某个`a
TODO:这里由于个人实力,时间及篇幅的问题,可能后面会再把Rust的常用设计模式及代码好好复现一遍
写代码确实很容易,但是如何合理的设计和规划项目结构,使用设计模式,写出的代码简洁易懂,不冗余,可靠性高才是难上加男
Q.E.D.