混合模式:命令模式+責任鏈模式

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章