“设计”你的代码
我的回答是:“编码本身就是一种设计,你可以设计你的代码。”
其实正如概要设计与详细设计,系统设计与架构设计一样,编码与设计也是没有明显的边界,每个正确成长的程序员,都必须从编码开始,慢慢锻炼抽象思维、逻辑思维、面向对象思维,然后慢慢的过渡到系统设计,再随着经验和知识的积累,慢慢过渡到架构设计。下面我将会以最近的一个手头的编码任务,简单介绍一下如何“设计”你的代码。
任务是这样的,某银行支付系统的客户端接收银行用户录入的转账数据,当转账数据被审批通过后,状态转变为“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
}
}
到目前为止,我已经用纯面向对象的思维方式“设计”好我的代码了,这时,我思维非常清晰,因而代码结构也非常清晰,职责单一,内聚高,耦合低,最后,我可以根据需求文档的细节(没有描述)慢慢的编写我的实现了。
复杂的事物总是由一些较简单的事物组成,而这些较简单的事物也是由更简单的事物组成,如此类推。因此,在编写代码的时候,先用面向对象的思维把复杂的问题分解,再进一步分解,最后把简单的问题各个击破,这就是一种设计。开发人员只要养成这种习惯,即使你每天都只是做最底层的编码工作,其实你已经在参与设计工作了,随着知识和经验的累积,慢慢的,你从设计代码开始,上升为设计类、方法,进而是设计模块,进而设计子系统,进而设计系统……,最终,一步一步成为一个优秀的架构师。
最后,有一个真理奉献给浮躁的程序员:
优秀的架构师、设计师,必定是优秀的程序员,不要因为你的职位上升了,就放弃编码。
补充说明:本博文纯粹是讨论一种思维习惯,不要把其做法生搬硬套,不管实际情况,直接在编码的时候这样做,不见得是最好的选择。在实际编码中,有如下问题你必须考虑:
- 你需要考虑业务逻辑的可重用性和复杂程度,是否有必要设计出新的类或抽取新的私有方法来封装逻辑,或者直接在原方法上编码(如果足够简单)。
- 新的业务逻辑,是否在某些地方已经存在,可以复用,即使不存在,这些逻辑是应该封装到新的类中,还是应该放置到现有的类中,这需要进行清晰的职责划分。
- 需要在设计和性能上作出权衡。
- 如果在现成的系统中增加新的功能,而现成系统的编码风格与你想要的相差很远,但你又没有足够的时间成本来进行重构,那么还是应该让你的代码与现成系统保持一致的风格。
Feedback
我比较质疑以下这两点
“纯面向对象的思维方式” 和 “内聚高,耦合低”。
和原来的代码比较的话就是把原来集中在一起的代码分散了。
首先 InstructionHandleDecisionMaker 和 InstructionWorkFlowSelector 就不是面对对象的设计, 用的是都是static函数。 实际上就是把原来代码中的
onMessage 中的代码, 归了一下类,拆成一些小函数, 然后再插到InstructionHandleDecisionMaker 和 InstructionWorkFlowSelector 文件中去。 其实际上就是
public void onMessage(InstructionInfo instructionInfo) {
if(isHandledByBackEnd(...) ) {
WorkFlow wf =getWorkFlow(...);
//TODO Implment the logic
}
}
private static Map mapping = new HashMap();
static {
mapping.input("YYNN",WorkFlow.A);
mapping.input("NNYY",WorkFlow.B);
...
}
private WorkFlow getWorkFlow(Instruction info) {
...
}
private String isA(...) {}
private String isB(...) {}
private String isC(...) {}
private String isD(...) {}
不能继承,不能重用。
其次代码是高耦合的, 当流程的判断条件变更的话是需要修改代码的,因为判断条件是写死在代码里面的。 (当然这就是为什么需要工作流框架的原因) 回复 更多评论
呵呵,回复一下这为同学的两个评论,首先,你说得对,static就不是面向对象,纯面向对象是没有static函数的,但我要解释两点,上面的代码纯属演示如何改变一种思维方式,我并没有过于斟酌于代码的细节,如果要纯面向对象的话,我可以把static声明为对象方法,然后让这个类变成Singleton;其次,如果所有东西都要考虑继承的话,就是过度设计对了,正如我在本博文的最后的特别说明,设计是要针对需求的,假如我这个流程相当稳定,不存在多态的情况,那么我就(至少在目前)不需要过度的把它设计为接口,然后再提供实现类,再通过依赖注入,而关于你提到的private方法不能继承和重用,这也是一个好问题,假如根据实际情况,我不希望我的类或方法被继承或重写,我就需要声明其为final/private了,君不见JDK的很多类都是final的吗?这同样也回答了你第二个评论的问题,没需要多态,或没需求切换实现,就没必要接口。
总之,谢谢你的发言,我只能强调,上面的代码纯属表达一种思维方式,况且,不考虑现实环境和实际需求,孤立的去讨论一个类是否有接口,一个方法是否需求继承,一个静态方法是否必须设计为对象方法,都是没有实际意义的,搞不好就是一种“过度设计”。 回复 更多评论
我只是就博文中我不太认同的地方发表一下看法,大家探讨一下相互提高。
首先我非常同意 设计是要针对需求的 的这句话, 这个流程相当稳定,不存在多态的情况,那么第一种写法
public void onMessage(InstructionInfo instructionInfo) {
if(xx && yy && zz) { // 停留在后端等待执行指定的工作流程
// 根据每种组合进行条件判断,走哪个流程
if(a==true && b==true && c==true && d==true {
...
}
else if(...) {...}
else if(...) {...}
...
else(...) {...}
}
}
我觉的完全可行。 何必再拆分出两个类? 还便于阅读,便于修改。 因为逻辑都集中在一起了。 这就是面对过程的设计, 非常的合理。
正如博文题目设计你的代码: 每个正确成长的程序员,都必须从编码开始,慢慢锻炼抽象思维、逻辑思维、面向对象思维,然后慢慢的过渡到系统设计,再随着经验和知识的积累,慢慢过渡到架构设计。
既然我们要抽象上述的代码, 要使用面对对象思维,要重构上面的代码, 就应该搞清楚为什么要用抽象,为什么要面对对象思维。 抽象和面对对象编程的目的无非是最大限度的重用。 那么就应该面对接口编程, 解耦关系。
我的观点就是既然要设计,就要好好设计。 如果要用省事的方法,那就用最省事的方法。
@onkyo
明白你的意思,谢谢你的意见,我往后会发表一些针对如何使用面向对象思维进行设计,及其真正的好处的博文,当中就会详细的说明使用面向对象思维在某些场景中的好处。 回复 更多评论