关于在保证健壮性的前提下,高效实现命令行交互程序的一些思考

关于在保证健壮性的前提下,高效实现命令行交互程序的一些思考

​ 在本学期《软件构造》课程的三个实验中,都涉及命令行交互程序的开发,以实验三为尤,需要面向三个场景开发三个不同的交互程序。本人在完成实验三的过程中,在前面ADT的设计和扩展中并没有花费太多时间,但是在设计交互程序时,为了追求一些程序的健壮性,牺牲了实现效率,投入了大量的时间。

​ 在实验后的期末复习期间,我对设计模式和软件健壮性进行了一些思考,最终发现我当初在设计方法上存在着一些问题,因而导致效率很低,因此在本文中用它作为例子来探讨高效实现命令行终端交互程序的方法。

先开发框架,再分别进行个性化实现。

​ 首先,这三个程序在用户交互流程上存在着极大的相似性:

​ 它们都是先输入一组Label信息,再进行一些关于时间段的增删改查,并且都需要打印信息。

​ 既然主体流程是一致的,我们就可以利用template设计模式来进行开发:

​ 大致框架如下:

public abstract class AbstractFramework {
    
	/**
	 * 所有子程序遵循一致的框架主流程,不可被重写。
	 */
	public final void mainOrder() {
		init();
		int op = choose();
		switch (op) {
		case 1:
			function1();
			break;
		case 2:
			function2();
			break;
		case 3:
			function3();
			break;
		default:
			break;
		}
	}
	
	private void function1() {
		inputLabels();
		interact();
	}
	
	protected abstract void init();
	protected abstract int choose();
	protected abstract void inputLabels();
	protected abstract void interact();
	protected abstract void function2();
	protected abstract void function3();
	
	// ...

}

​ 这个框架也是可扩展功能的,比如如果我们想给任务1多加一个从文本文件读入解析语法的功能,只需在choose里多开一个分支,然后实现这个功能即可。

​ 而由于所有程序的第一个功能都是与用户进行命令行交互,所以这里将function1做了进一步的分解。

将数据读入与数据处理分离,保证健壮性的同时提高复用性。

​ 这三个程序不仅在流程层面有着相似性,在数据读入的处理上也有着相似性,而数据读入是程序健壮性的关键,在这方面去抽象提炼一些共性的操作有助于提高开发效率。

首先抛出我们要解决的问题:在命令行交互程序中,我们通常要不断地要求用户输入,直到输入信息合法,这个逻辑虽然很简单,但是一旦程序的输入量多起来,输入的参数变得复杂,这种检查就会消耗大量的编码时间。

​ 不过,我在编码时,注意到了Date的读入十分简单:这是我对读取Date的设计:

private static Date nextDate(Scanner sc) {
    while(true) {
        String str = sc.nextLine();
        try {
            Date date = fmt.parse(str);
            return date;
        } catch (ParseException e) {
            System.out.println("date format error: yyyy-MM-DD");
        }
    }
}

​ 它的实现非常简洁,得益于fmt.parse(String)函数:它将一个字符串解析为一个Date,如果格式错误则抛出ParseException异常,直到用户格式正确。我们沿用这种思路,为我们的三个Label设计这样的读入函数:

​ 这里用Pair举例,首先假定我们已经设计了Pair的接收String[] args的构造方法,那么,类似的,getNextPair的设计变得十分简单:

private String[] split(String line) {
    String[] args = line.split(",");
    for(int i = 0; i < args.length; i++)
        args[i] = args[i].strip();
    return args;
}

private Pair getNextPair(Scanner sc) {
    while(true) {
        String line = sc.nextLine();
        try {
            return new Pair(split(line));
        } catch (MyParseException e) {
            info();
        }
    }
}

​ 我们的Pair是一个int和String的pair,且要求String非空,于是我们可以这样编写构造方法:

private final int a;
private final String b; // 要求b非空

public Pair(String[] args) throws MyParseException {
    if(args.length != 2) throw new MyParseException();
    try {
        a = Integer.parseInt(args[0]);
    } catch (NumberFormatException e) {
        throw new MyParseException();
    }
    if(args[1].length() == 0) throw new MyParseException();
    else {
        b = args[1];
    }
}

​ 注意到在这里我们通过重抛出的方式将异常类型统一为MyParseException,方便外部方法的接收。

​ 到了这里,只要是对于一个需要被整行读取,切割,然后分别按参数解析的类型(不论它实验三的一个Label,还是用户输入的一个查询操作),我们都有了一个统一的外部框架来保证格式正确地进行读取,这样一来我们实现了输入和处理数据的分离,使得开发效率变高。

一个问题

​ 尽管此时我们已经能够处理各种类型的读入,但问题是每于一个新类型XXX,我们就要重写一遍getNextXXX,而这些getNext的代码基本都是重复的。是否有方法来提高复用性呢?

​ 很容易想到,这种情形,可以利用模板/策略的设计模式,但是有一个最大的问题,就是返回类型:如果我们想把getNext抽象出来,那么它的返回类型就应该是众多可读取类型的父类。那么这个类型就很难再包含我们要读的类型的具体信息,因而就失去了意义。除非我们使用强制转换,而这又违反了OOP的一些原则。

​ 下面是我按照直接的想法,用template模式写出来的一个实现方案,在一处用到了从父类到子类的强转。

import java.util.Scanner;
// template 模式的 抽象类:Reader
public abstract class Reader {
	
	private String[] split(String line) {
		String[] args = line.split(",");
		for(int i = 0; i < args.length; i++)
			args[i] = args[i].strip();
		return args;
	}
	
	public Object getNext(Scanner sc) {
		while(true) {
	        String line = sc.nextLine();
	        try {
	            Object res = parse(split(line));
	            return res;
	        } catch (MyParseException e) {
	            info();
	        }
	    }
	}
	
	/**
	 * 将args解析为一个Object,并进行格式错误检查
	 * @param args 参数列表
	 * @return 解析后的Object
	 * @throws ParseException 如果有格式错误或不满足约束条件,抛出
	 */
	protected abstract Object parse(String[] args) throws MyParseException;
	
	protected abstract void info();
}

​ 客户端可以这样继承并调用:

import java.util.Scanner;

class PairReader extends Reader {
	
	@Override
	protected Pair parse(String[] args) throws MyParseException { // 将parse功能委托到Pair的构造器上
		return new Pair(args); 
	}

	@Override
	protected void info() { // 提示用户格式信息
		System.out.println("format: a, b. a is an interger, len of b > 0");
	}
	
	@Override
	public Pair getNext(Scanner sc) {
		return (Pair) super.getNext(sc); // !! 强转
	}
	
}

public class Main {
	
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		Pair pr = new PairReader().getNext(sc);
		pr.show();
	}
}
原文地址:https://www.cnblogs.com/komeiji-green/p/14979767.html