文章摘要(AI生成)
观察者模式定义定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并被自动更新角色Subject(观察对象):Subject 角色表示观察对象。Subject角色定义了注册观察者和删除观察者的方法。此外,它还声明了“获取现在的状态”的方法。ConcreteSubj
观察者模式
定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并被自动更新
观察者模式也叫做发布/订阅模型
优缺点
优点
-
观察者和被观察者之间是抽象耦合
如此设计,则不管是增加观察者还是被观察者都非常容易扩展,而且在Java中都已经实 现的抽象层级的定义,在系统扩展方面更是得心应手。
-
建立一套触发机制
根据单一职责原则,每个类的职责是单一的,那么怎么把各个单一的职责串联成真实世 界的复杂的逻辑关系呢?比如,我们去打猎,打死了一只母鹿,母鹿有三个幼崽,因失去了 母鹿而饿死,尸体又被两只秃鹰争抢,因分配不均,秃鹰开始斗殴,然后羸弱的秃鹰死掉, 生存下来的秃鹰,则因此扩大了地盘……这就是一个触发机制,形成了一个触发链。观察者 模式可以完美地实现这里的链条形式。
缺点
观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发 和调试就会比较复杂,而且在Java中消息的通知默认是顺序执行,一个观察者卡壳,会影响 整体的执行效率。在这种情况下,一般考虑采用异步的方式。 多级触发时的效率更是让人担忧,大家在设计时注意考虑。
使用场景
- 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。
- 事件多级触发场景。
- 跨系统的消息交换场景,如消息队列的处理机制。
最佳实践
观察者模式在实际项目和生活中非常常见,我们举几个经常发生的例子来说明
文件系统
比如,在一个目录下新建立一个文件,这个动作会同时通知目录管理器增加该目录,并 通知磁盘管理器减少1KB的空间,也就说“文件”是一个被观察者,“目录管理器”和“磁盘管理 器”则是观察者。
猫鼠游戏
夜里猫叫一声,家里的老鼠撒腿就跑,同时也吵醒了熟睡的主人,这个场景中,“猫”就 是被观察者,老鼠和人则是观察者。
ATM取钱
比如你到ATM机器上取钱,多次输错密码,卡就会被ATM吞掉,吞卡动作发生的时 候,会触发哪些事件呢?第一,摄像头连续快拍,第二,通知监控系统,吞卡发生;第三, 初始化ATM机屏幕,返回最初状态。一般前两个动作都是通过观察者模式来完成的,后一个 动作是异常来完成。
广播收音机
电台在广播,你可以打开一个收音机,或者两个收音机来收听,电台就是被观察者,收 音机就是观察者。
角色
- Subject(观察对象):Subject 角色表示观察对象。Subject角色定义了注册观察者和删除观察者的方法。此外,它还声明了“获取现在的状态”的方法。
- ConcreteSubject(具体的观察对象):ConcreteSubject角色表示具体的被观察对象。当自身状态发生变化后,它会通知所有已经注册的Observer角色。
- Observer (观察者):Observer角色负责接收来自Subject角色的状态变化的通知。为此,它声明了update方法。
- ConcreteObserver(具体的观察者):ConcreteObserver角色表示具体的Observer。当它的update方法被调用后,会去获取要观察的对象的最新状态。
spring实现
下面我们来看一段使用了Observer模式的示例程序。这是一段简单的示例程序,观察者将观察一个会生成数值的对象,并将它生成的数值结果显示出来。不过,不同的观察者的显示方式不一样。DigitObserver会以数字形式显示数值,而Graph0bserver则会以简单的图示形式来显示数值。
观察对象
表示生成数值对象的抽象类
public abstract class NumberGenerator {
private ArrayList<Observer> observers = new ArrayList<Observer>();
public void deleteObserver(Observer observer){
observers.remove(observer);
}
public void addObserver(Observer observer){
observers.add(observer);
}
public void notifyObservers(){
Iterator< Observer> it = observers.iterator();
while (it.hasNext()) {
Observer o = (Observer) it.next();
o.update(this);
}
}
public abstract int getNumber();
public abstract void execute();
}
具体的观察对象
表示生成具体随机数的抽象类
@Component
public class RandomNumberGenerator extends NumberGenerator {
private Random random = new Random();
private int number;
@Override
public int getNumber() {
// TODO Auto-generated method stub
return number;
}
@Override
public void execute() {
// TODO Auto-generated method stub
for(int i=0;i<20;i++){
number = random.nextInt(50);
notifyObservers();
}
}
}
观察者
表示观察者接口
public interface Observer {
void update(NumberGenerator generator);
}
具体观察者
获取打印具体数字
@Component
@Slf4j
public class DigitObserver implements Observer {
@Override
public void update(NumberGenerator generator) {
// TODO Auto-generated method stub
log.info("DigitObserver:"+generator.getNumber());
try{
Thread.sleep(100);
}catch(InterruptedException e){
}
}
}
用图形显示数值:
@Component
@Slf4j
public class GraphObserver implements Observer {
@Override
public void update(NumberGenerator generator) {
// TODO Auto-generated method stub
StringBuilder stringBuilder = new StringBuilder("GraphObserver:");
int count = generator.getNumber();
for(int i=0;i<count;i++){
stringBuilder.append("*");
}
log.info(stringBuilder.toString());
try{
Thread.sleep(100);
}catch(InterruptedException e){
}
}
}
测试
@RestController
public class ObserverController {
@Resource
NumberGenerator numberGenerator;
@Resource
DigitObserver digitObserver;
@Resource
GraphObserver graphObserver;
@GetMapping("observer")
public String observer(){
numberGenerator.addObserver(digitObserver);
numberGenerator.addObserver(graphObserver);
numberGenerator.execute();
return "SUCCESS";
}
}
测试结果如下:
2022-09-03 14:12:23.029 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:48
2022-09-03 14:12:23.132 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:************************************************
2022-09-03 14:12:23.241 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:20
2022-09-03 14:12:23.352 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:********************
2022-09-03 14:12:23.462 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:22
2022-09-03 14:12:23.574 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:**********************
2022-09-03 14:12:23.684 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:41
2022-09-03 14:12:23.793 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:*****************************************
2022-09-03 14:12:23.903 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:31
2022-09-03 14:12:24.012 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:*******************************
2022-09-03 14:12:24.124 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:23
2022-09-03 14:12:24.233 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:***********************
2022-09-03 14:12:24.344 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:40
2022-09-03 14:12:24.455 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:****************************************
2022-09-03 14:12:24.566 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:19
2022-09-03 14:12:24.678 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.GraphObserver : GraphObserver:*******************
2022-09-03 14:12:24.790 INFO 12672 --- [nio-8080-exec-2] c.e.d.Observer.DigitObserver : DigitObserver:2
状态模式
定义
当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。
优缺点
优点
-
结构清晰
避免了过多的switch…case或者if…else语句的使用,避免了程序的复杂性,提高系统的可 维护性。
-
遵循设计原则
很好地体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就要增 加子类,你要修改状态,你只修改一个子类就可以了。
-
封装性非常好
这也是状态模式的基本要求,状态变换放置到类的内部来实现,外部的调用不用知道类 内部如何实现状态和行为的变换。
缺点
状态模式既然有优点,那当然有缺点了。但只有一个缺点,子类会太多,也就是类膨 胀。如果一个事物有很多个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好管理,这个需要大家在项目中自己衡量。其实有很多方式可以解决这个状态问题,如在数据库中建立一个状态表,然后根据状态执行相应的操作
使用场景
-
行为随状态改变而改变的场景
这也是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结 果也会不同,在这种情况下需要考虑使用状态模式。
-
条件、分支判断语句的替代者
在程序中大量使用switch语句或者if判断语句会导致程序结构不清晰,逻辑混乱,使用 状态模式可以很好地避免这一问题,它通过扩展子类实现了条件的判断处理。
最佳实践
允许状态间的跳跃
状态间的自由切换k恶意通过建造者模式按照一定顺序进行重新组装
一个节点会有初始状态、挂起状态呢、完成状态等,可以通过上下文做妆台机的管理
角色
- State(状态):State角色表示状态,定义了根据不同状态进行不同处理的接口(API)。该接口(API)是那些处理内容依赖于状态的方法的集合。
- ConcreteState(具体状态):ConcreteState角色表示各个具体的状态,它实现了state接口。
- Context(状况、前后关系、上下文):Context角色持有表示当前状态的ConcreteState角色。此外,它还定义了供外部调用者使用State模式的接口(API )。
spring实现
这里我们来看一个警戒状态每小时会改变一次的警报系统。该系统并不会真正呼叫警报中心,只是在页面上显示呼叫状态。此外,如果以现实世界中的时间来测试程序就太慢了,所以我们假设程序中的1秒对应现实世界中的一个小时。
状态接口
提供了每个状态中对应的事件接口
public interface State {
void doClock(Context context,int hour); //设置时间
void doUse(Context context);//使用金库
void doAlarm(Context context);//按下警铃
void doPhone(Context context);//正常通话
}
具体状态
表示白天的状态
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class DayState implements State {
@Resource
private StateGenerator stateGenerator;
@Override
public void doClock(Context context, int hour) {
// TODO Auto-generated method stub
if(hour<9||17<=hour){
context.changeState(stateGenerator.nightState());
}
}
@Override
public void doUse(Context context) {
// TODO Auto-generated method stub
context.recordLog("使用金库(白天)");
}
@Override
public void doAlarm(Context context) {
// TODO Auto-generated method stub
context.callSecurityCenter("按下警铃(白天)");
}
@Override
public void doPhone(Context context) {
// TODO Auto-generated method stub
context.callSecurityCenter("正常通话(白天)");
}
@Override
public String toString() {
return "DayState [白天]";
}
}
表示晚上的状态:
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class NightState implements State {
@Resource
private StateGenerator stateGenerator;
@Override
public void doClock(Context context, int hour) {
// TODO Auto-generated method stub
if(9<=hour&&hour<17){
context.changeState(stateGenerator.dayState());
}
}
@Override
public void doUse(Context context) {
// TODO Auto-generated method stub
context.callSecurityCenter("紧急:晚上使用金库");
}
@Override
public void doAlarm(Context context) {
// TODO Auto-generated method stub
context.callSecurityCenter("按下警铃(晚上)");
}
@Override
public void doPhone(Context context) {
// TODO Auto-generated method stub
context.recordLog("晚上的通话录音");
}
@Override
public String toString() {
return "NightState [晚上]";
}
}
上下文
提供管理金库状态的接口
public interface Context {
void setClock(int hour);
void changeState(State state);
void callSecurityCenter(String msg);
void recordLog(String msg);
}
SafeFrame实现了context接口,提供按钮报警和画面显示等信息:
@Component
@Slf4j
public class SafeFrame implements Context {
private State state;
@Resource
private StateGenerator stateGenerator;
@PostConstruct
private void init(){
state = stateGenerator.dayState();
}
@Override
public void setClock(int hour) {
// TODO Auto-generated method stub
String clockstring = "现在时间是 ";
if(hour<10){
clockstring += "0"+hour+":00";
}else{
clockstring +=hour+":00";
}
log.info(clockstring);
state.doClock(this, hour);
}
@Override
public void changeState(State state) {
// TODO Auto-generated method stub
log.info("从"+this.state+"状态变为了"+state+"状态。");
this.state = state;
}
@Override
public void callSecurityCenter(String msg) {
// TODO Auto-generated method stub
log.info("call!"+msg+"\n");
}
@Override
public void recordLog(String msg) {
// TODO Auto-generated method stub
log.info("record..."+msg+"\n");
}
public void actionPerformed(String button) {
// TODO Auto-generated method stub
log.info(button);
if(Objects.equals(button, "buttonUse")){
state.doUse(this);
}else if(Objects.equals(button, "buttonAlarm")){
state.doAlarm(this);
}else if(Objects.equals(button, "buttonPhone")){
state.doPhone(this);
}else{
log.error("?");
}
}
}
测试
我们在一天中,每3个小时使用金库,每5个小时联络安全中心,每4个小时按下警铃
@RestController
public class StateController {
@Resource
private SafeFrame frame;
@GetMapping("state")
public String state() {
for (int hour = 0; hour < 24; hour++) {
frame.setClock(hour);
if(hour%3 == 0) {
frame.actionPerformed("buttonUse");
}
if(hour%5 == 0){
frame.actionPerformed("buttonPhone");
}
if(hour%4 == 0){
frame.actionPerformed("buttonAlarm");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
return "SUCCESS";
}
}
测试结果为:
2022-09-03 16:14:06.607 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 00:00
2022-09-03 16:14:06.608 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 从DayState [白天]状态变为了NightState [晚上]状态。
2022-09-03 16:14:06.608 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:06.608 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!紧急:晚上使用金库
2022-09-03 16:14:06.608 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonPhone
2022-09-03 16:14:06.608 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : record...晚上的通话录音
2022-09-03 16:14:06.608 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonAlarm
2022-09-03 16:14:06.608 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!按下警铃(晚上)
2022-09-03 16:14:07.613 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 01:00
2022-09-03 16:14:08.616 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 02:00
2022-09-03 16:14:09.621 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 03:00
2022-09-03 16:14:09.621 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:09.621 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!紧急:晚上使用金库
2022-09-03 16:14:10.628 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 04:00
2022-09-03 16:14:10.628 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonAlarm
2022-09-03 16:14:10.628 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!按下警铃(晚上)
2022-09-03 16:14:11.636 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 05:00
2022-09-03 16:14:11.636 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonPhone
2022-09-03 16:14:11.636 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : record...晚上的通话录音
2022-09-03 16:14:12.644 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 06:00
2022-09-03 16:14:12.644 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:12.644 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!紧急:晚上使用金库
2022-09-03 16:14:13.651 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 07:00
2022-09-03 16:14:14.658 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 08:00
2022-09-03 16:14:14.658 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonAlarm
2022-09-03 16:14:14.658 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!按下警铃(晚上)
2022-09-03 16:14:15.666 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 09:00
2022-09-03 16:14:15.666 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 从NightState [晚上]状态变为了DayState [白天]状态。
2022-09-03 16:14:15.666 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:15.666 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : record...使用金库(白天)
2022-09-03 16:14:16.671 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 10:00
2022-09-03 16:14:16.671 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonPhone
2022-09-03 16:14:16.671 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!正常通话(白天)
2022-09-03 16:14:17.678 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 11:00
2022-09-03 16:14:18.681 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 12:00
2022-09-03 16:14:18.681 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:18.681 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : record...使用金库(白天)
2022-09-03 16:14:18.681 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonAlarm
2022-09-03 16:14:18.681 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!按下警铃(白天)
2022-09-03 16:14:19.686 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 13:00
2022-09-03 16:14:20.692 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 14:00
2022-09-03 16:14:21.693 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 15:00
2022-09-03 16:14:21.693 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:21.693 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : record...使用金库(白天)
2022-09-03 16:14:21.693 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonPhone
2022-09-03 16:14:21.693 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!正常通话(白天)
2022-09-03 16:14:22.696 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 16:00
2022-09-03 16:14:22.696 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonAlarm
2022-09-03 16:14:22.696 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!按下警铃(白天)
2022-09-03 16:14:23.699 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 17:00
2022-09-03 16:14:23.699 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 从DayState [白天]状态变为了NightState [晚上]状态。
2022-09-03 16:14:24.703 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 18:00
2022-09-03 16:14:24.703 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:24.703 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!紧急:晚上使用金库
2022-09-03 16:14:25.707 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 19:00
2022-09-03 16:14:26.713 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 20:00
2022-09-03 16:14:26.713 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonPhone
2022-09-03 16:14:26.713 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : record...晚上的通话录音
2022-09-03 16:14:26.713 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonAlarm
2022-09-03 16:14:26.713 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!按下警铃(晚上)
2022-09-03 16:14:27.720 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 21:00
2022-09-03 16:14:27.720 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : buttonUse
2022-09-03 16:14:27.720 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : call!紧急:晚上使用金库
2022-09-03 16:14:28.725 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 22:00
2022-09-03 16:14:29.730 INFO 2416 --- [nio-8080-exec-1] c.example.designpattern.State.SafeFrame : 现在时间是 23:00
策略模式
定义
定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换
优缺点
优点
-
算法可以自由切换
这是策略模式本身定义的,只要实现抽象策略,它就成为策略家族的一个成员,通过封 装角色对其进行封装,保证对外提供“可自由切换”的策略。
-
避免使用多重条件判断
如果没有策略模式,我们想想看会是什么样子?一个策略家族有5个策略算法,一会要 使用A策略,一会要使用B策略,怎么设计呢?使用多重的条件语句?多重条件语句不易维 护,而且出错的概率大大增强。使用策略模式后,可以由其他模块决定采用何种策略,策略 家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。
-
扩展性良好
这甚至都不用说是它的优点,因为它太明显了。在现有的系统中增加一个策略太容易 了,只要实现接口就可以了,其他都不用修改,类似于一个可反复拆卸的插件,这大大地符 合了OCP原则。
缺点
-
策略类数量增多
每一个策略都是一个类,复用的可能性很小,类数量增多。
-
所有的策略类都需要对外暴露
上层模块必须知道有哪些策略,然后才能决定使用哪一个策略,这与迪米特法则是相违 背的,我只是想使用了一个策略,我凭什么就要了解这个策略呢?那要你的封装类还有什么 意义?这是原装策略模式的一个缺点,幸运的是,我们可以使用其他模式来修正这个缺陷, 如工厂方法模式、代理模式或享元模式。
使用场景
- 多个类只有在算法或行为上稍有不同的场景。
- 算法需要自由切换的场景。
- 需要屏蔽算法规则的场景。
最佳实践
策略模式是一个非常简单的模式。它在项目中使用得非常多,但它单独使用的地方就比 较少了,因为它有致命缺陷:所有的策略都需要暴露出去,这样才方便客户端决定使用哪一 个策略。例如,在例子中的赵云,实际上不知道使用哪个策略,他只知道拆第一个锦囊,而 不知道是BackDoor这个妙计。是的,诸葛亮已经在规定了在适当的场景下拆开指定的锦囊, 我们的策略模式只是实现了锦囊的管理,但是我们没有严格地定义“适当的场景”拆开“适当 的锦囊”,在实际项目中,我们一般通过工厂方法模式来实现策略类的声明,可以参考混编模式
角色
- Strategy(策略):Strategy角色负责决定实现策略所必需的接口(API)。
- ConcreteStrategy(具体的策略):ConcreteStrategy角色负责实现Strategy角色的接口(API),即负责实现具体的策略(战略、方向、方法和算法)。
- Context(上下文):负责使用Strategy角色。Context角色保存了ConcreteStrategy角色的实例,并使用ConcreteStrategy角色去实现需求(总之,还是要调用Strategy角色的接口(API))。
spring实现
下面我们来看一段使用了Strategy模式的示例程序。这段示例程序的功能是让电脑玩“猜拳”游戏。
我们考虑了两种猜拳的策略。第一种策略是“如果这局猜拳获胜,那么下一局也出一样的手势”( winningstrategy),这是一种稍微有些笨的策略;另外一种策略是“根据上一局的手势从概率上计算出下一局的手势”( ProbStrategy )。
定义实例,表示猜拳游戏中的手势:
public class Hand {
public static final int HANDVALUE_GUU = 0;
public static final int HANDVALUE_CHO = 1;
public static final int HANDVALUE_PAA = 2;
public static final Hand[] hand = {
new Hand(HANDVALUE_GUU),
new Hand(HANDVALUE_CHO),
new Hand(HANDVALUE_PAA),
};
private static final String[] name ={
"石头","剪刀","步",
};
private int handvalue;
public Hand(int handvalue) {
super();
this.handvalue = handvalue;
}
public static Hand getHand(int handvalue){
return hand[handvalue];
}
public boolean isStrongerThan(Hand h){
return fight(h) == 1;
}
private int fight(Hand h) {
// TODO Auto-generated method stub
if(this==h){
return 0;
}else if((this.handvalue+1)%3==h.handvalue){
return 1;
}else{
return -1;
}
}
public String toString(){
return name[handvalue];
}
}
策略接口
表示猜拳游戏中策略的类
public interface Strategy {
Hand nextHand();
void study(boolean win);
void setRandom(int seed);
}
具体策略
如果这局猜拳获胜,那么下一局也出一样的手势:
@Component
@StrategyName(StrategyEnum.WINNING_STRATEGY)
public class WinningStrategy implements Strategy {
private Random random;
private boolean won = false;
private Hand preHand;
public void setRandom(int seed) {
random = new Random(seed);
}
@Override
public Hand nextHand() {
// TODO Auto-generated method stub
if (!won) {
preHand = Hand.getHand(random.nextInt(3));
}
return preHand;
}
@Override
public void study(boolean win) {
// TODO Auto-generated method stub
won = win;
}
}
根据上一局的手势从概率上计算出下一局的手势
@Component
@StrategyName(StrategyEnum.PROB_STRATEGY)
public class ProbStrategy implements Strategy {
private Random random;
private int prevHandValue = 0;
private int currentHandValue = 0;
private int[][] history = {
{1,1,1,},
{1,1,1,},
{1,1,1,},
};
public void setRandom(int seed){
random = new Random(seed);
}
@Override
public Hand nextHand() {
// TODO Auto-generated method stub
int bet = random.nextInt(getSum(currentHandValue));
int handvalue = 0;
if(bet<history[currentHandValue][0]){
handvalue = 0;
}else if(bet<history[currentHandValue][0]+history[currentHandValue][1]){
handvalue = 1;
}else{
handvalue = 2;
}
prevHandValue = currentHandValue;
currentHandValue = handvalue;
return Hand.getHand(handvalue);
}
private int getSum(int hv) {
// TODO Auto-generated method stub
int sum=0;
for(int i=0;i<3;i++){
sum+=history[hv][i];
}
return sum;
}
@Override
public void study(boolean win) {
// TODO Auto-generated method stub
if(win){
history[prevHandValue][currentHandValue]++;
}else{
history[prevHandValue][(currentHandValue+1)%3]++;
history[prevHandValue][(currentHandValue+2)%3]++;
}
}
}
一种纯随机策略
@Component
@StrategyName(StrategyEnum.RANDOM_STRATEGY)
public class RandomStrategy implements Strategy {
private Random random;
private Hand hand;
public void setRandom(int seed) {
random = new Random(seed);
}
@Override
public Hand nextHand() {
// TODO Auto-generated method stub
hand = Hand.getHand(random.nextInt(3));
return hand;
}
@Override
public void study(boolean win) {
}
}
上下文
用来进行猜拳游戏的选手负责选用不同的策略进行出招
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class Player {
private String name;
private Strategy strategy;
private int wincount;
private int losecount;
private int gamecount;
public void init(String name, Strategy strategy) {
this.name = name;
this.strategy = strategy;
}
public Hand nextHand(){
return strategy.nextHand();
}
public void win(){
strategy.study(true);
wincount++;
gamecount++;
}
public void lose(){
strategy.study(false);
losecount++;
gamecount++;
}
public void even(){
gamecount++;
}
@Override
public String toString() {
return "Player [name=" + name + ", strategy=" + strategy
+ ", wincount=" + wincount + ", losecount=" + losecount
+ ", gamecount=" + gamecount + "]";
}
}
策略生成器
定义玩家生成方式
@Component
public abstract class StrategyGenerator {
@Lookup
protected abstract Player player();
@Resource
private List<Strategy> strategyList;
private Map<StrategyEnum, Strategy> strategyMap = new HashMap<>();
public Player genPlayer(String username, StrategyEnum strategyEnum, int seed){
Player player = player();
Strategy strategy = strategyMap.get(strategyEnum);
strategy.setRandom(seed);
player.init(username, strategy);
return player;
}
@PostConstruct
public void initMap(){
for (Strategy strategy: strategyList){
strategyMap.put(strategy.getClass().getAnnotation(StrategyName.class).value(), strategy);
}
}
}
测试
定义两个玩家,一个使用赢相同手势策略,一个使用随机手势策略
@RestController
@Slf4j
public class StrategyController {
@Resource
private StrategyGenerator strategyGenerator;
@GetMapping("strategy")
private String strategy(Integer seed1, Integer seed2){
if (seed1 == null || seed2 == null) {
log.info("Useage: java Main randomseed1 randomseed2");
log.info("Example: java Main 314 15");
return "ERROR";
}
Player player1 = strategyGenerator.genPlayer("Trao", StrategyEnum.WINNING_STRATEGY, seed1);
Player player2 =strategyGenerator.genPlayer("Hana", StrategyEnum.RANDOM_STRATEGY, seed2);
for (int i = 0; i < 10000; i++) {
Hand nextHand1 = player1.nextHand();
Hand nextHand2 = player2.nextHand();
if (nextHand1.isStrongerThan(nextHand2)) {
log.info("Winner:" + player1);
player1.win();
player2.lose();
} else if (nextHand2.isStrongerThan(nextHand1)) {
log.info("WInner:" + player2);
player1.lose();
player2.win();
} else {
log.info("Even....");
player1.even();
player2.even();
}
}
log.info("Total result:");
log.info(player1.toString());
log.info(player2.toString());
return "SUCCESS";
}
}
测试输出结果为:
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Even....
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Even....
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : WInner:Player [name=Hana, strategy=com.example.designpattern.Strategy.RandomStrategy@632aa1a3, wincount=0, losecount=0, gamecount=2]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Winner:Player [name=Trao, strategy=com.example.designpattern.Strategy.WinningStrategy@20765ed5, wincount=0, losecount=1, gamecount=3]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : WInner:Player [name=Hana, strategy=com.example.designpattern.Strategy.RandomStrategy@632aa1a3, wincount=1, losecount=1, gamecount=4]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : WInner:Player [name=Hana, strategy=com.example.designpattern.Strategy.RandomStrategy@632aa1a3, wincount=2, losecount=1, gamecount=5]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Winner:Player [name=Trao, strategy=com.example.designpattern.Strategy.WinningStrategy@20765ed5, wincount=1, losecount=3, gamecount=6]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : WInner:Player [name=Hana, strategy=com.example.designpattern.Strategy.RandomStrategy@632aa1a3, wincount=3, losecount=2, gamecount=7]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Winner:Player [name=Trao, strategy=com.example.designpattern.Strategy.WinningStrategy@20765ed5, wincount=2, losecount=4, gamecount=8]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : WInner:Player [name=Hana, strategy=com.example.designpattern.Strategy.RandomStrategy@632aa1a3, wincount=4, losecount=3, gamecount=9]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Total result:
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Player [name=Trao, strategy=com.example.designpattern.Strategy.WinningStrategy@20765ed5, wincount=3, losecount=5, gamecount=10]
2022-09-03 17:43:24.739 INFO 17864 --- [nio-8080-exec-2] c.e.d.Strategy.StrategyController : Player [name=Hana, strategy=com.example.designpattern.Strategy.RandomStrategy@632aa1a3, wincount=5, losecount=3, gamecount=10]
评论区