代码整洁之道

读<<代码整洁之道>>笔记.

函数

什么是一个好的函数或者叫方法,只要能让函数明确的表达其意图,让读者能够一眼看出是一个怎样的函数,其接收什么参数,返回什么结果,做了什么事情。能做到这,大概就能算作一个好的函数了,看上去很简单。那么问题来了,如果做到这点呢?

短小

想象一下,一个击败航的复杂函数和一个只有十几行的函数,哪一个能够让人一眼看出其意图并理解其行为呢?显然是后者。当然,现在大多数语言完全可以将几百行代码写到一行,那样毫无格式的代码只会让人难以理解。

代码块和缩进

对于if、else等语句来说,其中包含的代码块最好只有一行,而这一行应该是一个函数调用语句,让读者可以一看看出其意图

函数名

一个好的函数名十分重要,如何定义一个好的函数名呢?只可意会。如果一个函数,你仅看函数名,就能明白他是做了什么,返回什么,那它就是一个好的函数名了。

只做一件事

函数应该只做一件事,并且做好这一件事足矣。这个大家都承认吧,并且已经是一个共识了,但是说起来容易,做起来却并没有那么简单。那么如何确保函数只做一件事呢?我们可以尝试这在函数中再拆出一个函数来,当然,拆出的函数不能仅仅是将代码搬过去,它应该有自己的责任,能够对新的函数起一个好的函数名。如果不能再拆出这样的函数,那么他应该可以了。

函数参数

最理想的参数数量是0,其次递增,3个参数就已经很多了。

就函数测试而言,没有参数的函数测试简直小菜一碟,若有了一个参数,就需要测试很多种组合,之后每多一个参数,测试的组合数量都是指数级增长。

就调用者而言,没有参数的函数直接调用即可,而有了参数就需要理解每一个参数是什么,也增加了调用者的时间。

如果函数的参数中存在布尔值,不好意思,并不推荐这样做,因为它明确的告诉调用者如果为true就会这样做,如果为false就会那样做。更好的做法是其拆分成两个函数。

同时,如果一个函数需要三个以上的参数,就可以考虑将其中的一些参数封装成类了。比如描绘笛卡尔坐标的x、y。

无副作用

副作用是函数的一个谎言,函数名承诺只做这一件事,但是他偷偷的做了其他事情。

比如,一个checkOrderStatus函数,明显它是在检查订单状态,但是如果它在检查的同时对状态进行了修改,就会让人很困惑,甚至在排查错误的时候,看到这样一个函数都不会点进去看。更好的做法是将其拆成两个函数。

每个函数一个抽象层级

说起来很简单,我们在阅读代码时,往往采用自顶向下的顺序来看,如果每个函数都只处理自己所在层级的逻辑,阅读和维护就很舒服了。

这个虽然我觉得很有道理,但是我没看懂啊。

格式

对于代码的格式,每个人都有不同的代码风格,这没什么。但是对于一个团队来说,最好能够统一代码风格,在同一个项目中,如果到处充斥着不同的代码风格,相比读起来并不是那么让人舒适,比如在什么地方放置括号,缩进几个字符,如何命名常量、变量和方法等,整个团队都应该遵循同一套规则,甚至可以将这些规则编写到IDE的代码格式中,利用IDE的提示功能来帮助。

刚才说的风格随不同,但是并不会影响读者阅读。但是有些代码风格让人难以阅读,而有些则会让读者心旷神怡。那么怎样的格式能够让人愉悦的阅读呢?简单的总结几条,可以看看是否会帮助阅读,若是,请这样做。

  1. 在每个方法前加空行,起强调作用
  2. 局部变量的生命尽量靠近其使用的位置
  3. 类变量应在类的顶部声明
  4. 若某个函数调用了另一个函数,应该将调用者放到被调用这的上面,这样有一个自然阅读的顺序
  5. 执行相似操作的一组函数应该放在一起
  6. 每行代码不应过长,至少不能超出屏幕阅读的宽度
  7. 代码的缩进,不用解释,阅读缩进不规范的代码是在令人痛苦
  8. 如果循环语句的循环体为空,也要确保空循环体的缩进,否则很容易将后一句看做循环体
  9. 等等

对象和数据结构

现在,有一个计算面积的需求,其中一种实现如下:

class Square{
    public $side;
}
class Geometry{
    public function area($shape){
        if($shape instanceof Square){
            return $shape->side * $shape->side;
        }
        return 0;
    }
}

有人看了,你这抽象的有问题啊,很明显是面向过程的,如果新加一个类型,Geometry类中的所有方法都要修改。嗯,却是是这样,但反过来想,如果新加一个方法,所有现有的形状类都不用动,只要在Geometry类中添加方法就行了。

当然了,还有一种多态的实现方式:

class Square implements Shape{
    private $side;
    public function area(){
        return $this->side * $this->side;
    }
}

你以为这样就万事大吉了?并没有。确实这样实现,添加新的类型,只要新写一个类实现方法即可,很简单。但是如果要添加一个新的函数,那不好意思,所有的类都要进行修改。

简单总结下,就是说:

  • 过程式代码便于在不改动现由数据结构的前提下添加新的函数,面向对象便于在不改动现由函数的前提先添加新的类型
  • 过程式代码难以添加新的数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类
  • 对象暴露行为,隐藏数据。便于添加新的数据类型而无需修改现有行为,同时也难以在现有对象中添加新的行为
  • 数据结构(上面的第一种实现)暴露数据,没有明显的行为。便于向现有数据结构添加新行为,同时也难以向现有函数添加新的数据结构。

