PHP is a programming language with a high degree of freedom. It is a dynamic language and has great tolerance for programmers. As a PHP programmer, you need to understand a lot of specifications to make your code more efficient. Over the years, I have read many programming books and discussed coding style issues with many senior programmers. I’m sure I won’t remember which rule comes from which book or person, but this article (and another to follow) expresses my view on how to write better code: it can stand up to the test. The code is usually very readable and understandable. With such code, others can find problems more easily and reuse the code more easily.
Reduce the complexity of the function body
In the method or function body, reduce complexity as much as possible. The relatively low complexity makes it easier for others to read the code. In addition, doing so can also reduce the possibility of code problems, make it easier to modify, and fix problems if they occur.
Reduce the number of parentheses in functions
Use if, elseif, else and switch statements as little as possible. They add more brackets. This makes the code more understandable and harder to test (because each bracket needs to be covered by a test case). There are always ways to avoid this problem.
Agent decision-making ("Tell, don't ask")
Sometimes the if statement can be moved to another object to make it clearer. For example:
if($a->somethingIsTrue()) { $a->doSomething(); }
can be changed to:
Here, the specific judgment is done by the doSomething() method of the $a object. We don't need to think any more about this, we just need to call doSomething() safely. This approach elegantly follows the command-don’t-query principle. I suggest you take a closer look at this principle, which can be applied whenever you query an object for information and make judgments based on that information.
Use map
Sometimes map statements can be used to reduce the use of if, elseif or else, for example:
can be simplified to:
Using map in this way also allows your code to follow the principle of opening for expansion and closing for modification.
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' ); }
Can be simplified by forcing $a to use type A:
Of course, we can support the "null" case in other ways. This will be mentioned in a later article.
if($a instanceof A) { // happy path return $a->someInformation(); }elseif($a=== null) { // alternative path return 'default information'; }
return $a->someInformation();
The if statement here is not a branch of function execution, it is just a check of a precondition. Sometimes we can let PHP do the precondition checking itself (such as using appropriate type hints). However, PHP cannot complete all precondition checks, so you still need to keep some in the code. In order to reduce complexity, we need to return as early as possible when we know in advance that the code will go wrong, when the input is wrong, and when the result is already known.
The effect of returning as early as possible is that the subsequent code does not need to be indented as before:
if(!$a instanceof A) { throw new \InvalidArgumentException(...); } // happy path return $a->someInformation();
Create small logical units
如果函数体过长,就很难理解这个函数到底在干什么。跟踪变量的使用、变量类型、变量声明周期、调用的辅助函数等等,这些都会消耗很多脑细胞。如果函数比较小,对于理解函数功能很有帮助(例如,函数只是接受一些输入,做一些处理,再返回结果)。
使用辅助函数
在使用之前的原则减少括号之后,你还可以通过把函数拆分成更小的逻辑单元做到让函数更清晰。你可以把实现一个子任务的代码行看做一组代码,这些代码组直接用空行来分隔。然后考虑如何把它们拆分成辅助方法(即重构中的提炼方法)。
辅助方法一般是 private 的方法,只会被所属的特定类的对象调用。通常它们不需要访问实例的变量,这种情况需要定义为 static 的方法。在我的经验中,private (static)的辅助方法通常会汇总到分离的类中,并且定义成 public (static 或 instance)的方法,至少在测试驱动开发的时候使用一个协作类就是这种情形。
减少临时变量
长的函数通常需要一些变量来保存中间结果。这些临时变量跟踪起来比较麻烦:你需要记住它们是否已经初始化了,是否还有用,现在的值又是多少等等。
上节提到的辅助函数有助于减少临时变量:
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();
这样做可以简化函数的组合。
虽然减少临时变量通常会带来好的设计,不过上面的例子中也没必要干掉所有的临时变量。有时候临时变量的用处是很清晰的,作用也是一目了然的,就没必要精简。
使用简单的类型
追踪变量的当前取值总是很麻烦的,当不清楚变量的类型时尤其如此。而如果一个变量的类型不是固定的,那简直就是噩梦。
数组只包含同一种类型的值
使用数组作为可遍历的容器时,不管什么情况都要确保只使用同一种类型的值。这可以降低遍历数组读取数据的循环的复杂度:
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)也是降低代码复杂度的一种手段,因为你可以不在函数的头部去做类型的检查。
简单的返回值类型
只要函数的返回值可能有不同的类型,就会极大的增加调用端代码的复杂度:
$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,告诉你如何去处理它们。
可读的表达式
我们已经讨论过不少降低函数的整体复杂度的方法。在更细粒度上我们也可以做一些事情来减少代码的复杂度。
隐藏复杂的逻辑
通常可以把复杂的表达式变成辅助函数。看看下面的代码:
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) { ... }
不要 Yoda 风格的表达式
Yoda 风格的表达式就像这样:
if('hello'===$result) { ... }
这种表达式主要是为了避免下面的错误:
if($result='hello') { ... }
这里 'hello' 会赋值给 $result,然后成为整个表达式的值。'hello' 会自动转换成 bool 类型,这里会转换成 true。于是 if 分支里的代码在这里会总是被执行。
使用 Yoda 风格的表达式可以帮你避免这类问题:
if('hello'=$result) { ... }
我觉得实际情况下不太会有人出现这种错误,除非他还在学习 PHP 的基本语法。而且,Yoda 风格的代码也有不小的代价:可读性。这样的表达式不太易读,也不太容易懂,因为这不符合自然语言的习惯。
以上就是本文的全部内容,希望对大家的学习有所帮助。