代码中应该怎么写函数

目录

 

函数

短小

代码块和缩进

只做一件事

函数中的区段

每个函数一个抽象层级

switch语句

使用描述性的名称

函数参数

一元函数的普遍形式

标识参数

二元函数

三元函数

参数对象

参数列表

动词和关键字

无副作用

输出参数

分隔指令与询问

使用异常替代返回错误码

抽离try/catch代码块

错误处理就是一件事

依赖磁铁

别重复自己

结构化编程


函数

短小

函数的第一规则是短小。第二条规则是更短小。程序的每个函数都应为2-4行长。每个函数都一目了然。每个函数都只做一件事。而且每个函数都依序把你带到下一个函数。

代码块和缩进

if、else、while语句,其中的代码块应该只有一行。该行应为函数调用语句。函数具有说明性的名称,以增加阅读性。

只做一件事

函数应该做一件事。做好这件事,只做这一件事。

拆分方法:

  1. 一个抽象层级对应一个函数,一个函数只做一件事。
  2. 看是否能拆出一个函数,该函数不仅只是单纯地诠释其实现。G34123???????????

函数中的区段

只做一件事的函数无法被合理地且分为多个区段。

每个函数一个抽象层级

自顶向下读代码:向下规则。——让每个函数后面都跟着位于下一抽象层级的函数。这样一来,在查看函数列表时,就能循抽象层级向下阅读了。

switch语句

避开switch语句是不可能的,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复——利用多态来实现这一点。

整理前的代码

public Money CalculatePay(Employee e)
{

    switch (e.GetType())
    {
        case COMMISSIONED:
            return CalculateCommissionedPay(e);
        case HOURLY:
            return CalculateHourlyPay(e);
        case SALARIED:
            return CalculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.GetType());
    }
}

此代码的问题:

  1. 如有新的雇员,会加长。
  2. 不止做了一件事
  3. 违反了单一职责原则(SRP)
  4. 违反了开放闭合原则(OCP)

使用多态的代码

public abstract class Employee
{
    public abstract bool IsPayDay();
    public abstract Money CaculatePay();
    public abstract void DeliverPay(Money pay);
}

public interface IEmployeeFactory
{
    Employee MakeEmployee(EmployeeRecord r);
}

public class EmployeeFactory : IEmployeeFactory
{
    public Employee MakeEmployee(EmployeeRecord r)
    {
        switch (r.GetType())
        {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return HourlyEmployee(r);
            case SALARIED:
                return SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.GetType());
        }
    }
}

使用描述性的名称

函数越短小,功能越集中,就越便于去个好名字。

  1. 不怕长名称。长且具有描述性的名称要比短且令人费解的名称要好,也比描述性的长注释要好。
  2. 不怕花时间取名字。
  3. 描述性的名称能理清你关于模块的设计思路,并且帮你改进。
  4. 命名方式要一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如:IncludeSetupAndTearDownPages、IncludeSetupPages、IncludeSuiteSetupPages。这些名称使用了类似的措辞,依序讲出一个故事。

函数参数

最理想的参数是零,其次是一,再次是二,应尽量避免三。有足够特殊的理由才能用三个以上的参数。

一元函数的普遍形式

  1. 询问关于那个参数的问题,如:bool FileExists("myFile");
  2. 操作参数,或者转变为其他东西,如 FileStream FileOpen("myFile");

标识参数

标识参数丑陋不堪。这样做方法名称会变得复杂起来。如果标识为true这样做,标识为false那样做。

例如,方法为Render(bool isSuite)一分为二:RenderForSuite()和RenderForSingleTest()。

二元函数

有意义的应用范围:

  1. 单个值的有序组成部分。如:Point p = new Point(0,0);
  2. 使用一些机制转为一元函数。如:
    1. 将WriteField写成OutputStream的成员之一:outputStream.WriteField(name)
    2. 将outputStream作为当前类的成员变量
    3. 分离出类似FieldWriter的新类,在其构造函数中调用outputStream,并且包含一个Write方法。

三元函数

三元函数会让人参数理解不全,尽量不要使用。

参数对象

可以将参数对象封装为参数对象,以达到减少参数的目的。如

Circle makeCircle(double x,double y,double radius);
Circle makeCircle(Point center,double radius);

参数列表

可变参数虽说是可变的,但认为是单参(params)。

动词和关键字

对于一元函数,函数和参数应当形成动词/名词对形式,如:WriteField(name),它告诉我们name是一个Field。

无副作用

public bool CheckPassword(string username, string password)
{
    User user = UserService.FindByName(username);
    if (user != null)
    {
        string codePhrase = user.GetPhraseEncodeByPassword();
        string phrase = cryptographer.Decrypt(codePhrase, password);

        if (phrase == "Valid Password")
        {
            Session.Initialize();
            return true;
        }
    }
    return false;
}

副作用在于Session.Initialize(),CheckPassword没有暗示需要初始化Session,语义不明确。应将CheckPassword改为CheckPasswordAndInitializeSession(),虽然还是违反了"只做一件事"的规则。

输出参数

如:appendFooter(str); 这个方法不清楚其意义,str是输出函数还是输入函数?最好是这样调用report.appendFooter(str);

分隔指令与询问

指令与询问合在一起:

public bool Set(string attribute,string value);

if(Set("username","unclebob"))
{
    ...
}

Set方法本意为:如果"username"属性值之前已被设置为"unclebob"。

指令与询问拆分开:

if(AttributeExists("username"))
{
    SetAttribute("username","unclebob")
}

使用异常替代返回错误码

 

if (DeletePage(page) == E_OK)
{
    if (registry.DeleteReference(page.Name) == E_OK)
    {
        if (configKeys.DeleteKey(page.Name.MakeKey()) == E_OK)
        {
            logger.Log("page deleted");
        }
        else
        {
            logger.Log("configKeys not deleted");
        }
    }
    else
    {
        logger.Log("DeleteReference from registry faild");
    }
}
else 
{
    logger.Log("deleteed faild");
    return E_ERROE;
}

使用异常替代返回错误码,将错误代码从主路径中分离出来。

try
{
    DeletePage(page);
    registry.DeleteReference(page.Name);
    configKeys.DeleteKey(page.Name.MakeKey());
}catch(Exception e)
{
     logger.Log(e);
}

抽离try/catch代码块

最好将try和catch代码主体部分抽离出来,另外形成函数。

public Delete(Page page)
{
    try
    {
        DeletePageAndAllRefences(page);
    }catch(Exception e)
    {
        LogError(e);
    }
}

private void DeletePageAndAllRefences(Page page)
{
    DeletePage(page);
    registry.DeleteReference(page.Name);
    configKeys.DeleteKey(page.Name.MakeKey());
}

private void LogError(Exception e)
{
    logger.Log(e);
}

错误处理就是一件事

函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。

依赖磁铁

返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。

public enum Error
{
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WAITING_FOR_EVENT
}


这样的类就是一块依赖磁铁(dependency magnet);其他许多类都得导入和使用它。当Error枚举修改时,所有这些其他的类都需要重新编译和部署。这对Error类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们就复用旧的错误码,而不添加新的。

使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。(也是OCP的一个范例)

别重复自己

当你重复了4次,当算法改变是需要修改4处。而且也会增加4次放过错误的可能性。

结构化编程

结构化编程认为:每个函数、函数中的每个代码块应该只有一个入口、一个出口(单入单出原则)。意味只能有一个return语句,不能有break、coutinue,永远不能有goto。——应用在大函数中。

只要函数保持短小,那么偶尔出现的return、break、coutinue没有坏处。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章