当然,具体使用哪一种还是应该灵活选择,不必拘泥。

如果希望灵活添加新行为,就使用数据结构的方式。如果希望灵活添加新类型,就使用面向对象的方式。

在面向对象的编程中,类是其中的基本单位,就像面向过程中的函数一样。所以在说类时,可以借鉴一下前面的函数,不如只做一件事,也就是所说的单一职责。

函数应该短小,对于类来说,也是这样。函数的短小可以通过代码行数来判断,对于类来说,如何判断呢?可以通过判断其职责,也就是看它是否只做一件事情。同时做多件事情的类,命名也是一件痛苦的事情。比如一个类名叫 Manneger,很容易让人误解,它管理什么呢?

所以,简单来说,一个类应该只做好一件事,足矣。

当然,大概率写出的第一版不会遵循这个规则。所以对其拆分就不可避免了。

比如,在拆分一个方法时,新的方法使用了原来的4个变量,那么是不是就要将这4个变量作为参数传递进去呢?我不要。其实完全可以将这4个变量作为类的属性。但是这样的话问题来了,难以避免的会导致类中出现很多只在个别方法中用到的属性,很难受。等等,只在个别方法中用到,为什么不能把他单独拆解作为一个类呢?很好。

首先,我觉得这样做很好,感觉很好,但是说不上来的那种好。虽然感觉这样写出来的会很清晰,但还从来没有这样写过,之后自己试一试,看看效果是否真如作者所说一样。各位看官也尽可一试。

2019/9/25: 个人理解

其实我也在想,原本只需要两个类就能解决问题,这样写之后,可能会出现十个类。

如果把函数比做一个人,这个人只有一个工作,只需要做一件事,那么类就像是一个公司,这个公司聚集了一大堆向同一个目标奋斗的人,规模很小时, 怎么搞都好. 但是随着公司越来越大,势必不好管理,这时候就要把公司再分成多个子公司,或者多个部门,虽然各个部门合力奋斗的目标是一致的,但是部门的划分还是会使得分工明确,便于管理。

对比类来说,大概就是这个道理

小结

以下总结一些不好的代码规范, 借此警示自己不要犯这种错误

注释

1.不恰当的注释

注释应该仅用来描述有关代码和设计的技术性信息。像修改历史等信息不应出现在注释中

2.废弃的注释

过时、无关或错误的注释就是废弃的注释,不要写这种注释,如果发现了请尽快更新或删除,否则它会越来越远离它开始描述的代码

3.多余的注释

如果代码自身就能说明,就不要去写注释,例如:

i++; // i自增

注释应该说明代码自身没有提到的事情。

4.注释掉的代码

一般注释掉的代码,很可能已经与现有系统无关了,它调用的变量或函数可能已经改名,变得毫无用处

函数和类

1.过多的参数

一个函数的参数个数最好为零,如果有三个以上,可能就要考虑封装了

2.标识参数

如果函数的参数存在布尔值,只会让人迷惑,完全可以将其拆分为两个函数,枚举也是同样的道理

3.死函数

没有被用到的函数,应该尽早删除

4.死代码

死代码就是不会被执行到的代码,它可能出现在不会触发的if语句中,或者不会抛出异常的try catch中。如果找到这样的代码,请今早删除。

5.垂直距离

变量应该在首次使用的上面声明。函数应该放到首次被调用的下面一点点

6.前后不一致

如果在某个地方把响应对象命名为response, 则在其他地方也应该如此命名。如果把某个方法命名为getCommonModule, 则处理相似事情的方法应该取相似的名字,如:getUserModule。这会让代码扔易于阅读和修改。

7.错误位置的安放

对于一个函数或常亮的位置,应该放在读者自然而然期待它出现的地方

8.不恰当的静态方法

如果一个方法被命名为静态方法,说明它所需要的变量全部来源与参数,而不是对象的属性,同时也不会用到多态的特性。

当然有些方法命名为静态是有必要的,如:Math.max(a,b), 如果还需要 new Math().max(a,b), 也太蠢了

9.不恰当的函数名

如果看到一个函数名之后,仍然对其意图存在疑惑,甚至还要去查看源码,那么请换个名字吧。

10.错误的抽象层级

编写方法时,很容易想到,函数应该是一层一层往下调用的,不应该出现地层函数调用高层函数的现象,就像是基类不应该依赖于派生类一样。

11.封装条件

在if, while等条件语句中,下面这种写法:

if(shouldBeDeleted(timer)){}

明显要好于:

if(timer->hasExpired() && !timer->isRecurrent()){}

12.隐藏调用顺序

在一个类中,有这样一段代码: 

public function eat(Person $p){
    buyGreens();
    makeDinner();
    eatDinner($p);
}

这段代码很好理解,先买菜,然后做饭,最后吃饭。很显然,买菜的时候会对类的菜品属性进行赋值,在做饭的时候使用。但如果他们的调用顺序写错了,就会使用到未初始化的变量。如果改成下面这样:

public function eat(Person $p){
    $greens = buyGreens();
    $dinner = makeDinner($greens);
    eatDinner($dinner, $p);
}

这样通过函数,暴露了调用顺序,每个函数都产出下一个函数所需要的结果,这样一来就没理由不按照顺序调用了吧。


那么问题来了,如何写出符合这样规范的代码呢?

其实没有必要再一开始写的时候就按照规则来写,那样很容易打乱思路,甚至事倍功半。完全可以在写完后在返回来认真打磨、拆解函数、修改名称、消除重复代码等.

订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x