John Jiang

a cup of Java, cheers!
https://github.com/johnshajiang/blog

   :: 首页 ::  :: 联系 :: 聚合  :: 管理 ::
  131 随笔 :: 1 文章 :: 530 评论 :: 0 Trackbacks
编写好的面向对象代码

    本文是java.net上的一篇博客,作者Curtis Cooley对编写好的面向对象代码有些建议,希望对大家都有所帮助。(2008.10.08最后更新)

获取经验没有捷径。编写好的面向对象代码需要经验,但这儿有三种做法能帮你在一开始就很顺利,即便你是老顽固:
    1. 使用测试驱动开发(TDD)编写你所有的代码
    2. 遵循简单法则
    3. 告之而非问之

使用TDD编写所有代码
    按测试先行编写的代码与按测试后行编写的代码是极为不同的代码。按测试先行编写的代码是松耦合与高聚合的。当某个属性或私有方法需要暴露给测试程序时,按测试后行编写的代码常会打破封装,因为该类并不是为了测试而设计的。如果你首先编写测试代码,你的依赖将会更好,你的代码将是松耦合与高聚合的。后面会有更多关于测试能帮助你设计更佳代码的内容。

遵循简单法则
    代码是简单的,只要当它:
    1. 执行了所有的测试
    2. 不包含重复
    3. 表达了所有的意图
    4. 使用最少的类和方法
注意到我用的是个被排序了的列表是很重要的。顺序是重要的。只有一个main()方法的的GodClass[1]不会是简单的。这个类可能执行了所有的测试,但在任何比"Hello, world!"更复杂的程序中,它肯定包含了重复,并且也没有表达出全部的意图。
我努力使用简单法则去关注If问题。我不知道如何使用简单法则去阻止某人编写重量级的If代码。有人可能会提出不同意见,我也尝试过,但这样的重量级If代码确实无法表达意图。但当你阅读如下代码时

if (mobile.getType() == MobileTypes.STANDARD) {
  alert();
}
确实难以看出其中的意图。这些代码无论处于哪个方法的上下文环境中,我们都能知道,如果mobile是STANDARD类型的话,那么就报警。而你所需要的更多意图呢?
我还有一点儿灵感显现。如果有那样的代码,那么在其它地方肯定还会有更多那样的代码。这些代码可能就像:
if (mobile.getType() == MobileTypes.GAS) {
  registerGasReading();
}

if (mobile.getType() == MobileTypes.TEXT) {
  sendTextMessage();
}

if (mobile.getType() == MobileTypes.LOCATION) {
  notifyLocation();
}
你看出来了吗?我是看出来了。它违反了规则2,有很多地方都违反了规则2,并且是一种最坏的情形。这段代码有多处重复。重复将极难发现。所以,请帮助防止这种情形的发生,我已包含其中了。

告之而非问之
简言之,告之而非问之意指不要先问一个对象的状态,然后才让它去工作。而应该告之对象如何去工作。这就意味着之前所有的那些If例子应该变为:

mobile.alert();

mobile.registerGasReading();

mobile.sendTextMessage();

mobile.notifyLocation();
现假设遍布该程序中的一些If语句块有重复的实现。在"重量级If"版本的程序中,可能很难发现它们;但在"告之而非问之"版本的程序中,所有的实现都在Mobile中。所有的实现都在一处,这就便于察觉并根除问题。
    倾听你的测试程序也能帮助你保持代码的简洁。

public interface Alarm {
  
void alert(Mobile mobile);
}

public class Siren implements Alarm {
  
public void alert(Mobile mobile) {
    
if (mobile.getType == MobileTypes.STANDARD) {
      soundSiren();
    }
  }
}

public class TestSiren extends TestCase {
  
public void test_alert() {
    LocationMobile mobile 
= new LocationMobile();
    Siren siren 
= new Siren();
    siren.alert(mobile);
    
assert(sirenSounded());
  }
}
如果你密切地倾听测试程序,它可能会问你,"为什么你需要一个LocationMobile去测试Siren呢?"的确,为什么呢?看起来,Siren应该还不知道LocationMobile吧。
public class LocationMobile {
  
private Alarm alarm;
  
public LocationMobile(Alarm alarm) {
    
this.alarm = alarm;
  }
  
public void alert() {
    alarm.alert(); 
// alert on Alarm no longer needs a mobile
  }
}

public class TestLocationMobile() extends TestCase {
  
public void test_alert() {
    Alarm alarm 
= EasyMock.createMock(Alarm.class);
    alarm.alert();
    EasyMock.replay(alarm);
    Mobile mobile 
= new LocationMobile(alarm);
    mobile.alert();
    EasyMock.verify(alarm);
}
好像我只是交换了依赖关系。Alarm不再依赖Mobile,现在是Mobile依赖Alarm。但如果你仔细地观察这个测试程序,你会发现真正的依赖关系是,Siren知晓了LocationMobile。一个具体类依赖另一个具体类,这违反了依赖反转原则(DIP)。第二个例子就让LocationMobile依赖Alarm接口。具体类依赖抽象,这就满足DIP了。
    如果你使用TDD,并遵循简单法则和告之而非问之原则去编写所有的代码,你就处于成为一个更好的面向对象程序员的道路上了。好的面向对象代码易于阅读和维护,但难以编写,至少,在开始时是这样的。你写的越多,你就会变得越好,也会获得更多的经验。同时,这些实践经验也会使你在自己的道路上受益匪浅。

译注
[1]GodClass(上帝类)指包含了太多内容的类。
posted on 2008-10-07 17:06 John Jiang 阅读(1854) 评论(7)  编辑  收藏 所属分类: Java翻译UnitTestMethodology

评论

# re: 编写好的面向对象代码(译) 2008-10-08 08:57 Jack.Wang
very good, well done boy!  回复  更多评论
  

# re: 编写好的面向对象代码(译) 2008-10-08 09:05 Sha Jiang
Thanks :-  回复  更多评论
  

# re: 编写好的面向对象代码(译) 2008-10-08 11:15 大卫
Thank you very much!  回复  更多评论
  

# re: 编写好的面向对象代码(译) 2008-10-08 12:42 Sha Jiang
@大卫
我对TDD也没什么研究,兴趣倒是有一些。
但在实际的工作中,我没有应用过TDD,只是使用过单元测试罢了。  回复  更多评论
  

# re: 编写好的面向对象代码(译) 2008-10-10 15:37 战争与和平
实话实说,没太明白。
1. 告之而非问之:不用if怎么进行流程控制?
2. 交换依赖关系的描述明白,不过例子不甚明白。  回复  更多评论
  

# re: 编写好的面向对象代码(译) 2008-10-11 10:09 大卫
@战争与和平
其实就是一个再封装的问题,将复杂的东西封装起来。简化上层逻辑并使之清晰。  回复  更多评论
  

# re: 编写好的面向对象代码(译) 2008-10-19 15:02 谢亚力·帕它
活着正好
  回复  更多评论
  


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


网站导航: