SOLID 是面向对象编程的五个基本原则的缩写,由 Robert C. Martin(Uncle Bob)提出,Michael Feathers 创造了这个缩写词。这些原则帮助我们编写更清晰、更易维护的代码。
虽然大多数 PHP 开发者都听说过 SOLID,但真正理解并能在实践中运用这些原则的人却不多。本文将深入浅出地解释这五个原则,并通过实际的 PHP 代码示例来展示如何应用它们。
一个类应该有一个,且只有一个改变的理由。
这是最容易被误解的原则。很多人认为"一件事"就是字面意思,但实际上我们需要结合"高内聚低耦合"来理解。
class BlogPost
{
private Author $author;
private string $title;
private string $content;
private \DateTime $date;
public function getData(): array
{
return [
'author' => $this->author->fullName(),
'title' => $this->title,
'content' => $this->content,
'timestamp' => $this->date->getTimestamp(),
];
}
public function printJson(): string
{
return json_encode($this->getData());
}
public function printHtml(): string
{
return "<article>
<h1>{$this->title}</h1>
<article>
<p>{$this->date->format('Y-m-d H:i:s')}</p>
<p>{$this->author->fullName()}</p>
<p>{$this->content}</p>
</article>
</article>";
}
}
这个类违反了 SRP,因为它既负责数据管理,又负责格式输出。
// 数据模型只负责数据
class BlogPost
{
private Author $author;
private string $title;
private string $content;
private \DateTime $date;
public function getData(): array
{
return [
'author' => $this->author->fullName(),
'title' => $this->title,
'content' => $this->content,
'timestamp' => $this->date->getTimestamp(),
];
}
}
// 输出接口
interface BlogPostPrinter
{
public function print(BlogPost $blogPost): string;
}
// JSON 输出器
class JsonBlogPostPrinter implements BlogPostPrinter
{
public function print(BlogPost $blogPost): string {
return json_encode($blogPost->getData());
}
}
// HTML 输出器
class HtmlBlogPostPrinter implements BlogPostPrinter
{
public function print(BlogPost $blogPost): string {
$data = $blogPost->getData();
return "<article>
<h1>{$data['title']}</h1>
<article>
<p>{$data['timestamp']}</p>
<p>{$data['author']}</p>
<p>{$data['content']}</p>
</article>
</article>";
}
}
软件实体应该对扩展开放,对修改关闭。
这个原则的核心是:当需要添加新功能时,应该通过添加新代码来实现,而不是修改现有代码。
class Dog
{
public function bark(): string
{
return 'woof woof';
}
}
class Duck
{
public function quack(): string
{
return 'quack quack';
}
}
class Communication
{
public function communicate($animal): string
{
switch (true) {
case $animal instanceof Dog:
return $animal->bark();
case $animal instanceof Duck:
return $animal->quack();
default:
throw new \InvalidArgumentException('Unknown animal');
}
}
}
要添加新动物,必须修改 Communication
类,这违反了 OCP。
interface Communicative
{
public function speak(): string;
}
class Dog implements Communicative
{
public function speak(): string
{
return 'woof woof';
}
}
class Duck implements Communicative
{
public function speak(): string
{
return 'quack quack';
}
}
class Communication
{
public function communicate(Communicative $animal): string
{
return $animal->speak();
}
}
// 添加新动物不需要修改 Communication 类
class Cat implements Communicative
{
public function speak(): string
{
return 'meow meow';
}
}
子类对象应该能够替换其父类对象,而不会破坏程序的正确性。
这个原则确保继承关系的正确性。
class Rectangle
{
protected int $width;
protected int $height;
public function setWidth(int $width): void
{
$this->width = $width;
}
public function setHeight(int $height): void
{
$this->height = $height;
}
public function calculateArea(): int
{
return $this->width * $this->height;
}
}
class Square extends Rectangle
{
public function setWidth(int $width): void
{
$this->width = $width;
$this->height = $width; // 违反 LSP
}
public function setHeight(int $height): void
{
$this->width = $height; // 违反 LSP
$this->height = $height;
}
}
测试代码会失败:
public function testCalculateArea()
{
$shape = new Rectangle();
$shape->setWidth(10);
$shape->setHeight(2);
$this->assertEquals(20, $shape->calculateArea());
$shape->setWidth(5);
$this->assertEquals(10, $shape->calculateArea());
// 如果用 Square 替换 Rectangle,测试会失败
$shape = new Square(); // 这里会出问题
}
interface Shape
{
public function calculateArea(): int;
}
class Rectangle implements Shape
{
private int $width;
private int $height;
public function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
public function calculateArea(): int
{
return $this->width * $this->height;
}
}
class Square implements Shape
{
private int $side;
public function __construct(int $side)
{
$this->side = $side;
}
public function calculateArea(): int
{
return $this->side * $this->side;
}
}
客户端不应该被迫依赖它不使用的接口。
这个原则鼓励我们创建小而专注的接口,而不是大而全的接口。
interface Exportable
{
public function getPDF();
public function getCSV();
public function getExcel();
}
class Invoice implements Exportable
{
public function getPDF() {
// 实现 PDF 导出
}
public function getCSV() {
// 实现 CSV 导出
}
public function getExcel() {
// 实现 Excel 导出
}
}
class CreditNote implements Exportable
{
public function getPDF() {
throw new \NotImplementedException('PDF not supported');
}
public function getCSV() {
// 只实现 CSV 导出
}
public function getExcel() {
throw new \NotImplementedException('Excel not supported');
}
}
CreditNote
被迫实现它不需要的方法。
interface PdfExportable
{
public function getPDF();
}
interface CsvExportable
{
public function getCSV();
}
interface ExcelExportable
{
public function getExcel();
}
class Invoice implements PdfExportable, CsvExportable, ExcelExportable
{
public function getPDF() {
// 实现 PDF 导出
}
public function getCSV() {
// 实现 CSV 导出
}
public function getExcel() {
// 实现 Excel 导出
}
}
class CreditNote implements CsvExportable
{
public function getCSV() {
// 只实现 CSV 导出
}
}
高层模块不应该依赖低层模块,两者都应该依赖抽象。
这个原则是依赖注入的基础。
class DatabaseLogger
{
public function logError(string $message): void
{
// 记录到数据库
}
}
class MailerService
{
private DatabaseLogger $logger;
public function __construct(DatabaseLogger $logger)
{
$this->logger = $logger;
}
public function sendEmail(): void
{
try {
// 发送邮件逻辑
} catch (\Exception $exception) {
$this->logger->logError($exception->getMessage());
}
}
}
MailerService
直接依赖 DatabaseLogger
,难以测试和扩展。
interface Logger
{
public function logError(string $message): void;
}
class DatabaseLogger implements Logger
{
public function logError(string $message): void
{
// 记录到数据库
}
}
class FileLogger implements Logger
{
public function logError(string $message): void
{
// 记录到文件
}
}
class MailerService
{
private Logger $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function sendEmail(): void
{
try {
// 发送邮件逻辑
} catch (\Exception $exception) {
$this->logger->logError($exception->getMessage());
}
}
}
// 使用依赖注入
$mailer = new MailerService(new DatabaseLogger());
// 或者
$mailer = new MailerService(new FileLogger());
不要试图一次性应用所有原则。从 SRP 开始,逐步改进代码。
过度设计比设计不足更糟糕。SOLID 原则是指导,不是教条。
编写测试可以帮助你发现违反 SOLID 原则的地方。
在代码审查中,使用 SOLID 原则作为讨论的基础。
SOLID 原则不是银弹,但它们提供了编写可维护、可扩展代码的指导。在 PHP 开发中,这些原则与 Laravel、Symfony 等框架的设计理念高度一致。
记住,好的代码应该是:
通过实践 SOLID 原则,我们可以写出更好的 PHP 代码,构建更强大的应用程序。
参考资源: