在一个case 中需要写这样一段代码, 写一个Spring的bean 来解析HttpServletRequest的传递的command 信息,command 的名字作为后台request 记录的键值。需要写入的代码类如下:
@Bean public class CommandParser implements CommandParser<HttpServletRequest> { private static final String CMDKEY = "CMDKEY"; public boolean accept(HttpServletRequest request) { // write your code here return false; } public String getCommand(HttpServletRequest request) { String callName = (String) request.getAttribute(CMDKEY); request.removeAttribute(CMDKEY); return callName; } }
需求
This command parser need resolve a command name and if it can resolve the command name , then set command name into request attribute with key CMDKEY, then return true,otherwise, do nothing, return false;
It will try to resolve two type of calls, the first from ‘/api‘ , the call name is from header, the header key is ‘X-API-CALL-NAME‘ the other one is from ‘/websvc‘, the value is from url parameter , the parameter key is ‘callname‘.
Write a logic in accept method to
例子
这个需求并不难,在code review 的时候发现大部分的思路是下面这样的
@Bean public class CommandParser implements CommandParser<HttpServletRequest> { private static final String CMDKEY = "CMDKEY"; private static final String HEADERKEY = "X-API-CALL-NAME"; private static final String CALLNAME = "callname"; private static final String AP_SUFFIX = "/api"; private static final String WEBSVC_SUFFIX = "/ws/websvc"; public boolean accept(HttpServletRequest request) { String uri = request.getRequestURI(); String cmdName = null; if (uri.endsWith(API_DLL_SUFFIX)){ cmdName = request.getHeader(HEADERKEY); } else if (uri.endsWith(WEBSVC_SUFFIX)){ cmdName = request.getParameter(CALLNAME); } if (cmdName != null){ request.setAttribute(CMDKEY, cmdName); return true; } return false; } public String getCommand(HttpServletRequest request) { String callName = (String) request.getAttribute(CMDKEY); request.removeAttribute(CMDKEY); return callName; } }
这个写法本身的逻辑并没有错,但是从代码的角度有没有更好的写法哪?
分析
这个需求实际上包含两个维度, 一个维度是request URI, 一个维度是要做的动作。 如下表所示。
我们写程序的时候实际上可以从两个维度来切分。一个是从request URI 的切面来进行切分,一个是从动作来进行切分。 从上文的代码来看大家的切分是从命令这个层面进行切分。从表上看就是水平切分。
那我们想,如果从另外垂直切面进行切分,即从request URI 方向进行。代码的表现是什么样。
代码:
@Bean public class CommandParser implements CommandParser<HttpServletRequest> { private static final String CMDKEY = "CMDKEY"; private static final String HEADERKEY = "X-API-CALL-NAME"; private static final String CALLNAME = "callname"; private static final String AP_SUFFIX = "/api"; private static final String WEBSVC_SUFFIX = "/ws/websvc"; public boolean accept(HttpServletRequest request) { return trySetCmdNameByApidll(request) || trySetCmdNameByWebsvc(request); } private boolean trySetCmdNameByApidll(HttpServletRequest request) { String cmdName = null; if (request.getRequestURI().contains(API_DLL_SUFFIX) && (cmdName = request.getHeader(HEADERKEY)) != null) { request.setAttribute(CMDKEY, cmdName); } return cmdName != null; } private boolean trySetCmdNameByWebsvc(HttpServletRequest request) { String cmdName = null; if (request.getRequestURI().contains(WEBSVC_SUFFIX) && (cmdName = request.getParameter(CALLNAME)) != null) { request.setAttribute(CMDKEY, cmdName); } return cmdName != null; } public String getCommand(HttpServletRequest request) { String callName = (String) request.getAttribute(CMDKEY); request.removeAttribute(CMDKEY); return callName; } }
这两种写法都是可以正常工作的,但是如果二选一,你会选择哪一种?大家投票后,发现更多的人喜欢垂直切面的代码。有些人说,虽然说不出来,但是感觉第二种会更好一点。
那么我们就比较一下这两种写法,从需求上看,动作维度是固定的,就是两种, 但是URI
的维度可能是变化的。大家都知道软件的开闭原则,但是开闭原则的应用的方向是对变化的对象应用,而不是对固定的对象应用。因此我们要对URI的变化修改说No,而欢迎URI的扩展说Yes.
在第一个逻辑中,加入出现一个新的需求,如果header 出现一个OperationName,如果出现就优先从个OperationName中取,那么代码很有可能写成如下:
@Bean public class CommandParser implements CommandParser<HttpServletRequest> { private static final String CMDKEY = "CMDKEY"; private static final String HEADERKEY = "X-API-CALL-NAME"; private static final String CALLNAME = "callname"; private static final String AP_SUFFIX = "/api"; private static final String WEBSVC_SUFFIX = "/ws/websvc"; public boolean accept(HttpServletRequest request) { String uri = request.getRequestURI(); String cmdName = null; cmdName = request.getHeader(“OperationName”); if(cmdName !=null){ return true; } String uri = request.getRequestURI(); String cmdName = null; if (uri.endsWith(API_DLL_SUFFIX)){ cmdName = request.getHeader(HEADERKEY); } else if (uri.endsWith(WEBSVC_SUFFIX)){ cmdName = request.getParameter(CALLNAME); } if (cmdName != null){ request.setAttribute(CMDKEY, cmdName); return true; } return false; } public String getCommand(HttpServletRequest request) { String callName = (String) request.getAttribute(CMDKEY); request.removeAttribute(CMDKEY); return callName; } }
这个逻辑有可越加越多,到了后期,比如有个URI 需要根据条件来进行OperationName的读取,这个逻辑就很难剥离。而第二种写法所有的URI的逻辑都是内聚的,任何逻辑的变化都是发生在URI的逻辑内部,这完全符合开闭原则。
有的人问:看起来只有resolveCommand 在不同URI下是变化的。如果我不完全垂直切分,而是在resolveCommand垂直分, 在setAttribute 水平切分怎么样?
那这样的代码我们可以看看变成什么样:
代码:
@Bean public class CommandParser implements CommandParser<HttpServletRequest> { private static final String CMDKEY = "CMDKEY"; private static final String CALLNAME = "callname"; private static final String API_DLL_SUFFIX = "/api.dll"; private static final String WEBSVC_SUFFIX = "/ws/websvc"; public boolean accept(HttpServletRequest request) { String cmdName = resolveAPIName(request); if (cmdName == null) { return false; } else { request.setAttribute(TRADINGAPICALLNAME, cmdName); return true; } } public class CommandName { private String cmdName; public String getCmdName() { return cmdName; } public void setCmdName(String cmdName) { this.cmdName = cmdName; } } private boolean tryResolveCmdNameByApidll(HttpServletRequest request, CommandName cmdName) { String cmdName = null; if (request.getRequestURI().contains(API_DLL_SUFFIX) && (cmdName = request.getHeader(HEADERKEY)) != null) { cmdName.seCmdName(cmdName) } return cmdName != null; } private boolean trySetCmdNameByWebsvc(HttpServletRequest request) { String cmdName = null; if (request.getRequestURI().contains(WEBSVC_SUFFIX) && (cmdName = request.getParameter(CALLNAME)) != null) { cmdName.seCmdName(cmdName); } return cmdName != null; } private String resolveAPIName(HttpServletRequest request) { CommandName cmd = new CommandName(); if (!trySetApidllCmd(request, cmd) && !trySetWebsvcCmd(request, cmd)) { return null; } return cmd.getCmdName(); } public String getCommand(HttpServletRequest request) { String callName = (String) request.getAttribute(CMDKEY); request.removeAttribute(CMDKEY); return callName; } }
你看到这样的逻辑不仅不简单,而且更复杂了。由于是单例的Bean,考虑多线程的数据传递,不得不创造一个类来进行传递command。这样不仅每个线程多创造了一个对象,而且代码逻辑也并没有得到改善。
总结:
简单逻辑的代码编写,如果仔细考虑,也是有不同的写法。目的都是一致的, 但是方向不同导致代码的不同。看起来很一致,但是内在的思想完全不一样。总体来说,一定要进行对代码的元素进行分析,开闭原则要应用在变化的部分。这样才能让代码看起来更舒服。
原文:https://www.cnblogs.com/developernotes/p/14676411.html