给 PHP 开发者的编程指南-第一部分: 降低复杂程度 已翻译 100%

oschina 投递于 2016/01/12 15:02 (共 9 段, 翻译完成于 01-13)
阅读 6382
收藏 188
13
加载中

本系列的介绍

PHP 是一门自由度很高的编程语言。它是动态语言,对程序员有很大的宽容度。作为 PHP 程序员,要想让你的代码更有效,需要了解不少的规范。很多年来,我读过很多编程方面的书籍,与很多资深程序员也讨论过代码风格的问题。具体哪条规则来自哪本书或者哪个人,我肯定不会都记得,但是本文(以及接下来的另一篇文章) 表达了我对于如何写出更好的代码的观点:能经得起考验的代码,通常是非常易读和易懂的。这样的代码,别人可以更轻松的查找问题,也可以更简单的复用代码。

降低函数体的复杂度

在方法或者函数体里,尽可能的降低复杂性。相对低一些的复杂性,可以便于别人阅读代码。另外,这样做也可以减少代码出问题的可能性,更易修改,有问题也更易修复。

zicode
zicode
翻译于 2016/01/12 17:27
5

在函数里减少括号数量

尽可能少的使用 if, elseif, else 和 switch 这些语句。它们会增加更多的括号。这会让代码更难懂、更难测试一些(因为每个括号都需要有测试用例覆盖到)。总是有办法来避免这个问题的。

代理决策 ("命令,不用去查询(Tell, don't ask)")

有的时候 if 语句可以移到另一个对象里,这样会更清晰些。例如:

  if($a->somethingIsTrue()) {
   $a->doSomething();
  }

可以改成:

                    $a->doSomething();

这里,具体的判断由 $a 对象的 doSomething() 方法去做了。我们不需要再为此做更多的考虑,只需要安全的调用 doSomething() 即可。这种方式优雅的遵循了命令,不要去查询原则。我建议你深入了解一下这个原则,当你向一个对象查询信息并且根据这些信息做判断的时候都可以适用这条原则。

zicode
zicode
翻译于 2016/01/12 17:42
1

使用map

有时可以用 map 语句减少 if, elseif 或 else 的使用,例如:

if($type==='json') {
    return $jsonDecoder->decode($body);
}elseif($type==='xml') {
    return $xmlDecoder->decode($body);
}else{
    throw new \LogicException(
        'Type "'.$type.'" is not supported'
    );
}

可以精简为:

$decoders= ...;// a map of type (string) to corresponding Decoder objects
 
if(!isset($decoders[$type])) {
    thrownew\LogicException(
        'Type "'.$type.'" is not supported'
    );
}

这样使用 map 的方式也让你的代码遵循扩展开放,关闭修改的原则。

强制类型

很多 if 语句可以通过更严格的使用类型来避免,例如:

if($a instanceof A) {
    // happy path
    return $a->someInformation();
}elseif($a=== null) {
    // alternative path
    return 'default information';
}

可以通过强制 $a 使用 A 类型来简化:

return $a->someInformation();

当然,我们可以通过其他方式来支持 "null" 的情况。这个在后面的文章会提到。

Return early

很多时候,函数里的一个分支并非真正的分支,而是前置或者后置的一些条件,就像这样:

// 前置条件
if(!$a instanceof A) {
    throw new \InvalidArgumentException(...);
}
 
// happy path
return $a->someInformation();

这里 if 语句并不是函数执行的一个分支,它只是对一个前置条件的检查。有时我们可以让 PHP 自身来完成前置条件的检查(例如使用恰当的类型提示)。不过,PHP 也没法完成所有前置条件的检查,所以还是需要在代码里保留一些。为了降低复杂度,我们需要在提前知道代码会出错时、输入错误时、已经知道结果时尽早返回。

zicode
zicode
翻译于 2016/01/12 18:07
2

尽早返回的效果就是后面的代码没必要像之前那样缩进了:

// check precondition
if(...) {
    thrownew...();
}
 
// return early
if(...) {
    return...;
}
 
// happy path
...
 
return...;

像上面这个模板这样,代码会变动更易读和易懂。

创建小的逻辑单元

