Johnny's Collections

生活总是有太多的无奈与失望,让我们以在努力学习和工作中获得的成就感和快乐来冲淡它们。

BlogJava 首页 新随笔 联系 聚合 管理
  10 Posts :: 0 Stories :: 80 Comments :: 0 Trackbacks
今天一个曾经共事的同行问我:“要从编码转为设计,大概需要多长时间?”
我的回答是:“编码本身就是一种设计,你可以设计你的代码。”

其实正如概要设计与详细设计,系统设计与架构设计一样,编码与设计也是没有明显的边界,每个正确成长的程序员,都必须从编码开始,慢慢锻炼抽象思维、逻辑思维、面向对象思维,然后慢慢的过渡到系统设计,再随着经验和知识的积累,慢慢过渡到架构设计。下面我将会以最近的一个手头的编码任务,简单介绍一下如何“设计”你的代码。

任务是这样的,某银行支付系统的客户端接收银行用户录入的转账数据,当转账数据被审批通过后,状态转变为“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
    }
}

到目前为止,我已经用纯面向对象的思维方式“设计”好我的代码了,这时,我思维非常清晰,因而代码结构也非常清晰,职责单一,内聚高,耦合低,最后,我可以根据需求文档的细节(没有描述)慢慢的编写我的实现了。

复杂的事物总是由一些较简单的事物组成,而这些较简单的事物也是由更简单的事物组成,如此类推。因此,在编写代码的时候,先用面向对象的思维把复杂的问题分解,再进一步分解,最后把简单的问题各个击破,这就是一种设计。开发人员只要养成这种习惯,即使你每天都只是做最底层的编码工作,其实你已经在参与设计工作了,随着知识和经验的累积,慢慢的,你从设计代码开始,上升为设计类、方法,进而是设计模块,进而设计子系统,进而设计系统……,最终,一步一步成为一个优秀的架构师。

最后,有一个真理奉献给浮躁的程序员:

优秀的架构师、设计师,必定是优秀的程序员,不要因为你的职位上升了,就放弃编码。

补充说明:本博文纯粹是讨论一种思维习惯,不要把其做法生搬硬套,不管实际情况,直接在编码的时候这样做,不见得是最好的选择。在实际编码中,有如下问题你必须考虑:
  • 你需要考虑业务逻辑的可重用性和复杂程度,是否有必要设计出新的类或抽取新的私有方法来封装逻辑,或者直接在原方法上编码(如果足够简单)。
  • 新的业务逻辑,是否在某些地方已经存在,可以复用,即使不存在,这些逻辑是应该封装到新的类中,还是应该放置到现有的类中,这需要进行清晰的职责划分。
  • 需要在设计和性能上作出权衡。
  • 如果在现成的系统中增加新的功能,而现成系统的编码风格与你想要的相差很远,但你又没有足够的时间成本来进行重构,那么还是应该让你的代码与现成系统保持一致的风格。

posted on 2010-04-28 00:51 Johnny.Liang 阅读(4913) 评论(8)  编辑  收藏 所属分类: 编程技巧系统设计

Feedback

# re: “设计”你的代码 2010-04-28 10:41 zcl
很有启发,希望楼主能多写这方面的文章,赞一个!  回复  更多评论
  

# re: “设计”你的代码 2010-04-28 11:50 grass
太精辟了。前几天我还在网上发贴,被人骂的一踏糊涂,原来答案在楼主这里。  回复  更多评论
  

# re: “设计”你的代码 2010-04-28 15:02 乐蜂网美丽俏佳人
是空间上看简单的  回复  更多评论
  

# re: “设计”你的代码[未登录] 2010-04-29 00:42 onkyo
小小的砸一下砖,大家探讨一下:

我比较质疑以下这两点
“纯面向对象的思维方式” 和 “内聚高,耦合低”。

和原来的代码比较的话就是把原来集中在一起的代码分散了。

首先 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(...) {}

不能继承,不能重用。

其次代码是高耦合的, 当流程的判断条件变更的话是需要修改代码的,因为判断条件是写死在代码里面的。 (当然这就是为什么需要工作流框架的原因)  回复  更多评论
  

# re: “设计”你的代码[未登录] 2010-04-29 00:54 onkyo
个人觉得比较好的方案是声明一个interface

public interface WorkflowFactroy {
Workflow create(Instruction info)
}

把逻辑写在WorkflowFactoryImpl里面, 用ioc注入WorkflowFactoryImpl.  回复  更多评论
  

# re: “设计”你的代码 2010-04-29 09:40 Johnny.Liang
@onkyo
呵呵,回复一下这为同学的两个评论,首先,你说得对,static就不是面向对象,纯面向对象是没有static函数的,但我要解释两点,上面的代码纯属演示如何改变一种思维方式,我并没有过于斟酌于代码的细节,如果要纯面向对象的话,我可以把static声明为对象方法,然后让这个类变成Singleton;其次,如果所有东西都要考虑继承的话,就是过度设计对了,正如我在本博文的最后的特别说明,设计是要针对需求的,假如我这个流程相当稳定,不存在多态的情况,那么我就(至少在目前)不需要过度的把它设计为接口,然后再提供实现类,再通过依赖注入,而关于你提到的private方法不能继承和重用,这也是一个好问题,假如根据实际情况,我不希望我的类或方法被继承或重写,我就需要声明其为final/private了,君不见JDK的很多类都是final的吗?这同样也回答了你第二个评论的问题,没需要多态,或没需求切换实现,就没必要接口。

总之,谢谢你的发言,我只能强调,上面的代码纯属表达一种思维方式,况且,不考虑现实环境和实际需求,孤立的去讨论一个类是否有接口,一个方法是否需求继承,一个静态方法是否必须设计为对象方法,都是没有实际意义的,搞不好就是一种“过度设计”。  回复  更多评论
  

# re: “设计”你的代码[未登录] 2010-04-30 16:39 onkyo
@Johnny.Liang
我只是就博文中我不太认同的地方发表一下看法,大家探讨一下相互提高。

首先我非常同意 设计是要针对需求的 的这句话, 这个流程相当稳定,不存在多态的情况,那么第一种写法

public void onMessage(InstructionInfo instructionInfo) {
if(xx && yy && zz) { // 停留在后端等待执行指定的工作流程
// 根据每种组合进行条件判断,走哪个流程
if(a==true && b==true && c==true && d==true {
...
}
else if(...) {...}
else if(...) {...}
...
else(...) {...}
}
}

我觉的完全可行。 何必再拆分出两个类? 还便于阅读,便于修改。 因为逻辑都集中在一起了。 这就是面对过程的设计, 非常的合理。

正如博文题目设计你的代码: 每个正确成长的程序员,都必须从编码开始,慢慢锻炼抽象思维、逻辑思维、面向对象思维,然后慢慢的过渡到系统设计,再随着经验和知识的积累,慢慢过渡到架构设计。

既然我们要抽象上述的代码, 要使用面对对象思维,要重构上面的代码, 就应该搞清楚为什么要用抽象,为什么要面对对象思维。 抽象和面对对象编程的目的无非是最大限度的重用。 那么就应该面对接口编程, 解耦关系。

我的观点就是既然要设计,就要好好设计。 如果要用省事的方法,那就用最省事的方法。

  回复  更多评论
  

# re: “设计”你的代码 2010-04-30 17:50 Johnny.Liang
@onkyo
明白你的意思,谢谢你的意见,我往后会发表一些针对如何使用面向对象思维进行设计,及其真正的好处的博文,当中就会详细的说明使用面向对象思维在某些场景中的好处。  回复  更多评论
  


只有注册用户登录后才能发表评论。


网站导航: