函数


函数

1. 函数参数(最好少于2个)

2. 函数应该只做一件事

3. 函数名应体现他做了什么事

4. 函数里应当只有一层抽象abstraction

5. 不要用flag作为函数的参数

6. 避免副作用

7. 不要写全局函数

8. 不要使用单例模式

9. 封装条件语句

10. 避免用反义条件判断

11. 避免条件判断

12. 避免类型检查 (part 1)

13. 避免类型检查 (part 2)

14. 移除僵尸代码

1. 函数参数(最好少于2个)

限制函数参数个数极其重要,这样测试你的函数容易点。有超过3个可选参数参数导致一个爆炸式组合增长,你会有成吨独立参数情形要测试。

无参数是理想情况。1个或2个都可以,最好避免3个。再多就需要加固了。通常如果你的函数有超过两个参数,说明他要处理的事太多了。 如果必须要传入很多数据,建议封装一个高级别对象作为参数。

坏:

 function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void
{
    // ...
}

好:

 class MenuConfig
{
    public $title;
    public $body;
    public $buttonText;
    public $cancellable = false;
}
 
$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;
 
function createMenu(MenuConfig $config): void
{
    // ...
}

2. 函数应该只做一件事

这是迄今为止软件工程里最重要的一个规则。当一个函数做超过一件事的时候,他们就难于实现、测试和理解。当你把一个函数拆分到只剩一个功能时,他们就容易被重构,然后你的代码读起来就更清晰。如果你光遵循这条规则,你就领先于大多数开发者了。

坏:

 function emailClients(array $clients): void
{
    foreach ($clients as $client) {
        $clientRecord = $db->find($client);
        if ($clientRecord->isActive()) {
            email($client);
        }
    }
}

好:

 function emailClients(array $clients): void
{
    $activeClients = activeClients($clients);
    array_walk($activeClients, 'email');
}
 
function activeClients(array $clients): array
{
    return array_filter($clients, 'isClientActive');
}
 
function isClientActive(int $client): bool
{
    $clientRecord = $db->find($client);
 
    return $clientRecord->isActive();
}

3. 函数名应体现他做了什么事

坏:

 class Email
{
    //...
 
    public function handle(): void
    {
        mail($this->to, $this->subject, $this->body);
    }
}
 
$message = new Email(...);
// 啥?handle处理一个消息干嘛了?是往一个文件里写吗?
$message->handle();

好:

 class Email
{
    //...
 
    public function send(): void
    {
        mail($this->to, $this->subject, $this->body);
    }
}
 
$message = new Email(...);
// 简单明了
$message->send();

4. 函数里应当只有一层抽象abstraction

当你抽象层次过多时时,函数处理的事情太多了。需要拆分功能来提高可重用性和易用性,以便简化测试。 (译者注:这里从示例代码看应该是指嵌套过多)

坏:

 function parseBetterJSAlternative(string $code): void
{
    $regexes = [
        // ...
    ];
 
    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            // ...
        }
    }
 
    $ast = [];
    foreach ($tokens as $token) {
        // lex...
    }
 
    foreach ($ast as $node) {
        // parse...
    }
}

坏:

我们把一些方法从循环中提取出来,但是parseBetterJSAlternative()方法还是很复杂,而且不利于测试。

 function tokenize(string $code): array
{
    $regexes = [
        // ...
    ];
 
    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            $tokens[] = /* ... */;
        }
    }
 
    return $tokens;
}
 
function lexer(array $tokens): array
{
    $ast = [];
    foreach ($tokens as $token) {
        $ast[] = /* ... */;
    }
 
    return $ast;
}
 
function parseBetterJSAlternative(string $code): void
{
    $tokens = tokenize($code);
    $ast = lexer($tokens);
    foreach ($ast as $node) {
        // 解析逻辑...
    }
}

好:

最好的解决方案是把 parseBetterJSAlternative()方法的依赖移除。

 class Tokenizer
{
    public function tokenize(string $code): array
    {
        $regexes = [
            // ...
        ];
 
        $statements = explode(' ', $code);
        $tokens = [];
        foreach ($regexes as $regex) {
            foreach ($statements as $statement) {
                $tokens[] = /* ... */;
            }
        }
 
        return $tokens;
    }
}
 