如果函数体过长,就很难理解这个函数到底在干什么。跟踪变量的使用、变量类型、变量声明周期、调用的辅助函数等等,这些都会消耗很多脑细胞。如果函数比较小,对于理解函数功能很有帮助(例如,函数只是接受一些输入,做一些处理,再返回结果)。

使用辅助函数

在使用之前的原则减少括号之后,你还可以通过把函数拆分成更小的逻辑单元做到让函数更清晰。你可以把实现一个子任务的代码行看做一组代码,这些代码组直接用空行来分隔。然后考虑如何把它们拆分成辅助方法(即重构中的提炼方法)。

辅助方法一般是 private 的方法,只会被所属的特定类的对象调用。通常它们不需要访问实例的变量,这种情况需要定义为 static 的方法。在我的经验中,private (static)的辅助方法通常会汇总到分离的类中,并且定义成 public (static 或 instance)的方法,至少在测试驱动开发的时候使用一个协作类就是这种情形。

zicode
zicode
翻译于 2016/01/12 21:57
1

减少临时变量

长的函数通常需要一些变量来保存中间结果。这些临时变量跟踪起来比较麻烦:你需要记住它们是否已经初始化了,是否还有用,现在的值又是多少等等。

上节提到的辅助函数有助于减少临时变量:

public function capitalizeAndReverse(array $names) {
    $capitalized = array_map('ucfirst', $names);
    $capitalizedAndReversed = array_map('strrev', $capitalized);
    return $capitalizedAndReversed;
}

使用辅助方法,我们可以不用临时变量了:

public function capitalizeAndReverse(array $names) {
    return self::reverse(
        self::capitalize($names)
    );
}
 
private static function reverse(array $names) {
    return array_map('strrev', $names);
}
 
private static function capitalize(array $names) {
    return array_map('ucfirst', $names);
}

正如你所见,我们把函数变成新函数的组合,这样变得更易懂,也更容易修改。某种方式上,代码还有点符合“扩展开放/修改关闭”,因为我们基本上不需要再修改辅助函数。

由于很多算法需要遍历容器,从而得到新的容器或者计算出一个结果,此时把容器本身当做一个“一等公民”并且附加上相关的行为,这样做是很有意义的:

classNames
{
    private $names;
 
    public function __construct(array $names)
    {
        $this->names = $names;
    }
 
    public function reverse()
    {
        return new self(
            array_map('strrev', $names)
        );
    }
 
    public function capitalize()
    {
        return new self(
            array_map('ucfirst', $names)
        );
    }
}
 
$result = (newNames([...]))->capitalize()->reverse();

这样做可以简化函数的组合。

虽然减少临时变量通常会带来好的设计,不过上面的例子中也没必要干掉所有的临时变量。有时候临时变量的用处是很清晰的,作用也是一目了然的,就没必要精简。

zicode
zicode
翻译于 2016/01/12 22:18
3

    使用简单的类型

    追踪变量的当前取值总是很麻烦的,当不清楚变量的类型时尤其如此。而如果一个变量的类型不是固定的,那简直就是噩梦。

    数组只包含同一种类型的值

    使用数组作为可遍历的容器时,不管什么情况都要确保只使用同一种类型的值。这可以降低遍历数组读取数据的循环的复杂度:

foreach($collection as $value) {
    // 如果指定$value的类型,就不需要做类型检查
}

你的代码编辑器也会为你提供数组值的类型提示:

/**
 * @param DateTime[] $collection
 */
public function doSomething(array $collection) {
    foreach($collection as $value) {
        // $value是DateTime类型
    }
}

而如果你不能确定 $value 是 DateTime 类型的话,你就不得不在函数里添加前置判断来检查其类型。beberlei/assert库可以让这个事情简单一些:

useAssert\Assertion
 
public function doSomething(array $collection) {
    Assertion::allIsInstanceOf($collection, \DateTime::class);
 
    ...
}

如果容器里有内容不是 DateTime 类型,这会抛出一个 InvalidArgumentException 异常。除了强制输入相同类型的值之外,使用断言(assert)也是降低代码复杂度的一种手段,因为你可以不在函数的头部去做类型的检查。

zicode
zicode
翻译于 2016/01/13 09:54
1

简单的返回值类型

只要函数的返回值可能有不同的类型,就会极大的增加调用端代码的复杂度:

$result= someFunction();
if($result=== false) {
    ...
}else if(is_int($result)) {
    ...
}

