这里我们使用命令模式、责任链模式、模板方法模式,制作一个把UNIX上的命令移植到Windows上的工具。
UNIX下的命令,一条命令分为命令名、选项和操作数,例如命令"ls -l /usr"。其中,ls是命令名,l是选项,/usr是操作数,后两项都是可选项。
UNIX命令一定遵守以下几个规则:
- 命令名为小写字母。
- 命令名、选项、操作数之间以空格分隔,空格数量不受限制。
- 选项之间可以组合使用,也可以单独拆分使用。
- 选项以横杠(-)开头。
其中,ls命令中有几个常用的命令:
- ls:简单列出一个目录下的文件。
- ls -l:详细列出目录下的文件。
- ls -a:列出目录下包含的隐藏文件,主要是点号(.)开头的文件。
- ls -s:列出文件的大小。
当然,还有类似"ls -la"、"ls -ls"等的组合。
这里针对一个ls命令族的算法要求:
- 每一个ls命令都有操作数,默认操作数为当前目录。
- 选项不可重复,例如对于"ls-l-l-s",解析出的选项应该只有两个:l选项和s选项。
- 每个选项返回不同的结果,也就是说每个选项应该由不同的业务逻辑来处理。
- 为提高扩展性,ls命令族内的运算应该是对外封闭的,减少外界访问ls命令族内部细节的可能性。
各个类的职责:
- ClassUtils:工具类,根据一个接口、父类查找到所有的子类。
- CommandVO:命令的值对象,它把一个命令解析为命令名、选项、操作数。
- CommandEnum:枚举类型,是主要的命令配置文件。
- CommandName:实现责任链的遍历,同时也是命令模式中的命令接收者。
- Command:定义命令的执行方法,同时负责命令族(责任链)的建立。
public abstract class CommandName {
private CommandName nextOperator;//责任链的下一个执行者
public final String handleMessage(CommandVO vo) {
//处理结果
String result = "";
//判断是否是自己处理的参数
if (vo.getParam().size() == 0 || vo.getParam().contains(this.getOperateParam())){
result = this.echo(vo);
} else {
if (this.nextOperator != null) {
result = this.nextOperator.handleMessage(vo);
} else {
result = "命令无法执行";
}
}
return result;
}
//设置下一个执行者
public void setNext(CommandName operator) {
this.nextOperator = operator;
}
//每个处理者都要处理一个后缀参数
protected abstract String getOperateParam();
//每个处理者都必须实现处理任务
protected abstract String echo(CommandVO vo);
}
public abstract class AbstractLS extends CommandName {
public final static String DEFAULT_PARAM = ""; //默认参数
public final static String A_PARAM = "a"; //参数a
public final static String L_PARAM = "l"; //参数l
}
该类的职责:标记ls命令族 和 个性化处理。因为现在还没有思考清楚ls有什么个性(可以把命令的选项也认为是其个性化数据),所以先写个空类放在这里,以后想清楚了再填写上去,留下一些可扩展的类也许会给未来带来不可估量的优点。
public class LS extends AbstractLS {
//参数为空
@Override
protected String getOperateParam() {
return super.DEFAULT_PARAM;
}
//最简单的ls命令
@Override
protected String echo(CommandVO vo) {
return FileManager.ls(vo.getCommandName());
}
}
public class LS_L extends AbstractLS {
//l选项
@Override
protected String getOperateParam() {
return super.L_PARAM;
}
@Override
protected String echo(CommandVO vo) {
return FileManager.ls_l(vo.getCommandName());
}
}
public class LS_A extends AbstractLS {
//a选项
@Override
protected String getOperateParam() {
return super.A_PARAM;
}
@Override
protected String echo(CommandVO vo) {
return FileManager.ls_a(vo.getCommandName());
}
}
这3个实现类都关联到了FileManager,这个类是负责与操作系统交互的。要把UNIX的命令迁移到Windows上运行,就需要调用Windows的低层函数,这里采用示例性代码代替。
public class FileManager {
//ls命令
public static String ls(String path) {
return "file1\nfile2\nfile3\nfile4";
}
//ls -l命令
public static String ls_l(String path) {
String str = "drw-rw-rw root system 1024 2020-5-18 23:14 file1\n";
str = str + "drw-rw-rw root system 1024 2020-5-18 23:14 file2\n";
str = str + "drw-rw-rw root system 1024 2020-5-18 23:14 file3";
return str;
}
//ls -a命令
public static String ls_a(String path) {
String str = ".\n..\nfile1\nfile2\nfile3";
return str;
}
}
Command抽象类中定义了命令的执行方法和命令族(责任链)的建立。其中buildChain方法负责建立一个责任链,它通过接收一个抽象的命令族类,然后通过ClassUtils类遍历其子类,就可以建立一条命令解析链。
import java.util.ArrayList;
import java.util.List;
public abstract class Command {
//命令的执行方法
public abstract String execute(CommandVO vo);
//建立链表
protected final List<? extends CommandName> buildChain(Class<? extends CommandName> abstractClass){
//取出所有的命令名下的子类
List<Class> classes = ClassUtils.getSonClass(abstractClass);
//存放命令的实例,并建立链表关系
List<CommandName> commandNamesList = new ArrayList<CommandName>();
for(Class c:classes) {
CommandName commandName = null;
try {
//产生实例
commandName = (CommandName)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
// TODO: handle exception
}
//建立链表
if (commandNamesList.size() > 0) {
commandNamesList.get(commandNamesList.size() - 1).setNext(commandName);
}
commandNamesList.add(commandName);
}
return commandNamesList;
}
}
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
public class ClassUtils {
//根据父亲查找到所有的子类,默认情况是子类和父类都在同一个包名下
public static List<Class> getSonClass(Class fatherClass) {
//定义一个返回值
List<Class> returnClassList = new ArrayList<Class>();
//获得包名称
String packageName = fatherClass.getPackage().getName();
//获得包中的所有类
List<Class> packClasses = getClasses(packageName);
//判断是否是子类
for(Class c:packClasses) {
if (fatherClass.isAssignableFrom(c) && !fatherClass.equals(c)) {
returnClassList.add(c);
}
}
return returnClassList;
}
//从一个包中查找出所有的类,在jar包中不能查找
private static List<Class> getClasses(String packageName) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = packageName.replace('.', '/');
Enumeration<URL> resources = null;
try {
resources = classLoader.getResources(path);
} catch (IOException e) {
e.printStackTrace();
}
List<File> dirs = new ArrayList<File>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
ArrayList<Class> classes = new ArrayList<Class>();
for(File directory:dirs) {
classes.addAll(findClasses(directory,packageName));
}
return classes;
}
private static List<Class> findClasses(File directory, String packageName){
List<Class> classes = new ArrayList<Class>();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
for(File file:files) {
if (file.isDirectory()) {
assert !file.getName().contains(".");
classes.addAll(findClasses(file, packageName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
try {
classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
return classes;
}
}
具体的命令实现类LSCommand类:
public class LSCommand extends Command {
@Override
public String execute(CommandVO vo) {
//返回链表的首节点
CommandName firstNode = super.buildChain(AbstractLS.class).get(0);
return firstNode.handleMessage(vo);
}
}
先建立一个命令族的责任链,然后将一个命令对象从首节点注入。在该类中我们使用 CommandVO类,它封装了一个命令对象。
import java.util.ArrayList;
import java.util.HashSet;
public class CommandVO {
//定义参数名与参数的分割符号,一般是空格
public final static String DIVIDE_FLAG = " ";
//定义参数前的符合,Unix一般是-,如ls -la
public final static String PREFIX = "-";
//命令名,如ls、du
private String commandName = "";
//参数列表
private ArrayList<String> paramList = new ArrayList<String>();
//操作数列表
private ArrayList<String> dataList = new ArrayList<String>();
//通过构造函数传递进来命令
public CommandVO(String commandStr) {
//常规判断
if (commandStr != null && commandStr.length() != 0) {
//根据分割符号拆分出执行符合
String[] complexStr = commandStr.split(CommandVO.DIVIDE_FLAG);
//第一个参数是执行符号
this.commandName = complexStr[0];
//把参数放到List中
for(int i=1;i<complexStr.length;i++){
String str = complexStr[i];
//包含前缀符号,认为是参数
if(str.indexOf(CommandVO.PREFIX)==0) {
this.paramList.add(str.replace(CommandVO.PREFIX, "").trim());
} else {
this.dataList.add(str.trim());
}
}
} else {
//传递的命令错误
System.out.println("命令解析失败,必须传递一个命令才能执行!");
}
}
//得到命令名
public String getCommandName() {
return this.commandName;
}
//获得参数
public ArrayList<String> getParam(){
//为了方便处理空参数
if (this.paramList.size() == 0) {
this.paramList.add("");
}
return new ArrayList(new HashSet(this.paramList));
//HashSet具有值唯一的优点,这样处理就是为了避免出现两个相同的参数。
}
//获得操作数
public ArrayList<String> getData(){
return this.dataList;
}
}
在Invoker类实现命令的分发,从CommandEnum中获得命令与命令类的配置信息,然后建立一个命令实例,调用其execute方法,完成命令的执行操作。
public class Invoker {
//执行命令
public String exec(String _commandStr) {
//定义返回值
String result = "";
//首先解析命令
CommandVO vo = new CommandVO(_commandStr);
//检查是否支持该命令
if (CommandEnum.getNames().contains(vo.getCommandName())) {
//产生命令对象
String className = CommandEnum.valueOf(vo.getCommandName()).getValue();
Command command;
try {
command = (Command)Class.forName(className).newInstance();
result = command.execute(vo);
} catch (Exception e) {
// TODO: handle exception
}
} else {
result = "无法执行命令,请检查命令格式";
}
return result;
}
}
import java.util.ArrayList;
import java.util.List;
public enum CommandEnum {
ls("com.sfq.action.LSCommand");
private String value = "";
//定义构造函数,目的是Data(value)类型的相匹配
private CommandEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
//返回所有的enum对象
public static List<String> getNames(){
CommandEnum[] commandEnums = CommandEnum.values();
List<String> names = new ArrayList<String>();
for(CommandEnum c:commandEnums) {
names.add(c.name());
}
return names;
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import com.sfq.action.Invoker;
public class Client {
public static void main(String[] args) throws IOException {
Invoker invoker = new Invoker();
while (true) {
//UNIX下的默认提示符合
System.out.print("#");
//捕获输出
String input = (new BufferedReader(new InputStreamReader(System.in))).readLine();
//输入quit或exit则退出
if (input.equals("quit") || input.equals("exit")) {
return;
}
System.out.println(invoker.exec(input));
}
}
}
结果
#ls
file1
file2
file3
file4
#ls -l
drw-rw-rw root system 1024 2020-5-18 23:14 file1
drw-rw-rw root system 1024 2020-5-18 23:14 file2
drw-rw-rw root system 1024 2020-5-18 23:14 file3
#ls -a
.
..
file1
file2
file3
总体算法流程:
- (1)Client将ls指令输送到Invoker的exec()方法中
- (2)将ls指令在Invoker类中使用CommandVO类解析成vo命令对象,然后将vo对象输送到LSCommand类的execute()方法中
- (3)在LSCommand类中调用Command中的buildChain()方法生成责任链,并获得责任链的首节点,然后将vo对象输送到首节点中
- (4)CommandName中从首节点开始对责任链遍历,一直找到可以执行该命令的执行者
- (5)执行完成后,结果逐级返回,从Client中输出
我们此时呢,相对上述方法进行扩充,在ls命令的基础上,增加一个df命令族,显示磁盘的大小,获得类图如下:
首先,添加类图右下角的抽象类和相应的实现:
public abstract class AbstractDF extends CommandName {
public final static String DEFAULT_PARAM = ""; //默认参数
public final static String K_PARAM = "k"; //参数k
public final static String G_PARAM = "g"; //参数g
}
public class DF extends AbstractDF {
@Override
protected String getOperateParam() {
return super.DEFAULT_PARAM;
}
@Override
protected String echo(CommandVO vo) {
return DiskManager.df();
}
}
public class DF_K extends AbstractDF {
@Override
protected String getOperateParam() {
return super.K_PARAM;
}
@Override
protected String echo(CommandVO vo) {
return DiskManager.df_k();
}
}
public class DF_G extends AbstractDF {
@Override
protected String getOperateParam() {
return super.G_PARAM;
}
@Override
protected String echo(CommandVO vo) {
return DiskManager.df_g();
}
}
public class DiskManager {
//默认的计算大小
public static String df() {
return "/\t10484848\n/user\t10484848\n/home\t10484848\n";
}
//按照kb来计算
public static String df_k() {
return "/\t10484\n/user\t10484\n/home\t10484\n";
}
//按照gb来计算
public static String df_g() {
return "/\t10\n/user\t10\n/home\t10\n";
}
}
然后,定义实际的命令类,并在枚举类中加入相应的类型:
public class DFCommand extends Command {
@Override
public String execute(CommandVO vo) {
return super.buildChain(AbstractDF.class).get(0).handleMessage(vo);
}
}
import java.util.ArrayList;
import java.util.List;
public enum CommandEnum {
ls("com.sfq.action.LSCommand"),
df("com.sfq.action.DFCommand");
private String value = "";
//定义构造函数,目的是Data(value)类型的相匹配
private CommandEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
//返回所有的enum对象
public static List<String> getNames(){
CommandEnum[] commandEnums = CommandEnum.values();
List<String> names = new ArrayList<String>();
for(CommandEnum c:commandEnums) {
names.add(c.name());
}
return names;
}
}
场景类不变,依然如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import com.sfq.action.Invoker;
public class Client {
public static void main(String[] args) throws IOException {
Invoker invoker = new Invoker();
while (true) {
//UNIX下的默认提示符合
System.out.print("#");
//捕获输出
String input = (new BufferedReader(new InputStreamReader(System.in))).readLine();
//输入quit或exit则退出
if (input.equals("quit") || input.equals("exit")) {
return;
}
System.out.println(invoker.exec(input));
}
}
}
结果
#df
/ 10484848
/user 10484848
/home 10484848
#df -g
/ 10
/user 10
/home 10
#df -k
/ 10484
/user 10484
/home 10484
我们在原本业务逻辑不变的情况下,请示的实现了业务的扩展。在这个例子中用到了以下模式。
- 责任链模式:负责对命令的参数进行解析,而且所有的扩展都是增加链数量和节点,不涉及原有的代码变更。
- 命令模式:负责命令的分发,把适当的命令分发到指定的链上。
- 模板方法模式:在Command类以及子类中,buildChain方法是模板方法,只是没有基本方法而已;在责任链模式的CommandName类中,用了一个典型的模板方法handlerMessage,它调用了基本方法,基本方法由各个实现类实现,非常有利于扩展。
- 迭代器模式:在for循环中我们多次用到类似for(Class c:classes)的结构,JDK已经把迭代器模式融入到了API中,更方便使用了。
如果想要继续处理,如:"ls-l-a"这样的组合选项。这里提供以下两个思路:
- 独立处理:"ls-l-a"等同于"ls-la",也等同于"ls-al"命令,可以把"ls-la"中的选项"la"作为一个参数来进行处理,扩展一个类就可以了。该方法的缺点是类膨胀得太大,但是简单。
- 混合处理:修正命令族处理链,每个命令处理节点运行完毕后,继续由后续节点处理,最终由Command类组装结果,根据每个节点的处理结果,组合后生成完整的返回信息,如"ls-l-a"就应该是LS_L类与LS_A类两者返回值组装的结果,当然链上的节点返回值就要放在Collection类型中了。
该框架叫做命令链(Chain of Command)模式,具体来说就是命令模式作为责任链模式的排头兵,由命令模式分发具体的消息到责任链模式。