设计模式二:策略模式

概述

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

策略模式旨在解决不同逻辑下相同的对象执行不同策略的问题。

当我们遇到同一个方法,里面会根据需要多个逻辑的分支,分支里的行为都不同,但是都服务于同一个功能,这个时候就可以使用策略模式,将行为抽象为一个策略接口中的抽象方法,由接口的实现类——也就是策略类——去实现各中具体的行为。

策略模式也是一种比较常见且好用的设计模式,线程池的拒绝策略就使用了策略模式。

一、简单实现

简单的拿一个根据情况需要导出不同文件的接口举例:

public void exportFile(String type){
    if (type == "excel") {
        // 导出excel
    } else if (type == "word") {
        // 导出word
    } else if (type == "pdf") {
        // 导出pdf
    }else {
        throw new RuntimeException("错误的文件类型!");
    }
}

这些分支里目的都是导出文件,但是各自有各的实现代码。换而言之,这些不同逻辑分支下的代码只有行为是不同的。现在我们将导出方法抽象成为一个策略接口中的抽象方法,将每个逻辑分支的处理代码都抽成实现策略接口的各个策略类。

// 文件导出接口
public interface IExportFile(){
    void export();
}

// 实现类
public class ExportExcel implements IExportFile{
    @Override
   public void export() {
      // 导出excel
   }
}
public class ExportWord implements IExportFile{
    @Override
   public void export() {
      // 导出word
   }
}
public class ExportPdf implements IExportFile{
    @Override
   public void export() {
      // 导出pdf
   }
}

然后调用的时候直接将接口实现类作为参数传入:

// 改造原有方法,将策略接口的实现作为参数传入
public void exportFile(String type){
    IExportFile exportFile;
    if (type == "excel") {
        exportFile = new ExportExcel();
    } else if (type == "word") {
        exportFile = new ExportWord();
    } else if (type == "pdf") {
        exportFile = new ExportPdf();
    }else {
        throw new RuntimeException("错误的文件类型!");
    }
    exportFile.export();
}

我们可以看到,原本耦合在调用方法里的行为被解放出来了,通过为策略接口更换策略类,我们可以很方便的切换行为。

策略模式改进原有方法

二、策略池与上下文对象

策略池

根据上文的简单例子,我们将具体的策略通过策略接口与调用方法耦合了,但是我们不难发现,现在的实现仍然需要通过大量的 if-else 判断去选择执行策略

实际上,我们可以这么考虑,代码被封装到实现类里以后,实际上一个策略跟对应的判断条件实际上就是一种 key 和 value 之间的映射关系了,我们可以根据这个思路,换一个更简洁一些的方式去替换 if-else 的代码:比如将条件字段与策略类放入 Map 集合实现的一个策略池中,直接通过 key 去获取对应的策略类

还是基于上述导出接口的例子:

// 策略池
static Map<String,IExportFile> exportStrategyPool = new HashMap<>();
static {
    exportStrategyPool.put("excel",new ExportExcel());
    exportStrategyPool.put("word",new ExportWord());
    exportStrategyPool.put("pdf",new ExportPdf());
}

// 导出文件
public void exportFile(String type){
    if (!exportStrategyPool.containsKey(type)) {
        throw new RuntimeException("错误的文件类型!");
    }
    IExportFile exportFile = exportStrategyPool.get(type);
    exportFile.export();
}

我们可以看到,通过策略池,我们直接简化了大量的 if-else 代码。

实际上,考虑到实现类是无状态的,那么策略类和策略池都应该是单例的,因此,这里使用了饿汉式去创建策略池,这里同样有许多优化的地方:比如可以手动创建改为通过反射自动装填策略类;可以创建枚举类或者将条件作为常量来规范策略的对应关系;如果在 spring 项目中,也可以考虑通过 spring 去创建......

这些的都可以根据需求进行优化的,但是核心仍然是在调用前建立条件与策略的映射关系

策略池的实现

上下文对象

现在,出于优化 if-else 的原因,我们为导出方法加入了策略池,但是这个类的其他方法未必用得到,为此我们不妨将整个策略池和导出方法都封装到另一个单独的类里,只提供一个带条件参数的方法。现在策略池归上下文对象管理了,那么这个上下文对象也应该是单例的,就个人观点,单独放到一个独立的 Service 和对应一个实现类,由 spring 管理应该是比较合适的。

