PHP 中的 SOLID 设计原则详解 写出符合 SOLID 设计的优秀代码

SOLID 是面向对象编程的五个基本原则的缩写,由 Robert C. Martin(Uncle Bob)提出,Michael Feathers 创造了这个缩写词。这些原则帮助我们编写更清晰、更易维护的代码。

虽然大多数 PHP 开发者都听说过 SOLID,但真正理解并能在实践中运用这些原则的人却不多。本文将深入浅出地解释这五个原则,并通过实际的 PHP 代码示例来展示如何应用它们。

单一职责原则 (SRP)

一个类应该有一个,且只有一个改变的理由。

这是最容易被误解的原则。很多人认为"一件事"就是字面意思,但实际上我们需要结合"高内聚低耦合"来理解。

什么是内聚和耦合?

  • 内聚性:类或方法内部元素的关联程度。高内聚意味着类专注于一个特定的功能。
  • 耦合性:类与其他类的依赖程度。低耦合意味着类可以独立使用,易于测试和重用。

反例:违反 SRP 的 BlogPost 类

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,因为它既负责数据管理,又负责格式输出。

正例:遵循 SRP 的重构

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(),
        ];
    }
}

// 输出接口
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>";
    }
}

开闭原则 (OCP)

软件实体应该对扩展开放,对修改关闭。

这个原则的核心是:当需要添加新功能时,应该通过添加新代码来实现,而不是修改现有代码。

反例:违反 OCP 的动物交流系统

php
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。

正例:遵循 OCP 的重构

php
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';
    }
}

里氏替换原则 (LSP)

子类对象应该能够替换其父类对象,而不会破坏程序的正确性。

这个原则确保继承关系的正确性。

反例:经典的矩形-正方形问题

php
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;
    }
}

测试代码会失败:

php
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(); // 这里会出问题
}

正例:使用组合而非继承

php
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;
    }
}

接口隔离原则 (ISP)

客户端不应该被迫依赖它不使用的接口。

这个原则鼓励我们创建小而专注的接口,而不是大而全的接口。

反例:胖接口问题

php
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 被迫实现它不需要的方法。

正例:接口分离

php
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 导出
    }
}

依赖倒置原则 (DIP)

高层模块不应该依赖低层模块,两者都应该依赖抽象。

这个原则是依赖注入的基础。

反例:直接依赖具体实现

php
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,难以测试和扩展。

正例:依赖抽象

php
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());

实际应用建议

1. 循序渐进

不要试图一次性应用所有原则。从 SRP 开始,逐步改进代码。

2. 适度原则

过度设计比设计不足更糟糕。SOLID 原则是指导,不是教条。

3. 测试驱动

编写测试可以帮助你发现违反 SOLID 原则的地方。

4. 代码审查

在代码审查中,使用 SOLID 原则作为讨论的基础。

总结

SOLID 原则不是银弹,但它们提供了编写可维护、可扩展代码的指导。在 PHP 开发中,这些原则与 Laravel、Symfony 等框架的设计理念高度一致。

记住,好的代码应该是:

  • 可读的:其他开发者能快速理解
  • 可测试的:容易编写单元测试
  • 可扩展的:添加新功能不需要修改现有代码
  • 可维护的:修改现有功能不会破坏其他部分

通过实践 SOLID 原则,我们可以写出更好的 PHP 代码,构建更强大的应用程序。


参考资源:

JaguarJack
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接