PHP는 자유도가 높은 프로그래밍 언어입니다. 이는 동적 언어이며 프로그래머에게 매우 관대합니다. PHP 프로그래머로서 코드를 보다 효율적으로 만들기 위해서는 많은 사양을 이해해야 합니다. 수년에 걸쳐 나는 많은 프로그래밍 책을 읽었고 많은 선배 프로그래머들과 코딩 스타일 문제에 대해 논의했습니다. 어떤 규칙이 어떤 책이나 사람에게서 나온 것인지는 기억나지 않을 것입니다. 하지만 이 기사(그리고 다음에 나올 다른 기사)는 더 나은 코드를 작성하는 방법에 대한 나의 견해를 표현합니다. 코드는 일반적으로 테스트에 매우 잘 견딥니다. 읽을 수 있고 이해할 수 있습니다. 이러한 코드를 사용하면 다른 사람들이 더 쉽게 문제를 발견하고 코드를 더 쉽게 재사용할 수 있습니다.
함수 본체의 복잡성을 줄입니다
메서드나 함수 본문의 복잡성을 최대한 줄이세요. 상대적으로 복잡성이 낮기 때문에 다른 사람이 코드를 더 쉽게 읽을 수 있습니다. 또한 이렇게 하면 코드 문제가 발생할 가능성이 줄어들고 수정이 쉬워지며 문제가 발생할 경우 수정할 수도 있습니다.
함수에서 괄호 수 줄이기
if, elseif, else 및 switch 문을 가능한 한 적게 사용하세요. 그들은 더 많은 괄호를 추가합니다. 이렇게 하면 코드를 더 이해하기 쉽고 테스트하기가 더 어려워집니다(각 괄호는 테스트 케이스에서 다루어야 하기 때문입니다). 이 문제를 피할 수 있는 방법은 항상 있습니다.
상담원 의사결정("묻지 말고 말하세요")
때로는 if 문을 더 명확하게 만들기 위해 다른 개체로 이동할 수 있습니다. 예:
if($a->somethingIsTrue()) { $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' ); }유형을 보다 엄격하게 사용하면 많은 if 문을 피할 수 있습니다. 예:
if($a instanceof A) { // happy path return $a->someInformation(); }elseif($a=== null) { // alternative path return 'default information'; }
조기 귀국
return $a->someInformation();함수 분기는 실제 분기가 아니지만 다음과 같은 사전 또는 사후 조건이 있는 경우가 많습니다. // 사전 조건
가능한 한 빨리 반환하면 후속 코드를 이전처럼 들여쓰기할 필요가 없습니다.
if(!$a instanceof A) { throw new \InvalidArgumentException(...); } // happy path return $a->someInformation();
像上面这个模板这样,代码会变动更易读和易懂。
创建小的逻辑单元
如果函数体过长,就很难理解这个函数到底在干什么。跟踪变量的使用、变量类型、变量声明周期、调用的辅助函数等等,这些都会消耗很多脑细胞。如果函数比较小,对于理解函数功能很有帮助(例如,函数只是接受一些输入,做一些处理,再返回结果)。
使用辅助函数
在使用之前的原则减少括号之后,你还可以通过把函数拆分成更小的逻辑单元做到让函数更清晰。你可以把实现一个子任务的代码行看做一组代码,这些代码组直接用空行来分隔。然后考虑如何把它们拆分成辅助方法(即重构中的提炼方法)。
辅助方法一般是 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 风格的代码也有不小的代价:可读性。这样的表达式不太易读,也不太容易懂,因为这不符合自然语言的习惯。
以上就是本文的全部内容,希望对大家的学习有所帮助。