class Lexer
{
    public function lexify(array $tokens): array
    {
        $ast = [];
        foreach ($tokens as $token) {
            $ast[] = /* ... */;
        }
 
        return $ast;
    }
}
 
class BetterJSAlternative
{
    private $tokenizer;
    private $lexer;
 
    public function __construct(Tokenizer $tokenizer, Lexer $lexer)
    {
        $this->tokenizer = $tokenizer;
        $this->lexer = $lexer;
    }
 
    public function parse(string $code): void
    {
        $tokens = $this->tokenizer->tokenize($code);
        $ast = $this->lexer->lexify($tokens);
        foreach ($ast as $node) {
            // 解析逻辑...
        }
    }
}

这样我们可以对依赖做mock,并测试BetterJSAlternative::parse()运行是否符合预期。

5. 不要用flag作为函数的参数

flag就是在告诉大家,这个方法里处理很多事。前面刚说过,一个函数应当只做一件事。 把不同flag的代码拆分到多个函数里。

坏:

 function createFile(string $name, bool $temp = false): void
{
    if ($temp) {
        touch('./temp/'.$name);
    } else {
        touch($name);
    }
}
好:
 
function createFile(string $name): void
{
    touch($name);
}
 
function createTempFile(string $name): void
{
    touch('./temp/'.$name);
}

避免副作用

一个函数做了比获取一个值然后返回另外一个值或值们会产生副作用如果。副作用可能是写入一个文件,修改某些全局变量或者偶然的把你全部的钱给了陌生人。

 

现在,你的确需要在一个程序或者场合里要有副作用,像之前的例子,你也许需要写一个文件。你想要做的是把你做这些的地方集中起来。不要用几个函数和类来写入一个特定的文件。用一个服务来做它,一个只有一个。

 

重点是避免常见陷阱比如对象间共享无结构的数据,使用可以写入任何的可变数据类型,不集中处理副作用发生的地方。如果你做了这些你就会比大多数程序员快乐。

坏:

 // Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
$name = 'Ryan McDermott';
 
function splitIntoFirstAndLastName(): void
{
    global $name;
 
    $name = explode(' ', $name);
}
 
splitIntoFirstAndLastName();
 
var_dump($name); // ['Ryan', 'McDermott'];

好:

 function splitIntoFirstAndLastName(string $name): array
{
    return explode(' ', $name);
}
 
$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);
 
var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott'];

6. 不要写全局函数

在大多数语言中污染全局变量是一个坏的实践,因为你可能和其他类库冲突 并且调用你api的人直到他们捕获异常才知道踩坑了。让我们思考一种场景: 如果你想配置一个数组,你可能会写一个全局函数config(),但是他可能 和试着做同样事的其他类库冲突。

坏:

 function config(): array
{
    return  [
        'foo' => 'bar',
    ]
}

好:

class Configuration
{
    private $configuration = [];
 
    public function __construct(array $configuration)
    {
        $this->configuration = $configuration;
    }
 
    public function get(string $key): ?string
    {
        return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
    }
}
加载配置并创建 Configuration 类的实例
 
$configuration = new Configuration([
    'foo' => 'bar',
]);

现在你必须在程序中用 Configuration 的实例了

7. 不要使用单例模式