// FileExporter 屏蔽了方法的具体实现
public class FileExporter {

    private Map<String,IExportFile> exportStrategyPool;

    public FileExporter() {
        exportStrategyPool = new HashMap<>();

        exportStrategyPool.put("excel",FileExporter::exportExcel);
        exportStrategyPool.put("word",FileExporter::exportWord);
        exportStrategyPool.put("pdf",FileExporter::exportPdf);
    }

    // 导出文件
    public void exportFile(String type){
        if (!exportStrategyPool.containsKey(type)) {
            throw new RuntimeException("错误的文件类型!");
        }
        IExportFile exportFile = exportStrategyPool.get(type);
        exportFile.export();
    }
}

// 调用
new FileExporter().exportFile("word");

通过 FileExporter 这个承上启下的上下文对象,我们屏蔽了具体代码的实现,同时通过“人”去调用“行为”这个逻辑也更符合面向对象的思想。

上下文对象

三、配合函数式接口使用

策略模式+策略池的手段已经可以解决传统 if-else 的大多数问题了,但是他也随之带来了两个问题:

  • 一个方法要用到的策略会产生大量实现类;
  • 业务逻辑被分散到了各个实现类,无法方便的总览。

为此,JDK8 的函数式接口刚好能非常完美的解决这些痛点。

关于函数式接口,我已在JDK1.8新特性(三):Lambda表达式里介绍过了,这里就不再赘述,直接上手。

还是以上文的文件导出接口为例:

当我们完成这个功能以后,我们会在原来的基础上多处一个上下文对象,一个策略接口,以及 n 多个实现接口的策略类,一个策略对应一个策略实现类的问题是导致类数量膨胀的原因,因此我们可以将策略接口替换为函数式接口,这样就可以在需要的时直接通过 Lambda 表达式传入实现类,避免新建类

// 将原本的IExportFile改为函数式接口
@FunctionalInterface
public interface IExportFile {
    void export();
}

// 在策略池阶段直接放入匿名实现类
static Map<String,() -> System.out.println("excel"));
    exportStrategyPool.put("word",() -> System.out.println("excel"));
    exportStrategyPool.put("pdf",() -> System.out.println("excel"));
}

当然,实际工作中的方法肯定比区区一句 System.out.println()要复杂的多,我们不可能集中在策略池里写实现。这个问题也很好解决,我们可以将具体的方法抽出来放到一个实现类里,比 service 的实现类,或者上下文对象中:

public class FileExporter {

    private Map<String,IExportFile> exportStrategyPool;

    private FileExporter() {
        exportStrategyPool = new HashMap<>();

        exportStrategyPool.put("excel",this::exportExcel);
        exportStrategyPool.put("word",this::exportWord);
        exportStrategyPool.put("pdf",this::exportPdf);
    }

    // 导出文件
    public void exportFile(String type){
        if (!exportStrategyPool.containsKey(type)) {
            throw new RuntimeException("错误的文件类型!");
        }
        IExportFile exportFile = exportStrategyPool.get(type);
        exportFile.export();
    }
	
    // 导出的策略
    public void exportExcel() {
        System.out.println("导出excel");
    }
    public void exportWord() {
        System.out.println("导出word");
    }
    public void exportPdf() {
        System.out.println("导出pdf");
    }
}

现在,业务代码可以都放在一个类里面了,总览起来也非常方便。

四、总结

通过策略模式,我们可以做到:

  1. 通过将行为抽象为一个策略接口,具体的行为作为接口的实现类,来分离方法和逻辑分支中的代码;
  2. 通过策略池来避免大量的 if-else 判断;
  3. 通过将策略池和方法封装到上下文对象来对外部屏蔽底层的实现;

对于策略模式带来的策略类过多,业务逻辑分散的问题:

  1. 将策略接口改为函数式接口,省去创建实现类,直接通过 Lambda 表达式直接传入匿名实现类;
  2. 在上述基础上,将实现方法统一写在一个类里,策略池在创建时通过 Lambda 表达式把类中的方法传入策略池。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结