Fork me on GitHub

重构手法之简化函数调用【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 ParameterRemove Parameter都是很常见的重构手法。刚接触面向对象技术的程序员往往使用很长的参数列。但是,使用对象技术,可以保持参数列的简短。如果来自同一个对象的多个值被当做参数传递,可以运用Preserve Whole Object将它们替换为单一对象,从而缩减参数列。如果此前并不存在这样一个对象,可以运用Introduce Parameter Object将它创建出来。如果函数参数来自该函数可获取的一个对象,则可以使用Replace Parameter with Methods避免传递参数。如果某些参数被用来在条件表达式中做选择依据,可以实施Replace Parameter with Explicit Method。另外,还可以使用Parameterize Method为数个相似函数添加参数,将它们合并到一起。

明确地将“修改对象状态”的函数和“查询对象状态”的函数分开设计是一个很好的习惯。如果看到这两种函数混在一起,可以使用Separate Query from Modifier将它们分开。

良好的接口只向用户展现必须展现的东西。如果一个接口暴露了过多细节,可以将不必要暴露的东西隐藏起来,从而改进接口的质量。进行重构时,往往需要暂时暴露某些东西,最后再以Hide MethodRemove Setting Method将它们隐藏起来。

构造函数往往比较“麻烦”,因为它强迫你必须知道要创建的对象属于哪个类,而你往往并不需要知道这一点。可以使用Replace Constructor with Factory Method避免了这不必要的信息。

和许多现代编程语言一样,C#也有异常处理机制,这使得错误处理相对容易一些。不习惯使用异常的程序员,往往会以错误码表示程序遇到麻烦。可以使用Replace Error Code with Exception来运用新的异常特性。但有时候异常也并不是最合适的选择,应该实施Replace Exception with Test先测试一番。

 

 

To Be Continued……

posted @ 2017-12-05 08:52  NaYoung  阅读(717)  评论(0编辑  收藏  举报