今天一个曾经共事的同行问我:“要从编码转为设计,大概需要多长时间?”
我的回答是:“编码本身就是一种设计,你可以设计你的代码。”
其实正如概要设计与详细设计,系统设计与架构设计一样,编码与设计也是没有明显的边界,每个正确成长的程序员,都必须从编码开始,慢慢锻炼抽象思维、逻辑思维、面向对象思维,然后慢慢的过渡到系统设计,再随着经验和知识的积累,慢慢过渡到架构设计。下面我将会以最近的一个手头的编码任务,简单介绍一下如何“设计”你的代码。
任务是这样的,某银行支付系统的客户端接收银行用户录入的转账数据,当转账数据被审批通过后,状态转变为“transfer”,同时,该客户端需要通过JMS以异步的方式向支付系统后台发送一条带有转账记录(Instruction)的消息,后端在接收到信息之后,需要根据Instruction的一些相关信息,首先确定这笔转账数据是直接发送给真正进行转账的清算(Clearing)银行系统,还是停留在后端系统,等待后端系统中需要执行的工作流程(work flow)。而后端系统需要对Instruction执行的工作流程有两个,同时需要根据Instruction的一些相关信息进行选择。
为了简化复杂度,我这里假设系统有一个InstructionHandleMsgDrivenBean,该bean有一个onMessage()方法,所有业务逻辑需要在该方法中实现。
同时解释一下详细的业务细节:
- 判断Instruction是否需要停留在后端等待执行指定的工作流程有三个条件:xx、yy、zz,当三个条件都为true时,停留。
- 判断Instruction需要走A流程还是B流程,由4个因素的组合确定,如果用“Y”代表true,“N”代表false,那么由这个四个因素组成的“XXXX”一共有16种组合,不同的组合分别走A和B流程,如:YYNN、YYNY to A,NNYY、NNNY to B,……不累赘。
好了,对于一个纯编程人员来说,拿到这样的需求,感觉逻辑很简单,可以直接编码了,于是,他开始一行一行的编写代码(伪代码):
public void onMessage(InstructionInfo instructionInfo) {
if(xx && yy && zz) { // 停留在后端等待执行指定的工作流程
// 根据每种组合进行条件判断,走哪个流程
if(a==true && b==true && c==true && d==true {
...
}
else if(...) {...}
else if(...) {...}
...
else(...) {...}
}
}
这种做法是最为开发人员欢迎的,因为它简单、直接,但这种做法也恰恰反映了开发人员的通病——使用Java编写纯面向过程的代码。
好了,说了一大堆,如何“设计”你的代码呢?答案是:使用面向对象思维:
我们拿到需求之后,可以分析,这个需求大体上分为两部分:
- 判断是否需要停留在后端等待执行指定的工作流程的部分
- 选择走哪个工作流程的部分
有了这个前提,我可以设计出两个职责单一的对象了:
public class InstructionHandleDecisionMaker {
public static boolean isHandledByBackEnd(InstructionInfo info) {
return (isXX(...) && isYY(...) && isZZ(...));
}
private booolean isXX(...) {
//TODO Implement the logic
return false;
}
private booolean isYY(...) {
//TODO Implement the logic
return false;
}
private booolean isZZ(...) {
//TODO Implement the logic
return false;
}
}
public class InstructionWorkFlowSelector {
private static Map mapping = new HashMap();
static {
mapping.input("YYNN",WorkFlow.A);
mapping.input("NNYY",WorkFlow.B);
...
}
public static WorkFlow getWorkFlow(Instruction info) {
StringBuilder result = new StringBuilder();
result.append(isA(...)).append(isB(...));
result.append(isC(...)).append(isD(...));
return mapping.get(result.toString());
}
private static String isA(...) {
//TODO Implment the logic
return "N";
}
private static String isB(...) {
//TODO Implment the logic
return "N";
}
private static String isC(...) {
//TODO Implment the logic
return "N";
}
private static String isD(...) {
//TODO Implment the logic
return "N";
}
}
可以看到,我先按职责划分了类,再按职责抽取了私有方法,“框架”设计好 ,为了让编译通过,我上面完整的填写了代码的,然后加上TODO标识,然后,我可以编写我的onMessage方法了:
public void onMessage(InstructionInfo instructionInfo) {
if( InstructionHandleDecisionMaker.isHandledByBackEnd(...) ) {
WorkFlow wf =InstructionWorkFlowSelector.getWorkFlow(...);
//TODO Implment the logic
}
}
到目前为止,我已经用纯面向对象的思维方式“设计”好我的代码了,这时,我思维非常清晰,因而代码结构也非常清晰,职责单一,内聚高,耦合低,最后,我可以根据需求文档的细节(没有描述)慢慢的编写我的实现了。
复杂的事物总是由一些较简单的事物组成,而这些较简单的事物也是由更简单的事物组成,如此类推。因此,在编写代码的时候,先用面向对象的思维把复杂的问题分解,再进一步分解,最后把简单的问题各个击破,这就是一种设计。开发人员只要养成这种习惯,即使你每天都只是做最底层的编码工作,其实你已经在参与设计工作了,随着知识和经验的累积,慢慢的,你从设计代码开始,上升为设计类、方法,进而是设计模块,进而设计子系统,进而设计系统……,最终,一步一步成为一个优秀的架构师。
最后,有一个真理奉献给浮躁的程序员:
优秀的架构师、设计师,必定是优秀的程序员,不要因为你的职位上升了,就放弃编码。
补充说明:本博文纯粹是讨论一种思维习惯,不要把其做法生搬硬套,不管实际情况,直接在编码的时候这样做,不见得是最好的选择。在实际编码中,有如下问题你必须考虑:
- 你需要考虑业务逻辑的可重用性和复杂程度,是否有必要设计出新的类或抽取新的私有方法来封装逻辑,或者直接在原方法上编码(如果足够简单)。
- 新的业务逻辑,是否在某些地方已经存在,可以复用,即使不存在,这些逻辑是应该封装到新的类中,还是应该放置到现有的类中,这需要进行清晰的职责划分。
- 需要在设计和性能上作出权衡。
- 如果在现成的系统中增加新的功能,而现成系统的编码风格与你想要的相差很远,但你又没有足够的时间成本来进行重构,那么还是应该让你的代码与现成系统保持一致的风格。