通过了解各自的取舍,选择正确的设计工具 - 一份契约、一个算法骨架,还是横向复用。
在 Kodokon 中打开本课PHP 给了你四种彼此部分重叠的设计工具:具体继承、抽象类、接口和 trait。选择归结为一个问题:你想共享什么?一份契约:接口。一份契约再加一个实现骨架:抽象类。在没有层级关系的类之间共享的代码:trait。至于具体继承,则越来越难说得通:它耦合紧密,而且 PHP 只允许一个父类。
<?php
declare(strict_types=1);
abstract class Exporter
{
public function export(array $rows): string
{
return $this->head() . $this->body($rows);
}
abstract protected function head(): string;
abstract protected function body(
array $rows
): string;
}
final class CsvExporter extends Exporter
{
protected function head(): string
{
return "id;name\n";
}
protected function body(array $rows): string
{
$lines = array_map(
fn (array $r): string => implode(';', $r),
$rows,
);
return implode("\n", $lines);
}
}
$csv = new CsvExporter();
echo $csv->export([[1, 'Ada'], [2, 'Linus']]);接口是纯粹的契约:只有签名和常量,没有状态也没有实现。它相对抽象类的决定性优势:一个类可以实现多个接口,而且任何现有的类都可以事后采纳这份契约。相比之下,抽象类携带状态和共享代码,但会占用你唯一的继承名额。实践中:用接口(Cache,而不是 ArrayCache)来标注你的参数类型,这样你就能替换实现,无论是在测试中还是在生产中。
<?php
declare(strict_types=1);
interface Cache
{
public function get(string $key): ?string;
public function set(
string $key,
string $value
): void;
}
final class ArrayCache implements Cache
{
private array $items = [];
public function get(string $key): ?string
{
return $this->items[$key] ?? null;
}
public function set(
string $key,
string $value
): void {
$this->items[$key] = $value;
}
}
$cache = new ArrayCache();
$cache->set('lang', 'PHP');
echo $cache->get('lang');trait 解决的是横向共享:把相同的方法注入到没有共同祖先的类里。当两个 trait 提供了同名方法时,PHP 拒绝替你做选择,并要求一个显式的解决方案:insteadof 指定要保留的版本,as 创建一个别名以便另一个仍然可以访问。这种冗长是刻意为之的 - 冲突必须在代码中保持可见。
<?php
declare(strict_types=1);
trait FileLogger
{
public function log(string $message): void
{
echo "[file] $message\n";
}
}
trait AuditLogger
{
public function log(string $message): void
{
echo "[audit] $message\n";
}
}
final class PaymentService
{
use FileLogger, AuditLogger {
FileLogger::log insteadof AuditLogger;
AuditLogger::log as audit;
}
}
$service = new PaymentService();
$service->log('Payment received');
$service->audit('Payment received');log() 方法。会发生什么?insteadof 显式解决