概述

先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。 作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。 定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

结构

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。

  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

案例实现

【例】促销活动

一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下: 代码如下:

定义百货公司所有促销活动的共同接口

定义具体策略角色(Concrete Strategy):每个节日具体的促销活动

定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员

优缺点

优点:

  • 策略类之间可以自由切换:由于策略类都实现同一个接口,所以使它们之间可以自由切换。

  • 易于扩展:增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“

  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。

  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

使用场景

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。

  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。

  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。

  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。

  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

源码解析 - Comparator

有一个Person类,需要进行排序,有两种方式:

  • Comparable:让对象自己会比较

一个类实现了 Comparable接口,就意味着这个类的对象天生就是可以相互比较排序的。这被称为“自然排序”,比如 String按字典序、Integer按数值大小。

使用方式:

这里直接调用 Collections.sort(list),排序规则由 Person类自己定义的 compareTo方法决定

  • Comparator:外部定义多种比较规则

如果想按姓名排序,但又不能或不想修改 Person类怎么办?或者,希望有不止一种排序方式(比如有时按年龄,有时按姓名)?这时 Comparator就派上用场了。它体现了策略模式,将比较算法与对象分离开

使用方式:

甚至可以不创建新类,直接使用匿名内部类或Lambda表达式,非常灵活

这里我们在调用Collections 的sort方法时,第二个参数传递的是Comparator接口的子实现类对象。所以Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。

重构-替换 if-else

若遇到大量流程判断语句,几乎满屏都是if-else语句,其实if-else是一种面向过程的实现。那么,如何避免在面向对象编程里大量使用if-else呢?

假如有这样一个需求,需实现一周七天内分别知道要做事情的备忘功能,这里面就会涉及到一个流程判断,你可能会立马想到用if-else,那么,可能是会这样实现

这种代码,在业务逻辑里,少量还好,若是几百个判断呢,可能整块业务逻辑里都是满屏if-else,既不优雅也显得很少冗余。

工厂类

这种方法将对象的创建逻辑封装到一个专门的工厂类中,符合“开闭原则”,后续新增课程类型时,只需扩展工厂类即可。

重构步骤:

  1. 定义公共接口:创建一个接口(如 CourseService),声明公共方法(如 getToDo())。

  2. 实现具体类:为每一种课程(如英语、语文)创建一个类,实现该接口,完成各自的复杂逻辑。

  3. 创建工厂类:构建一个工厂类(如 CourseServiceFactory),在静态代码块或初始化时,建立“星期几”到具体课程服务实例的映射关系(通常使用 Map)。

  4. 修改主方法:在原方法中,通过工厂类根据输入参数 day获取对应的服务实例,并调用其方法。

枚举

枚举非常适合管理一组固定的常量,并且可以将每个常量对应的行为直接封装在枚举项内部,使代码非常清晰。

重构步骤:

  1. 定义枚举类型:创建一个枚举(如 DayCourse),枚举值就是星期几。

  2. 封装行为与属性:为枚举定义一个抽象方法(如 getToDo()),然后在每个枚举值中实现这个方法的具体逻辑。

  3. 修改主方法:原方法通过 DayCourse.valueOf(day)获取对应的枚举实例,并调用其方法。

命令模式

命令模式将请求封装为对象,使得你可以用不同的请求参数化其他对象,并支持请求的排队、记录、撤销等。

重构步骤:

  1. 定义命令接口:创建一个接口(如 Command),其中定义一个执行方法(如 execute())。

  2. 实现具体命令:为每一种课程创建一个类,实现 Command接口。

  3. 修改主方法:原方法中,根据 day创建对应的具体命令对象,并调用其 execute()方法。

规则引擎

当业务规则非常复杂且需要动态变更时,规则引擎是理想选择,它实现了业务逻辑与执行逻辑的彻底分离。

重构步骤:

  1. 定义规则接口:创建一个接口(如 Rule),包含评估条件的方法(如 evaluate)和执行操作的方法(如 execute)。

  2. 实现具体规则:为每一天创建一个规则类,实现 Rule接口。

  3. 创建规则引擎:创建一个规则引擎类,用于注册和管理所有规则,并提供一个方法遍历所有规则,找到条件满足的规则并执行。

  4. 修改主方法:原方法中,调用规则引擎来执行。

策略模式

策略模式定义一族算法,并封装每个算法,使它们可以相互替换,让算法的变化独立于使用算法的客户。

重构步骤:

  1. 定义策略接口:与命令模式类似,定义一个策略接口(如 CourseStrategy)。

  2. 实现具体策略:为每一天创建一个策略类,实现该接口。

  3. 创建策略上下文:创建一个上下文类(如 CourseContext),它包含一个策略引用,并提供一个方法来执行当前策略。

  4. 修改主方法:原方法中,根据 day设置上下文对象的策略,然后执行。

