PHP SOLID原则实战:用SRP、OCP、LSP重构电商系统
2026/6/22 7:13:35 网站建设 项目流程

1. SOLID不是“编程技巧”,而是面向对象系统能否活过三年的体检报告

我第一次在项目里看到SOLID这个词,是在接手一个PHP电商后台的第三天。当时团队正为“为什么改个订单状态要动七个类、牵扯十二个文件”而集体沉默。老架构师没说话,只把一张泛黄的A4纸推过来,上面手写着五条原则——不是代码,是五句带血的诊断结论:“你这个系统,已经得了面向对象贫血症。”

SOLID从来就不是什么“高大上设计规范”,它是五个经过三十年工业级验证的反脆弱性检查点。它不告诉你“怎么写代码”,而是用五把手术刀,精准切开你的类结构、依赖关系和接口定义,告诉你:哪里正在腐烂,哪里即将崩溃,哪里还能抢救。比如你写的那个OrderService类,如果同时承担了计算运费、生成PDF、发送邮件、更新库存四个职责,那它已经违反了SOLID里的第一个原则——SRP(单一职责原则),这不是风格问题,是技术债务的定时炸弹。

更关键的是,SOLID的每一条都直指现代PHP开发中最痛的现实:我们用Laravel写业务,却用原生PHP思维组织代码;我们调用Composer加载包,却把所有逻辑塞进一个Controller;我们用Eloquent建模,却让Model承担了不该有的业务规则校验。这些不是“小问题”,它们会让单元测试写到一半就放弃,让新同事读三天代码还找不到主流程,让一次促销活动上线后,数据库连接池直接打满。

你可能注意到热搜词里混进了“SolidWorks”“STLinkV2接口”甚至“Type-C充电电路”——这恰恰说明SOLID被严重误读了。它和机械设计软件、硬件接口协议毫无关系,它的“SOLID”是五个英文单词首字母的缩写,每个字母背后都对应着一个具体、可测量、能立刻验证的代码特征。比如当你看到一个PHP接口定义里出现public function process($data)这种模糊参数名时,你就该警觉:这很可能违反了LSP(里氏替换原则)——因为任何实现了这个接口的类,都无法保证对$data的处理逻辑一致。

所以别再把它当成“理论课”。接下来我要带你做的,不是背诵五条定义,而是用真实PHP代码当X光片,逐条扫描:你的类是否在SRP上出现内聚断裂?你的继承体系是否在LSP上埋了多态雷区?你的依赖注入是否在DIP上构建了脆弱耦合?我会用你每天都在写的Controller、Repository、Service为例,告诉你哪一行代码在透支未来,哪一次重构能立竿见影。这不是学术讨论,这是给你的PHP系统做一次CT扫描。

2. SRP:为什么你的Controller里藏着三个本该独立的系统

2.1 单一职责不是“一个方法只做一件事”,而是“一个类只对一种变化负责”

很多PHP开发者把SRP理解成“每个方法只干一件小事”,于是写出这样的Controller:

class OrderController extends Controller { public function store(Request $request) { // 1. 验证请求数据(职责:输入校验) $validated = $request->validate([ 'user_id' => 'required|exists:users,id', 'items' => 'required|array', 'items.*.product_id' => 'required|exists:products,id' ]); // 2. 创建订单记录(职责:领域模型操作) $order = Order::create([ 'user_id' => $validated['user_id'], 'status' => 'pending', 'total_amount' => $this->calculateTotal($validated['items']) ]); // 3. 发送邮件通知(职责:外部系统集成) Mail::to($order->user->email)->send(new OrderPlaced($order)); // 4. 推送站内信(职责:内部消息分发) Notification::send($order->user, new OrderPlacedNotification($order)); // 5. 记录审计日志(职责:系统监控) Log::info("Order created", ['order_id' => $order->id, 'user_id' => $order->user_id]); return response()->json(['order_id' => $order->id]); } }

表面看,每个步骤都很清晰。但SRP的真正判据是:当需求变更时,有多少个原因会导致这个类被修改?

