代码——你的思想能走多远

    相信每一位从事开发的同行,都深有感受,一段代码,可识其人,识其品性,识其思想,识其修为。然而,在快餐式代码兴行的今天,越来越多的人开始迷失,盲目追求简洁的代码,抛弃设计,抛弃思想,直至走火入魔,言必排斥复杂。

 

    没错,设计的境界,应当简洁。可是,不应走火入魔。知其一,不知其三,盲目舍弃,却完全不懂其原理,只会显得肤浅。

    我们应该追求的,不是代码的结果,而是其中的思想。

    真正的代码,是可以不写,但不可以没有思想。我从不浪费时间重构我的代码,但我绝对知道如何重构自己的代码。

追求境界,不应该是追求几十万几百万行的代码量,而是看重内功,看你的重构思想究竟有多远。点到,即止,又何须真的写代码?


    我尝试用一个简单的实际例子来说明一下,究竟什么才是设计,什么才是简洁,什么才是,代码

一个简单的应用场景,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来管理,已经够简洁了。而且,这才是真正简洁的代码。简洁,并不只是代码写得少,它其实应是另一层面的东西。是思想所表达出来的结果足够简洁,才是真正的简洁。

 

 

一个简简单单的重构,你自己,究竟重构到了那个阶段。亦或,你有更优美的方案。但是,代码,不在于写,而在于思想,这才是重构所在。

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值