策略模式与工厂模式

2024-03-10

策略模式

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

1.意图

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

2.单一职责

单一指责:定义统一的行为声明(接口),让具有相同功能但不同解决方式的方法都实现这一接口。(定义一个统一的接口,让实现类实现这一接口,他们就具有了统一的调用方法,只需按需调用类即可,同时每种实现方法又相互隔离,这样就实现了类的单一职责)

image-20240310203811591

如上图所示,三个压缩方法都实现了compress,调用者只需要按需注入类并调用compress即可,他们具有相同的行为,这样在调用他们的时候可以灵活替换。

3.二次抽取

除此之外,策略模式还建议我们在整个层次之上再抽取一个上下文的对象。在对象中,可以封装一些其他的操作,比如负责准备算法行为所需的参数,或者记录一些日志信息等等。也可以仅仅只是转发请求到压缩处理器上,尽管看起来多此一举,但实际上该上下文对象承担着屏蔽底层变动的风险。整体结构如下:

image-20240310204127295

CompressContext作为调用的统一入口,可以封装调用前的准备工作,如日志、数据清理等。有什么好处?如果我们后续有需求需要记录日志,可以直接在抽取的方法中进行统一编写,而无需改动业务层代码或在每种实现方法中去编写。

4.调用

通过调用方法我们可以发现,方法直接已经可以相互替换,只需要改变实现类即可,接下来我们会使用简单工厂模式搭配策略模式彻底消除if -else。

public class Client {

public static void main(String[] args) throws IOException {
    System.out.println("|==> Start -------------------------------------------------------|");
    // 压缩磁盘文件为zip格式压缩包
    CompressContext context = new CompressContext(new ZipCompressor());
    context.compressFile(Paths.get("opt", "test", "funny"), Paths.get("opt", "test", "output.zip"));

    // 压缩磁盘文件为tar格式压缩包
    context = new CompressContext(new TarCompressor());
    context.compressFile(Paths.get("opt", "test", "one"), Paths.get("opt", "test", "output.tar"));

    // 压缩class为jar包
    context = new CompressContext(new JarCompressor());
    Class<?>[] toPackageClasses = { JarCompressor.class, CompressStrategy.class, Client.class };
    context.compressClasses(toPackageClasses, Paths.get("opt", "test", "output.jar"));
}

}

对此我们已经实现了策略模式的意图

  • 定义一系列算法,把它们一个个封装起来:算法就是策略类的内部处理逻辑,压缩到不同的压缩文件格式就属于不同的算法。一个一个封装起来指的是一个把原本在一个类中的多个算法逻辑拆分到不同的类中。简而言之就是将不同的算法逻辑拆分到不同的类中进行实现;
  • 并且使它们可相互替换:同一种类型的算法之间可相互替换,比如JarCompressor#compressTarCompressor#compress二者可以互相替换,因为他们的行为实现自同一个接口;
  • 使得算法可独立于使用它的客户而变化:这句描述的是,算法逻辑的变化不会造成客户端代码的改变,比如改变JarCompressor#compress的方法代码就意味着算法的逻辑已经发生了变化,但我们并不需要修改使用处的代码。

工厂模式+策略模式

与策略模式的区别:

  • 一个关注对象创建(工厂)
  • 一个关注行为的封装(策略)

1.意图

工厂模式(三种: 简单工厂, 工厂方法, 抽象工厂) -> 进阶改进策略加工厂

简单工厂:其实不属于23种设计模式,反而是一种编程习惯。

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

抽象工厂:略

最大特点:解耦。

2.如何实现

  • 定义一个用于创建对象的接口:工厂接口中的生产产品的方法;
  • 让子类决定实例化哪一个类:把产品的生产过程交给具体的工厂实现,各个具体的工厂生产具体的产品。

基于上述策略模式,我们依然需要用if-else来判断何时去实例化一个指定的类。

我们可以使用一个map来接收key-类型,value-对应实例化的beanName(反射),从而根据类型直接获取指定的实例化类,无需使用if-else。存储介质可以是一个map、一张关系数据库表或一串json。

这里以map为例,将所有实现了IPaymentStrategy接口的类都存储到PAYMENT_STRATEGY_MAP中,在实际使用时按需获取:

/**
 * 支付服务策略工厂
 */
@Component
public class PaymentFactory implements ApplicationContextAware {

/**
 * 支付方式策略map
 */
private static final Map<String, IPaymentStrategy> PAYMENT_STRATEGY_MAP = new ConcurrentHashMap<>();

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    // 返回实现IPaymentStrategy接口的策略bean
    Map<String, IPaymentStrategy> paymentBeanMap = applicationContext.getBeansOfType(IPaymentStrategy.class);
    paymentBeanMap.values().forEach(paymentBean -> {
        PAYMENT_STRATEGY_MAP.put(paymentBean.getPayName(), paymentBean);
    });
}

/**
 * 根据具体的支付方式获取对应的支付服务
 * @param paymentName 支付方式
 * @param <T>
 * @return
 */
public <T extends IPaymentStrategy> IPaymentStrategy getPaymentInstance(String paymentName) {
    return PAYMENT_STRATEGY_MAP.get(paymentName);
}
}

使用,根据payment获取bean:

@Autowired
private PaymentFactory paymentFactory;

@GetMapping("/payment/{paymentName}")
public Object payment(@PathVariable("paymentName") String paymentName) {
    IPaymentStrategy paymentInstance = paymentFactory.getPaymentInstance(paymentName);
    paymentInstance.payMoney();
    return paymentInstance.getPayName() + "支付成功!";