概述
先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择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,既不优雅也显得很少冗余。
工厂类
这种方法将对象的创建逻辑封装到一个专门的工厂类中,符合“开闭原则”,后续新增课程类型时,只需扩展工厂类即可。
重构步骤:
定义公共接口:创建一个接口(如
CourseService),声明公共方法(如getToDo())。实现具体类:为每一种课程(如英语、语文)创建一个类,实现该接口,完成各自的复杂逻辑。
创建工厂类:构建一个工厂类(如
CourseServiceFactory),在静态代码块或初始化时,建立“星期几”到具体课程服务实例的映射关系(通常使用Map)。修改主方法:在原方法中,通过工厂类根据输入参数
day获取对应的服务实例,并调用其方法。
枚举
枚举非常适合管理一组固定的常量,并且可以将每个常量对应的行为直接封装在枚举项内部,使代码非常清晰。
重构步骤:
定义枚举类型:创建一个枚举(如
DayCourse),枚举值就是星期几。封装行为与属性:为枚举定义一个抽象方法(如
getToDo()),然后在每个枚举值中实现这个方法的具体逻辑。修改主方法:原方法通过
DayCourse.valueOf(day)获取对应的枚举实例,并调用其方法。
命令模式
命令模式将请求封装为对象,使得你可以用不同的请求参数化其他对象,并支持请求的排队、记录、撤销等。
重构步骤:
定义命令接口:创建一个接口(如
Command),其中定义一个执行方法(如execute())。实现具体命令:为每一种课程创建一个类,实现
Command接口。修改主方法:原方法中,根据
day创建对应的具体命令对象,并调用其execute()方法。
规则引擎
当业务规则非常复杂且需要动态变更时,规则引擎是理想选择,它实现了业务逻辑与执行逻辑的彻底分离。
重构步骤:
定义规则接口:创建一个接口(如
Rule),包含评估条件的方法(如evaluate)和执行操作的方法(如execute)。实现具体规则:为每一天创建一个规则类,实现
Rule接口。创建规则引擎:创建一个规则引擎类,用于注册和管理所有规则,并提供一个方法遍历所有规则,找到条件满足的规则并执行。
修改主方法:原方法中,调用规则引擎来执行。
策略模式
策略模式定义一族算法,并封装每个算法,使它们可以相互替换,让算法的变化独立于使用算法的客户。
重构步骤:
定义策略接口:与命令模式类似,定义一个策略接口(如
CourseStrategy)。实现具体策略:为每一天创建一个策略类,实现该接口。
创建策略上下文:创建一个上下文类(如
CourseContext),它包含一个策略引用,并提供一个方法来执行当前策略。修改主方法:原方法中,根据
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.ENGLISH的toDo()方法,所有关联的日期(如周一、周二、周三)都会自动生效
尽管这种写法在特定场景下非常优雅,但也存在一些限制:
编译时绑定:枚举最大的特点是所有值在编译时就必须确定。无法在程序运行时不修改代码、通过外部配置就动态地添加一个新的
DayEnum.Weekend或一个新的课程类型Type.MUSIC。这对于需要高度动态配置的系统是一个硬约束。单一职责的权衡:这种结构将路由和策略都塞进了一个枚举文件中。当策略逻辑非常复杂时(例如,
Type.CHINESE.toDo()方法内部需要调用多个Service,完成一系列复杂操作),可能会导致这个枚举类变得臃肿。虽然可以通过在策略方法内部调用外部Service类来缓解,但这仍是需要权衡的点。
如何选择与小结
这几种重构方法各有侧重,可以根据实际场景选择:
如果业务逻辑相对稳定且类型固定,追求简洁直观,枚举是不错的选择。
如果每个分支的逻辑非常复杂且独立,希望将对象的创建与使用解耦,工厂类非常合适。
如果希望在运行时灵活切换算法,或者未来可能频繁增加新的课程类型,策略模式的扩展性更好。
如果需要将请求的发送与执行解耦,或者未来可能支持命令队列、撤销等高级功能,可以考虑命令模式。
如果业务规则极其复杂、多变,甚至需要从外部(如数据库、配置文件)动态加载规则,那么规则引擎能提供最大的灵活性。