JAVA

设计模式 | 策略模式及典型应用

2019-01-05  本文已影响0人  小旋锋的简书

本文的主要内容:

策略模式

在软件开发中,我们也常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。

譬如商场购物场景中,有些商品按原价卖,商场可能为了促销而推出优惠活动,有些商品打九折,有些打八折,有些则是返现10元等。

而优惠活动并不影响结算之外的其他过程,只是在结算的时候需要根据优惠方案结算

商场促销场景

角色

Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。

Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。

ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

示例

如果要写出一个商场优惠场景的Demo可以很快的写出来,譬如

import java.text.MessageFormat;

public class Shopping {
    private String goods;
    private double price;
    private double finalPrice;
    private String desc;

    public Shopping(String goods, double price) {
        this.goods = goods;
        this.price = price;
    }

    public double calculate(String discountType) {
        if ("dis9".equals(discountType)) {
            finalPrice = price * 0.9;
            desc = "打九折";
        } else if ("dis8".equals(discountType)) {
            finalPrice = price * 0.8;
            desc = "打八折";
        } else if ("cash10".equals(discountType)) {
            finalPrice = price >= 10 ? price - 10 : 0;
            desc = "返现10元";
        } else {
            finalPrice = price;
            desc = "不参与优惠活动";
        }
        System.out.println(MessageFormat.format("购买的物品:{0},原始价格:{1},{2},最终价格为:{3}", goods, price, desc, finalPrice));
        return finalPrice;
    }
}

测试

public class Test {
    public static void main(String[] args) {
        Shopping shopping1 = new Shopping("书籍-深入理解Java虚拟机", 54.00);
        shopping1.calculate("dis9"); // 九折

        Shopping shopping2 = new Shopping("Apple 妙控鼠标", 588.00 );
        shopping2.calculate("dis8");

        Shopping shopping3 = new Shopping("戴尔U2417H显示器", 1479.00);
        shopping3.calculate("cash10");

        Shopping shopping4 = new Shopping("索尼ILCE-6000L相机", 3599.00);
        shopping4.calculate(null);
    }
}

以上代码当然完成了我们的需求,但是存在以下问题:

所以我们需要使用策略模式对 Shopping 类进行重构,将原本庞大的 Shopping 类的职责进行分解,将算法的定义和使用分离。

抽象策略类 Discount,它是所有具体优惠算法的父类,定义了一个 discount 抽象方法

import lombok.Data;

@Data
public abstract class Discount {
    protected double finalPrice;
    protected String desc;

    public Discount(String desc) {
        this.desc = desc;
    }

    abstract double discount(double price);
}

四种具体策略类,继承自抽象策略类 Discount,并在 discount 方法中实现具体的优惠算法

public class Dis9Discount extends Discount {
    public Dis9Discount() {
        super("打九折");
    }

    @Override
    double discount(double price) {
        finalPrice = price * 0.9;
        return finalPrice;
    }
}

public class Dis8Discount extends Discount{
    public Dis8Discount() {
        super("打八折");
    }

    @Override
    double discount(double price) {
        finalPrice = price * 0.8;
        return finalPrice;
    }
}

public class Cash10Discount extends Discount {
    public Cash10Discount() {
        super("返现10元");
    }

    @Override
    public double discount(double price) {
        this.finalPrice = price >= 10 ? price - 10 : 0;
        return finalPrice;
    }
}

public class NoneDiscount extends Discount {
    public NoneDiscount() {
        super("不参与优惠活动");
    }

    @Override
    double discount(double price) {
        finalPrice = price;
        return finalPrice;
    }
}

环境类 Shopping,维护了一个 Discount 引用

public class Shopping {
    private String goods;
    private double price;
    private Discount discount;

    public Shopping(String goods, double price, Discount discount) {
        this.goods = goods;
        this.price = price;
        this.discount = discount;
    }

    public double calculate() {
        double finalPrice = discount.discount(this.price);
        String desc = discount.getDesc();
        System.out.println(MessageFormat.format("购买的物品:{0},原始价格:{1},{2},最终价格为:{3}", goods, price, desc, finalPrice));
        return finalPrice;
    }
}

测试

public class Test {
    public static void main(String[] args) {
        Shopping shopping1 = new Shopping("书籍-深入理解Java虚拟机", 54.00, new Dis9Discount());
        shopping1.calculate();

        Shopping shopping2 = new Shopping("Apple 妙控鼠标", 588.00, new Dis8Discount());
        shopping2.calculate();

        Shopping shopping3 = new Shopping("戴尔U2417H显示器", 1479.00, new Cash10Discount());
        shopping3.calculate();

        Shopping shopping4 = new Shopping("索尼ILCE-6000L相机", 3599.00, new NoneDiscount());
        shopping4.calculate();
    }
}

结果

