一文细读策略模式、策略模式与Spring的碰撞

前言

策略模式是GoF23种设计模式中比较简单的了,也是常用的设计模式之一,今天我们就来看看策略模式。

实际案例

我工作第三年的时候,重构旅游路线的机票查询模块,旅游路线分为四种情况:

  • 如果A地-B地往返都可以直达,那么查询两张机票(往返)
  • 如果A地-B地去程无法直达,需要中转,但是返程可以直达,那么查询三张机票(去程两张,返程一张)
  • 如果A地-B地去程可以直达,但是返程需要中转,那么查询三张机票(去程一张,返程两张)
  • 如果A地-B地往返都无法直达,那么查询四张机票(去程两张,返程两张)

在我重构前,代码差不多是这样的:

        int type = 1;
        // 往返都可以直达
        if (type == 1) {
            // 查询出两张机票
            return;
        }

        // 去程无法直达,需要中转,但是返程可以直达
        if (type == 2) {
            // 查询出三张机票(去程两张,返程一张)
            return;
        }
        // 去程可以直达,但是返程需要中转
        if (type == 3) {
            // 查询出三张机票(去程一张,返程两张)
            return;
        }
        // 往返都无法直达
        else{
            // 查询出四张机票(去程两张,返程两张)
            return;
        }

当时我还是菜鸡(现在也是),也不懂什么设计模式,就是感觉代码都写在一个类中,实在是太长了,不够清爽,不管是哪种类型的线路,最终都是返回机票集合,只是处理逻辑不同,可以提取一个接口出来,再开四个类去实现此接口,最后定义一个Map,Key是Type,Value是接口(实现类),根据Type决定调用哪个实现类,就像下面的酱紫:

public class Ticket {
    private String desc;

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

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "Ticket{" +
                "desc='" + desc + '\'' +
                '}';
    }
}

public interface QueryTicketService {
    List<Ticket> getTicketList();
}

public class QueryTicketAService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程机票"));
        list.add(new Ticket("返程机票"));
        return list;
    }
}

public class QueryTicketBService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一张机票"));
        list.add(new Ticket("去程第二张机票"));
        list.add(new Ticket("返程机票"));
        return list;
    }
}

public class QueryTicketCService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程机票"));
        list.add(new Ticket("返程第一张机票"));
        list.add(new Ticket("返程第二张机票"));
        return list;
    }
}

public class QueryTicketDService implements QueryTicketService {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一张机票"));
        list.add(new Ticket("去程第二张机票"));
        list.add(new Ticket("返程第一张机票"));
        list.add(new Ticket("返程第二张机票"));
        return list;
    }
}

public class Main {
    static Map<Integer, QueryTicketService> map = new HashMap<>();

    static {
        map.put(1, new QueryTicketAService());
        map.put(2, new QueryTicketBService());
        map.put(3, new QueryTicketCService());
        map.put(4, new QueryTicketDService());
    }

    public static void main(String[] args) {
        int type = 1;
        System.out.println(map.get(type).getTicketList());
    }
}

运行结果:

[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}]

当初我也不知道什么设计模式,就是感觉这样写完,代码清爽多了,后来才知道这就是策略模式的雏形了。

GoF23种设计模式真正应用广泛的设计模式不多,但是策略模式绝对算其中之一了,你看,当初我都不懂这些,就写出了策略模式的雏形。

原始的策略模式

如果我们遇到类似于上面的需求,第一反应肯定是用if else语句或者switch语句,根据不同的情况执行不同的代码,这样做也没什么大问题,但是我们的项目会越来越复杂,这么做的缺陷就慢慢的显现了出来:如果现在线路新增了一个类型,需要中转两次,就又得加好几个判断的分支(去程中转一次,返程中转两次;去程中转两次,返程中转一次;去程直达,返程中转两次等等),想想就恐怖,这样分支会越来越多,代码会越来越长,越来越难以维护,所以策略模式出现了。

当一个逻辑中,有很多if else语句或者switch语句,而且它们需要解决的问题是一样的,就可以考虑策略模式。

最原始的策略模式有三个角色:

  • Strategy:抽象策略角色,对算法、策略的抽象,定义每个算法、策略所必需的方法,通常为接口。
  • ConcreteStrategy:具体策略角色,实现抽象策略角色,完成具体的算法、策略。
  • Context:上下文环境角色,保存了ConcreteStrategy,负责调用ConcreteStrategy。

而我上面的代码,就有了策略模式的味道,有了Strategy,也有了ConcreteStrategy,缺少的就是Context,如果用最原始的设计模式的写法来实现,是酱紫的:

public class Context {
    static Map<Integer, QueryTicketStrategy> map = new HashMap<>();

    static {
        map.put(1, new QueryTicketAConcreteStrategy());
        map.put(2, new QueryTicketBConcreteStrategy());
        map.put(3, new QueryTicketCConcreteStrategy());
        map.put(4, new QueryTicketDConcreteStrategy());
    }

    public void getTicketList(int type) {
        System.out.println(map.get(type).getTicketList());
    }
}

public class Main {
    public static void main(String[] args) {
        Context context = new Context();
        context.getTicketList(1);
    }
}

运行结果:

[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}]

在这里,我把类名重新定义了下,让人一眼就可以看出这里使用了策略模式,这也是阿里推荐的命名方法。

策略模式是不是很简单(我在学习设计模式的时候,甚至觉得它比单例、简单工厂还要简单),而且特别实用,下面我们来看看策略模式的UML图:


