0Pricing
PHP Academy · 课时

实践 SOLID 原则

将五项 SOLID 原则应用于真实的 PHP 类。

实践 SOLID 原则 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。

为什么使用 SOLID

SOLID 是五项设计原则,可以让面向对象的 PHP 保持灵活、可测试且不易腐化。它们不是必须盲目遵循的规则,而是能够减少耦合并明确职责的经验法则。在本课中,我们会将每项原则应用于实际需要交付的具体 PHP 类。

  • Single Responsibility(单一职责)
  • Open/Closed(开闭原则)
  • Liskov Substitution(Liskov 替换)
  • Interface Segregation(接口隔离)
  • Dependency Inversion(依赖倒置)

单一职责

一个类应该只有一个变更原因。同时生成报告、将其格式化为 HTML 并发送电子邮件的类,就有三个变更原因。请将持久化、格式化和传输拆分开来。

下面的 Invoice 只负责建模数据;渲染和保存由其他位置负责。

<?php
final class Invoice {
    public function __construct(
        public readonly string $number,
        public readonly int $cents
    ) {}
    public function total(): float { return $this->cents / 100; }
}

final class InvoiceRenderer {
    public function toText(Invoice $i): string {
        return sprintf('Invoice %s: $%.2f', $i->number, $i->total());
    }
}

$i = new Invoice('INV-1', 12500);
echo (new InvoiceRenderer())->toText($i), PHP_EOL;

SRP:发现异味

典型的 SRP 违反情况是类名中包含并且,或者存在一个什么都做的 Manager。请留意那些会因无关业务原因而发生变化的方法。如果税收规则的变化和 PDF 布局的变化都会涉及同一个类,那么该类承担的职责就过多了。

开闭原则

软件实体应该对扩展开放,对修改关闭。添加新的折扣类型不应迫使您修改一个巨大的 switch。请使用多态:每条规则都是一个实现共享接口的独立类。

<?php
interface Discount { public function apply(float $total): float; }

final class PercentOff implements Discount {
    public function __construct(private float $pct) {}
    public function apply(float $t): float { return $t * (1 - $this->pct / 100); }
}
final class FlatOff implements Discount {
    public function __construct(private float $amount) {}
    public function apply(float $t): float { return max(0, $t - $this->amount); }
}

function checkout(float $total, Discount ...$discounts): float {
    foreach ($discounts as $d) { $total = $d->apply($total); }
    return $total;
}
echo checkout(100, new PercentOff(10), new FlatOff(5)), PHP_EOL; // 85

Liskov 替换

在需要基类型的任何位置,都必须可以使用其子类型,而不会让调用方感到意外。著名的例子是:Square extends Rectangle 会破坏 LSP,因为设置宽度不应悄悄改变高度。请优先将它们建模为位于共同 Shape 接口之后的独立类型。

<?php
interface Shape { public function area(): float; }

final class Rectangle implements Shape {
    public function __construct(private float $w, private float $h) {}
    public function area(): float { return $this->w * $this->h; }
}
final class Square implements Shape {
    public function __construct(private float $side) {}
    public function area(): float { return $this->side ** 2; }
}

$shapes = [new Rectangle(2, 3), new Square(4)];
foreach ($shapes as $s) { echo $s->area(), PHP_EOL; }

LSP 与契约

LSP 也会约束方法契约。子类型可以放宽前置条件并加强后置条件,但绝不能反过来。抛出基类型从未声明的新异常类型,或在基类型保证返回对象的地方返回 null,都会违反可替换性。PHP 的协变返回类型和逆变参数类型会在类型层面强制执行其中一部分约束。

接口隔离

客户端不应依赖它们不会使用的方法。包含 work() 和 eat() 的臃肿 Worker 接口会迫使 RobotWorker 为 eat() 提供存根。请拆分为专注于不同角色的接口,使每个实现者只需承担它真正执行的职责。

<?php
interface Workable { public function work(): string; }
interface Feedable { public function eat(): string; }

final class Human implements Workable, Feedable {
    public function work(): string { return 'coding'; }
    public function eat(): string { return 'lunch'; }
}
final class Robot implements Workable {
    public function work(): string { return 'welding'; }
}

foreach ([new Human(), new Robot()] as $w) { echo $w->work(), PHP_EOL; }

依赖倒置

高层策略应该依赖抽象,而不是具体细节。请注入接口,而不要硬编码类。这样,您就可以在测试中用伪造的邮件发送器替换真实邮件发送器,并在生产环境中更换供应商,而无需修改使用方。

<?php
interface Mailer { public function send(string $to, string $body): void; }

final class SmtpMailer implements Mailer {
    public function send(string $to, string $body): void {
        echo "SMTP -> $to: $body" . PHP_EOL;
    }
}

final class SignupService {
    public function __construct(private Mailer $mailer) {}
    public function register(string $email): void {
        $this->mailer->send($email, 'Welcome!');
    }
}

(new SignupService(new SmtpMailer()))->register('a@b.com');

DIP 与容器

依赖倒置是一项原则;依赖注入是实现它的一种技术。DI 容器(PSR-11)会在组合根中将具体的 SmtpMailer 连接到 Mailer 抽象。使用方从不写 new SmtpMailer(),因此源代码依赖的方向会指向抽象,而不是具体细节。

<?php
// Composition root wiring (pseudo-container)
$bindings = [
    Mailer::class => fn() => new SmtpMailer(),
];
$resolve = fn(string $id) => $bindings[$id]();

$service = new SignupService($resolve(Mailer::class));
$service->register('user@example.com');

SOLID 协同运用

这些原则会相互强化。ISP 使接口保持精简,从而让 DIP 注入保持专注;OCP 依赖多态,而 LSP 保证多态的安全性;SRP 帮助您创建小型类,使这一切成为可能。请以内部高内聚、彼此松耦合为目标。

  • 不要对只使用一次的类进行过度抽象。
  • 当出现第二种实现或测试替身时,再引入接口。

实用主义

SOLID 是手段,而不是目的。只有一个实现的过早接口不会带来任何收益,反而会增加间接层(“推测性通用化”)。请在变化真正到来或显然即将到来时应用这些原则。拥有测试后,朝 SOLID 方向重构的成本很低;在第一天就盲目朝这个方向重构则是在浪费时间。

快速检查

Square/Rectangle 的继承问题违反了哪项原则?

回顾

您已将全部五项 SOLID 原则应用于实际 PHP:

  • SRP:每个类只有一个变更原因。
  • OCP:通过新的多态类进行扩展,而不是修改 switch。
  • LSP:子类型遵守基类型契约。
  • ISP:使用面向角色的小型接口,而不是臃肿接口。
  • DIP:依赖抽象,在组合根中注入具体细节。

请使用这些原则指导重构,而不要将其变成形式主义。

常见问题解答

「实践 SOLID 原则」课时是免费的吗?

是的 — 「实践 SOLID 原则」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。

「实践 SOLID 原则」这节课中我会学到什么?

将五项 SOLID 原则应用于真实的 PHP 类。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PHP Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「实践 SOLID 原则」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PHP Academy 课中编写并运行代码吗?

能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 实践 SOLID 原则
  2. 创建型模式:工厂、构建器、单例
  3. 结构型模式:适配器、装饰器、外观
  4. 行为型模式:策略、观察者、命令
← 返回 PHP Academy