  • 如果运营要求“邮件模板增加促销信息”,要改;
  • 如果风控部门要求“高风险订单需人工审核”,要改;
  • 如果DBA说“审计日志要接入ELK”,要改;
  • 如果产品说“站内信要加跳转链接”,还要改。

这个Controller对邮件策略、风控规则、日志格式、通知渠道四种完全无关的变化都负有责任——它早已不是控制器,而是个“变化磁铁”。

提示:SRP的检验标准极其朴素——问自己:“如果明天产品经理说‘取消邮件通知’,我需要删掉这个类里的几行代码?如果答案超过3行,这个类就违反了SRP。”

2.2 PHP中SRP落地的三道物理隔离墙

真正的SRP实现,不是靠“写得更细”,而是靠物理层面的职责切割。我在三个不同规模的PHP项目中验证过,以下三道隔离墙缺一不可:

第一道墙:Controller只做“请求-响应”翻译器
它不碰业务逻辑,不调用Model,不发邮件。它的唯一工作是:

  • 接收HTTP请求(含验证)
  • 调用Application Service(应用服务层)
  • 将返回值转换为HTTP响应

重构后的OrderController应该长这样:

class OrderController extends Controller { public function store(StoreOrderRequest $request, OrderApplicationService $service) { // 仅做两件事:1. 接收已验证数据 2. 转发给应用服务 $result = $service->createOrder($request->validated()); // 仅做一件事:将结果映射为HTTP响应 if ($result->isSuccess()) { return response()->json(['order_id' => $result->orderId], 201); } return response()->json(['error' => $result->message], 400); } }

注意:StoreOrderRequest是Laravel的表单请求类,它把验证逻辑从Controller中剥离;OrderApplicationService是独立的服务类,它封装了所有业务动作。

第二道墙:Application Service封装“用例”而非“功能”
OrderApplicationService不是工具箱,而是业务场景的执行者。它不暴露calculateTotal()sendEmail()方法,只提供createOrder()这个完整用例:

class OrderApplicationService { public function createOrder(array $data): OrderCreationResult { // 这里才开始真正的业务编排 try { // 步骤1:创建领域模型(由Domain Service协调) $order = $this->orderDomainService->create($data); // 步骤2:触发领域事件(解耦后续动作) event(new OrderCreated($order)); return OrderCreationResult::success($order->id); } catch (ValidationException $e) { return OrderCreationResult::failure($e->getMessage()); } } }

关键点:event(new OrderCreated($order))不是直接调用邮件服务,而是发布领域事件。邮件、站内信、日志等后续动作,由独立的Event Listener处理——这才是真正的职责分离。

第三道墙:Domain Service与Infrastructure Service的边界
很多人以为“把方法拆到不同类里”就算SRP,但真正的危险在于领域逻辑与基础设施的混淆。比如计算运费,如果写成:

// ❌ 错误:领域逻辑污染了基础设施细节 public function calculateShipping($address) { // 直接调用快递API获取实时运费 $response = Http::get('https://api.express.com/rates', [ 'from' => config('app.warehouse'), 'to' => $address ]); return $response['rate']; }

这就把运费计算规则(领域知识)和快递API调用(基础设施)绑死了。正确的做法是:

// ✅ 正确:领域服务定义接口,基础设施实现 interface ShippingCalculator { public function calculateForAddress(string $address): float; } // Infrastructure层实现(可随时替换) class ExpressApiShippingCalculator implements ShippingCalculator { public function calculateForAddress(string $address): float { // 实际调用API } } // Domain层使用(不关心实现) class OrderDomainService { public function create(array $data, ShippingCalculator $calculator): Order { $shipping = $calculator->calculateForAddress($data['address']); // ... 创建订单逻辑 } }

注意:这里ShippingCalculator接口定义在Domain层,实现类在Infrastructure层。当公司换快递服务商时,你只需新增一个SFExpressShippingCalculator类,完全不用动领域核心逻辑——这就是SRP带来的可维护性。

2.3 PHP开发者最常踩的SRP陷阱:Repository模式的滥用

在Laravel项目中,我见过最普遍的SRP破坏行为,就是把Repository写成“万能DAO”:

// ❌ 典型反模式:Repository承担了不该有的职责 class OrderRepository { public function getAllWithUserAndItems() { /* ... */ } // 数据查询 public function updateStatus($id, $status) { /* ... */ } // 状态变更 public function sendReminder($id) { /* ... */ } // 发送邮件 public function exportToExcel($ids) { /* ... */ } // 文件导出 public function calculateRevenue($month) { /* ... */ } // 统计计算 }

这个Repository同时负责:数据访问、业务操作、外部集成、报表生成、文件IO——它比Controller还像“上帝类”。正确做法是:

  • Repository只做一件事:按领域规则存取聚合根
    OrderRepository::find($id)OrderRepository::save($order)是合法的;
    OrderRepository::sendReminder()是绝对禁止的。

  • 业务操作交给Application Service
    发送提醒是业务用例,应由OrderApplicationService::sendReminder($orderId)处理。

  • 报表导出交给专门的ExportService
    它可以调用多个Repository,组装数据,生成Excel——但绝不修改领域状态。

  • 统计计算交给AnalyticsService
    它可能直接查视图或物化表,绕过ORM,追求性能——这和领域模型的CRUD是两条平行线。

我在一个日均订单10万的PHP电商系统里实践过这套分离。当运营部门要求“给VIP用户加专属运费折扣”时,改动范围是:

  1. 新增VipShippingCalculator类(实现ShippingCalculator接口)
  2. 在服务容器中绑定VIP计算器
  3. 仅此而已。

没有改Controller,没有动Repository,没有碰Model。整个系统像乐高一样,只替换一块积木就完成升级——这就是SRP赋予系统的抗变化能力。

3. OCP:如何让“加新功能”变成“只写新代码,不碰旧代码”

3.1 开闭原则的本质是“用抽象隔离变化”,不是“提前设计所有扩展点”

OCP(Open/Closed Principle)常被误解为“要为所有可能的未来变化预留接口”。结果就是PHP项目里充斥着PaymentStrategyInterfaceNotificationChannelInterfaceReportGeneratorInterface……但90%的接口只有一个实现类,且从未被替换过。这不仅没带来灵活性,反而增加了理解成本。

OCP的真相是:当需求明确要扩展时,你能否通过添加新类、新文件来实现,而不是修改现有类?它不要求你预测未来,只要求你为已知的、确定要变的维度建立抽象。

以支付模块为例。最初只有支付宝支付:

// ❌ 违反OCP:硬编码支付方式 class PaymentService { public function pay($order, $method) // $method = 'alipay' { if ($method === 'alipay') { return $this->alipayPay($order); } if ($method === 'wechat') { return $this->wechatPay($order); } throw new Exception('Unsupported payment method'); } }

当微信支付上线时,你必须修改PaymentService——这违反了OCP。但OCP不是让你一开始就写PaymentStrategyInterface,而是当第二个支付方式确定要接入时,立即重构:

// ✅ 符合OCP:抽象出支付行为,新支付方式只需新增类 interface PaymentGateway { public function charge(Order $order, array $params): PaymentResult; } class AlipayGateway implements PaymentGateway { public function charge(Order $order, array $params): PaymentResult { // 支付宝SDK调用 } } class WechatGateway implements PaymentGateway { public function charge(Order $order, array $params): PaymentResult { // 微信SDK调用 } } // Application Service中使用策略模式 class PaymentApplicationService { public function processPayment(Order $order, string $gatewayName, array $params) { // 从容器解析具体网关(Laravel自动绑定) $gateway = app(PaymentGateway::class . ucfirst($gatewayName) . 'Gateway'); return $gateway->charge($order, $params); } }

现在,当银联支付要接入时,你只需:

