《代码整洁之道》阅读笔记 3函数

647 阅读8分钟

"Function should do one thing. They should do it well. They should do it only. "(函数只应该做一件事情,把一件事情做好,而且只由它来做这一件事情在这里插入图片描述

1.短小

  • 1.函数的第一规则是要短小,第二条规则是还要更短小20行封顶最佳

  • 2.if语句、else语句、while语句等,其中的代码块应该只有一行,该行大抵是一个函数调用语句 ,块内调用的函数应拥有较具说明性的名称。(从而增加了文档上的价值

  • 3.函数不应该大到足以容纳嵌套结构,所以,函数的缩进层级不该多于一层或两层

2.只做一件事

  • 1.函数应该做一件事。做好这件事,只做这一件事 。 刚开始学习编程讲函数就是用来做一件事的,可是真正做到的大概就微乎其微了。

  • 2.如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。

  • 3.要判断函数是否不止做了一件事,就是看看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现

  • 4.只做一件事的函数无法被合理地切分为多个区段

3.每个函数一个抽象层级

  • 1.要确保函数只做一件事,函数中的语句都要在同一抽象层级上

  • 2.自顶向下读代码:向下规则,让代码拥有自顶向下的阅读顺序,让每个函数后面都跟着下一抽象层级的函数(最下面有优美的代码)。

4.switch语句

  • 1.写出短小的switch语句很维,写出只做一件事的switch语句也很难,Switch天生要做N件事 。

  • 2.将switch语句埋到抽象工厂底下,不让任何人看到。

  • 3.如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍。

示例:

public Money calculatePay(Employee e) throws InvalidEmployeeType{
    switch (e.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.type);
    }
}

该函数(包括它调用的其他函数)问题:

  • 第一,它太长,当出现新的雇员类型时还会变得更长。
  • 第二,它明显做了不止一件事。
  • 第三,它违反了单一权责原则(Single Responsibility Principle, SRP), 因有好几个修改它的理由。
  • 第四,它违反了开放闭合原则(Open Closed Principle, OCP), 因为每当添加新类型时,就必须修改之。不过,最麻烦的是每个调用的函数中都会有类似结构的函数,如:isPayday(Employee e, Date date)或deliverPay(Employee e, Money pay)。

解决方案是多态将switch语句埋到抽象工厂底下 。calculatePay、isPayday和deliverPay等函数则由Employee接口多态地接受派遣。

public abstract class Employee{
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
-------------另一个文件中-----------
public interface EmployeeFactory{
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-------------另一个文件中-----------
public class EmployeeFactoryImpl implements EmployeeFactory{
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType{
        switch (r.type){
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

5.使用描述性的名称

  • 1.沃德原则:“如果每个例程都让你感到深合已意,那就是整洁代码”。

  • 2.函数越短小,功能越集中,就越便于取个好名字。

  • 3.别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好 。

  • 4.命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。

includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage。这些名称使用了类似的措辞,依序讲述了一个故事

6.函数参数

  • 1.最理想的参数数量是零,有足够的理由才能用三个以上参数。

  • 2.事件:在这种形式中,有输入参数而无输出参数,程序将函数看作一个事件,使用该参数修改系统状态。

  • 3.对于转换,使用输出参数而非返回值令人迷惑,如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。

  • 4.向函数传入布尔值会使方法签名立刻变得复杂起来,大声宣布函数不止做一件事。

  • 5.如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。

  • 6.有可变参数的函数可能是一元、二元甚至三元,超过这个数量就可能要犯错了。

  • 7.对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。

7.无副作用

  • 1.函数承诺只做一件事,但还是会做其他被藏起来的事,会导致古怪的时序性耦合及顺序依赖

自己写的一个函数名叫做 requestOrganList() ,意为请求机构列表信息,在处理响应结果的时候顺便增加了一个默认选择第一个机构的功能。多亏今天遇到的 bug 让我发现了这个问题。

  • 2.参数多数会被自然而希地看作是函数的输入。

8.分割指令与询问

函数要么做什么事,要么回答什么事,但二者不可得兼

set 和 get 大概是最能说明的两个函数了。

示例

bool set(int x, int val){
    if (vis[x]) {
        return 0;
    } else {
        vis[x] = val;
        return 1;
    }
}
int main(){
    if (set(5, 1)) {
        printf("error\n");
    } else {
        printf("succsee\n");
    }
    return 0;
}

但是这样这个函数其实是做了两个事情,判断和插入。其实应该修改为这样的形式:

bool isVisisted(int x){
    return vis[x];
}
void set(int x, int val){
    vis[x] = val;
}
int main(){
    if(isVisisted(5)){
        printf("error\n");
    } else {
        set(5, 1);
        printf("success\n");
    }
    return 0;
}

9.使用异步替代返回错误码

  • 1.从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在if语句判断中 ==把指令当作表达式== 使用。
if (deletePage(page) == E_OK)
  • 2.try/catch代码块把错误处理与正常流程混为一谈,最好把try和catch代码块的主体部分抽离出来,另外形成函数 .错误处理就是一件事,处理错误的函数不该做其他事 。
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("configKey not deleted");
	} 
  } else {
  	logger.log("deleteReference from registry failed");
  }
} else {
  logger.log("delete failed");
  return E_ERROR;
}

改为:

try {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
  logger.log(e.getMessage());
}
  • 3.如果使用异常代替返回错误码,错误处理代码就能从主路径中分离出来,得到简化
  • 4.依赖磁铁(dependency magnet):其他许多类都得导入和使用它。
public enum Error{
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WAITING_FOR_ENVENT;
}
  • 5.当返回错误码时,就是在要求调用者立刻处理错误

优势

  • 错误不可忽略,必须显式处理
  • 不需要额外的措施就能传播到上层
  • 可以携带更丰富的信息
  • 常错误码在不同的库中有不同的定义,必须手动转换成统一的形式,而异常不需要这样的转换

10.别重复自己

重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为控制与消除重复而创建

11.结构化编程

  • 1.每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或者continue语句,而且永永远远不能有任何的goto语句

  • 2.只有在大函数中这些规则才会有明显好处,因为,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,goto语句尽量避免

12.如何写出这样的函数

  • 1.打磨代码,分解函数、修改名称、消除重复

  • 2.缩短和重新安置方法、拆散类、保持测试通过

13.小结

编程艺术是且一直就是语言设计的艺术

真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。

14.附录

package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki .;

public class SetupTeardownIncluder{

    private PageData pageData;
    private boolean isSuite;
    private WikiPage testPage;
    private StringBuffer newPageContent;
    private PageCrawler pagecrawler;
    
    public static String render(pageData) throws Exception {
        return render(pageData, false);
    }
    
    public static String render(PageData pageData, boolean isSuite)throws Exception{
        return new SetupTeardownIncluder(pageData).render(isSuite);
    }

    private SetupTeardownIncluder(PageData pageData){
        this.pageDatapageData;
        testPage=pageData.getWiki Page);
        pageCrawler=testPage.get PageCrawler();
        newPageContent new StringBuffer();
    }
    
    private String render(boolean isSuite)throws Exception {
        this.isSuite isSuite;
        if (isTestPage())
            includeSetupAndTeardownPages();
        return pageData.getHtml();
    }
    
    private boolean isTestPage)throws Exception{
        return pageData.hasAttribute("Test");
    }
    
    private void includeSetupAndTeardownPages()throws Exception{
        includeSetupPages();
        includePageContent();
        includeTeardownPages();
        updatePageContent();
    }
    
    private void includeSetupPages throws Exception{
        if(isSuite)
            includeSuiteSetupPage);
        includeSetupPage();
    }
    
    private void includeSuiteSetupPage()throws Exception{
        include(SuiteResponder.SUITE_SETUP_NAMB,"-setup");
    }
    
    private void includeSetupPage()throws Exception{
        include("SetUp","-setup");
    }
    
    private void include PageContent(throws Exception {
        newPageContent.append(pageData.getcontent());
    }
    
    private void includeTeardownPages(throws Exception {
        includeTeardownPage();
        if (isSuite)
            includeSuiteTeardownPage();
    }
    
    private void includeTeardownPage(throws Exception {
        include("TearDown", "-teardown");
    }
    
    private void includeSuiteTeardownPage()throws Exception {
        include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
    }
    
    private void updatePageContent)throws Exception{
        pageData.setContentnewPageContent.tostring());
    }
    
    private void include(String pageName,string arg)throws Exception{
        WikiPage inheritedPage findInheritedPag(pageName);
        if(inheritedPage=null){
            string pagePathName-getPathNameFor(inheritedPage);
            buildIncludeDirective(page PathName,arg):
        }
    }
    
    private WikiPage findInheritedPage(String pageName)throws Exception {
        return PageCrawlerImpl.getInheritedPa(pageName, testPage);
    }
    
    private String getPathName ForPage(WikiPage page)throws Exception {
        WikiPagePath pagePath pageCrawler.getFullPath(page);
        return PathParser.render(pagePath);
    }
    
    private void buildIncludeDirective(String pagePathName,String arg){
        newPagecontent
        .append("n! include")
        .append(arg)
        .append(".")
        .append(page PathName)
        .append("\n");
    }
}

15.参考文献

cloud.tencent.com/developer/a… www.zybuluo.com/king/note/6… theprimone.top/2019/04/08/… www.jianshu.com/p/c4aab60da…

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享