函数参数不要超过两个

限制函数的参数数量是非常重要的,因为它使你的函数更容易测试。超过三个参数会导致参数之间的组合过多,你必须对每个单独的参数测试大量不同的情况。

没有参数是最理想的情况,一个或两个参数是可以接受的,三个以上则是应该避免的。这很重要。如果你有两个以上的参数,那么你的函数可能试图做的太多,如果不是,你可能需要将一个高级别的对象传当做参数传进去。

Bad:

1
2
3
4
5

function createMenu($title, $body, $buttonText, $cancellable)
{
// ...
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
{
// ...
}

一个函数只做一件事

这是软件工程中一个重要的原则。这会让你的代码清晰易懂以及易于复用。

Bad:

1
2
3
4
5
6
7
8
9
function emailClients($clients)
{
foreach ($clients as $client) {
$clientRecord = $db->find($client);
if ($clientRecord->isActive()) {
email($client);
}
}
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function emailClients($clients)
{
$activeClients = activeClients($clients);
array_walk($activeClients, 'email');
}

function activeClients($clients)
{
return array_filter($clients, 'isClientActive');
}

function isClientActive($client)
{
$clientRecord = $db->find($client);

return $clientRecord->isActive();
}

函数名要能说明它是做什么的

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Email
{
//...

public function handle()
{
mail($this->to, $this->subject, $this->body);
}
}

$message = new Email(...);
// 这是什么?一条消息的句柄?还是要写一个文件?(读者的疑问)
$message->handle();

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Email 
{
//...

public function send()
{
mail($this->to, $this->subject, $this->body);
}
}

$message = new Email(...);
// 一目了然
$message->send();

函数应该只做一层抽象

当你有多个层次的抽象时,你的函数就已经做的太多了。拆分这些函数,可以让代码可重用性更高且更易测试。
Bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

function parseBetterJSAlternative($code)
{
$regexes = [
// ...
];

$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
// ...
}
}

$ast = [];
foreach ($tokens as $token) {
// lex...
}

foreach ($ast as $node) {
// parse...
}
}

Bad too:
我们从函数中迁出去了一些工作,但是 parseBetterJSAlternative() 函数还是很复杂,不可测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function tokenize($code)
{
$regexes = [
// ...
];

$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
$tokens[] = /* ... */;
}
}

return $tokens;
}

function lexer($tokens)
{
$ast = [];
foreach ($tokens as $token) {
$ast[] = /* ... */;
}

return $ast;
}

function parseBetterJSAlternative($code)
{
$tokens = tokenize($code);
$ast = lexer($tokens);
foreach ($ast as $node) {
// parse...
}
}

Good:

最好的解决方案是移除 parseBetterJSAlternative 函数的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Tokenizer
{
public function tokenize($code)
{
$regexes = [
// ...
];

$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
$tokens[] = /* ... */;
}
}

return $tokens;
}
}

class Lexer
{
public function lexify($tokens)
{
$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($code)
{
$tokens = $this->tokenizer->tokenize($code);
$ast = $this->lexer->lexify($tokens);
foreach ($ast as $node) {
// parse...
}
}
}

不要使用标志作为函数的参数

当你在函数中使用标志来作为参数时,你的函数就不是只做一件事情了,这与我们前面所讲的每个函数只做一件事的原则相违背,所以不要使用标志作为函数的参数。

Bad:

1
2
3
4
5
6
7
8
function createFile($name, $temp = false)
{
if ($temp) {
touch('./temp/'.$name);
} else {
touch($name);
}
}

Good:

1
2
3
4
5
6
7
8
9
10

function createFile($name)
{
touch($name);
}

function createTempFile($name)
{
touch('./temp/'.$name);
}

避免副作用

如果一个函数做了“拿到一个值并返回一个值或者多个值”以外的事情,那么这个函数就有可能产生副作用,副作用可能是意外的写入了文件、修改了全局变量、或者打钱给了陌生人。

现在假如你确实要在函数中做一些有可能产生副作用的事情。 比如要写一个文件,你需要做的是将写文件的操作集中到一处,而不是在几个函数或者类里对同一个文件做操作,实现一个服务(函数或者类)去操作它,有且仅有一个。

关键是要能避免常见的陷阱:像是在没有结构的对象之间共享状态、使用可能被写入任何值的可变数据类型、 不集中处理有可能产生副作用的操作。 如果你能做到这些,你会比绝大多数程序员更快乐。

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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()
{
global $name;

$name = explode(' ', $name);
}

splitIntoFirstAndLastName();

var_dump($name); // ['Ryan', 'McDermott'];

Good:

1
2
3
4
5
6
7
8
9
10
11

function splitIntoFirstAndLastName($name)
{
return explode(' ', $name);
}

$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);

var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott'];

不要修改全局变量

在许多编程语言中污染全局是一种糟糕的做法,因为你的库可能会与另一个库冲突,但是你的库的用户却一无所知,直到在生产环境中爆发异常。让我们来考虑一个例子:如果你想要拿到配置数组怎么办?你可以编写全局函数,如config(),但是它可能与另一个试图做同样事情的库冲突。

Bad:

1
2
3
4
5
6
function config()
{
return [
'foo' => 'bar',
]
}

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Configuration
{
private $configuration = [];

public function __construct(array $configuration)
{
$this->configuration = $configuration;
}

public function get($key)
{
return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
}
}


$configuration = new Configuration([
'foo' => 'bar',
]);

避免条件判断

人们会问“如果不用 if 语句我该怎么做?”,答案是在许多情况下,你可以用多态来实现同样的效果。那这样做什么好处,还是那句话:“一个函数应该只做一件事”, 当你的类或函数中有了 if 语句,你的函数就不止是只做一件事情了。

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Airplane
{
// ...

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

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
interface Airplane
{
// ...

public function getCruisingAltitude();
}

class Boeing777 implements Airplane
{
// ...

public function getCruisingAltitude()
{
return $this->getMaxAltitude() - $this->getPassengerCount();
}
}

class AirForceOne implements Airplane
{
// ...

public function getCruisingAltitude()
{
return $this->getMaxAltitude();
}
}

class Cessna implements Airplane
{
// ...

public function getCruisingAltitude()
{
return $this->getMaxAltitude() - $this->getFuelExpenditure();
}
}