  1. 新建UnionpayGateway类,实现PaymentGateway接口
  2. 在服务提供者中绑定PaymentGatewayUnionpayGateway
  3. 前端传gateway_name=unionpay即可

零修改旧代码,零影响现有支付流程——这才是OCP的实战价值。

3.2 PHP中OCP落地的关键:依赖注入容器是你的“扩展引擎”

很多PHP开发者知道要抽象,却卡在“怎么让新类自动生效”。答案就在Laravel的服务容器中。OCP的实现,80%依赖于容器的绑定策略。

以通知系统为例。最初只有邮件通知:

class NotificationService { public function send($user, $message) { Mail::to($user->email)->send(new NotificationMail($message)); } }

当要支持短信、站内信时,不要改成if-else,而是利用容器的上下文绑定

// 在AppServiceProvider中注册 public function register() { // 默认绑定邮件通知 $this->app->bind(NotificationSender::class, EmailNotificationSender::class); // 为特定场景绑定其他实现 $this->app->when(OrderShippedNotification::class) ->needs(NotificationSender::class) ->give(SmsNotificationSender::class); $this->app->when(UserRegisteredNotification::class) ->needs(NotificationSender::class) ->give(InternalNotificationSender::class); }

此时,你的通知服务类可以这样写:

class NotificationService { public function __construct(private NotificationSender $sender) {} public function send($user, $message) { $this->sender->send($user, $message); // 自动根据上下文注入正确实现 } }

当产品说“订单发货用短信,用户注册用站内信,密码重置用邮件”时,你不需要改任何业务逻辑代码,只需在服务提供者里调整几行绑定配置。容器成了你的“扩展开关”,OCP从理论变成了可配置的工程实践。

3.3 避免OCP的伪实现:警惕“过度设计”的三重陷阱

我在Code Review中发现,PHP项目最容易在OCP上犯的错误,不是没做,而是做错了。以下是三个高频陷阱:

陷阱一:抽象层与实现层混在同一命名空间
错误示例:

// app/Services/Payment/ // - PaymentGateway.php (接口) // - AlipayGateway.php (实现) // - WechatGateway.php (实现) // - PaymentService.php (又一个实现?还是协调者?)

当所有类都在app/Services/Payment/下,新人无法分辨哪些是契约(应该稳定)、哪些是实现(可以随意增删)。正确做法是严格分层:

app/Domain/Interfaces/PaymentGateway.php // 契约层(稳定) app/Application/Services/PaymentService.php // 应用层(协调者) app/Infrastructure/Payments/AlipayGateway.php // 基础设施层(易变) app/Infrastructure/Payments/WechatGateway.php

陷阱二:接口方法签名过度通用,失去约束力
错误示例:

interface PaymentGateway { public function execute(array $params): array; // 参数和返回值都是数组! }

这种接口等于没抽象——每个实现类都要自己解析$params['amount']$params['currency'],根本无法保证一致性。正确做法是用DTO(数据传输对象)强约束

class PaymentRequest { public function __construct( public readonly int $amount, public readonly string $currency, public readonly string $returnUrl ) {} } interface PaymentGateway { public function charge(PaymentRequest $request): PaymentResponse; }

陷阱三:把OCP和配置文件混为一谈
有些团队用YAML配置支付方式:

# config/payment.php gateways: alipay: class: App\Infrastructure\Payments\AlipayGateway enabled: true wechat: class: App\Infrastructure\Payments\WechatGateway enabled: false

然后在代码里$class = config('payment.gateways.'.$name.'.class')。这看似灵活,实则破坏了类型安全——IDE无法提示方法,运行时才报错。OCP的优雅在于编译期(PHPStan/IDE)就能验证的契约,而不是运行时字符串拼接。

实战心得:OCP不是“为了抽象而抽象”,而是“当第二个同类需求出现时,立刻重构”。我在一个PHP SaaS项目中定下铁律:任何功能,只要确认会有第二个变体,当天就必须提取接口并重构调用方。这条规则让我们的支付、通知、导出模块在接入7种新渠道时,平均每次改动时间从4小时降到15分钟。

4. LSP:为什么你的子类一替换,系统就崩了

4.1 里氏替换原则不是“子类能运行”,而是“子类行为不破坏父类契约”

LSP(Liskov Substitution Principle)常被简化为“子类可以替换父类”,但这只是表象。它的核心是:当你用子类实例替换父类实例时,所有基于父类契约的代码,其行为必须保持不变。换句话说,子类不能偷偷改变父类承诺的行为。

在PHP中,最常见的LSP破坏发生在集合操作上。比如你定义了一个UserCollection类:

class UserCollection implements IteratorAggregate { private array $users; public function __construct(array $users = []) { $this->users = $users; } public function add(User $user): void { $this->users[] = $user; } public function first(): ?User { return $this->users[0] ?? null; } public function count(): int { return count($this->users); } }

一切正常。但某天你需要一个“只读用户集合”,于是写了子类:

class ReadOnlyUserCollection extends UserCollection { public function add(User $user): void { throw new Exception('Cannot add to read-only collection'); } public function first(): ?User { // ❌ 重大LSP破坏:父类承诺返回User|null,这里可能返回null // 但调用方假设first()总有值,导致NPE return parent::first(); } }

问题出在哪?first()方法看似没改,但ReadOnlyUserCollection的构造函数允许空数组,而父类UserCollectionfirst()方法在空集合时返回null——这本身没问题。但调用方代码可能是:

$user = $collection->first(); $user->getName(); // 当$user为null时,直接报错!

父类UserCollection的文档或约定可能隐含“非空集合”,而子类打破了这个隐式契约。LSP要求:子类必须满足父类的所有前置条件(precondition)、后置条件(postcondition)和不变量(invariant)

4.2 PHP中LSP的四大死亡场景及修复方案

场景一:子类加强前置条件(Precondition Strengthening)

错误示例:父类方法接受任意整数,子类却要求必须为正数。

class Calculator { public function divide(int $a, int $b): float { return $a / $b; } } class SafeCalculator extends Calculator { public function divide(int $a, int $b): float { if ($b <= 0) { // ❌ 加强了前置条件:父类允许b=0(会抛除零异常),子类直接拒绝 throw new InvalidArgumentException('Divisor must be positive'); } return parent::divide($a, $b); } }

修复方案:子类不能比父类更严格。如果需要安全除法,应该用组合而非继承:

class SafeCalculator { private Calculator $calculator; public function __construct(Calculator $calculator) { $this->calculator = $calculator; } public function safeDivide(int $a, int $b): ?float { if ($b === 0) { return null; // 明确处理边界情况 } return $this->calculator->divide($a, $b); } }
场景二:子类削弱后置条件(Postcondition Weakening)

错误示例:父类承诺返回非null对象,子类却可能返回null。

interface UserRepository { public function find(int $id): User; // 契约:必须返回User实例 } class DatabaseUserRepository implements UserRepository { public function find(int $id): User { $user = User::find($id); if (!$user) { throw new UserNotFoundException(); // 严格遵守契约 } return $user; } } class CacheUserRepository extends DatabaseUserRepository // ❌ 错误继承! { public function find(int $id): ?User // ❌ 削弱后置条件:返回?User而非User { return cache()->get("user:{$id}"); } }

修复方案:用组合替代继承,或重新设计接口。正确做法是:

interface UserRepository { public function find(int $id): ?User; // 从一开始契约就允许null public function findByIdOrFail(int $id): User; // 另一个方法处理“必须存在”场景 }
场景三:子类破坏不变量(Invariant Violation)

错误示例:父类保证某个属性始终为正,子类却允许设为负。

class BankAccount { private float $balance = 0.0; public function deposit(float $amount): void { $this->balance += $amount; } public function getBalance(): float { return $this->balance; // 不变量:balance >= 0 } } class OverdraftAccount extends BankAccount { private float $overdraftLimit = 1000.0; public function withdraw(float $amount): void { // 允许余额为负,但不超过透支限额 $this->balance -= $amount; if ($this->balance < -$this->overdraftLimit) { throw new Exception('Overdraft limit exceeded'); } } }

问题:OverdraftAccount::getBalance()可能返回负数,破坏了父类BankAccount的不变量。调用方代码可能依赖balance >= 0做判断。

修复方案:子类不应继承父类的不变量,而应重新定义自己的契约。更好的设计是:

interface Account { public function getBalance(): float; } class SavingsAccount implements Account // 普通账户 { private float $balance = 0.0; public function getBalance(): float { return max(0, $this->balance); // 保证非负 } } class CreditAccount implements Account // 信用账户 { private float $creditLine = 1000.0; private float $usedCredit = 0.0; public function getBalance(): float { return $this->creditLine - $this->usedCredit; // 返回可用额度 } }
场景四:子类改变方法的副作用(Side Effect Change)

错误示例:父类方法无副作用,子类却引入日志、缓存等。

interface Logger { public function log(string $message): void; // 契约:仅记录日志 } class FileLogger implements Logger { public function log(string $message): void { file_put_contents('app.log', $message . PHP_EOL, FILE_APPEND); } } class DatabaseLogger extends FileLogger // ❌ 错误:继承了FileLogger,却改变行为 { public function log(string $message): void { // ❌ 同时写文件和数据库!调用方只期望写文件 parent::log($message); DB::table('logs')->insert(['message' => $message]); } }

修复方案:用装饰器模式(Decorator Pattern)显式叠加行为

class DatabaseLoggerDecorator implements Logger { public function __construct( private Logger $decorated, private DatabaseLogger $dbLogger ) {} public function log(string $message): void { $this->decorated->log($message); // 保持原有行为 $this->dbLogger->log($message); // 显式添加新行为 } }

关键洞察:LSP不是关于“能不能跑”,而是关于“会不会意外”。在PHP的动态类型世界里,LSP的保障主要靠代码审查+单元测试+静态分析(PHPStan)。我要求团队为每个继承关系写测试:用子类替换父类后,所有父类的单元测试必须100%通过。这比任何文档都可靠。

5. ISP:为什么你的接口里堆了27个方法,却只用3个

5.1 接口隔离原则的核心是“客户端不应该被迫依赖它不使用的方法”

ISP(Interface Segregation Principle)直击PHP开发者的痛点:我们习惯定义一个“全能接口”,然后让所有类去实现它。比如一个经典的UserServiceInterface

interface UserServiceInterface { // 用户管理 public function create(array $data): User; public function update(int $id, array $data): User; public function delete(int $id): void; // 用户认证 public function login(string $email, string $password): ?User; public function logout(int $userId): void; public function refreshToken(string $token): string; // 用户权限 public function assignRole(int $userId, string $role): void; public function revokeRole(int $userId, string $role): void; public function hasPermission(int $userId, string $permission): bool; // 用户通知 public function sendEmail(int $userId, string $subject, string $body): void; public function sendSms(int $userId, string $message): void; // 用户统计 public function getActiveUsersCount(): int; public function getNewUsersThisMonth(): int; }

这个接口有13个方法,但实际场景中:

