這裏我們使用命令模式、責任鏈模式、模板方法模式,製作一個把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)模式,具體來說就是命令模式作爲責任鏈模式的排頭兵,由命令模式分發具體的消息到責任鏈模式。