关闭

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

628人阅读 评论(0) 收藏 举报
分类:

本文介绍了如何通过使用设计模式来优化命令行交互程序的开发。传统的命令行交互模式,不具备回退、跳转等功能,缺少用户交互的灵活性。本文通过几种设计模式的组合,实现了一个通用的轻量级的命令行交互程序的解决方案,同时为系统重构、功能扩展以及代码的维护提供了方便。

背景介绍

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

本文以一个简单的部署 war 包的实例,说明在命令行交互程序设计中遇到的问题,以及如何使用设计模式来解决这些问题。

实例简介

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

图 1. 单个 war 包部署实例
图 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 包部署实例
图 2. 多个 war 包部署实例

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

称谓约定

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

----------------------------------------------------------------------------------------------------------------------------------------------

用设计模式优化命令交互程序设计

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

应用命令模式(Command Pattern)解耦问题和流程

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

命令模式将一组行为抽象为对象,通过对对象的操作来实现这些行为,从而实现“行为请求者”与“行为实现者”的解耦合。结合本文的实例,使用命令模式将问题抽象为对象,使问题的调用和问题本身实现解耦合,为实现问题的回退功能提供了可能。在该实例中,问题流程中的所有问题都使用了相同的接口,因此,问题流程不用关心某一步是哪一个问题,只需要关心这些问题是如何组织,如何流转即可。如图 3 所示,任何一个问题都可以是问题流程中的任意一步,问题流程不会对问题区分处理。

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

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

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

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

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

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

应用模版模式(Template Pattern)管理问题流程

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

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

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

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

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

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

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

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

应用单例模式(Single Pattern)实现提问问题队列

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

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

应用工厂方法模式(Factory Mathod)提高软件扩展性