  • Controller只用logincreateupdate
  • 权限中间件只用hasPermissionassignRole
  • 运营后台只用getActiveUsersCountgetNewUsersThisMonth

结果就是:

  • 为测试login,你得Mock所有13个方法(PHPUnit里写27行Mock代码)
  • 为实现一个轻量级的ApiUserService,你得写10个throw new \BadMethodCallException()
  • sendSms需要加风控校验时,所有实现类都得改——哪怕它们根本不发短信

ISP要求:把大接口按客户端(Client)分组,拆成多个小接口。这里的“客户端”不是最终用户,而是调用这个接口的类

5.2 PHP中ISP落地的三层拆分策略

第一层:按调用方角色拆分(最有效)

针对上面的UserServiceInterface,按实际调用方拆:

// Controller需要的接口 interface UserManagementService { public function create(array $data): User; public function update(int $id, array $data): User; public function delete(int $id): void; } // 认证中间件需要的接口 interface UserAuthenticationService { public function login(string $email, string $password): ?User; public function logout(int $userId): void; public function refreshToken(string $token): string; } // 权限系统需要的接口 interface UserPermissionService { public function assignRole(int $userId, string $role): void; public function revokeRole(int $userId, string $role): void; public function hasPermission(int $userId, string $permission): bool; } // 运营系统需要的接口 interface UserAnalyticsService { public function getActiveUsersCount(): int; public function getNewUsersThisMonth(): int; }

现在,AuthController只依赖UserAuthenticationServiceAdminController只依赖UserManagementServiceUserAnalyticsService。当运营要加“用户留存率”统计时,只需扩展UserAnalyticsService,完全不影响登录流程。

第二层:按技术边界拆分(防污染)

很多PHP项目把数据库操作、缓存、外部API调用全塞进一个Repository接口。正确做法是:

// 领域层只定义数据契约 interface UserRepository { public function find(int $id): ?User; public function findByEmail(string $email): ?User; public function save(User $user): void; } // 基础设施层实现,并额外提供技术能力 interface DatabaseUserRepository extends UserRepository { public function withRoles(int $id): ?User; // Eloquent关系预加载 public function search(string $keyword): Collection; // 复杂查询 } interface CacheUserRepository extends UserRepository { public function clearCache(int $id): void; // 缓存管理 }

这样,领域层代码只依赖纯净的UserRepository,而具体实现类可以自由扩展技术能力,互不干扰。

第三层:按稳定性拆分(保核心)

对于高频变化的功能(如通知渠道),用“核心接口+扩展接口”模式:

// 核心契约:所有通知都必须能发送 interface Notifiable { public function notify(string $message): void; } // 扩展能力:邮件特有功能 interface EmailNotifiable extends Notifiable { public function emailSubject(): string; public function emailTemplate(): string; } // 扩展能力:短信特有功能 interface SmsNotifiable extends Notifiable { public function smsTemplate(): string; public function smsSender(): string; } // 用户类可以选择实现 class User implements EmailNotifiable, SmsNotifiable { public function notify(string $message): void { // 统一入口,内部路由到具体渠道 } public function emailSubject(): string { /* ... */ } public function emailTemplate(): string { /* ... */ } public function smsTemplate(): string { /* ... */ } public function smsSender(): string { /* ... */ } }

当产品说“VIP用户要加推送通知”时,你只需新增PushNotifiable接口和实现,无需修改User类的现有代码——这就是ISP带来的演进弹性。

5.3 ISP的终极检验:用Laravel的Facade和Contract验证

Laravel框架本身就是ISP的教科书案例。看看它的Cache门面:

// Illuminate\Contracts\Cache\Repository interface Repository { public function get($key, $default = null); public function put($key, $value, $ttl = null); public function forget($key); // ... 共12个核心方法 } // 但Laravel还提供了更细粒度的Contract interface Store { public function get($key); public function set($key, $value, $seconds); } interface TaggableStore extends Store { public function tags($names); }

当你在Controller里用Cache::get(),你依赖的是Repository接口;当你在队列任务里用Cache::tags(),你依赖的是TaggableStore接口。同一个底层实现(RedisStore),暴露不同的接口给不同的客户端——这正是ISP的精髓。

实战技巧:在PHPStorm中,右键点击接口名 → “Find Usages”,查看哪些类实现了它、哪些类调用了它。如果一个接口的实现类分布在app/Domainapp/Infrastructureapp/Presentation三个目录,且调用方跨越Controller、Job、Command,那它大概率违反了ISP——立刻拆分。我在一个PHP微服务项目中,用这个技巧把一个37方法的OrderServiceInterface拆成了5个专注接口,单元测试覆盖率从62%提升到94%,因为每个测试只需关注3-4个方法。

6. DIP:为什么你的Service类里new了十几个Repository

6.1 依赖倒置原则不是“用接口编程”,而是“让高层模块决定低层模块的抽象”

DIP(Dependency Inversion Principle)常被简化为“面向接口编程”,但这只是手段,不是目的。它的本质是:高层模块(业务逻辑)不应依赖低层模块(数据访问、网络请求),二者都应依赖抽象。更关键的是,这个抽象应由高层模块定义,而不是低层模块

在PHP中,最典型的DIP违反是“Service类里直接new Repository”:

class OrderService { public function process(Order $order) { // ❌ 高层模块(OrderService)直接依赖低层模块(DatabaseOrderRepository) $repository = new DatabaseOrderRepository(); $repository->save($order); $notification = new EmailNotificationService(); // 又一个低层依赖 $notification->send($order->user, 'Order processed'); $logger = new FileLogger(); // 再一个低层依赖 $logger->log("Order {$order->id} processed"); } }

问题不仅是

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询