【设计模式】状态模式

一、前言

状态模式,就是用类表示状态,好处是能通过切换类来方便地改变对象的状态,如果需要新加状态时也不用修改之前的代码。

二、介绍

假设现在有一个金库,金库和警报中心相连,金库里有警铃和通话用的电话,还有个时钟监视着现在的时间,白天或晚上使用警铃或打电话的表现都不一样。

不使用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逻辑判断后面难以维护,代码如下:

  1. /** 
  2. * @author Ship 
  3. * @date 2020-01-08 17:10 任务结果service 
  4. */ 
  5. @Slf4j 
  6. @Service 
  7. public class SupplierTaskResultServiceImpl implements SupplierTaskResultService
  8.  
  9.  
  10.  
  11. @Override 
  12. public TaskResultDTO findTaskResult(Integer taskId)
  13. TaskResultDTO taskResultDTO = new TaskResultDTO(); 
  14. SupplierTaskDTO supplierTaskDTO = supplierTaskMapper.queryById(taskId); 
  15. Byte source = supplierTaskDTO.getSource(); 
  16. Byte type = supplierTaskDTO.getType(); 
  17. if (TaskSourceType.STANDARD.getCode().equals(source)) { 
  18. // 标准 
  19. if (type.equals(TaskType.ACTIVE_VISIT.getValue())) { 
  20. ... 
  21. if (type.equals(TaskType.NEW_CUSTOMER_INFO.getValue()) || type.equals(TaskType.UPDATE_CUSTOMER_INFO.getValue())) { 
  22. ... 
  23.  
  24. if (type.equals(TaskType.COLLECT_REQUIREMENT_ORDER.getValue())) { 
  25. ... 
  26. } else if (TaskSourceType.SYS_AUTO.getCode().equals(source)) { 
  27. // 自动 
  28. ... 
  29.  
  30. } else if (TaskSourceType.DECISION.getCode().equals(source)) { 
  31. // 决策 
  32. if (TaskType.ADJUST_PRICE.getValue().equals(type)) { 
  33. ... 
  34. if (TaskType.COLLECT_INFO.getValue().equals(type)) { 
  35. ... 
  36. if (TaskType.SEND_MESSAGE.getValue().equals(type)) { 
  37. ... 
  38. } else
  39. throw new BusinessException("任务触发类型有误"); 
  40. if (Objects.isNull(taskResultDTO) || null == taskResultDTO.getType()) { 
  41. taskResultDTO = new TaskResultDTO(); 
  42. taskResultDTO.setType(type); 
  43. return taskResultDTO; 
  44.  
  45. @Override 
  46. public int addTaskResult(TaskResultDTO taskResultDTO)
  47. log.info("taskResultDTO--->{}", taskResultDTO); 
  48. if (Objects.isNull(taskResultDTO.getTaskSourceType())) { 
  49. throw new BusinessException("添加任务结果失败:任务触发类型不能为空"); 
  50. if (Objects.isNull(taskResultDTO.getTaskType()) && !TaskSourceType.SYS_AUTO.equals(taskResultDTO.getTaskSourceType())) { 
  51. throw new BusinessException("添加任务结果失败:任务类型不能为空"); 
  52.  
  53. if (Objects.isNull(taskResultDTO.getTaskId())){ 
  54. throw new BusinessException("添加任务结果失败:任务ID不能为空"); 
  55. taskResultDTO.setRecordTime(LocalDateTime.now()); 
  56. Byte type = taskResultDTO.getTaskType().getValue(); 
  57. Byte source = taskResultDTO.getTaskSourceType().getCode(); 
  58. if (TaskSourceType.STANDARD.getCode().equals(source)) { 
  59. // 标准 
  60. if (type.equals(TaskType.ACTIVE_VISIT.getValue())) { 
  61. ... 
  62.  
  63. if (type.equals(TaskType.NEW_CUSTOMER_INFO.getValue()) || type.equals(TaskType.UPDATE_CUSTOMER_INFO.getValue())) { 
  64. //建档 
  65. ... 
  66.  
  67. if (type.equals(TaskType.COLLECT_REQUIREMENT_ORDER.getValue())) { 
  68. ... 
  69.  
  70. } else if (TaskSourceType.SYS_AUTO.getCode().equals(source)) { 
  71. // 自动 
  72. ... 
  73.  
  74. } else if (TaskSourceType.DECISION.getCode().equals(source)) { 
  75. // 决策 
  76. if (TaskType.ADJUST_PRICE.getValue().equals(type)) { 
  77.  
  78. ... 
  79.  
  80. if (TaskType.COLLECT_INFO.getValue().equals(type)) { 
  81. ... 
  82.  
  83. if (TaskType.SEND_MESSAGE.getValue().equals(type)) { 
  84.  
  85. ... 
  86. } else
  87. throw new BusinessException("任务触发类型有误"); 
  88. return 0
  89.  
  90.  

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;
    }

}

四、总结

总之,状态模式的优缺点有这几个,要根据自己的实际场景决定是否使用。
优点:

  1. 避免了大量的if/else判断,每个类单独维护自己的业务代码,职责更清晰。
  2. 如果需要增加新的状态,只需要重新写个类实现Sate接口即可,不需要修改之前的代码。
  3. 对调用方隐藏了自己的内部实现

缺点:

  1. 会生成大量的类文件
  2. 如果Sate接口增加了一个方法,那么不管其他ConcreteState类需不需要这个方法都必须强制实现,而且类越多修改起来就越麻烦。
posted @ 2020-04-25 12:20  烟味i  阅读(287)  评论(0编辑  收藏  举报