乐者为王

Do one thing, and do it well.

一开始就编写优质的OO代码

英文原文:https://weblogs.java.net/blog/2008/10/03/writing-great-oo-code-day-one

没有获取经验的捷径。编写良好的面向对象代码需要经验。尽管如此,这里有三个实践可以帮助你一开始就有个良好的开端:

  1. 使用测试驱动开发(TDD)编写你所有的代码
  2. 遵循简单的规则
  3. 命令代替询问(Tell Don't Ask)

使用TDD编写你所有的代码

测试先行编写的代码和测试后行编写的代码是非常非常不同的代码。测试先行编写的代码是松耦合和高内聚的。测试后行编写的代码往往会破坏封装,当一些属性或私有方法需要被暴露给测试的时候,因为这些类没有被设计成要被测试的。如果你编写的代码测试先行,代码的依赖性会更好,你的代码将是松耦合和高内聚的。稍后详细讨论测试如何帮助你设计更好的代码。

遵循简单的规则

代码是简洁的,当它:

  1. 通过所有的测试
  2. 不包含重复代码
  3. 表达了所有的意图
  4. 使用了最少的类和方法

重要的是注意到我使用了一个有序列表。顺序很重要。带有单一main()方法的单一GodClass并不简单。它可以通过所有的测试,但在比“Hello, world!”更复杂的任何程序里它一定会包含重复代码和没有表达所有的意图。

我与简单的规则的斗争重点围绕在If Bug 。我不明白遵循简单的规则如何阻止某人编写大量的if代码。有人会说,我试过了,大量的if代码不会表达意图。但是,当你读到这样的代码

1
2
3
if (mobile.getType() == MobileTypes.STANDARD) {
    alert();
}

它实在是太容易看出意图了。无论该代码是在哪个方法的上下文中,如果mobile是STANDARD类型,那么警报。你还需要多少意图?

然后我灵光小闪。如果有那样的代码,那么在代码的其它地方肯定还有更多。可能是这样的代码:

1
2
3
if (mobile.getType() == MobileTypes.GAS) {
    registerGasReading();
}

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

1
2
3
if (mobile.getType() == MobileTypes.LOCATION) {
    notifyLocation();
}

你看见了吗?我当然知道。违反规则2。许多许多违反规则2。并且是违反规则2的最糟糕的那种。重复代码在许多不同的代码片段中。重复代码将非常非常难被找到。所以为了帮助防止这个,我列出来了。

命令代替询问

命令代替询问意味着不要询问一个对象的状态然后做些什么。应该命令那个对象去做些什么。这意味着所有这些if例子变成了:

1
mobile.alert();

1
mobile.registerGasReading();

1
mobile.sendTextMessage();

1
mobile.notifyLocation();

现在假设有一些if子句散落在有重复实现的整个代码中。在那个大量if代码的版本中,它们将非常难被找到,但在命令代替询问版本中,所有的实现都在Mobile类中。所有的都在一个地方寻找和消除。

聆听你的测试也将帮助你保持代码简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 SirenTest extends TestCase {
    public void testAlert() {
        LocationMobile mobile = new LocationMobile();
        Siren siren = new Siren();
        siren.alert(mobile);
        assert(sirenSounded());
    }
}

如果你仔细聆听你的测试,它会问你,“你为什么需要LocationMobile去测试Siren?”是呀,为什么呢?似乎Siren甚至不应该知道LocationMobile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 LocationMobileTest extends TestCase {
    public void testAlert() {
        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编写你所有的代码,遵循简单的规则,以及命令代替询问,那么你会在那条成为一个更好的OO程序员的路上。良好的OO代码容易阅读和维护,但是可能难于编写。至少开始是这样。你写得越多,你将会变得更好,你将得到的经验也越多。与此同时,这些实践会让你在你的路上走得更好。

Comments