`
ajoo
  • 浏览: 453104 次
社区版块
存档分类
最新评论

复杂还是不复杂?

阅读更多
问题是这样的。

一个MyService类里面,有一个MyResponse runService()函数。这个runService函数会调用一个web service来得到MyResponse对象。这个MyResponse对象在runService()函数中被缓存,然后返回。

现在的目标是,在runService返回以前,先把MyResponse clone一下,然后如果MyResponse.getCensus().getSalary()返回的带有几毛几分的零钱,就把这个salary truncate成整数。

需求不难,一个直观的解决方案是:

java 代码
 
  1. MyResponse runService() {  
  2.   MyResponse response = call web service;  
  3.   cache response;  
  4.   response = (MyResponse)CloneUtil.cloneSerializable(response);  
  5.   SalaryTruncationUtil.truncateSalaryIfNecessary(response);  
  6.   return response;  
  7. }  


CloneUtil是现成的。只要写一个SalaryTruncationUtil类就算完工了。

可是问题出现在,这个系统存在着一个ResponseManipulator接口(在另外一个package里面)。
ResponseManipulator的接口如下:

java 代码
 
  1. public interface ResponseManipulator {  
  2.   MyResponse manipulate(MyResponse response);  
  3. }  

从签名上,可以看出一个ResponseManipulator负责操作一个MyResponse,然后返回一个新的(或者原来的MyResponse)对象。
同时,另外还现存一个ChainResponseManipulator类,它负责把若干个ResponseManipulator对象串起来:
java 代码
 
  1. public class ChainResponseManipulator implements ResponseManipulator {  
  2.   private ResponseManipulator[] manipulators;  
  3.   public MyResponse manipulate(MyResponse response){  
  4.     for(ResponseManipulator manipulator : manipulators) {  
  5.       response = manipulator.manipulate(response);  
  6.     }  
  7.   }  
  8. }  
以及一个已经写好的DeepCloneManipulator类来负责对一个Response对象的clone。

于是,我的pair决定这样写:
java 代码
 
  1. MyResponse runService() {  
  2.   MyResponse response = call web service;  
  3.   cache response;  
  4.   response = getManipulator().manipulate(response);  
  5. }  
  6. ResponseMinipulator getManipulator() {  
  7.     return new ChainResponseManipulator(new ResponseManipulator[]{  
  8.       new DeepCloneManipulator(),  
  9.       new TruncateSalaryManipulator();  
  10.     });  
  11. }  


当然,还要实现一个TruncateSalaryManipulator。实现起来非常简单,就不写了。

我反对这个设计。虽然两者在代码量上不相伯仲,但是我认为这个设计无谓地增加复杂性,有一种绕着弯子解决简单问题的感觉。一个简单的顺序操作非要用一个特殊的接口和一个特殊的ChainResponseManipulator来实现。
一般来说,用接口是为了得到多态,降低耦合。可是在pair的代码里,该有的依赖一个没少,这个接口就显得意义寥寥。
而且这样做其实增大了MyService的依赖,因为凭空多了对ResponseManipulator和ChainResponseManipulator的依赖。

另外,ChainResponseManipulator对debug也不是非常友好,你单步运行每一个manipulator只能在ChainResponseManipulator的代码中,而不是MyService的代码中。

pair的观点有三:
1。看不出我的方案比用Manipulator有什么简单性的优势。
2。ChainResponseManipulator这一套设施已经存在,又不需要从头写。而且别人都是这么干的。
3。debug可以在每个不同的Manipulator类里面设置断点。


总而言之,没有达成一致意见。因为是pair主导键盘,所以最终pair的方案获胜。



今天在想怎么说服pair的时候,想了这么一个例子:

假设已经有一个StringAppender类:
java 代码
 
  1. class StringAppender {  
  2.   private Object[] objects;  
  3.   public String toString(){  
  4.     StringBuffer buf = new StringBuffer();  
  5.     for(Object obj : objects) {  
  6.       buf.append(obj);  
  7.     }  
  8.     return buf.toString();  
  9.   }  
  10. }  
那么在面对把两个对象连接成一个对象的时候,我们是:obj1.toString()+obj2.toString()呢?还是:new StringAppender(new Object[]{obj1, obj2}).toString()呢?

是不是已经有了的设施,为了保持一致性就必须要使用呢?即使有更简便的方法?


关于这个问题,你怎么看?
分享到:
评论
18 楼 dhxlsfn 2007-02-27  
没有一定之规
17 楼 canonical 2007-02-26  
这里需要区分是概念和实现。 我们所采取的办法是保持很直接很方便的方式直接调用实现代码,在需要的时候为实现代码增加概念包装类。实际上如果在系统架构上不存在通用的Manipulator的支持(例如动态配置,管理等),采用chain结构等并不是什么好主意。 很多情况下我们需要的是统一实现而不是概念依赖。
16 楼 ajoo 2007-02-26  
intolong 写道
MyResponse runService()现在的实现逻辑是:
1. 调用Web Service得到MyResponse
2. 对MyResponse做一系列后续处理

如果后续处理是可能发生变化的,那么你pair的方案就灵活了,但不要自己查找ResponseMinipulator,而是DI进来,由组装代码负责这个变化。

如果后续处理是一定的,那么hardcode更直观和简洁。

这话正点。如果manipulator是注射进来的话,我一点意见没有。

问题是绕了半天还要直接new出来,一点灵活性也没得到。这就像要计算1+2+3,不直接写,非要写成一个数组加循环:
int[] arr = {1,2,3};
int sum = 0;
for(int i=0; i<arr.length; i++){
  sum += arr[i];
}


这循环的好处是被计算的数据可以动态决定。可是如果你的数据本身是静态写死的,那么就不如直接1+2+3。
15 楼 intolong 2007-02-25  
MyResponse runService()现在的实现逻辑是:
1. 调用Web Service得到MyResponse
2. 对MyResponse做一系列后续处理

如果后续处理是可能发生变化的,那么你pair的方案就灵活了,但不要自己查找ResponseMinipulator,而是DI进来,由组装代码负责这个变化。

如果后续处理是一定的,那么hardcode更直观和简洁。
14 楼 cat 2007-02-11  
我还是觉得简单地直接写出来比较好,容易理解,容易调试,效率似乎也更高。

题外话:看到过有一些component的所谓“data-driven”把什么都写到XML里面,最后XML里面可以配置Condition, And, Or, Not,Op,感觉就是在用XML写代码。写起来不方便不说,调试起来更是累人。
13 楼 jellyfish 2007-02-08  
The original design is very sound. Don't see this kind of coding often.

The original design fits the open-close principle.

The design is just a template pattern, as those interceptors in web apps, in a chain. It's a post processing of a complex method.

The very naive, and commonly seen,  way to implement is through nested if-else blocks. This completely violates the OO principles.

The root reason is to make maintenance simple. If we started from nested if-else, when we add more if-else blocks, it's just so easy to break existing logic(especially those if-without-else blocks because they have implicit no-code default behaviors). One way to replace them is using interceptors. The interceptors, in a certain degree confine the individual post processing to their own classes so that we can change them independently.

In general, if we have less than 3(the magic number 3) postprocessings, we could hardcode them. But if there are 3 or more similar code patterns then we should definitely avoid it.

The nice thing about this design is that when you add even a simple requirement like this, you touch minimal original code(only the hook portion), plus the new code. A simple requirement is simple, but not 10 simple requirements, not when they interact each other, not when you need to apply them in 10 different places, not when they have heavy dependencies, not when 15 of them combines together.

If you think this is simple, it's because you are benefiting from the sound design. What if you are in a 15-level if-else block and you need to add another if-else there?

Even for a simple salary calculation, there could be many postprocessings, truncate to unit, currency conversion, add bonus, minus stock options, minus health insurrance, pay income tax, etc.

Even we use OO, this is still tough. First, there is an order in the sequence of postprocessings, who does first. For instance, health insurrance should be before income tax because it's "before tax". Then, there could be combination effect, e.g., you can't combine certain set of them. This makes testing very tedious because you need to test all combinations, make sure you get correct results or exceptions

OO is not just a textbook concept, it simplifies coding *and* maintenance. If you think it's overengineering, try inline them to see whether it's good or not. In this case, if you don't follow the original design, then what do you think the guy after you says?

There are several improvements to this design as well. If the creation of these postprocessors is complex, e.g., they have very heavy dependencies, then use a factory dedicated to the creation so that the dependencies won't spread everywhere(Spring is a nice candidate). Furthermore, the interface can be improved too, have two more methods:
initWith(response) - where we get values from response and inject all other dependencies, like get exchange rate for currencies.
isApplicable() - use the above method to determine whether we should apply this processor, based on the values from initWith() and others.

So not only this is a good approach, but we can make it more "OO", not because we like OO, instead because we want to separate things(like dependencies) out.

There are many similar examples in the real world, from web app interceptors for i18n conversion, currency conversion, validation, security, audit, logging, to simple spreadsheets(think about a data matrix as the response here and what we can do for columns and rows, scaling, number crunching, sum and %, etc)

In my experience, I had a case where I had 17 interceptors in 2 years(not knowing all of them upfront), sometimes I had only 10 minutes to add one under heavy pressure. Their combinations are very tricky, the first output(like response) could come from 3 different paths, each of them has heavy dependencies. This design stands well.

When we code, we have to think about how to maintain them. This is not college student homework, which can be thrown away after tests, :-). We may do simple things once or twice, but not more than that.

Just my experience, no point fingers.
12 楼 jianfeng008cn 2007-02-07  
hermitte 写道
这种情况很经常遇到,一般是根据项目的规模和时间来决定

刚开始编程的时候,我是吸收的J道那里的banq的思路。
就是在自己的业务层中,只调用更下层的代码,而不是复用已经有了的同层代码。

如果业务简单,就自己写一遍。

如果必须引用已经有了的同层代码,因为度量和减少代码冗余以便维护的缘故,我倾向于用Adapter或者Mediater模式,用接口封装和重构同层的代码。而不是直接引用其它人写的包的东西。

引用之前,把别人的类给包装下先。因为不知道其它人在编写代码的时候,会不会修改已经编写好的类,如果你依赖了他,然而在不知情的情况下,其它人又对你依赖的类进行了修改,就会非常麻烦。所以用结构型设计模式进行下解耦,是面向对象编程的一个基本功。

如果是原来,我倾向你的解决方案。因为那个符合“好莱坞原则”还有Banq的无为的道的思想。那个思想对我影响比较深刻。而且你的代码是高聚合,低耦合的。毕竟复用和耦合,在某种情况下是存在矛盾的。



不过现在我觉得OO这套有点过时了,有时候OO到成了代码复用的障碍,那个第二个应该用的是IOC的思路在里面,不过问题是由于JAVA接口方面的限制,反而导致耦合的加强,这个应该是JAVA语法的原因,不是编程的原因,毕竟依赖的只是接口而已。


感觉说得很好,这才是真正的理由!
11 楼 ajoo 2007-02-07  
BirdGu 写道
ResponseManipulator是原来就存在的,那么为什么会有这个机制呢?你确定你现在的情况不属于ResponseManipulator原来设计要处理的问题吗?

Manipulator在我看来本来就是个过度设计。

它的用法一直都是:
void f(){
  ...
  response = getManipulator().manipulate(response);
  ...
}
ResponseManipulator getManipulator(){
  return new ChainManipulator(new ResponseManipulator[]{
    new Manipulator1(), new Manipulator2()
  });
}


而这个设计相比于下面的代码好处非常有限:
void f(){
  ...
  response = manipulate(response);
  ...
}
Response manipulate(Response response){
  response = Manipulator1.doSomething(response);
  response = Manipulator2.doSomethingElse(response);
}

就为了节省敲“manipulate(response)”在我看来不是一个引入一个接口和ChainManipulator的理由。

10 楼 BirdGu 2007-02-07  
ResponseManipulator是原来就存在的,那么为什么会有这个机制呢?你确定你现在的情况不属于ResponseManipulator原来设计要处理的问题吗?
9 楼 ajoo 2007-02-07  
灵活度看怎么说了。

就在我们要完成这个功能的时候,我忽然想到:如果不需要truncate,那么根本就没必要clone。

跟同事一说,他愣了半天。因为要实现这个功能ChainManipulator就不好使了。最后只好说,先这么放着,如果真有效率问题再说。

其实这个功能如果不用Manipulator,非常简单。不过就是:
if(needsTruncation(response)){
  response = CloneUtils.cloneSerializable(response);
  truncateSalary(response);
}


Manipulator不过就是所谓的“高阶逻辑”。不过高阶逻辑虽然有抽象上的优势,也有不够直观,难于debug的问题。面对这么一个简单的需求,何必大炮打蚊子呢?(虽然,据说大炮和炮弹有现成的,不用额外买)

话说轮子,我感觉这个问题就是,本来要去隔壁,迈腿就到的,何必非要弄四个轮子开车?即使轮子白给,这车白开,不用你花钱加油。
8 楼 shaucle 2007-02-05  
<br/>
<strong>firebody 写道:</strong><br/>
<div class='quote_div'>光从这里的代码来看,看不出两个方案的优劣来,不过你的pair的方案确实存在“误用”<span><span>的嫌疑:  代码依赖于</span></span><span><span>ChainResponseManipulator这个类。<br/>
<br/>
但是,如果考虑别的</span></span>Service也需要你写的 功能的话,别人可能会这样调用:<br/>
<br/>
<ol class='dp-j'>
    <li class='alt'><span><span>MyResponse runService() {  </span></span> </li>
    <li class=''><span>  MyResponse response = call web service;  </span> </li>
    <li class='alt'><span>  cache response;  </span> </li>
    <li class=''><span>  response = ManupilatorFactory.getManupulate(</span><span><span>ResponseManipulator .A,</span></span><span><span>ResponseManipulator .B) </span></span><span>.manipulate(response);  </span> </li>
    <li class='alt'><span>}  <br/>
    </span></li>
</ol>
<br/>
这样的话,可能会发挥一点 <span><span>ResponseManipulator 的作用。 <br/>
<br/>
不过光从代码来看,可能也不会比你的方法少到哪里去。<br/>
<br/>
一种比较实在的写法可以这样:<br/>
<br/>
<br/>
</span></span><br/>
<ol class='dp-j'>
    <li class='alt'><span><span>MyResponse runService() {  </span></span> </li>
    <li class=''><span>  MyResponse response = call web service;  </span> </li>
    <li class='alt'><span>  cache response;  <br/>
    </span></li>
    <li class='alt'><span>//first ly<br/>
    </span></li>
    <li class=''><span>  response = ManupuateSupport.manipuate1(response)</span><span>;</span> </li>
</ol>
          //secondly
<ol class='dp-j'>
    <li class=''><span> response = ManupuateSupport.manipuate2(response)</span><span>;</span> </li>
    <li class=''><span>  </span> </li>
    <li class='alt'><span>} <br/>
    </span></li>
</ol>
<br/>
这样的写法一样可以达到代码复用的好处。 <br/>
<br/>
上面一种写法,把这种Response manipulate的设计上升到一个架构的高度,但是后一种是比较聪明的实用的角度。 <br/>
<br/>
两者,放在不同的环境里面,或许有各自的用处把。</div>
<p><br/>
<br/>
<br/>
<br/>
我选择第一种,</p>
<p>第一种也可以有灵活度,</p>
<p>如: ManupuateSupport.add(MyResponse...)<span>;</span></p>
<p><span>ManupuateSupport.addAndManipulate(MyResponse...)<span>;</span></span></p>
<p><span><span>还可以有batch或readConfig等等</span></span></p>
7 楼 firebody 2007-02-05  
光从这里的代码来看,看不出两个方案的优劣来,不过你的pair的方案确实存在“误用”<span><span>的嫌疑:  代码依赖于</span></span><span><span>ChainResponseManipulator这个类。<br/>
<br/>
但是,如果考虑别的</span></span>Service也需要你写的 功能的话,别人可能会这样调用:<br/>
<br/>
<ol class='dp-j' start='1'>
    <li class='alt'><span><span>MyResponse runService() {  </span></span></li>
    <li class=''><span>  MyResponse response = call web service;  </span></li>
    <li class='alt'><span>  cache response;  </span></li>
    <li class=''><span>  response = ManupilatorFactory.getManupulate(</span><span><span>ResponseManipulator .A,</span></span><span><span>ResponseManipulator .B) </span></span><span>.manipulate(response);  </span></li>
    <li class='alt'><span>}  <br/>
    </span></li>
</ol>
<br/>
这样的话,可能会发挥一点 <span><span>ResponseManipulator 的作用。 <br/>
<br/>
不过光从代码来看,可能也不会比你的方法少到哪里去。<br/>
<br/>
一种比较实在的写法可以这样:<br/>
<br/>
<br/>
</span></span> <br/>
<ol class='dp-j' start='1'>
    <li class='alt'><span><span>MyResponse runService() {  </span></span></li>
    <li class=''><span>  MyResponse response = call web service;  </span></li>
    <li class='alt'><span>  cache response;  <br/>
    </span></li>
    <li class='alt'><span>//first ly<br/>
    </span></li>
    <li class=''><span>  response = ManupuateSupport.manipuate1(response)</span><span>;</span></li>
</ol>
            //secondly
<ol class='dp-j' start='1'>
    <li class=''><span> response = ManupuateSupport.manipuate2(response)</span><span>;</span></li>
    <li class=''><span>  </span></li>
    <li class='alt'><span>} <br/>
    </span></li>
</ol>
<br/>
这样的写法一样可以达到代码复用的好处。 <br/>
<br/>
上面一种写法,把这种Response manipulate的设计上升到一个架构的高度,但是后一种是比较聪明的实用的角度。 <br/>
<br/>
两者,放在不同的环境里面,或许有各自的用处把。
6 楼 pupi 2007-02-04  
引用
那么在面对把两个对象连接成一个对象的时候,我们是:obj1.toString()+obj2.toString()呢?还是:new StringAppender(new Object[]{obj1, obj2}).toString()呢


感觉还是用StringAppender好些。
并非是因为有了StringAppender才想到要用这个,而是因为有了
obj1.toString()+obj2.toString()+obj3.toString()+...+objn.toString()

这样的表达式,才会想到要写一个StringAppender类的。
5 楼 hermitte 2007-02-04  
这种情况很经常遇到,一般是根据项目的规模和时间来决定

刚开始编程的时候,我是吸收的J道那里的banq的思路。
就是在自己的业务层中,只调用更下层的代码,而不是复用已经有了的同层代码。

如果业务简单,就自己写一遍。

如果必须引用已经有了的同层代码,因为度量和减少代码冗余以便维护的缘故,我倾向于用Adapter或者Mediater模式,用接口封装和重构同层的代码。而不是直接引用其它人写的包的东西。

引用之前,把别人的类给包装下先。因为不知道其它人在编写代码的时候,会不会修改已经编写好的类,如果你依赖了他,然而在不知情的情况下,其它人又对你依赖的类进行了修改,就会非常麻烦。所以用结构型设计模式进行下解耦,是面向对象编程的一个基本功。

如果是原来,我倾向你的解决方案。因为那个符合“好莱坞原则”还有Banq的无为的道的思想。那个思想对我影响比较深刻。而且你的代码是高聚合,低耦合的。毕竟复用和耦合,在某种情况下是存在矛盾的。



不过现在我觉得OO这套有点过时了,有时候OO到成了代码复用的障碍,那个第二个应该用的是IOC的思路在里面,不过问题是由于JAVA接口方面的限制,反而导致耦合的加强,这个应该是JAVA语法的原因,不是编程的原因,毕竟依赖的只是接口而已。
4 楼 dennis_zane 2007-02-04  
如果从编码风格一致性来讲,既然已经有现成的基础设施,那使用既有代码无可厚非,而且从维护者角度考虑,一致的写法更为友好。
3 楼 galaxystar 2007-02-04  
已经有轮子了,嫌老的轮子磨损太严重!那么可以重新造一只!
成本论,如果在赛场上,你领先其他人很多圈,那么有时间造一只全新的,也无可厚非!
但是如果你没时间,还是跑完全程再说吧!

复杂程度来说,前者肯定复杂!
2 楼 bencode 2007-02-04  
我同意 pojo 这一观点
不过我觉得从代码的维护角度讲, 简单的那种很容易让人理解.
而采用现成的 Manipulator , 写出来的代码, 让人看起来有点点 "玄".
1 楼 pojo 2007-02-04  
我觉得两种做法是半斤八两。如果系统没有现成的Manipulator的,那当然用简单的。现在已经有了Manipulator,少用一次多用一次并不改变依赖性。无可无不可的事,当然就是谁主导就听谁的,否则一起共事会很难过的。

相关推荐

Global site tag (gtag.js) - Google Analytics