代碼——你的思想能走多遠

    相信每一位從事開發的同行,都深有感受,一段代碼,可識其人,識其品性,識其思想,識其修爲。然而,在快餐式代碼興行的今天,越來越多的人開始迷失,盲目追求簡潔的代碼,拋棄設計,拋棄思想,直至走火入魔,言必排斥複雜。

 

    沒錯,設計的境界,應當簡潔。可是,不應走火入魔。知其一,不知其三,盲目捨棄,卻完全不懂其原理,只會顯得膚淺。

    我們應該追求的,不是代碼的結果,而是其中的思想。

    真正的代碼,是可以不寫,但不可以沒有思想。我從不浪費時間重構我的代碼,但我絕對知道如何重構自己的代碼。

追求境界,不應該是追求幾十萬幾百萬行的代碼量,而是看重內功,看你的重構思想究竟有多遠。點到,即止,又何須真的寫代碼?


    我嘗試用一個簡單的實際例子來說明一下,究竟什麼纔是設計,什麼纔是簡潔,什麼纔是,代碼

一個簡單的應用場景,A系統需要獲得某圖片目錄下所有圖片例表,B系統需要獲取某目錄下的所有文件列表。

於是,有了下面兩段代碼

 

	
	@RequestMapping
	public GmModelAndView getGalleries(GmJsonObject request, String cmdString) throws GmException
	{
		GmJsonObject json = new GmJsonObject();		
		
		List<String> directories=null;
		if(cmdString==null){
			cmdString="ls -l /app_data/portal/gallery/manufacturer/"+Long.valueOf((String)request.getParameter("compId"))%1000+"/"+request.getParameter("compId")+"/prod/|grep '^-' | awk '{print $9}'";
		}
		try {
			directories = ShellUtil.runShell(cmdString);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		json.setJsonObject(directories);
		
		return new GmModelAndView(json);
	}


	@RequestMapping
	public GmModelAndView getFacetalkFile(GmJsonObject request) throws GmException
	{
		GmJsonObject json = new GmJsonObject();

		List<String> directories = null;
		String cmdString=null;
		if(StringUtil.isNotEmpty(request.getParameter("type"))&&StringUtil.isNotEmpty(request.getParameter("compId"))){
			 cmdString= "ls -l /app_data/" + request.getParameter("type") + "/"+ Long.valueOf((String) request.getParameter("compId"))% 1000 + "/" + request.getParameter("compId")+ "/comp/|grep '^-' | awk '{print $9}'";
		}else{
			throw new GmException("param error");
		}
		try {
			directories = ShellUtil.runShell(cmdString);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		json.setJsonObject(directories);

		return new GmModelAndView(json);
	}
	
 

 

咋這麼一看,這兩段代碼實現了需求。可是,任何人,都知道如何重構。相信大家都很清楚,兩段代碼之中,不同的地方,僅僅是 cmdString,其實這是一行linux shell命令。

注意,上面有個方法犯錯嚴重,竟然可以接受來自客戶端的cmdString,這是災難性的人爲程序漏洞,用戶完全可以通過一句簡單的刪除命令而令系統崩潰,甚至是不可恢復的災難

因此,我們必須將這個cmdString參數去掉,任何shell命令,都必須只能靠服務端根據邏輯來生成,絕對不能通過客戶端直接注入。

我們再回頭看看變化的地方,根據要查看的業務對象不同,會採用不同的shell命令來獲得顯示列表。於是,按照我們開發的習慣約定,我們所有的系統,均用EntitySource來表達不同的業務對象類型。而且,我們要的功能是列表,於是我們進行重構,並且較爲合適的方法名 list, 結果得到下面的代碼

 

 

	
	@RequestMapping
	public GmModelAndView list(GmJsonObject request) throws GmException
	{
		GmJsonObject json = new GmJsonObject();
		List<String> directories = null;
		int entitySource = request.getParameter("entity_source")
		
		String cmd = "";
		switch(entitySource)
		{
			case EntitySource.PHOTO:
				cmd = "ls -l /app_data/portal/gallery/manufacturer/"+Long.valueOf((String)request.getParameter("compId"))%1000+"/"+request.getParameter("compId")+"/prod/|grep '^-' | awk '{print $9}'";
				break;
			
			case EntitySource.ATTACHMENT:
				cmd = "ls -l /app_data/" + request.getParameter("type") + "/"+ Long.valueOf((String) request.getParameter("compId"))% 1000 + "/" + request.getParameter("compId")+ "/comp/|grep '^-' | awk '{print $9}'";
				break;
		}
		try {
			directories = ShellUtil.runShell(cmd);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		json.setJsonObject(directories);
		return new GmModelAndView(json);
	}
	
 

重構之後,我們將變化控制在了switch裏面,當有新的業務對象需要獲取相應的文件列表時,我們只需往switch裏增加相應的邏輯代碼。看似這個重構很可靠。可是,重構其實並沒有完成,因爲這裏面的分析還不夠徹底。

變化仍然不可控。

仔細分析,可以發現,當entitySource不同值時,我們需要查找的路徑path也是不同的,而且會根據request接受的參數而不同。而這個變化,似曾相識。沒錯!!!在文件上傳的時候,我們會根據傳參的不同而將文件保存在相應的路徑下,而這個路徑,恰恰是由 uploadService.generatePath() 生成,因此我們應當複用此方法來達到目的。

 

而且, switch已經是可控的變化,其作用主要是根據entitySource產生相應的shell命令,因此應當將其抽離成爲獨立的方法,暫且將其命名爲 cmd()

 

那麼,我們重構之後又有了下面的代碼

 

 

	
	@RequestMapping
	public GmModelAndView list(GmJsonObject request) throws GmException
	{
		GmJsonObject json = new GmJsonObject();
		Map<String, Object> arguments = request.getParameterMap();
		List<String> directories = null;

		String path = "";
		path = uploadService.generatePath(getEntitySource(arguments,false), getEnSrcType(arguments), getEntityId(arguments), getOptionId(arguments), path, new Date());
		path = uploadService.generateLocation(path, getAppCode(arguments));
		
		String cmd = cmd(arguments, path);		
		try {
			directories = ShellUtil.runShell(cmd);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		json.setJsonObject(directories);
		return new GmModelAndView(json);
	}
	
	private String cmd(Map<String, Object> arguments, String path) throws GmException
	{
		String cmd;
		switch(getEnSrcType(arguments))
		{
			case EntitySource.PHOTO:
				cmd = "ls -1 " + path;
				break;
				
			case EntitySource.ATTACHMENT:
				cmd = "ls -lho --full-time " + path + " | grep -v total | awk '{print $4,$5,$6,$8}' | sort -r -k 2,3";
				break;
				
			default:
				cmd = "ls -l " + path + " | grep -v total";
				break;
		}
		return cmd;
	}
 

到了這裏,是否發現整段代碼和原來已經非常的不同,我們的path生成是直接調用service完成,這是代碼複用的好處,它包裝了變化,從而使得這裏的path邏輯不再變化

我們又抽離了cmd() 方法出來,從而將業務對象的不同處理這一變化包裝起來,因此這個時候看回list() 方法,你會發現已經沒有了變化的地方。

 

 

 

可是,往往許多人的腳步在這裏停止了。

其實,重構之美,在這裏只是開始。而你的思想決定了重構的深度。

仔細分析cmd()方法,我們可以發現,生成相應的shell cmd時,代碼閱讀起來並不那麼美觀。如果你真的是個老手,你肯定會第一時間反應,字符串與變量的連接操作,類似的東西是sql。那麼,你是否會想起我們的log4j那麼優雅的設計。

 

 

	log.info("Getting the " + facetalk + " list, total " + total + " files.");
	log.info("Getting the {0} list, total {1} files", facetalk, total);
	
 

你,是否覺得第二種方式讀起來舒服很多。

其實,同樣的道理,cmd的生成如果重構成pattern token,我們的代碼不僅便於閱讀,而且優美多了。還有,更加意想不到的東西,先看重構後的代碼

 

 

	
	private String cmd(Map<String, Object> arguments, String path) throws GmException
	{
		String cmd;
		switch(getEnSrcType(arguments))
		{
			case EntitySource.PHOTO:
				cmd = MessageFormat.format("ls -1 {0}", path);
				break;
				
			case EntitySource.ATTACHMENT:
				cmd = MessageFormat.format("ls -lho --full-time {0} | grep -v total | awk '{print $4,$5,$6,$8}' | sort -r -k 2,3", path);
				break;
				
			default:
				cmd = MessageFormat.format("ls -l {0} | grep -v total", path);
				break;
		}
		return cmd;
	}
	
 

 

這個時候,我們又發現變與不變的地方了。不管什麼時候,path不變,shell變化。

我們設想,如果要處理的entitySource很多,我們會case沒完沒了。可是,它們有個共同特點,用entitySource即可定位到相應的shell,因此我們應當將shell與entitySource的對應關係抽離。

相信聰明的你想到了key-value鍵值對,沒錯,它們正是這種關係。可是要真正的不變,應當是將其抽離java代碼。這時,properties文件隆重登場。

先看重構結果:

 

 

	
	@RequestMapping
	public GmModelAndView list(GmJsonObject request) throws GmException
	{
		GmJsonObject json = new GmJsonObject();
		Map<String, Object> arguments = request.getParameterMap();
		List<String> directories = null;

		String path = "";
		path = uploadService.generatePath(getEntitySource(arguments,false), getEnSrcType(arguments), getEntityId(arguments), getOptionId(arguments), path, new Date());
		path = uploadService.generateLocation(path, getAppCode(arguments));
		
		String cmd = cmd(getEntitySource(arguments,false), path);		
		try {
			directories = ShellUtil.runShell(cmd);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		json.setJsonObject(directories);
		return new GmModelAndView(json);
	}
	
	private String cmd(int entitySource, String path) throws GmException
	{
		return MessageFormat.format(props.getProperty("shell.entity."+entitySource), path);
	}
	
 

 

shell.properties文件

 

# 101 photo
shell.entity.101 = ls -1 {0}

# 102 attachment
shell.entity.102 = ls -lho --full-time {0} | grep -v total | awk '{print $4,$5,$6,$8}' | sort -r -k 2,3

# others
shell.entity.0 = ls -l {0} | grep -v total
	
 

到了這裏,shell變成了完全由外部的properties配置,這個時候你會發現它另一個好處。當你想驗證shell命令是否寫錯時,只需在shell.properties文件copy完整的命令,而不用很麻煩的尋找相應的java文件再定位到具體那一行上去小心翼翼地複製出來運行。

 

 

短短十幾分鍾,一個簡單的應用場景經歷了幾個階段的重構,由原本的變幻無常,到最後java代碼不再變化,而變化只由一個shell.properties文件控制,而且還帶來了shell易於維護調試的好處。這,就是重構之美。

 

可是,我們的重構完成了嗎?沒有,它還可以繼續重構。但是,我並不想進行任何重構了,因爲,點到即止。在這裏完全足夠了,再追求重構,只會走火入魔,迷失方向,失去本質。對我們來說,只用一個shell.properties來管理,已經夠簡潔了。而且,這纔是真正簡潔的代碼。簡潔,並不只是代碼寫得少,它其實應是另一層面的東西。是思想所表達出來的結果足夠簡潔,纔是真正的簡潔。

 

 

一個簡簡單單的重構,你自己,究竟重構到了那個階段。亦或,你有更優美的方案。但是,代碼,不在於寫,而在於思想,這纔是重構所在。

 

 

 

 

 

 

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