函数
使用默认参数替代短路或条件
Not good:
这样做并不好,因为 $breweryName
可以是 NULL
。
function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void
{
// ...
}
Not bad:
这个观点比之前的版本更容易理解,可以更好的控制了变量的值。
function createMicrobrewery($name = null): void
{
$breweryName = $name ?: 'Hipster Brew Co.';
// ...
}
Good:
您可以使用 类型提示 并确保 $breweryName
不会是 NULL
。
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void
{
// ...
}
函数参数(理想情况下为 2 个或更少)
限制函数参数的数量非常重要,可以轻松地测试您的功能。
一旦一个方法拥有三个以上的参数会导致组合爆炸,您必须使用每个单独的参数测试大量不同的情况。
零参数是理想的情况。 一两个参数是可以的,三个应该避免。
除此之外的任何东西都应该合并。
通常,如果您有两个以上参数这说明你的函数做的事情太多了。如果不是,大多数情况下一个更高级别的对象就足以作为一个参数。
Bad:
class Questionnaire
{
public function __construct(
string $firstname,
string $lastname,
string $patronymic,
string $region,
string $district,
string $city,
string $phone,
string $email
) {
// ...
}
}
Good:
class Name
{
private $firstname;
private $lastname;
private $patronymic;
public function __construct(string $firstname, string $lastname, string $patronymic)
{
$this->firstname = $firstname;
$this->lastname = $lastname;
$this->patronymic = $patronymic;
}
// getters ...
}
class City
{
private $region;
private $district;
private $city;
public function __construct(string $region, string $district, string $city)
{
$this->region = $region;
$this->district = $district;
$this->city = $city;
}
// getters ...
}
class Contact
{
private $phone;
private $email;
public function __construct(string $phone, string $email)
{
$this->phone = $phone;
$this->email = $email;
}
// getters ...
}
class Questionnaire
{
public function __construct(Name $name, City $city, Contact $contact)
{
// ...
}
}
函数名字应该语义化
Bad:
class Email
{
//...
public function handle(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 这是什么? 消息的句柄? 我们现在正在写入文件吗?
$message->handle();
Good:
class Email
{
//...
public function send(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 清晰明了
$message->send();
函数应该只是一个抽象级别
当您有多个抽象级别时,通常是因为您的函数做太多事情了。
拆分功能可以让代码可重用并且容易测试。
Bad:
function parseBetterPHPAlternative(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...
}
}
Bad too:
我们已经实现了一些功能,但是 parseBetterPHPAlternative()
函数仍然非常复杂,无法测试。
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 parseBetterPHPAlternative(string $code): void
{
$tokens = tokenize($code);
$ast = lexer($tokens);
foreach ($ast as $node) {
// parse...
}
}
Good:
最好的解决方案是移出 parseBetterPHPAlternative() 函数的依赖关系。
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 BetterPHPAlternative
{
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) {
// parse...
}
}
}
不要使用标志作为函数参数
标志会告诉您的用户,这个函数可以做不止一件事情。函数应该做一件事情。 如果函数遵循不同的代码路径,请基于布尔值将它们拆分。
坏的:
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);
}
避免副作用
如果函数取一个值并返回另外一个值或多个值之外,就会产生副作用。副作用可能是写入一个文件,修改一些全局变量,或者不小心将您的所有钱汇给一个陌生人。
现在,你确实需要在程序中偶尔出现副作用。就像之前的例子一样,你可能需要写入文件。你想要做的是集中执行此操作的位置。没有几个可以写入特定内容的函数和类文件。只有一个服务可以做到这一点。 也只有一个。
主要的一点是要避免常见的陷阱,比如在对象之间共享状态而不使用任何结构,使用任何可以被写入的数据类型,而不是集中在副作用发生的地方。如果你能做到这点,你将会更快乐,比绝大多数程序员都要好。
坏的:
// 通过以下函数引用的全局变量。
// 如果我们有另外一个使用这个名字函数,现在它将是一个数组,它可能会破坏它。
$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'];
不要写入全局函数
在许多语言中,污染全局函数是一个不好的做法,因为你可能会与另外一个库发生冲突,并且使用你 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
{
// null coalescing operator
return $this->configuration[$key] ?? null;
}
}
加载配置和创建 Configuration
类的实例
$configuration = new Configuration([
'foo' => 'bar',
]);
现在,你必须在应用程序中使用 Configuration
的实例。
不要使用单例模式
单例是一种 反模式。摘自布莱恩·巴顿:
- 它通常被用作 全局实例,为什么会这么糟糕呢?因为在您的代码中 隐藏了应用程序的依赖,而不是通过接口暴露它们。使之全局以避免传递是一种 代码味道。
- 它们违反了 单一责任原则:由于 它们控制自己的创建和生命周期。
- 它们内在地导致代码紧密的 耦合。在许多情况下,这使得 在测试中伪装它们 变得相当困难。
- 在应用程序的生命周期中,它们始终保持着状态。测试的另一个亮点是 您可能会遇到对测试需要排序的情况,这对于单元测试来说是一个很大的禁忌。为什么?因为每个单元测试应该相互独立。
Misko Hevery 也有关于 root of problem 非常好的想法。
坏的:
class DBConnection
{
private static $instance;
private function __construct(string $dsn)
{
// ...
}
public static function getInstance(): self
{
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
的实例。
封装条件
坏的:
if ($article->state === 'published') {
// ...
}
好的:
if ($article->isPublished()) {
// ...
}
避免否定条件
坏的:
function isDOMNodeNotPresent(DOMNode $node): bool
{
// ...
}
if (! isDOMNodeNotPresent($node)) {
// ...
}
好的:
function isDOMNodePresent(DOMNode $node): bool
{
// ...
}
if (isDOMNodePresent($node)) {
// ...
}
避免有条件
这似乎是一个不可能的任务。在第一次听到这个时,大多数人都说,“如果没有 if
语句我该怎么做呢?” 答案是在许多情况下,你可以使用多态来实现相同的任务。第二个问题通常是,“那太好了,但为什么要这样做?” 这答案是我们之前学到的代码整洁概念:一个函数只做一件事。当你的类和函数具有 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();
}
}
避免类型检查(第 1 部分)
PHP 是无类型的,这意味着您的函数可以采用任何类型的参数。
但我们有时不得不在函数中进行类型检查,这时候我们反而会被这种自由所困扰。有很多方法可以避免这样做。首先要考虑的是一致的 API。
Bad:
function travelToTexas($vehicle): void
{
if ($vehicle instanceof Bicycle) {
$vehicle->pedalTo(new Location('texas'));
} elseif ($vehicle instanceof Car) {
$vehicle->driveTo(new Location('texas'));
}
}
Good:
function travelToTexas(Vehicle $vehicle): void
{
$vehicle->travelTo(new Location('texas'));
}
避免类型检查(第 2 部分)
如果您正在使用基本的原始值,如字符串、整数和数组,并且您使用 PHP 7+ 并且不能使用多态,但您仍然觉得需要类型检查,你应该考虑 类型声明 或严格模式。
它在标准 PHP 语法之上为您提供静态类型。
手动类型检查的问题在于,这样做需要大量额外的废话,以至于你得到的虚假「类型安全」并不能弥补失去的可读性。
保持你的 PHP 干净,编写良好的测试,并进行良好的代码审查。否则,除了使用 PHP 严格类型声明或严格模式外,所有这些都可以。
Bad:
function combine($val1, $val2): int
{
if (! is_numeric($val1) || ! is_numeric($val2)) {
throw new Exception('Must be of type Number');
}
return $val1 + $val2;
}
Good:
function combine(int $val1, int $val2): int
{
return $val1 + $val2;
}
删除无效代码
无效代码代码与重复代码一样糟糕,没有理由保留它。如果它没有被调用,请删除它! 如果您仍然需要它,它在您的版本历史记录中仍然是安全的。
Bad:
function oldRequestModule(string $url): void
{
// ...
}
function newRequestModule(string $url): void
{
// ...
}
$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
Good:
function requestModule(string $url): void
{
// ...
}
$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。