代码整洁之道核心纪要(二)

本文介绍了代码整洁的关键要素,包括避免冗余注释,提倡异常而非错误码处理,使用不可控异常,根据调用者需求定制异常类,遵循TDD原则,以及如何编写简洁高效的单元测试。通过实例演示了如何重构代码和优化测试结构。
摘要由CSDN通过智能技术生成

代码整洁之道核心纪要(二)

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.opencatchPortDeviceFailure 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三定律

  1. 编写不能通过的单元测试前,不可编写生产代码;
  2. 只可编写刚好无法通过的单元测试,不能编译也算不过;
  3. 只可编写刚好足以通过当前失败测试的生产代码。
    这三条定律会促使你在编写生产代码时,同时完成测试代码。这样写程序写出的测试足以覆盖所有生产代码。

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. 双重标准

在生产代码与测试代码中使用不同的标准,测试代码应当简单、精悍、足具表达力,并与生产代码一样有效。


  1. 关于开放闭合原则,其核心的思想是:
    软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
    因此,开放封闭原则主要体现在两个方面:
    对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
    对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值