相信每一位从事开发的同行,都深有感受,一段代码,可识其人,识其品性,识其思想,识其修为。然而,在快餐式代码兴行的今天,越来越多的人开始迷失,盲目追求简洁的代码,抛弃设计,抛弃思想,直至走火入魔,言必排斥复杂。
没错,设计的境界,应当简洁。可是,不应走火入魔。知其一,不知其三,盲目舍弃,却完全不懂其原理,只会显得肤浅。
我们应该追求的,不是代码的结果,而是其中的思想。
真正的代码,是可以不写,但不可以没有思想。我从不浪费时间重构我的代码,但我绝对知道如何重构自己的代码。
追求境界,不应该是追求几十万几百万行的代码量,而是看重内功,看你的重构思想究竟有多远。点到,即止,又何须真的写代码?
我尝试用一个简单的实际例子来说明一下,究竟什么才是设计,什么才是简洁,什么才是,代码
一个简单的应用场景,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来管理,已经够简洁了。而且,这才是真正简洁的代码。简洁,并不只是代码写得少,它其实应是另一层面的东西。是思想所表达出来的结果足够简洁,才是真正的简洁。
一个简简单单的重构,你自己,究竟重构到了那个阶段。亦或,你有更优美的方案。但是,代码,不在于写,而在于思想,这才是重构所在。