JDK中的策略模式

既然策略模式那么实用,那么在JDK中有策略模式的应用吗?当然有。JDK中定义的Comparator接口就是策略模式的一种实践了:

public class SortLengthComparator implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        return (o1.length() - o2.length() > 0) ? 1 : -1;
    }
}

public class Main {
    public static void main(String[] args) {
        List<String>list=new ArrayList<>();
        list.add("hello");
        list.add("world");
        list.add("codebear");
        list.add("balabala");
        list.add("java");
        list.sort(new SortLengthComparator());
        System.out.println(list);
    }
}

我定义了一个比较器,实现了Comparator接口,重写了compare方法,实现了以比较字符串长度来比较字符串的功能。

运行结果:

[java, world, hello, balabala, codebear]

Comparator接口就是Strategy,我定义的SortLengthComparator就是ConcreteStrategy。

Comparator结合Lambda,会产生怎样的火花

定义一个比较器,虽然不难,但是总觉得不够简洁,不够方便,需要新建一个类,所以现在越来越多的人使用Lambda来进行排序,就像下面的酱紫:

        List<String>list=new ArrayList<>();
        list.add("hello");
        list.add("world");
        list.add("codebear");
        list.add("balabala");
        list.add("java");
        List<String> newList = list.stream().sorted((a, b) -> (a.length() - b.length() > 0) ? 1 : -1).collect(Collectors.toList());
        newList.forEach(System.out::println);

虽然底层还是用的Comparator,但是这样的写法清爽多了,如果比较的策略比较复杂,或者有多个地方都需要用到这个比较策略,还是用最原始的写法更好一些。

策略模式与Spring的碰撞

现在我们已经知道了什么是策略模式,如何使用策略模式,但是还有一个天大的问题,要知道,现在每个项目都在用Spring,如果你还是这么写的话:

public class Context {
    static Map<Integer, QueryTicketStrategy> map = new HashMap<>();

    static {
        map.put(1, new QueryTicketAConcreteStrategy());
        map.put(2, new QueryTicketBConcreteStrategy());
        map.put(3, new QueryTicketCConcreteStrategy());
        map.put(4, new QueryTicketDConcreteStrategy());
    }

    public void getTicketList(int type) {
        System.out.println(map.get(type).getTicketList());
    }
}

就意味着实现类里面的依赖需要自己去维护,无法使用神奇的@Autowired注解,所以策略模式与Spring碰撞,策略模式必须发生一点改变,而这改变让策略模式变得更加简单,性能更好,也更加迷人。

写法1

@Service
public class QueryTicketAConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程机票"));
        list.add(new Ticket("返程机票"));
        return list;
    }
}

@Service
public class QueryTicketDConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一张机票"));
        list.add(new Ticket("去程第二张机票"));
        list.add(new Ticket("返程第一张机票"));
        list.add(new Ticket("返程第二张机票"));
        return list;
    }
}

@Service
public class Context {

    @Autowired
    private QueryTicketStrategy queryTicketAConcreteStrategy;

    @Autowired
    private QueryTicketStrategy queryTicketDConcreteStrategy;

    private static Map<Integer, QueryTicketStrategy> map = new HashMap<>();

    @PostConstruct
    public void init() {
        map.put(1, queryTicketAConcreteStrategy);
        map.put(4, queryTicketAConcreteStrategy);
    }

    public void getTicketList(int type) {
        System.out.println(map.get(type).getTicketList());
    }
}

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
        run.getBean(Context.class).getTicketList(1);
    }
}

运行结果:

[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}]

原始的设计模式有一个缺点,不管是具体的策略实现类,还是上下文类,都不是单例模式,而我们的方法在大多数情况下是无状态的,所以改成单例模式是非常合适的,而结合了Spring,我们完全不需要手写单例模式,Spring就帮我们完成了。

写法2(自认为最优雅)

不管是原始的策略模式,还是Spring与策略模式结合的第一种写法,都没有完全符合开闭原则,如果有新的策略引入,必须修改上下文类,往map里面添加一组新的映射关系,而第二种写法完美的解决了这个问题,而且让策略模式变得非常优雅,下面直接放出代码:

@Service("1")
public class QueryTicketAConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程机票"));
        list.add(new Ticket("返程机票"));
        return list;
    }
}

@Service("4")
public class QueryTicketDConcreteStrategy implements QueryTicketStrategy {
    @Override
    public List<Ticket> getTicketList() {
        List<Ticket> list = new ArrayList<>();
        list.add(new Ticket("去程第一张机票"));
        list.add(new Ticket("去程第二张机票"));
        list.add(new Ticket("返程第一张机票"));
        list.add(new Ticket("返程第二张机票"));
        return list;
    }
}

@Service
public class Context {

    @Autowired
    private Map<String, QueryTicketStrategy> map = new HashMap<>();

    public void getTicketList(int type) {
        String typeStr = String.valueOf(type);
        System.out.println(map.get(typeStr).getTicketList());
    }
}

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
        run.getBean(Context.class).getTicketList(1);
    }
}

运行结果:

[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}]

这就是Spring和神奇、迷人之处了,竟然可以自动注入map,key就是beanName,value就是接口(具体的实现类)。

用这种写法不但完成了天然的单例模式,而且真正的符合了开闭原则,引入新的策略,完全不需要修改任何一行旧代码,自认为这种写法是最优雅、最迷人的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270