重构手法之简化函数调用【6】
本小节目录
13Replace Error Code with Exception(以异常取代错误码)
概要
某个函数返回一个特定的代码,用以表示某种特定的情况。改用异常。
动机
异常能清楚地将“普通程序”和“错误处理”分开了,这使得程序更容易理解。
代码的可理解性应该是我们虔诚追求的目标。
范例
class Account { /// <summary> /// 余额 /// </summary> private int _balance; /// <summary> /// 取款 /// </summary> /// <param name="amount">取款金额</param> /// <returns></returns> public int Withdraw(int amount) { if (amount > _balance) { return -1; } _balance -= amount; return 0; } public bool CanWithdraw(int amount) { return amount <= _balance; } public void HandOverdran() { } public void DoTheUsualThing() { } }
为了让这段代码使用异常,首先决定使用受控异常还是非受控异常。关键在于:调用者是否有责任在取款之前检查存款余额,还是应该由Withdraw()函数负责检查。如果“检查余额”是调用者的责任,那么“取款金额大于存款金额”就是一个编程错误,应该使用非受控异常。如果“检查余额”是Withdraw()函数的责任,就必须在函数中抛出这个异常。
范例:非受控异常
使用非受控异常就表示应该由调用者负责检查。首先需要检查调用端的代码,它不应该使用Withdraw()函数的返回值,因为返回值只是用来指出程序员的错误。如果看到这样的代码:
Account account = new Account(); if (account.Withdraw(100) == -1) { account.HandOverdran(); } else { account.DoTheUsualThing(); }
应该将它替换成这样的代码:
Account account = new Account(); if (!account.CanWithdraw(100)) { account.HandOverdran(); } else { account.Withdraw(100); account.DoTheUsualThing(); }
现在移除错误码,并在程序出错时抛出异常。由于这种行为是异常的、罕见的,所以使用卫语句检查这种情况:
public void Withdraw(int amount) { if (amount > _balance) { throw new ArgumentException("Amount too large."); } _balance -= amount; }
范例:使用受控异常
受控异常的处理方式略有不同。首先可以新建一个合适的异常:
class BalanceException:Exception { }
当然了,这里不新建也是可以的。
然后调整调用端如下:
Account account = new Account(); try { account.Withdraw(100); account.DoTheUsualThing(); } catch (BalanceException ex) { account.HandOverdran(); }
接下来修改Withdraw()函数,让它以异常表示错误状况:
public void Withdraw(int amount) { if (amount > _balance) { throw new BalanceException(); } _balance -= amount; }
小结
将错误码替换成异常之后,使得代码更容易理解。
15Replace Exception with Test(以测试取代异常)
概要
面对一个调用者可以预先检查的条件,你抛出了一个异常。
修改调用者,使它在调用函数之前先做检查。
动机
异常可以协助我们避免很多复杂的错误处理逻辑。但是,异常也会被滥用。“异常”只应该被用于异常的、罕见的行为,也就是那些产生意料之外的错误的行为,而不应该成为条件检查的替代者。
范例
下面的例子中,以一个ResourcePool对象管理一些创建代价高昂而又可以重复使用的资源。这个对象带有两个“池”:一个用以保存可用资源,一个用以保存已分配资源。当用户请求一份资源时,ResourcePool对象从“可用资源池”中取出一份资源交出,并将这份资源转移到“已分配资源池”。当用户释放一份资源时,ResourcePool对象就该将资源从“已分配资源池”放回“可用资源池”。如果“可用资源池”不能满足用户的请求,ResourcePool对象就创建一份新资源。
class ResourcePool { private Stack<Resource> _available; private Stack<Resource> _allocated; public Resource GetResource() { Resource result; try { result = _available.Pop(); _allocated.Push(result); return result; } catch (Exception ex) { result = new Resource(); _allocated.Push(result); return result; } } } class Resource { }
在这里,“可用资源用尽”并不是一件意料外的事件,因此不该使用异常表示这种情况。
为了去掉这里的异常,首先添加一个适当的提前测试,并在其中处理“可用资源为空”的情况:
class ResourcePool { private Stack<Resource> _available; private Stack<Resource> _allocated; public Resource GetResource() { Resource result; if (_available.Count == 0) { result = new Resource(); _allocated.Push(result); return result; } result = _available.Pop(); _allocated.Push(result); return result; } } class Resource { }
在这里,可以对条件代码加以整理,使用Consolidate Duplicate Conditional Fragments:
class ResourcePool { private Stack<Resource> _available; private Stack<Resource> _allocated; public Resource GetResource() { Resource result; if (_available.Count == 0) { result = new Resource(); } else { result = _available.Pop(); } _allocated.Push(result); return result; } } class Resource { }
小结
阶段性小结
在对象技术中,最红要的概念莫过于“接口”。容易被理解和被使用的接口,是开发良好面向对象软件的关键。
最简单也最重要的一件事就是修改函数名称。名称是程序写作者与阅读者交流的关键工具。只要能理解一段程序的功能,就应该大胆地使用Rename Method将所知道的东西传达给他人。
函数参数在接口中扮演十分重要的角色。Add Parameter和Remove Parameter都是很常见的重构手法。刚接触面向对象技术的程序员往往使用很长的参数列。但是,使用对象技术,可以保持参数列的简短。如果来自同一个对象的多个值被当做参数传递,可以运用Preserve Whole Object将它们替换为单一对象,从而缩减参数列。如果此前并不存在这样一个对象,可以运用Introduce Parameter Object将它创建出来。如果函数参数来自该函数可获取的一个对象,则可以使用Replace Parameter with Methods避免传递参数。如果某些参数被用来在条件表达式中做选择依据,可以实施Replace Parameter with Explicit Method。另外,还可以使用Parameterize Method为数个相似函数添加参数,将它们合并到一起。
明确地将“修改对象状态”的函数和“查询对象状态”的函数分开设计是一个很好的习惯。如果看到这两种函数混在一起,可以使用Separate Query from Modifier将它们分开。
良好的接口只向用户展现必须展现的东西。如果一个接口暴露了过多细节,可以将不必要暴露的东西隐藏起来,从而改进接口的质量。进行重构时,往往需要暂时暴露某些东西,最后再以Hide Method和Remove Setting Method将它们隐藏起来。
构造函数往往比较“麻烦”,因为它强迫你必须知道要创建的对象属于哪个类,而你往往并不需要知道这一点。可以使用Replace Constructor with Factory Method避免了这不必要的信息。
和许多现代编程语言一样,C#也有异常处理机制,这使得错误处理相对容易一些。不习惯使用异常的程序员,往往会以错误码表示程序遇到麻烦。可以使用Replace Error Code with Exception来运用新的异常特性。但有时候异常也并不是最合适的选择,应该实施Replace Exception with Test先测试一番。
To Be Continued……