应用设计模式开发命令行交互程序

背景介绍

人机交互的方式最初起始于命令行交互,虽然图形界面的交互方式应用越来越广泛,可是命令行交互仍然有着它不可替代的地位。命令行交互程序是以命令行方式进行的人机交互,即用户按着程序的提示,一步步进行输入,而程序负责解释并最终执行指令。

实例简介

在实例中,命令行交互程序给出了一组问题请求用户输入,然后根据用户的输入将 war 包部署在服务器上。如图 1 所示,应用程序共有 7 个问题,需要用户输入不同的部署信息。这些问题将以特定的顺序和用户进行交互,用户则依次给出问题的答案。

图 1. 单个 war 包部署实例

    问题 1. 传统命令行交互模式不支持回退和跳转

如图 1 所示,将 war 包部署到服务器上共有 7 个问题请求,而用户并不需要依次回答 7 个问题,当 war 包存在时,用户只需要回答问题 1、2、3、6、7;而当 war 包不存在时,用户需要回答 1、2、4、5、6、7。因此,根据用户输入的不同,可能遇到的问题流也不同。在传统的命令行交互模式中,用户只能按照问题流的顺序前进,不能回退和跳转,比如,用户行进到问题 3 时,无法回退至问题 2。

    问题 2. 传统命令行交互模式很难适应需求变化

当程序的需求发生变化时,传统的命令行交互模式也很难适应变化。以图 1 为例,当需要部署多个 war 包时,流程图将会变为图 2 所示,传统的程序在处理这种变化时,显得缺乏灵活性。

图 2. 多个 war 包部署实例

本节将使用设计模式给出图 1(单个 war 包部署)的设计方案,其后在图 1 的设计方案的基础上进行扩展,实现图 2(多个 war 包部署)的需求。

称谓约定

为了叙述方便,文中将程序的一个问题提示以及用户的一个输入组合称为一个问题。而程序提示问题的顺序称为问题流程。用户在任何问题时输入处输入特定字符返回上一个问题的功能称为回退,而用户输入特定字符和问题编 来显示某个已提问过的问题的功能称为跳转。

———————————————————————————————————————————————-

通过对单个 war 包部署实例(图 1) 的观察可以发现,每一个问题的主要功能均为请求并获得用户输入。因此,所有问题都可以抽象成一个接口来处理。同时,从该实例可以看出,如果要实现返回上一个问题的功能,则需要将问题和问题的流程解耦合。这些需求都可以通过命令模式很方便的实现。

在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些情况,比如要对行为进行“记录、撤销 / 重做、事务”等处理时,紧耦合很难适应变化。在这种情况下,需要将“行为请求者”与“行为实现者”解耦。

图 3. 应用命令模式的问题和流程设计

问题流程定义了一组问题的顺序,在使用了命令模式后,这些问题类可以放入问题流程中的任何一个位置。因此问题流程就可以实现问题类的回退、跳转等操作,而问题类只需关注问题本身。

通过使用命令模式,我们实现了对问题的抽象,以及问题与问题流程之间的解耦,接下来需要对问题流程进行处理。问题流程解决的是各个问题之间以什么样的顺序连接的问题,即一个问题接下一个问题,这种连接和数组类似。因此针对连接问题,有两种设计方案:

  • 与图 3 类似,可以定义固定的顺序数组,每一个数组节点为一个问题。然后依次调用这个数组上的所有节点。
  • 数组以链表的形式来实现,将问题流程变为一个特殊的链表,链表上的每一个节点为一个问题,并且指向下一个问题。如图 4 所示。
图 4. 以链表形式实现问题流程

相较于第一种方案,第二种方案明显更加具有灵活性,当问题的流程发生改变时,链表的组织形式更容易添加或删除某个问题。而且,将问题流程隐式的包含在问题对象中时,就可以动态的定制问题流程,结合图 1 所示的实例,这种设计更适合于命令行交互程序。

由于每一个问题对流程的处理应该是相同的,因此在该设计中引入模版模式。

模版模式(Template Pattern)定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

结合本例,每一个问题所处理的内容是一样的:请求用户输入、保存用户输入,连接到下一个问题。因此,在问题类中使用模版模式,将请求输入,保存输入等功能延迟到子类实现,从而固化处理的流程,保证每一个问题都有相同的处理流程。现在,抽象出来的问题类将实现两种功能,采集用户输入和返回下一个问题类。

由于需要同时支持回退和跳转功能,单纯的链表组织形式虽然可以实现回退,可是对于跳转的支持就不是很好。因此需要对这种链表的组织方式进行优化。

对比顺序数组和链表数组两种方案可以发现,数组的形式对跳转的支持更好,而链表的形式更加适合添加和删除节点,因此我们的解决方案是将数组的形式和链表的思想结合使用。观察图 1 可以发现,虽然可能的问题流程会有许多条,可是对于已经问过的问题,这一组问题流程是固定的。因此,我们以一个数组来记录这些已经问过的问题,从而实现问题的回退和跳转功能。

图 5 展示了用户正在输入问题时,生成提问问题数组的方式。黄色框表示用户正在输入的问题。首先,将问题 1 放入提问问题队列,而提问问题队列每一次都会调用队尾问题类,显示给用户。问题队列首先调用队尾的问题 1,当用户根据提示输入问题 1 的答案后,问题 1 的类接收用户输入,保存,然后判断下一个问题类是那个类,找到后将下一个问题类(这里为问题 2)放入提问问题队列,最后返回提问问题队列。然后,提问问题队继续调用队尾的问题类,这时队尾问题类是问题 2,将问题 2 显示给用户,以此类推,直到最后一个问题。这时,提问问题队列中保存的即为用户所被问及的所有问题。

