专栏名称: 逸言
文学与软件,诗意地想念。
目录
相关文章推荐
OSC开源社区  ·  继V3之后,沐曦GPU再完成DeepSeek ... ·  昨天  
OSC开源社区  ·  Gitee邀您参与SBOM行业调研:共建可信 ... ·  2 天前  
程序员小灰  ·  这个春节,小灰一天都没休息 ·  3 天前  
程序员的那些事  ·  热搜第一!DeepSeek百万年薪招AI人才 ... ·  3 天前  
51好读  ›  专栏  ›  逸言

职责驱动设计以及状态模式的变化

逸言  · 公众号  · 程序员  · 2018-12-25 12:11

正文

标签 | 设计模式 OO

字数 | 7082字

阅读 | 18分钟


需求


针对某通信产品,我们需要开发一个版本升级管理系统。该系统需要通过由Java开发的管理后台,由Telnet发起向前端基站设备的命令,以获取基站设备的版本信息,并在后台比较与当前最新版本的差异,以确定执行什么样的命令对基站设备的软件文件进行操作。基站设备分为两种:

  • 主控板(Master Board)

  • 受控板(Slave Board)


基站设备允许执行的命令包括transfer、active、inactive等。这些命令不仅受到设备类型的限制,还要受制于该设备究竟运行在什么样的终端。类型分为:

  • Shell

  • UShell


对命令的约束条件大体如下表所示(不代表真实需求):


通过登录可以连接到主控板的Shell终端,此时,若执行enterUshell命令则进入UShell终端,执行enterSlaveBoard则进入受控板的Shell终端。在受控板同样可以执行enterUshell进入它的UShell终端。系统还提供了对应的退出操作。整个操作引起的变迁如下图所示:


执行升级的流程是在让基站设备处于失效状态下,获取基站设备的软件版本信息,然后在后端基于最新版本进行比较。得到版本之间的差异后,通过transfer命令传输新文件,put命令更新文件,deleteFiles命令删除多余的文件。成功更新后,再激活基站设备。因此,一个典型的升级流程如下所示:

  • login (Master Board Shell)

  • inactive (Master Board UShell)

  • get (Slave Board Shell)

  • transfer(Master Board Shell)

  • put(Slave Board Shell)

  • deleteFiles(Slave Board Ushell)

  • active(Master Board UShell)

  • logout


整个版本升级系统要求:无论当前基站设备属于哪种分类,处于哪种终端,只要Telnet连接没有中断,在要求升级执行的命令必须执行成功。如果当前所处的设备与终端不满足要求,系统就需要迁移到正确的状态,以确保命令的执行成功。


寻找解决方案


根据这个需求,我们期待的客户端调用为(为简便起见,省略了所有的方法参数):

//client 
public void upgrade() {
  TelnetService service = new TelnetService();

  service.login();
  service.inactive();
  service.get();
  service.transfer();
  service.put();
  service.deleteFiles();
  service.active();
  service.logout();
}



这样简便直观的调用,实则封装了复杂的规则和转换逻辑。我们应该怎么设计才能达到这样的效果呢?


使用条件分支


一种解决方法是使用条件分支,因为对于每条Telnet命令而言,都需要判断当前的状态,以决定执行不同的操作,例如:

public class TelnetService {
  private String currentState = "INITIAL";

  public void transfer() {
      swich (currentState.toUpperCase()) {
          case "INITIAL":
              login();
              currentState = "MASTER_SHELL";
              break;
          case "MASTER_SHELL":
              // ignore
              ......
      }

      // 执行transfer命令
  }
}


然而这样的实现是不可接受的,因为我们需要对每条命令都要编写相似的条件分支语句,这就导致出现了重复代码。我们可以将这样的逻辑封装到一个方法中:

public class TelnetService {
  private String currentState = "INITIAL";
  public void transfer() {
      swichState("MASTER_SHELL");
      // 执行transfer命令
  }
  private void switchState(String targetState) {
      switch (currentState.toUpperCase()) {
          case "INITIAL":
              switch (targetState.toUpperCase()) {
                  case "INITIAL":
                      break;
                  case "MASTER_SHELL":
                      login();
                      break;
                  // 其他分支略
              }
              break;
          // 其他分支略
      }
  }
}


switchState()方法避免了条件分支的重复代码,但是它同时也加重了方法实现的复杂度,因为它需要同时针对当前状态与目标状态进行判断,这相当于是一个条件组合。


Kent Beck认为:“(条件分支的)所有逻辑仍然在同一个类里,阅读者不必四处寻找所有可能的计算路径。但条件语句的缺点是:除了修改对象本身的代码之外,没有其他办法修改它的逻辑。……条件语句的好处在于简单和局部化。”显然,由于条件分支的集中化,导致变化发生时,我们只需要修改这一处;但问题在于任何变化都需要对此进行修改,这实际上是重构中“发散式变化(Divergent Change)”坏味道。


引入职责驱动设计


职责驱动设计强调从“职责”的角度思考设计。职责是“拟人化”的思考模式,这实际上是面向对象分析与设计的思维模式:将对象看作是有思想有判断有知识有能力的“四有青年”。这也就是我所谓的“智能对象”。只要分辨出职责,就可以从知识和能力的角度入手,寻找哪个对象具备履行该职责的能力?