PHP 并不能阻止你返回不同类型的值(或者使用不同类型的参数)。但是这样做只会造成大量的混乱,你的程序里也会到处都充斥着 if 语句。

下面是一个经常遇到的返回混合类型的例子:

/**
 * @param int $id
 * @return User|null
 */
public function findById($id)
{
    ...
}

这个函数会返回 User 对象或者 null,这种做法是有问题的,如果不检查返回值是否合法的 User 对象,我们是不能去调用返回值的方法的。在 PHP 7之前,这样做会造成"Fatal error",然后程序崩溃。

下一篇文章我们会考虑 null,告诉你如何去处理它们。

可读的表达式

我们已经讨论过不少降低函数的整体复杂度的方法。在更细粒度上我们也可以做一些事情来减少代码的复杂度。

zicode
zicode
翻译于 2016/01/13 10:07
1

隐藏复杂的逻辑

通常可以把复杂的表达式变成辅助函数。看看下面的代码:

if(($a||$b) &&$c) {
    ...
}

可以变得更简单一些,像这样:

if(somethingIsTheCase($a,$b,$c)) {
    ...
}

阅读代码时可以清楚的知道这个判断依赖 $a, $b 和 $c 三个变量,而函数名也可以很好的表达判断条件的内容。

使用布尔表达式

if 表达式的内容可以转换成布尔表达式。不过 PHP 也没有强制你必须提供 boolean 值:

$a=new\DateTime();
...
 
if($a) {
    ...
}

$a 会自动转换成 boolean 类型。强制类型转换是 bug 的主要来源之一,不过还有一个问题是会对代码的理解带来复杂性,因为这里的类型转换是隐式的。PHP 的隐式转换的替代方案是显式的进行类型转换,例如:

if($a instanceof DateTime) {
    ...
}

如果你知道比较的是 bool 类型,就可以简化成这样:

if($b=== false) {
    ...
}

使用 ! 操作符则还可以简化:

if(!$b) {
    ...
}
zicode
zicode
翻译于 2016/01/13 10:25
1

不要 Yoda 风格的表达式

Yoda 风格的表达式就像这样:

if('hello'===$result) {
    ...
}

这种表达式主要是为了避免下面的错误:

if($result='hello') {
    ...
}

这里 'hello' 会赋值给 $result,然后成为整个表达式的值。'hello' 会自动转换成 bool 类型,这里会转换成 true。于是 if 分支里的代码在这里会总是被执行。

使用 Yoda 风格的表达式可以帮你避免这类问题:

if('hello'=$result) {
    ...
}

我觉得实际情况下不太会有人出现这种错误,除非他还在学习 PHP 的基本语法。而且,Yoda 风格的代码也有不小的代价:可读性。这样的表达式不太易读,也不太容易懂,因为这不符合自然语言的习惯。

进一步的阅读

如果你打算进一步了解代码及如何对其进行优化,推荐下面这些书:

  • 代码大全,Steve McConnell

  • 代码整洁之道,Robert C. Martin

  • 重构,Martin Fowler

下一篇文章我们会讨论一个引起代码更复杂的特殊的罪魁祸首:null。

zicode
zicode
翻译于 2016/01/13 10:51
2
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(44)

全体人员
全体人员

引用来自“0Scn”的评论

$飘啊,眼睛花啊,我还以为中了Powerball头奖……

引用来自“eechen”的评论

一看就是没见过Perl和Shell的人,这两门语言了同样用$来标记变量.甚至JS里的很多库,典型的如jQuery都喜欢用$来作为函数别名.为什么PHP跟Shell的亲和性这么高,就是因为PHP里的很多特性都是从Shell里得来的灵感,比如PHP像Shell一样用getopt获取命令行参数的方法如出一辙. 一些有用的Shell变量(都是$开头哦): ./start.sh php java echo $# #参数个数. 输出 2 echo $* #参数列表. 输出 php java echo $@ #参数列表. 输出 php java echo $1 #脚本的参数值,$1表示第1个参数. 输出 php echo $0 #脚本自身文件名. 输出 ./start.sh echo $$ #脚本的PID. 输出 20242 php -S 127.0.0.1:8181 >/dev/null 2>&1 & echo $! #脚本最后运行的后台进程(这里是PHP)的PID. 输出 20243 echo $? #脚本最后运行的命令的结束代码(返回值). 输出 0