购买的物品:书籍-深入理解Java虚拟机,原始价格:54,打九折,最终价格为:48.6
购买的物品:Apple 妙控鼠标,原始价格:588,打八折,最终价格为:470.4
购买的物品:戴尔U2417H显示器,原始价格:1,479,返现10元,最终价格为:1,469
购买的物品:索尼ILCE-6000L相机,原始价格:3,599,不参与优惠活动,最终价格为:3,599

可以看到,使用策略模式重构后,Shopping 类的 calculate 方法简洁了很多,当需要更改优惠算法的时候不需要再修改 Shopping 类的源代码;要扩展出新的优惠算法很方便,只需要继承抽象策略类 Discount 并实现 calculate 方法即可;优惠算法很容易重用。

画出类图如下

示例.策略模式类图

策略模式总结

策略模式的主要优点如下:

策略模式的主要缺点如下:

适用场景

源码分析策略模式的典型应用

Java Comparator 中的策略模式

java.util.Comparator 接口是比较器接口,可以通过 Collections.sort(List,Comparator)Arrays.sort(Object[],Comparator) 对集合和数据进行排序,下面为示例程序

一个学生类,有两个属性 idname

@Data
@AllArgsConstructor
public class Student {
    private Integer id;
    private String name;

    @Override
    public String toString() {
        return "{id=" + id + ", name='" + name + "'}";
    }
}

实现两个比较器,比较器实现了 Comparator 接口,一个升序,一个降序

// 降序
public class DescSortor implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o2.getId() - o1.getId();
    }
}

// 升序
public class AscSortor implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getId() - o2.getId();
    }
}

通过 Arrays.sort() 对数组进行排序

public class Test1 {
    public static void main(String[] args) {
        Student[] students = {
                new Student(3, "张三"),
                new Student(1, "李四"),
                new Student(4, "王五"),
                new Student(2, "赵六")
        };
        toString(students, "排序前");
        
        Arrays.sort(students, new AscSortor());
        toString(students, "升序后");
        
        Arrays.sort(students, new DescSortor());
        toString(students, "降序后");
    }

    public static void toString(Student[] students, String desc){
        for (int i = 0; i < students.length; i++) {
            System.out.print(desc + ": " +students[i].toString() + ", ");
        }
        System.out.println();
    }
}

输出

排序前: {id=3, name='张三'}, 排序前: {id=1, name='李四'}, 排序前: {id=4, name='王五'}, 排序前: {id=2, name='赵六'}, 
升序后: {id=1, name='李四'}, 升序后: {id=2, name='赵六'}, 升序后: {id=3, name='张三'}, 升序后: {id=4, name='王五'}, 
降序后: {id=4, name='王五'}, 降序后: {id=3, name='张三'}, 降序后: {id=2, name='赵六'}, 降序后: {id=1, name='李四'}, 

通过 Collections.sort() 对集合List进行排序

public class Test2 {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(3, "张三"),
                new Student(1, "李四"),
                new Student(4, "王五"),
                new Student(2, "赵六")
        );
        toString(students, "排序前");
        
        Collections.sort(students, new AscSortor());
        toString(students, "升序后");

        Collections.sort(students, new DescSortor());
        toString(students, "降序后");
    }

    public static void toString(List<Student> students, String desc) {
        for (Student student : students) {
            System.out.print(desc + ": " + student.toString() + ", ");
        }
        System.out.println();
    }
}

输出

排序前: {id=3, name='张三'}, 排序前: {id=1, name='李四'}, 排序前: {id=4, name='王五'}, 排序前: {id=2, name='赵六'}, 
升序后: {id=1, name='李四'}, 升序后: {id=2, name='赵六'}, 升序后: {id=3, name='张三'}, 升序后: {id=4, name='王五'}, 
降序后: {id=4, name='王五'}, 降序后: {id=3, name='张三'}, 降序后: {id=2, name='赵六'}, 降序后: {id=1, name='李四'}, 

我们向 Collections.sort()Arrays.sort() 分别传入不同的比较器即可实现不同的排序效果(升序或降序)

这里 Comparator 接口充当了抽象策略角色,两个比较器 DescSortorAscSortor 则充当了具体策略角色,CollectionsArrays 则是环境角色

Spring Resource 中的策略模式

Spring 把所有能记录信息的载体,如各种类型的文件、二进制流等都称为资源,譬如最常用的Spring配置文件。

在 Sun 所提供的标准 API 里,资源访问通常由 java.NET.URL 和文件 IO 来完成,尤其是当我们需要访问来自网络的资源时,通常会选择 URL 类。

URL 类可以处理一些常规的资源访问问题,但依然不能很好地满足所有底层资源访问的需要,比如,暂时还无法从类加载路径、或相对于 ServletContext 的路径来访问资源,虽然 Java 允许使用特定的 URL 前缀注册新的处理类(例如已有的 http: 前缀的处理类),但是这样做通常比较复杂,而且 URL 接口还缺少一些有用的功能,比如检查所指向的资源是否存在等。

