目录
函数
短小
函数的第一规则是短小。第二条规则是更短小。程序的每个函数都应为2-4行长。每个函数都一目了然。每个函数都只做一件事。而且每个函数都依序把你带到下一个函数。
代码块和缩进
if、else、while语句,其中的代码块应该只有一行。该行应为函数调用语句。函数具有说明性的名称,以增加阅读性。
只做一件事
函数应该做一件事。做好这件事,只做这一件事。
拆分方法:
- 一个抽象层级对应一个函数,一个函数只做一件事。
- 看是否能拆出一个函数,该函数不仅只是单纯地诠释其实现。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());
}
}
此代码的问题:
- 如有新的雇员,会加长。
- 不止做了一件事
- 违反了单一职责原则(SRP)
- 违反了开放闭合原则(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());
}
}
}
使用描述性的名称
函数越短小,功能越集中,就越便于去个好名字。
- 不怕长名称。长且具有描述性的名称要比短且令人费解的名称要好,也比描述性的长注释要好。
- 不怕花时间取名字。
- 描述性的名称能理清你关于模块的设计思路,并且帮你改进。
- 命名方式要一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如:IncludeSetupAndTearDownPages、IncludeSetupPages、IncludeSuiteSetupPages。这些名称使用了类似的措辞,依序讲出一个故事。
函数参数
最理想的参数是零,其次是一,再次是二,应尽量避免三。有足够特殊的理由才能用三个以上的参数。
一元函数的普遍形式
- 询问关于那个参数的问题,如:bool FileExists("myFile");
- 操作参数,或者转变为其他东西,如 FileStream FileOpen("myFile");
标识参数
标识参数丑陋不堪。这样做方法名称会变得复杂起来。如果标识为true这样做,标识为false那样做。
例如,方法为Render(bool isSuite)一分为二:RenderForSuite()和RenderForSingleTest()。
二元函数
有意义的应用范围:
- 单个值的有序组成部分。如:Point p = new Point(0,0);
- 使用一些机制转为一元函数。如:
- 将WriteField写成OutputStream的成员之一:outputStream.WriteField(name)
- 将outputStream作为当前类的成员变量
- 分离出类似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没有坏处。