图 5. 动态生成提问问题队列

当用户需要返回上一个问题时,只需要将提问问题队列中的最后一个问题移除即可实现了回退功能。而对于跳转功能,可以通过提问问题队列的索引来找到需要跳转到的问题。

在以上解决方案中,每一个问题类均访问相同的提问问题队列,所以提问问题队列在程序中必须是唯一的,我们采用单例模式来实现这一点。

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于被外界访问,从而方便对实例的控制。提问问题队列类使用一个单例模式来记录用户的输入以及已提问的问题顺序,从而使得任意问题类访问的提问问题队列都是同一个实例,保证了提问问题队列的唯一性。

至此,经过优化的命令行交互设计方案的基本框架已经建立,但基于软件的可扩展性的需求,该方案还引入了工厂方法模式来创建各个问题类。以下给出工厂方法模式的定义。

工厂方法(Factory Method)模式定义了一个创建产品对象的工厂接口,将实际创建工作推迟到子类中实现。核心工厂类不再负责具体创建,这样核心类成为一个抽象工厂 角色,仅负责具体工厂子类必须实现的接口定义,这样进一步抽象化的好处是使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品。

在本实例中使用工厂方法模式来实例化对象,可以使得在不修改其他代码的基础上,更改整个命令行流程。例如,在图 1 的实例中,问题 3 和问题 5 的下一个问题类是问题 6,问题 3 和问题 5 的类均需要生成一个问题 6 的实例放入提问问题队列。如果不使用工厂模式,生成问题 6 实例的代码需要固化在问题 3 和问题 5 的类中。当问题 6 的类需要以新的类替换时,必须打开问题 3 和问题 5 的类进行修改。

而使用工厂模式,只需要打开所属的工厂来修改即可,对于问题 3 和问题 5 本身的代码,无需更改。在极端的情况下,甚至不需要更改任何问题类,只用替换工厂类就可以实现整个问题流程的更改。

回页首

本节我们将讨论解决方案的整体类图,该类图适合于所有与图 1 类似的命令行交互程序的设计。

图 6 展示了方案的各个类之间的关系。由图 6 可见,有三个类是程序实现的关键类,分别是 Question 类、QuestionCaller 类以及 QuestionRecord 类。对应之前的设计模式的介绍,Question 类采用了命令模式和模版模式,而 QuestionRecord 类使用了单例模式,QuestionFactory 类使用了工厂模式,在本例中,QuestionFactory 使用简单工厂模式就可以满足需求,而对于更大型的应用,工厂方法将会更加适合。下面将具体解析每一个类以及它们的实现。

图 6. 命令行交互程序类图(查看大图)

代码整体目录结构如图 7 所示,下面将详细讲述每一个主要类的结构。

图 7. 整体代码目录结构

Question 类

Question 类即为实例中的问题类,是所有问题类的基类。该类为抽象类,定义了四个抽象方法以及一个 final 方法 execute()。execute() 方法即为模版模式中所指的算法骨架,指定了一个问题类的处理流程,代码如清单 1 所示:

清单 1. Question 类的 execute 方法

execute() 方法调用了四个抽象方法以及 isBack() 方法、isGoto() 方法,固化了一个问题类的处理流程,即:首先获得用户输入,如果用户输入为关键字“BACK”,表示用户需要返回上一步,则删除提问问题队列中的最后一个对象;如果用户输入为关键字“GOTO 数字”,表示用户需要跳转到数字所示的那一步,则返回提问问题队列中相应的问题;如果用户输入不属于以上两种,则认为是输入当前问题的答案,需检查用户输入, 当用户输入合法时,保存用户输入并将本问题的下一个问题存入提问问题队列。isBack() 和 isGoto() 是两个钩子方法,分别用来判断是否回退和是否跳转。在 Question 类中定义了 isBack() 和 isGoto() 的实现,如果某一个问题的回退和跳转功能与默认的 isBack() 和 isGoto() 函数不同,则可以在该类中复写 isBack() 和 isGoto() 方法,从而实现特定的功能。

Question 类的四个抽象方法,如清单 2 所示,分别是 getInput()、checkInput()、getQuestionKey() 和 getNextQuestion()。getInput():用以向用户显示问题,并获得用户输入;checkInput():用以检查用户输入;getQuestionKey():用来获得问题类的键值,这个值将用来存储用户的输入;getNextQuestion():该函数将返回具体的问题类的下一个问题类。这四个方法将由具体的问题类来实现。图 8 显示了 execute() 方法的流程图。

图 8. execute() 流程图

以图 1 实例中的问题 1 为例,清单 2 为问题 1 的实现类 QuestionOne。图 1 中的其他问题所抽象出来的问题类均与 QuestionOne 类似。

清单 2. QuestionOne 类

QuestionOne 类实现了 Question 类的四个抽象方法。在 getNextQuestion() 方法中,当用户输入 N 时,返回 null,Question 类的 execute() 方法将会将 null 存入提问问题队列的最后。当 QuestionOne 类返回时,提问问题队列类获取的下一个问题类为 null 时,程序结束。而当用户输入 Y 时,工厂类将返回 QuesitonTwo 的实例存入提问问题队列,而提问问题队列将获得 QuestionTwo 的实例,继续问题流程。

其他问题类与问题 1 的代码类似,均需要实现四个抽象类,并且在 getNextQuestion() 方法中返回下一个问题类的实例。

QuestionRecord 类

QuestionRecord 类即为实例中所示的提问问题类。该类是一个单例类,用以保存已经提问过的问题记录,以及用户的输入信息。QuestionRecord 类的实现如清单 3 所示:

清单 3. QuestionRecord 类

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2015年6月5日
下一篇 2015年6月6日

相关推荐