Spring 改进了 Java 资源访问的策略,Spring 为资源访问提供了一个 Resource 接口,该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。

public interface Resource extends InputStreamSource {
    boolean exists();    // 返回 Resource 所指向的资源是否存在
    boolean isReadable();   // 资源内容是否可读
    boolean isOpen();   // 返回资源文件是否打开
    URL getURL() throws IOException;
    URI getURI() throws IOException;
    File getFile() throws IOException;  // 返回资源对应的 File 对象
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String var1) throws IOException;
    String getFilename();
    String getDescription();    // 返回资源的描述信息
}

Resource 接口是 Spring 资源访问策略的抽象,它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略

Spring资源访问接口Resource的实现类

Spring 为 Resource 接口提供的部分实现类如下:

这些 Resource 实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。

它们之间的类关系如下所示:

Spring Resource 类图

可以看到 AbstractResource 资源抽象类实现了 Resource 接口,为子类通用的操作提供了具体实现,非通用的操作留给子类实现,所以这里也应用了模板方法模式。(只不过缺少了模板方法)

Resource 不仅可在 Spring 的项目中使用,也可直接作为资源访问的工具类使用。意思是说:即使不使用 Spring 框架,也可以使用 Resource 作为工具类,用来代替 URL

譬如我们可以使用 UrlResource 访问网络资源。

也可以通过其它协议访问资源,file: 用于访问文件系统;http: 用于通过 HTTP 协议访问资源;ftp: 用于通过 FTP 协议访问资源等

public class Test {
    public static void main(String[] args) throws IOException {
        UrlResource ur = new UrlResource("http://image.laijianfeng.org/hello.txt");

        System.out.println("文件名:" + ur.getFilename());
        System.out.println("网络文件URL:" + ur.getURL());
        System.out.println("是否存在:" + ur.exists());
        System.out.println("是否可读:" + ur.isReadable());
        System.out.println("文件长度:" + ur.contentLength());

        System.out.println("\n--------文件内容----------\n");
        byte[] bytes = new byte[47];
        ur.getInputStream().read(bytes);
        System.out.println(new String(bytes));
    }
}

输出的内容如下,符合预期

文件名:hello.txt
网络文件URL:http://image.laijianfeng.org/hello.txt
是否存在:true
是否可读:true
文件长度:47

--------文件内容----------

hello world!
welcome to http://laijianfeng.org

更多的示例可以参考:Spring 资源访问剖析和策略模式应用

Spring Bean 实例化中的策略模式

Spring实例化Bean有三种方式:构造器实例化、静态工厂实例化、实例工厂实例化

譬如通过构造器实例化bean的XML示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="person" class="com.demo.Person"></bean>
    
    <bean id="personWithParam" class="com.demo.Person">
        <constructor-arg name="name" value="小旋锋"/>
    </bean>
    
    <bean id="personWirhParams" class="com.demo.Person">
            <constructor-arg name="name" value="小旋锋"/>
            <constructor-arg name="age" value="22"/>
    </bean>
</beans>

具体实例化Bean的过程中,Spring中角色分工很明确,创建对象的时候先通过 ConstructorResolver 找到对应的实例化方法和参数,再通过实例化策略 InstantiationStrategy 进行实例化,根据创建对象的三个分支( 工厂方法、有参构造方法、无参构造方法 ), InstantiationStrategy 提供了三个接口方法:

public interface InstantiationStrategy {
    // 默认构造方法
    Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner) throws BeansException;

    // 指定构造方法
    Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner, Constructor<?> ctor,
            Object[] args) throws BeansException;

    // 指定工厂方法
    Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner, Object factoryBean,
            Method factoryMethod, Object[] args) throws BeansException;
}

InstantiationStrategy 为实例化策略接口,扮演抽象策略角色,有两种具体策略类,分别为 SimpleInstantiationStrategyCglibSubclassingInstantiationStrategy

Spring 实例化策略类图

SimpleInstantiationStrategy 中对这三个方法做了简单实现,如果工厂方法实例化直接用反射创建对象,如果是构造方法实例化的则判断是否有 MethodOverrides,如果有无 MethodOverrides 也是直接用反射,如果有 MethodOverrides 就需要用 cglib 实例化对象,SimpleInstantiationStrategy 把通过 cglib 实例化的任务交给了它的子类 CglibSubclassingInstantiationStrategy

参考:
刘伟:设计模式Java版
慕课网java设计模式精讲 Debug 方式+内存分析
Spring 资源访问剖析和策略模式应用
Spring源码阅读-实例化策略InstantiationStrategy
Spring学习之实例化bean的三种方式

后记

欢迎评论、转发、分享,您的支持是我最大的动力

更多内容可访问我的个人博客:http://laijianfeng.org

关注【小旋锋】微信公众号,及时接收博文推送

关注_小旋锋_微信公众号
上一篇 下一篇

猜你喜欢

热点阅读