引用来自“zonghua”的评论

嘿嘿嘿,存储过程可是用@做变量名
看到这嘿嘿嘿,突然觉得好污……-v-
mark35
mark35

引用来自“LAJS”的评论

php的命名规范到底是驼峰还是用下划线,为什么我见过是同一个人写的代码一会是驼峰一会是下划线

引用来自“mark35”的评论

还有一个名字开始是驼峰然后是下划线的呢。。。

引用来自“eechen”的评论

这我倒没见过,是哪个函数?还是某些人为了黑PHP而故意这样混着写呢?

引用来自“mark35”的评论

是我同事写出来的。。。

引用来自“南湖船老大”的评论

是PHP自己黑自己的。mysql类库用的下划线,PDO用的驼峰,str函数用的下划线和四不像命名法,GD库、BC用的四不像命名法,
这是微信支付官方文档中的 WxPay.Api.php: line46 //异步通知url未设置,则使用配置文件中的url if(!$inputObj->IsNotify_urlSet()){ $inputObj->SetNotify_url(WxPayConfig::NOTIFY_URL);//异步通知url } $inputObj->SetAppid(WxPayConfig::APPID);//公众账号ID $inputObj->SetMch_id(WxPayConfig::MCHID);//商户号 $inputObj->SetSpbill_create_ip($_SERVER['REMOTE_ADDR']);//终端ip //$inputObj->SetSpbill_create_ip("1.1.1.1"); $inputObj->SetNonce_str(self::getNonceStr());//随机字符串 够乱的吧。。。
木易水羊888

引用来自“0Scn”的评论

$飘啊,眼睛花啊,我还以为中了Powerball头奖……

引用来自“eechen”的评论

一看就是没见过Perl和Shell的人,这两门语言了同样用$来标记变量.甚至JS里的很多库,典型的如jQuery都喜欢用$来作为函数别名.为什么PHP跟Shell的亲和性这么高,就是因为PHP里的很多特性都是从Shell里得来的灵感,比如PHP像Shell一样用getopt获取命令行参数的方法如出一辙. 一些有用的Shell变量(都是$开头哦): ./start.sh php java echo $# #参数个数. 输出 2 echo $* #参数列表. 输出 php java echo $@ #参数列表. 输出 php java echo $1 #脚本的参数值,$1表示第1个参数. 输出 php echo $0 #脚本自身文件名. 输出 ./start.sh echo $$ #脚本的PID. 输出 20242 php -S 127.0.0.1:8181 >/dev/null 2>&1 & echo $! #脚本最后运行的后台进程(这里是PHP)的PID. 输出 20243 echo $? #脚本最后运行的命令的结束代码(返回值). 输出 0

引用来自“0Scn”的评论

你那神马真令人捉鸡……2333333 反反复复拿你攒的那几个老段子刷了几年OSC的墙了,你还不腻歪吗,能搞几个新小广告爬杆子上贴吗? 正在拿Perl 6刷前同事Perl写的工具链,一起来哟…… 新社会了,都2016了,真不是当年“站长们”(没错,80%++直接挂引号)争着去拍黄片的年代了。当年虐了下JSP、ASP,还真以为虐了全世界而千秋万载? 刷效率?现在的Ruby、Python、Go、Javascript、Erlang/Elixir、Perl6、Elm......谁TM不能嗖嗖嗖的,多了去了,找项目合适的,别伤了肾就是。对了,为什么只能去拍黄片呢? 拍黄片没有错,错的是拍过黄片的人以为人人都该拍黄片……
表示不知所云
0Scn
0Scn

引用来自“0Scn”的评论

$飘啊,眼睛花啊,我还以为中了Powerball头奖……

引用来自“eechen”的评论