回到版本升级系统这个例子,从诸如transfer、put等命令的角度思考职责,则可以识别职责为:

  • 执行Telnet命令

    • 迁移到正确的状态

    • 运行Telnet命令


TelnetService具有执行Telnet命令的能力,如果要运行的命令太多,也可以考虑将运行各个命令的职责再分派给对应的Command对象。那么,又该谁来执行“迁移到正确的状态”呢?看能力?——谁具有迁移状态的能力?一个对象能够履行某个职责,必须具备履行职责的知识,所以就要看知识。


迁移到正确状态需要哪些知识?——当前状态、目标状态以及如何迁移状态。只要确定了当前状态和目标状态,根据前面的状态变迁图就可以知道该如何迁移状态了。那么,谁确定地知道当前状态呢?——只有状态对象自身才知道!在条件分支实现中,状态是通过字符串表达的,字符串对象自身并不知道其值到底是什么,需要取出其值进行判断,这就是使用条件分支的原因。当状态从一个字符串升级为状态对象时,状态的值就是状态对象“自己知道”的知识。当每种状态都知道自己的状态值时,它们若要履行“迁移状态”的职责,就无需再对当前状态进行判断了,这正是为何多态能够替代条件分支的原因。


我们可以定义一个状态的继承树:

public interface NodeState {
  void switchTo(???);
}
public class InitialState implements NodeState {}
public class MasterShellState implements NodeState {}


当状态变为对象且具有职责时,对象就是有思想的职能对象。遗憾的是,它具有的知识还不足以完全履行“迁移到正确状态”的职责,因为它并不知道该迁移到哪个目标状态。这个知识只有具体的Telnet命令才知道,因而需要传递给它。一种做法是作为方法参数传入,但这会导致方法体内需要对传入的参数作条件分支判断。另一种方法则利用方法的多态,显式地定义多种方法来履行迁移到不同目标状态的职责:

interface NodeState {
   void switchToInitial();
   void switchToMasterShell();
   void switchToMasterUshell();
   void switchToSlaveShell();
   void switchToSlaveUshell();
}

public class InitialState implements NodeState {
   public InitialState(TelnetService service) {
       this.service = service;
   }

   public void switchToInitial() {
       // do nothing
   }

   public void switchToMasterShell() {
       service.login();
       service.setCurrentState(new MasterShellState(service));
   }

   public void switchToMasterUshell() {
       service.login();
       service.enterUshell();
       service.setCurrentState(new MasterUshellState(service));
   }
   public void switchToSlaveShell() {
       service.login();
       service.enterSlave();
       service.setCurrentState(new SlaveShellState(service));
   }

   public void switchToSlaveUshell() {
       service.login();
       service.enterSlave();
       service.enterUshell();
       service.setCurrentState(new SlaveShellState(service));
   }
}

public class MasterShellState implement NodeState {
   public MasterShell(TelnetService service) {
       this.service = service;
   }

   public void switchToInitial() {
       service.logout();
       service.setCurrentState(new InitialState(service));
   }

   public void switchToMasterShell() {
       //do nothing
   }

   public void switchToMasterUshell() {
       service.enterUshell();
       service.setCurrentState(new MasterUshellState(service));
   }

   public void switchToSlaveShell() {
       service.enterSlave();
       service.setCurrentState(new SlaveShellState(service));
   }

   public void switchToSlaveUshell() {
       service.enterSlave();
       service.enterUshell();
       service.setCurrentState(new SlaveShellState(service));
   }
}

class TelnetService {
   private NodeState currentState = new InitialState(this);
   public void setCurrentState(NodeState state) {
       this.currentState = state;
   }
   public void inactive() {
       currentState.switchToMasterUshell();
       //inactive impl
   }
   public void transfer() {
       currentState.switchToMasterShell();
       //real transfer impl
   }
       
   public void active() {
       currentState.switchToMasterUshell();
       // real active impl
   }

   public void get() {
       currentState.switchToSlaveShell();
       // get
   }
}


这样的设计并没有做到“开放封闭原则”,当增加了新的状态时,由于需要在NodeState接口中增加新的方法,使得所有实现该接口的状态类都需要修改。这相当于从条件分支的“发散式变化”坏味道变成了“霰弹式修改(Shotgun Surgery)”坏味道,即一个变化引起多处修改。然而比起条件分支方案而言,由于不用再判断当前状态,复杂度降低了许多,可以有效减少bug的产生。


状态模式


将一个状态进化为对象,这种设计思想是状态模式的设计。根据GOF的《设计模式》,一个标准的状态模式类图如下所示:


当我们要设计的业务具有复杂的状态变迁时,往往通过状态图来表现。利用状态图,可以非常容易地将其转换为状态模式。状态图的每个状态被封装一个状态对象,所有状态对象实现同一个抽象接口。该抽象接口的方法则为状态图上触发状态迁移的命令。Context对象持有一个全局变量,用以保存当前状态对象。每个状态对象持有Context对象,通过Context访问全局的当前状态变量,以完成状态的迁移。具体的状态对象在实现状态接口时,倘若是不符合条件的命令,则实现为空,或者抛出异常。


依据状态图,可以实现为状态模式:

interface NodeState {
   void






请到「今天看啥」查看全文