代码整洁之道【2】--函数

代码整洁之道,关于函数部分的总结

一、函数只做一件事

函数应该只做一件事、做好这件事、只做这件事。
判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新

二、函数尽量不要太长

按照作者的理论,函数长度20行封顶为最佳。
我的理解是,函数长度需要跟第一节的函数只做一件事结合起来,并不需要完全拘泥于20行的限制,只要函数在逻辑层次上不可再分解成新的函数(参见第三条),就可以。

三、每个函数一个抽象层次

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

我们想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。我把这叫做向下规则。

这里咱举个例子,加入你要对一个字符串进行一些处理(例如,append,substring等),然后再对处理后的字符串进行校验。
良好的实践应该类似如下:

String handledStr = handleStr(username);
validate(handledStr);

不是很优雅的实践类似如下:

String trimmedUserName = username.trim();
String handledStr = trimmedUserName.append("something");
validate(handledStr);

为什么说它不是最佳实践呢?因为前两行代码是关于对字符串进行处理的具体操作,应该把它们抽象成一个函数,这个抽象出来的函数是这两个具体操作的上一层概念,和validate方法同一层。

四、switch语句

写出精简的switch语句很难,写出只做一件事的switch语句也很难,它天生就要做N件事。我们无法避开switch语句,不过还是可以确保每个switch都放在较低的抽象层级,而且永远不重复。

利用多态实现switch的优化:

假设有下面的需求:根据雇员类型计算薪资。

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);
    }
}

这里仅仅依赖了雇员类型一种操作,就有好几个问题:

  1. 函数太长,当有新的雇员类型,还会更长。
  2. 违反开闭原则(OCP原则),每添加新类型,就必须修改它。
  3. 违反了单一权责原则,它做了多件事情。

更麻烦的是:到处都有类似的调用函数(传入的参数类似)。
比如可能多处调用
isPayday(Employee e, Date date);

deliverPay(Employee e, Money pay);

下面我们针对这样的问题进行优化。

对每个类都会有同样的操作,比如isPayday(), deliverPay()等,不如把类的行为抽象出来到一个抽象类Employee中。在抽象工厂中使用switch语句为Employee的派生物创建适当的实体。

对于switch语句,我们的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在其他系统看不到,就还能容忍。

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);
        }
    }
}

用优化后的代码,再有新的类型加入时,业务程序是不用修改的,因为类型已隐藏在了抽象类中,返回给业务的都是抽象的Employee,无需考虑类型的变化,只是调用抽象类的方法即可。我们需要改动的只是再创建一个抽象类的实体类,在EmployeeFactoryImpl中多加一个switch分支。

五、使用描述性的名称

长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。

六、函数参数

参数数量越少越好。 尽量不要有输出参数,而是将输出设置为返回值。 如果参数较多的时候可以考虑使用类进行封装。

七、无副作用

副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出没有预料到的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。

例如如下代码,改函数使用标准算法来匹配userName和passWord,如果匹配成功,返回true,如果匹配失败,返回false,但是它会有副作用:

副作用就在于对Session.initialize()调用。checkPassword函数,顾名思义,就是用来检查密码的。该名称并没有暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就会冒着抹除现有会话数据的风险。也就是说,这个副作用造成了一次时序性耦合。

八、分隔指令与询问

这实际上内生的包含于一个函数只做一件事的要求中,但是还是有必要单独指出。函数要么做什么事,要么回答什么事,但二者不可兼得。

函数应该修改某对象的状态,或者返回该对象的有关信息。两样都干会导致混乱。

举个例子:

if (set("username", "unclebob"))...

上面这个语句会让人迷惑:它是询问username属性之前是否已经被设置为unclebob了?还是在问username属性是否成功被设置为unclebob呢?从这行调用很难判断其含义。

要解决这个问题,可以按如下方式改造,防止混淆的发生:

if (attributeExists("username")) {
    setAttribute("username", "unclebob");
}

九、使用异常替代返回错误码

可减少过度嵌套(判断多种错误码及内层错误码)。
可减少对错误码枚举类的过度依赖(当修改了错误码枚举类时,所有依赖这个枚举类的其他类都得重新编译和部署)。

十、别重复自己

如果你发现某两个函数用到了相同甚至相近的代码块应该迅速思考是不是可以将其抽取成单独的函数。 重复就是万恶之源。

十一、结构化函数

Dijkstra认为,每个函数、每个代码块都应该只有一个入口一个出口。这意味着每个函数只能有一个return语句,循环中不能有break或continue,而且永远不能出现goto。 事实上,当代码相对较短的时候,适当多几个return、break、continue无伤大雅。当代码冗长时,这样的规则才能够发挥出其效力来。

十二、如何写出好的函数?

写代码和写别的东西很像,在写文章或者论文时,你先想些什么就写什么,然后再打磨它。 初稿也许粗陋无序,你就斟酌推敲,直到达到你心中的样子。

作者也并不是一开始就遵照规则写函数,估计也没有人做得到。都是不断打磨,分解函数、修改名称、消除重复。

参考
《代码整洁之道》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容