一看就是没见过Perl和Shell的人,这两门语言了同样用$来标记变量.甚至JS里的很多库,典型的如jQuery都喜欢用$来作为函数别名.为什么PHP跟Shell的亲和性这么高,就是因为PHP里的很多特性都是从Shell里得来的灵感,比如PHP像Shell一样用getopt获取命令行参数的方法如出一辙. 一些有用的Shell变量(都是$开头哦): ./start.sh php java echo $# #参数个数. 输出 2 echo $* #参数列表. 输出 php java echo $@ #参数列表. 输出 php java echo $1 #脚本的参数值,$1表示第1个参数. 输出 php echo $0 #脚本自身文件名. 输出 ./start.sh echo $$ #脚本的PID. 输出 20242 php -S 127.0.0.1:8181 >/dev/null 2>&1 & echo $! #脚本最后运行的后台进程(这里是PHP)的PID. 输出 20243 echo $? #脚本最后运行的命令的结束代码(返回值). 输出 0
你那神马真令人捉鸡……2333333 反反复复拿你攒的那几个老段子刷了几年OSC的墙了,你还不腻歪吗,能搞几个新小广告爬杆子上贴吗? 正在拿Perl 6刷前同事Perl写的工具链,一起来哟…… 新社会了,都2016了,真不是当年“站长们”(没错,80%++直接挂引号)争着去拍黄片的年代了。当年虐了下JSP、ASP,还真以为虐了全世界而千秋万载? 刷效率?现在的Ruby、Python、Go、Javascript、Erlang/Elixir、Perl6、Elm......谁TM不能嗖嗖嗖的,多了去了,找项目合适的,别伤了肾就是。对了,为什么只能去拍黄片呢? 拍黄片没有错,错的是拍过黄片的人以为人人都该拍黄片……
Robin3D
Robin3D
最终还是看程序员的个人修养,同样3年编码经验的,写出来的代码质量,可能天差地别的
都市网达
都市网达
说的东西大部分是《代码大全》思想。
南湖船老大
南湖船老大

引用来自“LAJS”的评论

php的命名规范到底是驼峰还是用下划线,为什么我见过是同一个人写的代码一会是驼峰一会是下划线

引用来自“橙汁儿”的评论

psr规范驼峰

引用来自“南湖船老大”的评论

你这不是打PHPer的脸么

引用来自“橙汁儿”的评论

php官方和大部分开源产品都是建议遵循psr
然而官方事实上不屌什么PSR。。PSR不过一个民间规范而已
南湖船老大
南湖船老大

引用来自“eechen”的评论

JAVA脑残粉的逻辑:不按驼峰命名的做法都不规范,包括Linux C函数:
http://man7.org/linux/man-pages/man2/
sethostname就不写成setHostname,epoll_wait就不写成epollWait,你哭呀.
我只想问,是不是Linux命令也得玩驼峰呀,笑死人!
PHP又不是Linux命令。。那你说说Linux有没有几种乱来混用啊?
南湖船老大
南湖船老大

引用来自“LAJS”的评论

php的命名规范到底是驼峰还是用下划线,为什么我见过是同一个人写的代码一会是驼峰一会是下划线

引用来自“mark35”的评论

还有一个名字开始是驼峰然后是下划线的呢。。。

引用来自“eechen”的评论

这我倒没见过,是哪个函数?还是某些人为了黑PHP而故意这样混着写呢?

引用来自“mark35”的评论

是我同事写出来的。。。

引用来自“南湖船老大”的评论

是PHP自己黑自己的。mysql类库用的下划线,PDO用的驼峰,str函数用的下划线和四不像命名法,GD库、BC用的四不像命名法,

引用来自“eechen”的评论

不懂瞎比比.看看PHP7的PHP Credits吧,MySQLi作者是Zak Greant, Georg Richter, Andrey Hristov, Ulf Wendel ,而MySQL driver for PDO作者是George Schlossnagle, Wez Furlong, Ilia Alshanetsky, Johannes Schlueter,根本就是两拨人,你凭什么要求开发MySQLi的这些C程序员要用Java的驼峰呢?你以为你是谁?Zend老板都不行.
这下子又是两拨人了,管你几波人,最后到了PHP的API层面不是应该统一吗?你这不是黑PHP开发组项目管理能力太弱吗?连个规范都互不遵守,各自为政?
eechen
eechen
JAVA脑残粉的逻辑:不按驼峰命名的做法都不规范,包括Linux C函数:
http://man7.org/linux/man-pages/man2/
sethostname就不写成setHostname,epoll_wait就不写成epollWait,你哭呀.
我只想问,是不是Linux命令也得玩驼峰呀,笑死人!
返回顶部
顶部