【设计模式】状态模式
一、前言
状态模式,就是用类表示状态,好处是能通过切换类来方便地改变对象的状态,如果需要新加状态时也不用修改之前的代码。
二、介绍
假设现在有一个金库,金库和警报中心相连,金库里有警铃和通话用的电话,还有个时钟监视着现在的时间,白天或晚上使用警铃或打电话的表现都不一样。
不使用State模式的伪代码如下:
警报系统的类{
使用金库时被调用的方法(){
if(白天){
向警报中心报告使用记录
}else if(晚上){
向警报中心报告紧急事态
}
}
警铃响起时被调用的方法(){
向警报中心报告紧急事态
}
正常通话时被调用的方法(){
if(白天){
呼叫警报中心
}else if(晚上){
呼叫警报中心的留言电话
}
}
}
使用State模式的伪代码:
表示白天的状态的类{
使用金库时被调用的方法(){
向警报中心报告使用记录
}
警铃响起时被调用的方法(){
向警报中心报告紧急事态
}
正常通话时被调用的方法(){
呼叫警报中心
}
}
表示晚上的状态的类{
使用金库时被调用的方法(){
向警报中心报告紧急事态
}
警铃响起时被调用的方法(){
向警报中心报告紧急事态
}
正常通话时被调用的方法(){
呼叫警报中心的留言电话
}
}
在使用了State模式后,我们用类表示白天还是晚上,在类的各个方法中就不需要if语句判断现在是白天还是晚上了。
State接口
public interface State {
// 设置时间
void doClock(Context context,int hour);
// 使用金库
void doUse(Context context);
/**
* 按下警铃
* @param context
*/
void doAlarm(Context context);
/**
* 正常通话
* @param context
*/
void doPhone(Context context);
}
表示白天的类
public class DayState implements State {
private static DayState singleton = new DayState();
private DayState(){
}
public static State getInstance(){
return singleton;
}
@Override
public void doClock(Context context, int hour) {
if (hour < 9 || 17 <= hour){
context.changeState(NightState.getInstance());
}
}
@Override
public void doUse(Context context) {
context.recordLog("使用金库(白天)");
}
@Override
public void doAlarm(Context context) {
context.callSecurityCenter("按下警铃(白天)");
}
@Override
public void doPhone(Context context) {
context.callSecurityCenter("正常通话(白天)");
}
public String toString(){
return "[白天]";
}
}
表示晚上的类
public class NightState implements State {
private static NightState singleton = new NightState();
private NightState() {
}
public static NightState getInstance() {
return singleton;
}
@Override
public void doClock(Context context, int hour) {
if (9 <= hour && hour < 17) {
context.changeState(DayState.getInstance());
}
}
@Override
public void doUse(Context context) {
context.callSecurityCenter("紧急:晚上使用金库!");
}
@Override
public void doAlarm(Context context) {
context.callSecurityCenter("按下警铃(晚上)");
}
@Override
public void doPhone(Context context) {
context.recordLog("晚上的通话录音");
}
public String toString() {
return "[晚上]";
}
}
上下文
public interface Context {
/**
* 设置时间
* @param hour
*/
void setClock(int hour);
/**
* 改变状态
* @param state
*/
void changeState(State state);
/**
* 联系警报中心
* @param msg
*/
void callSecurityCenter(String msg);
/**
* 在警报中心留下记录
* @param msg
*/
void recordLog(String msg);
}
SateFrame持有State
public class SateFrame extends Frame implements ActionListener, Context {
/**
* 显示当前时间
*/
private TextField textClock = new TextField(60);
/**
* 显示警报中心的记录
*/
private TextArea textScreen = new TextArea(10, 60);
private Button buttonUse = new Button("使用金库");
private Button buttonAlarm = new Button("按下警铃");
private Button buttonPhone = new Button("正常通话");
private Button buttonExit = new Button("结束");
/**
* 当前的状态
*/
private State state = DayState.getInstance();
public SateFrame(String title) {
super(title);
setBackground(Color.lightGray);
setLayout(new BorderLayout());
add(textClock, BorderLayout.NORTH);
textClock.setEditable(false);
add(textScreen, BorderLayout.CENTER);
textScreen.setEditable(false);
// 为界面添加按钮
Panel panel = new Panel();
panel.add(buttonUse);
panel.add(buttonAlarm);
panel.add(buttonPhone);
panel.add(buttonExit);
add(panel, BorderLayout.SOUTH);
pack();
show();
buttonUse.addActionListener(this);
buttonAlarm.addActionListener(this);
buttonPhone.addActionListener(this);
buttonExit.addActionListener(this);
}
@Override
public void setClock(int hour) {
String clockString = "现在时间是";
if (hour < 10) {
clockString += "0" + hour + ":00";
} else {
clockString += hour + ":00";
}
System.out.println(clockString);
textClock.setText(clockString);
state.doClock(this, hour);
}
@Override
public void changeState(State state) {
System.out.println("从" + this.state + "状态变为了" + state + "状态。");
this.state = state;
}
@Override
public void callSecurityCenter(String msg) {
textScreen.append("call! " + msg + "\n");
}
@Override
public void recordLog(String msg) {
textScreen.append("record...." + msg + "\n");
}
@Override
public void actionPerformed(ActionEvent e) {
System.out.println(e.toString());
if (e.getSource() == buttonUse) {
state.doUse(this);
} else if (e.getSource() == buttonAlarm) {
state.doAlarm(this);
} else if (e.getSource() == buttonPhone) {
state.doPhone(this);
} else if (e.getSource() == buttonExit) {
System.exit(0);
} else {
System.out.println("?");
}
}
}
测试类Main
public class Main {
public static void main(String[] args) {
SateFrame frame = new SateFrame("State Sample");
while (true) {
for (int hour = 0; hour < 24; hour++) {
frame.setClock(hour);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
状态模式通常有如下几种角色:
-
State(状态)
Sate角色表示状态,定义了根据不同状态进行不同处理的API,对应示例中的State接口。 -
ConcreteState(具体状态)
表示各个具体状态,对应示例中的DayState和NightState -
Context(状况、前后关系、上下文)
Context角色持有表示当前状态的ConcreteState角色。
三、实战
再看看我在项目中是如何使用State模式的。
3.1需求背景
公司的数字化营销系统为了调动销售的积极性,面向过程而不是结果,设计了一套任务系统,当某些前置条件(如客户提交了售后)满足时系统会自动给销售发送任务。
由于任务的类型很多有十几种而且每种任务的结果表单都不同,当时一番讨论后采用了将任务结果表拆分成多个表的方式,比如A类型任务的任务结果表是t_a,B类型的
任务的任务结果表是t_b。。。。
3.2痛点
随着任务种类越来越多,任务结果Service类的添加任务结果和查询任务结果方法中就充斥着大量的if/else逻辑判断后面难以维护,代码如下:
- /**
- * @author Ship
- * @date 2020-01-08 17:10 任务结果service
- */
- 4j
- public class SupplierTaskResultServiceImpl implements SupplierTaskResultService {
- public TaskResultDTO findTaskResult(Integer taskId) {
- TaskResultDTO taskResultDTO = new TaskResultDTO();
- SupplierTaskDTO supplierTaskDTO = supplierTaskMapper.queryById(taskId);
- Byte source = supplierTaskDTO.getSource();
- Byte type = supplierTaskDTO.getType();
- if (TaskSourceType.STANDARD.getCode().equals(source)) {
- // 标准
- if (type.equals(TaskType.ACTIVE_VISIT.getValue())) {
- ...
- }
- if (type.equals(TaskType.NEW_CUSTOMER_INFO.getValue()) || type.equals(TaskType.UPDATE_CUSTOMER_INFO.getValue())) {
- ...
- }
- if (type.equals(TaskType.COLLECT_REQUIREMENT_ORDER.getValue())) {
- ...
- }
- } else if (TaskSourceType.SYS_AUTO.getCode().equals(source)) {
- // 自动
- ...
- } else if (TaskSourceType.DECISION.getCode().equals(source)) {
- // 决策
- if (TaskType.ADJUST_PRICE.getValue().equals(type)) {
- ...
- }
- if (TaskType.COLLECT_INFO.getValue().equals(type)) {
- ...
- }
- if (TaskType.SEND_MESSAGE.getValue().equals(type)) {
- ...
- }
- } else {
- throw new BusinessException("任务触发类型有误");
- }
- if (Objects.isNull(taskResultDTO) || null == taskResultDTO.getType()) {
- taskResultDTO = new TaskResultDTO();
- taskResultDTO.setType(type);
- }
- return taskResultDTO;
- }
- public int addTaskResult(TaskResultDTO taskResultDTO) {
- log.info("taskResultDTO--->{}", taskResultDTO);
- if (Objects.isNull(taskResultDTO.getTaskSourceType())) {
- throw new BusinessException("添加任务结果失败:任务触发类型不能为空");
- }
- if (Objects.isNull(taskResultDTO.getTaskType()) && !TaskSourceType.SYS_AUTO.equals(taskResultDTO.getTaskSourceType())) {
- throw new BusinessException("添加任务结果失败:任务类型不能为空");
- }
- if (Objects.isNull(taskResultDTO.getTaskId())){
- throw new BusinessException("添加任务结果失败:任务ID不能为空");
- }
- taskResultDTO.setRecordTime(LocalDateTime.now());
- Byte type = taskResultDTO.getTaskType().getValue();
- Byte source = taskResultDTO.getTaskSourceType().getCode();
- if (TaskSourceType.STANDARD.getCode().equals(source)) {
- // 标准
- if (type.equals(TaskType.ACTIVE_VISIT.getValue())) {
- ...
- }
- if (type.equals(TaskType.NEW_CUSTOMER_INFO.getValue()) || type.equals(TaskType.UPDATE_CUSTOMER_INFO.getValue())) {
- //建档
- ...
- }
- if (type.equals(TaskType.COLLECT_REQUIREMENT_ORDER.getValue())) {
- ...
- }
- } else if (TaskSourceType.SYS_AUTO.getCode().equals(source)) {
- // 自动
- ...
- } else if (TaskSourceType.DECISION.getCode().equals(source)) {
- // 决策
- if (TaskType.ADJUST_PRICE.getValue().equals(type)) {
- ...
- }
- if (TaskType.COLLECT_INFO.getValue().equals(type)) {
- ...
- }
- if (TaskType.SEND_MESSAGE.getValue().equals(type)) {
- ...
- }
- } else {
- throw new BusinessException("任务触发类型有误");
- }
- return 0;
- }
- }
3.3使用State模式后
TaskResultProcessor相当于State接口
/**
* Created by 2YSP on 2020/2/12.
*/
public interface TaskResultProcessor {
/**
* 插入任务结果
*
* @param taskResultDTO
* @return
*/
Integer doAddTaskResult(TaskResultDTO taskResultDTO);
/**
* 查询任务结果
*
* @param taskId
* @param taskType
* @return
*/
TaskResultDTO doFindTaskResult(Integer taskId, TaskType taskType);
/**
* 更新业务员位置信息
*
* @param taskResultDTO
* @param resultId
*/
void updateSellerPosition(TaskResultDTO taskResultDTO, Integer resultId);
/**
* 支持的任务类型
*
* @return
*/
List<TaskType> supportTaskType();
}
每种任务的结果处理器实现TaskResultProcessor接口,如AdjustTaskResultProcessor
@Slf4j
@Service
@RequiredArgsConstructor
public class AdjustTaskResultProcessor implements TaskResultProcessor {
private final AdjustTaskResultMapper adjustTaskResultMapper;
@Override
public Integer doAddTaskResult(TaskResultDTO taskResultDTO) {
...
}
@Override
public TaskResultDTO doFindTaskResult(Integer taskId, TaskType taskType) {
...
}
@Override
public void updateSellerPosition(TaskResultDTO taskResultDTO, Integer resultId) {
...
}
@Override
public List<TaskType> supportTaskType() {
return Lists.newArrayList(TaskType.ADJUST_PRICE);
}
}
TaskResultProcessorHolder持有所有的任务结果处理器,利用构造方法注入在项目启动时将Bean注入到PROCESSOR_MAP中。
@Component
public class TaskResultProcessorHolder {
private static final Map<TaskType, TaskResultProcessor> PROCESSOR_MAP = new ConcurrentHashMap<>();
@Autowired
public TaskResultProcessorHolder(List<TaskResultProcessor> taskResultProcessors) {
taskResultProcessors.forEach(taskResultProcessor -> {
List<TaskType> taskTypes = taskResultProcessor.supportTaskType();
taskTypes.forEach(t -> PROCESSOR_MAP.put(t, taskResultProcessor));
});
}
public Optional<TaskResultProcessor> getProcessor(TaskType taskType) {
return Optional.ofNullable(PROCESSOR_MAP.get(taskType));
}
}
SupplierTaskResultServiceImpl作为调用方,代码就简化了很多。
/**
* @author Ship
* @date 2020-01-08 17:10 任务结果service
*/
@Slf4j
@Service
public class SupplierTaskResultServiceImpl implements SupplierTaskResultService {
@Autowired
private SupplierTaskMapper supplierTaskMapper;
@Autowired
private MQSender mqSender;
@Autowired
private TaskResultProcessorHolder processorHolder;
@Override
public TaskResultDTO findTaskResult(Integer taskId) {
SupplierTaskDTO supplierTaskDTO = supplierTaskMapper.queryById(taskId);
Byte type;
if (TaskSourceType.SYS_AUTO.getCode().equals(supplierTaskDTO.getSource())) {
type = TaskType.EARLY_WARNING_PROCESSING.getValue();
} else {
type = supplierTaskDTO.getType();
}
// 直接调用任务结果处理器的方法即可
TaskResultProcessor processor = getTaskResultProcessor(type);
return processor.doFindTaskResult(taskId, TaskType.getTaskType(type));
}
@Override
public int addTaskResult(TaskResultDTO taskResultDTO) {
log.info("taskResultDTO--->{}", JSON.toJSON(taskResultDTO));
// 参数校验。。。
taskResultDTO.setRecordTime(LocalDateTime.now());
// 自动任务时,type不需要
Byte type;
if (TaskSourceType.SYS_AUTO.equals(taskResultDTO.getTaskSourceType())) {
type = TaskType.EARLY_WARNING_PROCESSING.getValue();
} else {
type = taskResultDTO.getTaskType().getValue();
}
// 直接调用任务结果处理器的方法即可
TaskResultProcessor processor = getTaskResultProcessor(type);
Integer resultId = processor.doAddTaskResult(taskResultDTO);
mqSender.updateSellerPosition(getSellerPositionDTO(taskResultDTO, resultId));
return resultId;
}
@Override
public void updateSellerPosition(SellerPositionDTO positionDTO) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("异步更新任务结果中的业务员位置信息");
TaskResultDTO taskResultDTO = new TaskResultDTO();
// 根据经纬度解析地址...
Byte type;
if (TaskSourceType.SYS_AUTO.getCode().equals(positionDTO.getTaskSource())) {
type = TaskType.EARLY_WARNING_PROCESSING.getValue();
} else {
type = positionDTO.getTaskType();
}
TaskResultProcessor processor = getTaskResultProcessor(type);
processor.updateSellerPosition(taskResultDTO, positionDTO.getResultId());
stopWatch.stop();
log.info(stopWatch.prettyPrint());
}
/**
* 根据任务类型获取对应的结果处理器
*
* @param type
* @return
*/
private TaskResultProcessor getTaskResultProcessor(Byte type) {
if (Objects.isNull(type)) {
throw new BusinessException("任务类型不能为空");
}
TaskType taskType = TaskType.getTaskType(type);
TaskResultProcessor processor = processorHolder.getProcessor(taskType)
.orElseThrow(() -> new BusinessException("任务触发类型有误"));
return processor;
}
}
四、总结
总之,状态模式的优缺点有这几个,要根据自己的实际场景决定是否使用。
优点:
- 避免了大量的if/else判断,每个类单独维护自己的业务代码,职责更清晰。
- 如果需要增加新的状态,只需要重新写个类实现Sate接口即可,不需要修改之前的代码。
- 对调用方隐藏了自己的内部实现
缺点:
- 会生成大量的类文件
- 如果Sate接口增加了一个方法,那么不管其他ConcreteState类需不需要这个方法都必须强制实现,而且类越多修改起来就越麻烦。