PHPは自由度の高いプログラミング言語です。これは動的言語であり、プログラマに対して非常に寛容です。 PHP プログラマーは、コードをより効率的にするために多くの仕様を理解する必要があります。長年にわたって、私は多くのプログラミング本を読み、コーディング スタイルの問題について多くの上級プログラマーと議論してきました。どのルールがどの本や人から引用されたのかはきっと覚えていないでしょうが、この記事 (および今後の別の記事) は、より良いコードを書く方法についての私の見解を述べています。コードは通常、テストに耐えることができます。読みやすく、理解できる。このようなコードを使用すると、他の人が問題を見つけやすくなり、コードを再利用しやすくなります。
メソッドまたは関数本体で、可能な限り複雑さを軽減します。複雑さが比較的低いため、他の人がコードを読みやすくなります。さらに、そうすることで、コードに問題が発生する可能性が減り、問題が発生した場合の修正や修正が容易になります。
if、elseif、else、switch ステートメントの使用をできるだけ少なくします。さらに括弧が追加されます。これにより、コードがより理解しやすくなり、テストが難しくなります (各ブラケットをテスト ケースでカバーする必要があるため)。この問題を回避する方法は常にあります。
場合によっては、if ステートメントを別のオブジェクトに移動すると、より明確になります。例:
if($a->somethingIsTrue()) {
$a->doSomething();
}
は次のように変更できます:
$a->doSomething( );
ここでは、$a オブジェクトの doSomething() メソッドによって具体的な判断が行われます。これについてはこれ以上考える必要はありません。単に doSomething() を安全に呼び出す必要があるだけです。このアプローチは、「クエリを実行しない」というコマンドの原則にエレガントに従っています。この原則を詳しく見てみることをお勧めします。この原則は、オブジェクトに情報をクエリし、その情報に基づいて判断するときにいつでも適用できます。
場合によっては、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" をサポートすることもできます。方法ケース。これについては後の記事で触れます。
多くの場合、関数内の分岐は実際の分岐ではなく、次のような事前条件または事後条件です:
// 前置条件if(!$a instanceof A) { throw new \InvalidArgumentException(...);} // happy pathreturn $a->someInformation();
ここでの if ステートメントは関数実行の分岐ではなく、単なるチェックです前提条件として。場合によっては、PHP 自体に前提条件チェックを行わせることができます (適切な型ヒントの使用など)。ただし、PHP はすべての前提条件チェックを完了できるわけではないため、一部の前提条件チェックをコード内に残しておく必要があります。複雑さを軽減するには、コードが間違っていることが事前にわかっている場合、入力が間違っている場合、および結果がすでにわかっている場合に、できるだけ早く戻る必要があります。
できるだけ早く返すことの効果は、後続のコードを以前のようにインデントする必要がなくなることです:
// check preconditionif(...) { thrownew...();} // return earlyif(...) { return...;} // happy path... return...;
上記のテンプレートと同様に、コードはより読みやすく、理解しやすいものに変更されます。
関数本体が長すぎると、関数が何をしているのか理解しにくくなります。変数の使用状況、変数の型、ヘルパー関数と呼ばれる変数の宣言期間などを追跡することは、すべて多くの脳細胞を消費します。関数が比較的小さい場合は、関数を理解するのに役立ちます (たとえば、関数は入力を受け入れ、処理を実行し、結果を返すだけです)。
前の原則を使用してかっこを減らした後、関数をより小さな論理単位に分割して、関数をより明確にすることもできます。サブタスクを実装するコード行は、空白行で区切られたコードのグループとして考えることができます。次に、それらをヘルパー メソッド (つまり、リファクタリングで抽出されたメソッド) に分割する方法を検討します。
補助メソッドは通常プライベート メソッドであり、それらが属する特定のクラスのオブジェクトによってのみ呼び出されます。通常、インスタンスの変数にアクセスする必要はありません。その場合、静的として定義されたメソッドが必要です。私の経験では、少なくともテスト駆動開発で連携クラスを使用する場合、プライベート (静的) ヘルパー メソッドは通常、別のクラスにロールアップされ、パブリック (静的またはインスタンス) メソッドとして定義されます。
长的函数通常需要一些变量来保存中间结果。这些临时变量跟踪起来比较麻烦:你需要记住它们是否已经初始化了,是否还有用,现在的值又是多少等等。
上节提到的辅助函数有助于减少临时变量:
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 风格的表达式就像这样:
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。