单例是一种 反模式. 以下是解释:Paraphrased from Brian Button:

  1. 总是被用成全局实例。They are generally used as a global instance, why is that so bad? Because you hide the dependencies of your application in your code, instead of exposing them through the interfaces. Making something global to avoid passing it around is a code smell.

  2. 违反了单一响应原则They violate the single responsibility principle: by virtue of the fact that they control their own creation and lifecycle.

  3. 导致代码强耦合They inherently cause code to be tightly coupled. This makes faking them out under test rather difficult in many cases.

  4. 在整个程序的生命周期中始终携带状态。They carry state around for the lifetime of the application. Another hit to testing since you can end up with a situation where tests need to be ordered which is a big no for unit tests. Why? Because each unit test should be independent from the other.

  5. 这里有一篇非常好的讨论单例模式的[根本问题((http://misko.hevery.com/2008/08/25/root-cause-of-singletons/)的文章,是Misko Hevery 写的。

坏:

 class DBConnection
{
    private static $instance;
 
    private function __construct(string $dsn)
    {
        // ...
    }
 
    public static function getInstance(): DBConnection
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
 
        return self::$instance;
    }
 
    // ...
}
 $singleton = DBConnection::getInstance();

好:

 class DBConnection
{
    public function __construct(string $dsn)
    {
        // ...
    }
 
     // ...
}

创建 DBConnection 类的实例并通过 DSN 配置.

$connection = new DBConnection($dsn);

现在你必须在程序中 使用 DBConnection 的实例了

8. 封装条件语句

坏:

 if ($article->state === 'published') {
    // ...
}

好:

 if ($article->isPublished()) {
    // ...
}

9. 避免用反义条件判断

坏:

 function isDOMNodeNotPresent(\DOMNode $node): bool
{
    // ...
}
 
if (!isDOMNodeNotPresent($node))
{
    // ...
}

好:

 function isDOMNodePresent(\DOMNode $node): bool
{
    // ...
}
 
if (isDOMNodePresent($node)) {
    // ...
}

10. 避免条件判断

这看起来像一个不可能任务。当人们第一次听到这句话是都会这么说。 "没有if语句我还能做啥?" 答案是你可以使用多态来实现多种场景 的相同任务。第二个问题很常见, “这么做可以,但为什么我要这么做?” 答案是前面我们学过的一个Clean Code原则:一个函数应当只做一件事。 当你有很多含有if语句的类和函数时,你的函数做了不止一件事。 记住,只做一件事。

坏:

 class Airplane
{
    // ...
 
    public function getCruisingAltitude(): int
    {
        switch ($this->type) {
            case '777':
                return $this->getMaxAltitude() - $this->getPassengerCount();
            case 'Air Force One':
                return $this->getMaxAltitude();
            case 'Cessna':
                return $this->getMaxAltitude() - $this->getFuelExpenditure();
        }
    }
}

好:

interface Airplane
{
    // ...
 
    public function getCruisingAltitude(): int;
}
 
class Boeing777 implements Airplane
{
    // ...
 
    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude() - $this->getPassengerCount();
    }
}
 
class AirForceOne implements Airplane
{
    // ...
 
    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude();
    }
}
 
class Cessna implements Airplane
{
    // ...
 
    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude() - $this->getFuelExpenditure();
    }
}

11. 避免类型检查 (part 1)

PHP是弱类型的,这意味着你的函数可以接收任何类型的参数。 有时候你为这自由所痛苦并且在你的函数渐渐尝试类型检查。 有很多方法去避免这么做。第一种是统一API。

坏:

 function travelToTexas($vehicle): void
{
    if ($vehicle instanceof Bicycle) {
        $vehicle->pedalTo(new Location('texas'));
    } elseif ($vehicle instanceof Car) {
        $vehicle->driveTo(new Location('texas'));
    }
}

好:

 function travelToTexas(Vehicle $vehicle): void
{
    $vehicle->travelTo(new Location('texas'));
}

12. 避免类型检查 (part 2)

如果你正使用基本原始值比如字符串、整形和数组,要求版本是PHP 7+,不用多态,需要类型检测, 那你应当考虑类型声明或者严格模式。 提供了基于标准PHP语法的静态类型。 手动检查类型的问题是做好了需要好多的废话,好像为了安全就可以不顾损失可读性。 保持你的PHP 整洁,写好测试,做好代码回顾。做不到就用PHP严格类型声明和严格模式来确保安全。

坏:

 function combine($val1, $val2): int
{
    if (!is_numeric($val1) || !is_numeric($val2)) {
        throw new \Exception('Must be of type Number');
    }
 
    return $val1 + $val2;
}

好:

 function combine(int $val1, int $val2): int
{
    return $val1 + $val2;
}

13. 移除僵尸代码

僵尸代码和重复代码一样坏。没有理由保留在你的代码库中。如果从来没被调用过,就删掉! 因为还在代码版本库里,因此很安全。

坏:

function oldRequestModule(string $url): void
{
    // ...
}
 
function newRequestModule(string $url): void
{
    // ...
}
 
$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

好:

 function requestModule(string $url): void
{
    // ...
}
 
$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');