工厂方法(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. 命令行交互程序类图(查看大图
图 6. 命令行交互程序类图

实现

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

图 7. 整体代码目录结构
图 7. 整体代码目录结构

Question 类

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

清单 1. Question 类的 execute 方法
 public final void execute() 
 { 
    System.out.print(QuestionRecord.getInstance().getQuestionListSize() + ":"); 
    String input = getInput(); 
    
    // 是否回退 
    if (isBack(input)) { 
        // 弹出提问问题队列的最后一个问题
        QuestionRecord.getInstance().popLastQuestion(); 
    }else if (isGoto(input)){ // 是否跳转
        // 获得跳转的问题编号
        String index = input.substring(5); 
        try{ 
            // 跳转到目标问题
            QuestionRecord.getInstance().getGotoQuestion(Integer.valueOf(index)); 
        }catch (Exception x) 
        { 
            System.out.println("Please type 'goto x', 'x' is a number."); 
        } 
} else{ // 接收用户对当前问题的输入   
        if (checkInput(input)){    // 检查用户输入的合法性
           // 保存用户输入 
           QuestionRecord.getInstance().setRecord(getQuestionKey(), input); 
           // 将下一个问题存入提问问题队列
           QuestionRecord.getInstance().setNextQuestion(getNextQuestion(factory, input)); 
        } 
    } 
 }

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() 流程图
图 8. execute 流程图

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

清单 2. QuestionOne 类
 public class QuestionOne extends Question{ 
    public static final String ID = Utils.StepFlags.questionOne.toString(); 
    public QuestionOne(QuestionsFactory f){ 
        super(f); 
    } 
 /** 
 * 检查用户输入合法性
 */ 
  @Override 
  public boolean checkInput(String input){ 
        if (input.equalsIgnoreCase("y") || input.equalsIgnoreCase("n")) 
            return true; 
        else 
        { 
            System.out.println("请输入 Y(部署)或 N(不部署)"); 
            return false; 
        } 
    } 
 /** 
 * 获得用户输入
 */ 
 @Override 
  public String getInput(){ 
        try{ 
            System.out.println("是否部署 war 包? (Y/N)"); 
            BufferedReader in = new BufferedReader(new InputStreamReader( 
                    System.in)); 
            String input = in.readLine(); 
            return input; 
        }catch (Exception x){ 
            x.printStackTrace(); 
        } 
        return null; 
    } 
 /** 
 * 返回该问题的下一个问题类
 */ 
    @Override 
    public Question getNextQuestion(QuestionsFactory factory, String input){ 
        if (input.equalsIgnoreCase("y")) 
            return factory.getQuestion(Utils.StepFlags.questionTwo.toString()); 
        else 
            return null; 
    } 
 /** 
 * 返回问题类的 ID 
 */ 
    @Override 
    public String getQuestionKey(){ 
        return ID; 
    } 
}

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

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

QuestionRecord 类

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

清单 3. QuestionRecord 类
 public class QuestionRecord 
 { 
    // 变量 questionTrace 保存提问问题队列
 private List<Question>        questionTrace  = new ArrayList<Question>(); 
 // 变量 recordMap 保存用户输入
    private Map<String, Object>   recordMap      = new HashMap<String, Object>(); 
    private static QuestionRecord questionRecord = new QuestionRecord(); 

    private QuestionRecord() 
    { 
    } 

 /** 
 * 返回单例类实例
 */ 
    public static QuestionRecord getInstance() 
    { 
        return questionRecord; 
    } 

    /** 
     * 保存用户输入
     * key: 问题类 ID 
     * value: 该问题类的用户输入值
     */ 
    public void setRecord(String key, Object value) 
    { 
        recordMap.put(key, value); 
    } 

 /** 
 * 获得输入记录表
 */ 
    public Map<String, Object> getRecords() 
    { 
        return recordMap; 
    } 

    /** 
     * 在提问问题队列中添加下一个问题类
     * question: 问题类
     */ 
    public void setNextQuestion(Question c) 
    { 
        questionTrace.add(c); 
    } 

    /** 
     * 删除提问问题队列中的最后一个问题类
     */ 
    public void popLastQuestion() 
    { 
        int len = questionTrace.size(); 
        questionTrace.remove(len - 1); 
    } 

    /** 
     * 返回提问问题队列中的最后一个问题类
     */ 
    public Question getNext() 
    { 
        int len = questionTrace.size(); 
        return questionTrace.get(len - 1); 
    } 

    /** 
     * 检查提问问题队列最后一个问题类是否为 null 
     */ 
    public boolean hasNext() 
    { 
        int len = questionTrace.size(); 
        if (questionTrace.get(len - 1) == null) 
            return false; 
        else 
            return true; 
    } 

 /** 
 * 清除提问问题队列中的所有记录
 */ 
    public void cleanAllQuestion() 
    { 
        questionTrace.clear(); 
    } 

 /** 
 * 该方法供跳转功能使用,首先获得提问问题队列的子队列,该子队列包含
 * 从第一个问题类到跳转到的问题类。然后将提问问题队列设置为该子队列,
 * 最后调用 getNext() 方法,返回提问问题队列中的最后一个问题类。
 * index: 将要跳转到的问题编号
 */ 
    public Question getGotoQuestion(int index) 
    { 
        if (index < questionTrace.size()) 
        { 
            questionTrace = questionTrace.subList(0, index); 
        } 
        return getNext(); 
    } 

 /** 
 * 返回提问问题队列的长度
 */ 
    public int getQuestionListSize() 
    { 
        return questionTrace.size(); 
    } 
 }

在 QuestionRecord 类中定义了三个变量,数组变量 questionTrace 存放 Question 类,用来记录用户已经回答过的问题,这里将 questionTrace 数组当作一个后进先出的队列来使用。recordMap 用来记录每一个问题的选择,其 key 值为问题类的 ID。questionRecord 变量以及构造函数 QuestionRecord()、静态函数 getInstance() 用来使得 QuestionRecord 类为单例类。

在 QuestionRecord 类中还定义了一系列的方法,分别用来存取 questionTrace 和 recordMap 变量,这些方法保证了 Question 类的输入和调用轨迹可以保存起来。

QuestionCaller 类

QuestionCaller 用来使以上定义的这些类一起合作,实现对应功能。QuestionCaller 访问 QuestionRecord 类中的 questionTrace 数组,调用数组中最后一个对象的 execute() 方法。问题对象的 execute() 方法将会将下一个问题对象存入 questionTrace() ,然后 QuestionCaller 再调用这个新问题对象的 execute() 以实现问题流程。图 9 给出了这一过程的示意。

图 9. QuestionCaller 的原理
图 9. QuestionCaller 的原理

QuestionCaller 的代码如清单 4 所示:

清单 4. QuestionCaller 类
 public class QuestionCaller 
 { 
 /** 
 * 构造函数
 * factory: QuestionFactory 类
 * stepName:第一个问题类的 ID 
 */ 
    public QuestionCaller(QuestionsFactory factory, String stepName) 
    { 
        QuestionRecord.getInstance().cleanAllQuestion(); 
        QuestionRecord.getInstance().setNextQuestion( 
                factory.getQuestion(stepName)); 
    } 

 /** 
 * 依次调用提问问题队列中的问题类的 execute()函数
 */ 
    public void execute() 
    { 
        while (QuestionRecord.getInstance().hasNext()) 
        { 
            QuestionRecord.getInstance().getNext().execute(); 
        } 
    } 
 }

QuestionsFactory 类

QuestionsFactory 类实现了简单工厂模式,增加了系统的可扩展性。QuestionsFactory 为一接口类,只有一个函数 getQuestion(String type),需要具体的实现类来实现 QuestionFactory 接口。清单 5 给出了实现类 DemoQuestionsFactory 类。

清单 5. DemoQuestionFactory 类
 public class DemoQuestionsFactory implements QuestionsFactory 
 { 
    public Question getQuestion(String type) 
    { 
        // 根据 type 返回相应的问题类实例
        if (type.equals(Utils.StepFlags.questionOne.toString())) 
        { 
            return new QuestionOne(this); 
        } 
        else if (type.equals(Utils.StepFlags.questionTwo.toString())) 
        { 
            return new QuestionTwo(this); 
        } 
        ……
        else if (type.equals(Utils.StepFlags.deploy.toString())) 
        { 
            return new Deploy(this); 
        } 
        return null; 
    } 
 }

如上所示,Utils.StepFlags 定义了每一个问题类的 ID,在 getQuestion() 函数中,通过不同的 ID 来获得不同问题类的实例。至此,我们实现了本文实例中的命令行交互模式解决方案。运行结果演示如图 10 所示:

图 10. 运行结果演示
图 10. 运行结果演示

可以看出,程序按着图 1 实例的流程进行问题的询问,并且以关键字‘back’和‘goto’分别表示回退和跳转,实现了这两种功能。至此图 1 的实例已经实现。

命令行交互程序扩展

实现图 2 所示功能

当图 1 的实例变化为图 2 所示时,本程序可以很轻易的适应这种变化。

首先,添加一个类 QuestionEight,QuestionEight 类的 getNextQuestion() 方法返回 QuestionTwo 的实例。并且,依据程序设计,QuestionEight 回退和跳转功能都应该设置为无效,因此需要在 QuestionEight 类中重载 isBack() 和 isGoto() 方法,使之不能响应回退和跳转请求。清单 6 只列出了 QuestionEight 类的个体 NextQuestion() 方法和 isBack()、isGoto() 方法,该类的其他方法与 QuestionOne 类的方法类似。

清单 6. QuestionEight 类
 public class QuestionEight extends Question 
 { 
    private static final String ID = Utils.StepFlags.questionEight.toString(); 

……
    @Override 
    public Question getNextQuestion(QuestionsFactory factory, String input) 
    { 
        // TODO Auto-generated method stub 
        if (input.equalsIgnoreCase("y")) 
            return factory.getQuestion(Utils.StepFlags.questionTwo.toString()); 
        return null; 
    } 

 /** 
 * 重载 isBack()函数,使得该类不支持回退功能
 */ 
    public boolean isBack(String input) 
    { 
        if (super.isBack(input)) 
        { 
            System.out.println("不能返回上一步"); 
        } 
        return false; 
    } 
    
 /** 
 * 重载 isGoto() 函数,使得该类不支持跳转功能
 */ 
    public boolean isGoto(String input) 
    { 
        if (super.isGoto(input)) 
            System.out.println("不能跳转"); 
        return false; 
    } 
 }

参考图 2 所示,由于问题 8 只和部署问题类相关,因此,我们只需要修改部署问题类即可:修改 Deploy 类的 getNextQuestion() 方法,使之调用工厂类,返回 QuestionEight 类的实例。

除此之外,还需要在 DemoQuestionsFactory 添加 QuestionEight 的处理,如清单 7 所示,即可实现图 2 所示的功能。

清单 7. DemoQuestionFactory 的修改
 public Question getQuestion(String type) 
 { 
    // TODO Auto-generated method stub 
    ……
    // 添加以下对 QuestionEight 的支持
    else if (type.equals(Utils.StepFlags.questionEight.toString())) 
    { 
        return new QuestionEight(this); 
    } 
    return null; 
 }

修改后的代码运行结果如图 11 所示。

图 11. 实例 2 的运行结果
图 11. 实例 2 的运行结果

可以看到,当一次部署结束后,提示用户是否还需要继续部署,如用户选择继续部署,则返回问题 2,由用户输入下一次部署所需的参数。当在询问是否要继续部署时,根据应用需求,屏蔽了回退和跳转功能。

结构的优化

到目前为止,本文描述的应用设计模式开发的命令行交互程序,从代码上很难看出交互问题的顺序,开发者需要单独编写文档来更显式的表明交互问题的顺序。这个问题可以通过之前使用的工厂模式来解决。例如可以将 Question 类的 getNextQuestion() 推迟到工厂类中进行,这样在工厂类中就可以观察到所有的 Question 的先后顺序。而且这种修改的优点在于,只需要修改工厂类,就可以实现对整个问题流程的修改。

总结

设计模式使我们优化了命令行交互程序的设计,使之更具有通用性和扩展性,易修改、易维护。本文所描述的命令行交互程序实现是基于纯 Java 开发,支持跨平台开发,可以在任何支持 Java 的硬件上使用。开发者可以在本文描述的基础上很容易的开发满足自己需求的命令行交互程序,使得交互更加便利,而且修改、维护、优化都更加容易。

下载

描述 名字 大小
命令行交互代码示例 commandlineCode.zip 33k

参考资料

学习



0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:831472次
    • 积分:10319
    • 等级:
    • 排名:第1622名
    • 原创:23篇
    • 转载:1197篇
    • 译文:0篇
    • 评论:76条
    最新评论