策略枚举

这其实是属于 外层枚举(路由枚举)+ 内层策略枚举​ 的混合结构

首先,先定义一个getToDo()调用方法,假如传进的是“星期一”,即参数"Monday"。

在getToDo()方法里,通过DayEnum.valueOf(“Monday”)可获取到一个DayEnum枚举元素,这里得到的是Monday。

接下来,执行checkDay.day(DayEnum.valueOf(“Monday”)),会进入到day()方法中,这里,通过dayEnum.toDo()做了一个策略匹配时。注意一点,DayEnum.valueOf(“Monday”)得到的是枚举中的Monday,这样,实质上就是执行了Monday.toDo(),也就是说,会执行Monday里的toDo()

上面的执行过程为什么会是这样子呢?只有进入到DayEnum枚举当中,才知道是怎么回事了

在每个枚举元素当中,都重写了该toDo()抽象方法。这样,当传参DayEnum.valueOf(“Monday”)流转到dayEnum.toDo()时,实质上是去DayEnum枚举里找到对应Monday定义的枚举元素,然后执行其内部重写的toDo()方法。用if-esle形式表示,就类似"Monday".equals(day)匹配为true时,可得到其内部东西。

总结一下,策略枚举就是枚举当中使用了策略模式,所谓的策略模式,即给你一把钥匙,按照某种约定的方式,可以立马被指引找到可以打开的门。例如,我给你的钥匙叫“Monday”,那么,就可以通过约定方式dayEnum.toDo(),立马找到枚举里的Monday大门,然后进到门里,去做想做的事toDo(),其中,每扇门后的房间都有不同的功能,但它们都有一个相同抽象功能——toDo(),即各房间共同地方都是可以用来做一些事情的功能,但具体可以什么事情,就各有不同了。在本文的案例里,每扇大门里的toDo(),根据不同策略模式可得到不同字符串返回,例如,“今天上英语课”、“今天上语文课”,等等。

可见,把流程判断抽取到策略枚举当中,还可以把一堆判断解耦出来,避免在业务代码逻辑里呈现一大片密密麻麻冗余的if-else。

这里,会出现一种情况,即,假如有多个重复共同样功能的判断话,例如,在if-else里,是这样

那么,在策略枚举下应该如何使用从而避免代码冗余呢?

可以参考一下以下思路,设置一个内部策略枚举,将有相同功能的外部引用指向同一个内部枚举元素,这样即可实现调用重复功能了

若要扩展其判断流程,只需要直接在枚举增加一个属性和内部toDo(实现),就可以增加新的判断流程了,而外部,仍旧用同一个入口dayEnum.toDo()即可。

总结一下,这种方式核心在于将“路由”和“行为”进行了解耦

  • 外层枚举 (DayEnum)作为“路由表”:负责将具体的输入参数(如"Monday")映射到一个抽象的策略类型(如Type.ENGLISH)。这使得映射关系非常集中和清晰

  • 内层枚举 (Type)作为“策略实现集”:它封装了具体的业务逻辑。所有被路由到Type.ENGLISH的日期,都共享同一套复杂的业务逻辑。

这种做法极大地提升了代码的可维护性和复用性。如果需要修改“英语课”的逻辑,只需修改Type.ENGLISHtoDo()方法,所有关联的日期(如周一、周二、周三)都会自动生效

尽管这种写法在特定场景下非常优雅,但也存在一些限制:

  1. 编译时绑定:枚举最大的特点是所有值在编译时就必须确定。无法在程序运行时不修改代码、通过外部配置就动态地添加一个新的DayEnum.Weekend或一个新的课程类型Type.MUSIC。这对于需要高度动态配置的系统是一个硬约束。

  2. 单一职责的权衡:这种结构将路由和策略都塞进了一个枚举文件中。当策略逻辑非常复杂时(例如,Type.CHINESE.toDo()方法内部需要调用多个Service,完成一系列复杂操作),可能会导致这个枚举类变得臃肿。虽然可以通过在策略方法内部调用外部Service类来缓解,但这仍是需要权衡的点。

如何选择与小结

这几种重构方法各有侧重,可以根据实际场景选择:

  • 如果业务逻辑相对稳定且类型固定,追求简洁直观,枚举是不错的选择。

  • 如果每个分支的逻辑非常复杂且独立,希望将对象的创建与使用解耦,工厂类非常合适。

  • 如果希望在运行时灵活切换算法,或者未来可能频繁增加新的课程类型,策略模式的扩展性更好。

  • 如果需要将请求的发送与执行解耦,或者未来可能支持命令队列、撤销等高级功能,可以考虑命令模式

  • 如果业务规则极其复杂、多变,甚至需要从外部(如数据库、配置文件)动态加载规则,那么规则引擎能提供最大的灵活性。