代码整洁之道核心记要(二)
1. 注释
别给糟糕的代码加注释,重新写吧
——Brian W. Kernighan与P. J. Plaugher
良好的注释有利于理解代码;糟糕的注释不利于理解代码。编写一段好的注释并非一劳永逸,更不意味着永远这段注释永远是好注释,因为代码常常在不断地更新、变化,而注释往往无法及时更新、调整,所以一段好的注释也可能变成一个坏的注释。时间越长的注释越有可能是一段糟糕的注释。只有代码本身才能准确描述这段代码的功能,与其试着去为代码写一段注释诠释功能,不如费心写好一段代码,利用代码来为代码做注释!
2. 错误处理
错误处理是简洁代码中很重要的一部分,如果不能处理好错误与异常,将很容易搞乱代码逻辑,使得代码中充斥着错误处理代码。
1. 使用异常而非错误码
- 检测错误码
public class DeviceController {
…
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
//Check the state of the device
if (handle != DeviceHandle.INVALID) {
//Save the device status to the record field
retrieveDeviceRecord(handle);
//If not suspended, shut down
if (record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handle);
clearDeviceworkQueue(handle);
closeDevice(handle);
} else {
logger.log("Device suspended. Unable to shut down");
}
} else {
logger.log("Invalid handle for: "+ DEV1.toString());
}
}
…
}
- 采用异常处理
public class DeviceController {
…
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShut DownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceworkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id) {
throw new DeviceShutDownError("Invalid handle for: "+ id.tostring());
}
}
可以看出,采用异常处理的代码,将错误处理与算法分隔开,使得代码更整洁。
2. 使用不可控异常
可控异常有好有坏,使用的标准取决于是否值得这么做。
在使用可控异常时,将会打破开放闭合原则1 ,使得在软件底层的修改,与之对应的高层类均要做修改。不仅如此,还会要求高层类知道底层类的异常细节,这严重违反了开放闭合原则。
3. 依调用者需要定义异常类
除了使用时定义异常,还需要考虑如何处理异常、捕获异常。比如有以下异常处理代码:
- 源代码
ACMEPort port = new ACMEPort(12);
try{
port.open();
}
catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
…
}
- 简化代码
```java
LocalPort port = new LocalPort(12)
try {
port.open;
catch(PortDeviceFailure e){
reportError(e);
logger.log(e.getMessage(),e);
} finally {
…
}
LocalPort.class
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
catch (GMXError e)
throw new PortDeviceFailure(e);
}
}
…
}
打包类非常好用,这可以降低对第三方API的依赖,在改用其他代码库时也不会很困难。也有利于测试自己的代码,模拟对第三方API的调用。
3. 单元测试
1. TDD三定律
- 编写不能通过的单元测试前,不可编写生产代码;
- 只可编写刚好无法通过的单元测试,不能编译也算不过;
- 只可编写刚好足以通过当前失败测试的生产代码。
这三条定律会促使你在编写生产代码时,同时完成测试代码。这样写程序写出的测试足以覆盖所有生产代码。
2. 保持测试整洁
如果测试不整洁,那么等于没有测试。测试可以保证生产代码可扩展、可维护、可复用,只要测试代码在,就不必担心修改生产代码导致故障。在为生产代码增加功能时,请记得同时重构测试。
3. 整洁的测试
在保持测试代码整洁时,也需要依照一定标准重构测试。测试代码的标准与生产代码不一样。测试代码要求简单、精悍、足具表达力。可以看两个代码例子:
- 源代码
public void testGetPageHieratchyAsXml() throws Exception
{
crawler.addPage(root,PathParser.parse("PageOne"));
crawler.addPage(root,PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root,PathParser.parse("PageTwo"));
request.setResource("root");
request.addInput("type","pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root),request);
String xml = response.getContent();
assertEquals("text/xml",response.getContentType());
assertSubString("<name>PageOne</name>",xml);
assertSubstring("<name>PageTwo</name>",xml);
assertSubstring("<name>ChildOne</name>",xml);
}
public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() throws Exception {
WikiPage pageOne = crawler.addPage(root,PathParser.parse("PageOne"));
crawler.addPage(root,PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root,PathParser.parse("PageTwo"));
PageData data = pageOne.getData();
WikiPageProperties properties = data.getProperties();
WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
symLinks.set("SymPage", "PageTwo");
pageOne.comimit(data);
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubstring("<name>ChildOne</name>", xml);
assertNotSubString("SymPage", xml);
}
public void testGetDataAsHtml() throws Exception {
crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");
request.setResource("TestPageOne");
request.addInput("type", "data");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubstring("test page", xml);
assertSubstring("<Test", xml);
}
可以从源代码中看出,在执行真正的测试之前,程序还进行了许多操作,与测试毫无关系,使得测试的真正细节被掩盖。
- 重构后
public void testGetPageHierarchyAsXml() throws Exception {
makePages("PageOne", "PageOne. ChildOne", "PageTwo");
submitRequest("root", "type: pages");
assertResponseIsXML();
assertResponseContains("<name>PageOne</name>", "<name> PageTwo</name>", "<name>ChildOne</name>");
}
public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
WikiPage page = makePage("PageOne");
makePages("PageOne. ChildOne", "PageTwo");
addLinkTo(page, "PageTwo", "SymPage");
submitRequest("root", "type: pages");
assertResponseIsXML();
assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
assertResponseDoesNotContain("SymPage");
}
public void testGetDataAsXml() throws Exception {
makePagewithContent("TestPageOne", "test page");
submitRequest("TestPageOne", "type: data");
assertResponseIsXML();
assertResponseContains("test page", "<Test");
}
可以看出,重构后的测试,显然呈现了构造-操作-检验(BUILD-OPERATE-CHECK)模式。每个测试都清晰地拆分为三个环节。第一个环节构造测试数据,第二个环节操作测试数据,第三个部分检验操作是否得到期望的结果。这些测试将与其无关的细节进行封装简化,更能突出测试的内容。
1. 面向特定领域的测试语言
从重构后的测试中可以看出,一些通用的操作被封装成API,进而简化那些令人迷惑的细节,突出测试内容。这些API并非一开始就被设计好,而是随着开发与重构被抽象成API,这样不仅有助于编写测试,更有助于阅读测试。这正是一种面向领域的测试语言。
2. 双重标准
在生产代码与测试代码中使用不同的标准,测试代码应当简单、精悍、足具表达力,并与生产代码一样有效。
关于开放闭合原则,其核心的思想是:
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
因此,开放封闭原则主要体现在两个方面:
对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。 ↩︎