错误处理
错误处理很重要,不应当搞乱代码逻辑。
1.使用异常而非返回码
早期有些语言不支持异常,所以只能用错误标识或者返回给调用者状态码。
错误码会搞乱调用者代码,因为:调用这个函数之后需要立马检查错误,根据状态码处理相应逻辑。
遇到错误时,最好抛出一个异常,调用代码整洁,其逻辑不会被错误处理搞乱。
2.先写try-catch-finally语句
在编写可能抛出异常的代码时,先写try-Catch-Finally语句;
3.使用不可控异常(unchecked Exception)
4.给出异常发生的环境说明
应创建信息充分的错误消息,并和异常传递出去。包括失败的操作、失败的类型。
5.依调用者需要定义异常类
在定义异常类时,最重要考虑是它们如何被捕获。
将第三方API 打包 是个良好的实践手段,当你打包一个第三方 API时,会降低对它的依赖:可以较为轻松地改动其他代码库。(因为打包,会使得依赖第三方代码的点相对比较集中,方便修改)
// bad Practice
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",e);
} finally {
// .....
}
以上的代码包含了一大堆重复的代码。可以通过打包调用API,让它返回通用的自定义异常类型( PortDeviceFailue
),从而简化代码。
// good practice
LocalPort port = new LocalPort(12);
try {
port.open();
} catch(PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(),e);
} finally {
// .....
}
public class LocalPort{
private ACMEPort innerPort;
public LocalPort(int portNumber){
innerPort = new ACMEPort(portNumber);
}
public 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);
}
}
}
6.定义常规流程
特例模式,创建一个类或配置一个对象,用来处理特例。
你来处理特例,客户代码不用应付异常行为,异常行为被封装到特例对象中。
7.别返回null值
只要一处没检查null值,应用程序就会失控。
如果你打算在方法中返回null值,不如抛出异常,或是返回特例对象。如果你在调用某个第三方API中可能返回null值的方法,可以考虑用新方法打包这个方法,在新方法中抛出异常或返回特例对象。
适当返回空列表,从而省略判空语句,从而避免NPE。
List<Employee> emoployees = getEmployees();
if(employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
public List<Employee> getEmployees() {
if(.. ) {
return Collections.emptyList();
}
}
List<Employee> employees = getEmployess();
for(Employee e : employees) {
totalPay += e.getPay();
}
8.别传递null值
除非API要求你向他传递null值,否则尽可能避免传递null值。
因为:如果允许传递null值,需要做各种非空判断,否则会产生NPE。
大多数编程语言中,没有良好的方法对付由调用者意外传入的null值,恰当的做法就是禁止传入null值。
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
}
calculator.xProjection(null, new Point(12, 13));
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
if(p1 == null || p2 == null) {
throw new InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection");
}
return (p2.x - p1.x) * 1.5;
}
}
这种需要为异常定义处理器
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null.";
assert p2 != null : "p2 should not be null."
return (p2.x - p1.x) * 1.5;
}
}
传入null值,还是会得到运行时错误。
边界
边界case:
1)使用外部代码(第三方库,其他模块)与自身项目代码结合;
2)自身项目调用外来代码
需要干净利落将这些代码整合,就能保持软件边界整洁
1.使用第三方代码
第三方jar包和框架追求通用性,使用者想要集中满足特定需求的接口(方便开发),这两者的差异,致使系统边界上可能会出现问题。
举个例子,以 java.util.Map为例。Map丰富的功能,但是应用程序可能构造一个Map对象并到处传递它的引用。
假设设计初衷是:Map对象的所有接收者都不要删除映射里的东西,但是实际情况是任何使用者都能通过clear方法清空Map。
系统中无节制的传递Map对象的引用,意味着如果Map提供的接口被修改时,有非常多的地方需要跟着改动。
使用Map更整洁的方式如下:
边界上的接口(Map)是隐藏的,它能随着应用程序其他部分的极小的影响而变动。把类型转换等操作提取到Sensor类作为一个方法,这样在接口发生改变时,只需要修改这一个方法。
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
}
总结:并不建议总以这种方式封装Map,不要将Map(或者边界上的其他接口)在系统中传递。应将其保留在类中,避免从公共API中返回边界或借口,或者将边界接口作为参数传递给公共方法。
2.使用尚不存在的代码
协同开发经常会碰到代码调用的API还没有处理好的情况,这种情况下定义好自己的接口和方法,一旦API被定义出来,可以用适配器模式进行衔接。
Adapter封装了与API的互动,也提供了一个当API发生变动时唯一需要改动的地方。
3.整洁的边界
代码中应该尽量少的了解第三方代码中的特定信息,即需要尽量去封装好第三方边界接口。
如同封装Map、适配器将我们的接口转换为第三方接口。
这两种方法,能使代码更好地与我们沟通,第三方代码改动的时候,自己的代码修改点会更少。