1. 为什么这五个字母比你写的类还重要:SOLID不是教条,是面向对象的“防癌体检表”
在PHP项目里,我见过太多这样的场景:一个原本只有300行的
UserManager
类,三年后膨胀到2800行,方法名从
getUserById
变成
getUserByIdWithCacheAndRoleCheckAndAuditLogIfAdminOrSuperAdminExceptOnWeekends
;数据库查询逻辑、Excel导出、邮件通知、权限校验全挤在一个文件里;改个密码重置流程,得同时测试登录、短信发送、日志记录、前端提示——最后发现改崩了订单模块。这不是代码写得差,而是设计在早期就埋下了系统性衰变的种子。而SOLID这五个字母,就是一套提前识别、拦截、修复这种衰变的临床检查清单。它不告诉你“该写什么功能”,而是问你:“这个类,有没有可能在未来某天,因为一个新需求,被迫同时修改三处完全无关的逻辑?”如果答案是“有”,那它已经病了,只是还没发作。SOLID不是给初学者背诵的教条,它是资深开发者在无数个凌晨三点修复线上事故后,用血泪凝练出的五项“可维护性生命体征指标”。它和PHP强相关吗?不直接相关——但PHP的动态性、弱类型、历史包袱重等特点,恰恰让SOLID的缺失后果来得更快、更猛。当你在Laravel的Service层里塞进一个
file_put_contents()
写日志,又在同一个方法里调用
DB::table()->insert()
存数据,再顺手
Mail::to()->send()
发通知时,你已经在违反SRP(单一职责);当你发现所有Controller都依赖同一个
BaseService
,而这个基类里硬编码了MySQL连接,导致无法为单元测试注入Mock数据库时,OCP(开闭原则)已经亮起红灯。这五个原则,是写PHP代码时悬在头顶的达摩克利斯之剑,不是为了让你束手束脚,而是确保你每一次敲下的
class
、
function
、
return
,都在加固而不是腐蚀整个系统的骨架。
2. SRP:单一职责——别让一个类同时当厨师、会计和保安
SRP(Single Responsibility Principle)常被误解为“一个类只做一件事”,这就像说“一个医生只看一种病”一样荒谬。真正的SRP是:“一个类应该只有一个改变的理由”。这个“理由”,指的是业务需求变更的源头。在PHP开发中,这个原则的落地,往往体现在对“变化轴”的精准切割上。
2.1 从一个真实PHP案例看职责混杂的代价
去年重构一个电商后台的
OrderProcessor
类时,我遇到了典型反模式:
class OrderProcessor {
public function process($orderData) {
// 1. 验证订单数据(业务规则)
if (!$this->validateOrder($orderData)) {
throw new InvalidOrderException();
}
// 2. 计算价格(业务逻辑)
$total = $this->calculateTotal($orderData);
// 3. 保存到MySQL(数据访问)
$orderId = DB::table('orders')->insertGetId([
'total' => $total,
'status' => 'pending'
]);
// 4. 发送微信通知(外部服务集成)
WeChat::sendOrderNotification($orderId, $orderData['user_id']);
// 5. 记录操作日志(审计)
Log::info("Order {$orderId} processed by user {$orderData['admin_id']}");
return $orderId;
}
}
表面看逻辑清晰,但当业务方提出三个新需求时,问题爆发:
- 需求A :新增支付宝支付渠道,需在价格计算后增加“支付方式适配”逻辑;
- 需求B :日志系统升级为ELK,要求日志格式JSON化并添加trace_id;
-
需求C
:订单表要分库分表,
DB::table('orders')必须替换为分片路由逻辑。
这三个需求分别来自
支付团队
、
运维团队
、
DBA团队
——它们是三个完全独立的“变化轴”。而
OrderProcessor::process()
方法,却成了这三个轴的交汇点。每次修改,都得重新测试全部五段逻辑,上线前全员提心吊胆。这就是SRP失效的直接后果:
修改成本指数级上升,回归测试范围失控,故障隔离能力归零
。
2.2 PHP中的职责分离实操:接口驱动的契约拆解
解决之道不是删代码,而是建契约。我们按变化轴切分:
-
业务规则轴
→
OrderValidatorInterface -
核心计算轴
→
OrderCalculatorInterface -
数据持久化轴
→
OrderRepositoryInterface -
通知集成轴
→
OrderNotifierInterface -
审计日志轴
→
OrderAuditLoggerInterface
关键在于: 所有接口定义在领域层(Domain),不依赖任何框架或具体实现 。例如:
// Domain/Contracts/OrderRepositoryInterface.php
interface OrderRepositoryInterface {
public function save(Order $order): int;
public function findById(int $id): ?Order;
}
这个接口里没有
DB::table()
,没有
PDO
,甚至没有
MySQL
字样。它只声明“我要存一个订单,返回ID”。这样,当DBA要求分库分表时,只需新建一个
ShardedOrderRepository
实现该接口,而
OrderProcessor
完全不用动——因为它只依赖接口,不依赖实现。这就是OCP(开闭原则)与SRP的协同效应。
2.3 PHP开发者最容易踩的SRP陷阱
-
陷阱1:把“工具类”当万能胶
常见错误:创建Helper类,里面塞满formatDate()、generateToken()、sendEmail()、encryptPassword()。这本质是把所有“辅助性变化轴”强行合并。正确做法是按领域拆:DateTimeFormatter(时间)、SecurityTokenGenerator(安全)、EmailService(通信)、PasswordHasher(认证)。每个类只响应自己领域的变更。 -
陷阱2:在Controller里写业务逻辑
ThinkPHP/Laravel新手常把价格计算、库存扣减、优惠券核销全写在Controller里。Controller的唯一职责应该是“协调请求与响应”,即接收输入、调用领域服务、返回视图或JSON。把业务逻辑塞进去,等于让门卫(Controller)同时兼任财务总监、仓库管理员和销售经理——门卫一换岗,整个公司停摆。 -
陷阱3:用Trait逃避职责划分
trait Loggable { public function log() { ... } }看似优雅,实则危险。当Loggable需要对接新日志系统时,所有使用它的类都得跟着改。这违背了“高内聚低耦合”——日志逻辑本应集中管理,而非分散在各处。
提示:检验SRP是否达标,有个极简测试:打开你的类文件,用鼠标选中任意一段代码(比如数据库操作部分),然后问自己:“如果明天这个功能要下线,我能否安全删除这段代码,而不影响其他功能运行?”如果答案是否定的,说明职责已混杂。
3. OCP:开闭原则——对扩展开放,对修改关闭,PHP里的“热插拔”哲学
OCP(Open/Closed Principle)常被简化为“新增功能不改旧代码”,但这忽略了其精髓: 它不是关于代码行数的增减,而是关于抽象层次的控制权转移 。在PHP中,OCP的成败,取决于你是否把“易变的”和“稳定的”成功分隔在抽象边界两侧。
3.1 为什么PHP项目特别需要OCP:动态语言的双刃剑
PHP的灵活性是把双刃剑。你可以用
eval()
执行任意字符串,可以用
__call()
拦截不存在的方法,可以
include
任意文件——这些特性让快速原型开发如鱼得水,但也让系统像一座用橡皮泥搭的房子:每次需求变更,都得用手捏一捏,稍用力就变形。OCP正是为这种环境设计的“结构加固剂”。它要求你:
- 把稳定的部分(如业务流程骨架)放在高层抽象中 ;
- 把易变的部分(如支付方式、通知渠道、存储引擎)下沉为可插拔的实现 。
以电商的“订单状态流转”为例。状态机本身是稳定的(创建→支付→发货→完成),但每个状态的触发条件、后续动作却千变万化:
- 支付成功后,可能要调用微信回调、更新库存、生成物流单;
- 发货后,可能要发短信、同步ERP、计算返现;
- 完成后,可能要触发会员积分、发送评价提醒、归档数据。
如果把这些动作硬编码在状态机里,每加一个新动作,就得改状态机核心代码——这直接违反OCP。
3.2 PHP中的OCP落地:策略模式+依赖注入的黄金组合
我们用策略模式(Strategy Pattern)构建可扩展的状态处理器:
// Domain/Contracts/OrderStateHandlerInterface.php
interface OrderStateHandlerInterface {
public function handle(Order $order): void;
}
// Infrastructure/Handlers/WeChatPaymentCallbackHandler.php
class WeChatPaymentCallbackHandler implements OrderStateHandlerInterface {
public function handle(Order $order): void {
WeChat::notify($order->id);
$this->updateInventory($order);
}
}
// Infrastructure/Handlers/ErpSyncHandler.php
class ErpSyncHandler implements OrderStateHandlerInterface {
public function handle(Order $order): void {
ERPClient::syncOrder($order);
}
}
关键突破在于:
状态机不关心具体做什么,只关心“谁来处理”
。我们通过依赖注入容器(如Laravel的
Container
或PHP-DI)动态绑定:
// config/services.php
return [
'order.state.handlers' => [
'payment_success' => [WeChatPaymentCallbackHandler::class, ErpSyncHandler::class],
'shipped' => [SmsNotificationHandler::class, LogisticsGenerateHandler::class],
]
];
当需要新增“支付成功后发站内信”功能时,只需:
-
写一个
InAppNotificationHandler实现OrderStateHandlerInterface; -
在配置里把
InAppNotificationHandler::class加入payment_success数组; - 零修改状态机核心代码,零修改现有Handler 。
这就是OCP的PHP实践: 扩展靠配置,修改靠实现,核心永远不动 。它把“改代码”的风险,转化成了“加配置”的安全操作。
3.3 PHP特有的OCP强化技巧:运行时策略选择
PHP的动态性允许更激进的OCP应用。比如,不同客户需要不同的价格计算策略:
- A客户:满100减20;
- B客户:VIP折扣85折;
- C客户:阶梯价(1-10件9折,11-50件8折);
传统做法是写一堆
if-else
判断客户类型,再调用对应方法——这违反OCP。更好的方案是:
class PriceCalculator {
private array $strategies;
public function __construct(array $strategies) {
$this->strategies = $strategies; // ['a' => AStrategy::class, 'b' => BStrategy::class]
}
public function calculate(int $customerId, float $basePrice): float {
$strategyClass = $this->strategies[$customerId] ?? DefaultStrategy::class;
return (new $strategyClass())->calculate($basePrice);
}
}
客户ID作为策略选择键,运行时动态加载。新增客户D?只需在
$strategies
数组里加一行配置,无需碰
PriceCalculator
类本身。这种“配置即代码”的思路,是PHP发挥OCP威力的关键。
注意:OCP不是拒绝修改,而是把修改引导到安全区域。当业务规则发生根本性变化(如“满减”变成“买赠”),此时修改策略接口本身是合理的——因为变化轴已迁移。OCP保护的是“稳定不变的抽象”,而非“永不修改的代码”。
4. LSP:里氏替换原则——子类不是父类的“增强版”,而是“等价替代品”
LSP(Liskov Substitution Principle)是SOLID中最易被忽视、也最致命的一条。它直指面向对象的根基: 继承关系不是“is-a”(是什么),而是“can-do-the-same-as”(能做一样的事) 。在PHP中,LSP失效往往不会立刻报错,而是像慢性中毒,在某个深夜的线上报警中突然爆发。
4.1 一个PHP中真实的LSP崩溃现场
某支付SDK提供基类:
// SDK/PaymentGateway.php
abstract class PaymentGateway {
abstract public function charge(float $amount, string $currency): bool;
// 关键!基类承诺:只要charge返回true,钱就一定到账
public function getTransactionId(): string {
return $this->lastTransactionId;
}
}
我们继承它实现微信支付:
// App/Gateways/WeChatGateway.php
class WeChatGateway extends PaymentGateway {
public function charge(float $amount, string $currency): bool {
$result = WeChat::unifiedOrder($amount, $currency);
if ($result['success']) {
$this->lastTransactionId = $result['transaction_id'];
return true;
}
return false;
}
}
一切正常。直到某天,业务方要求支持“预授权”(先冻结资金,后确认扣款):
// App/Gateways/AlipayPreAuthGateway.php
class AlipayPreAuthGateway extends PaymentGateway {
public function charge(float $amount, string $currency): bool {
// 预授权只冻结,不扣款!返回true不代表钱已到账
$result = Alipay::preAuth($amount, $currency);
if ($result['success']) {
$this->lastTransactionId = $result['pre_auth_id']; // 这是预授权ID,非交易ID
return true;
}
return false;
}
// 重写getTransactionId,返回实际扣款后的交易ID
public function getTransactionId(): string {
return $this->actualTransactionId ?? parent::getTransactionId();
}
}
问题来了:所有调用
PaymentGateway::charge()
后立即调用
getTransactionId()
的代码,现在拿到的是预授权ID,而非交易ID。下游系统(如对账、风控)基于“charge返回true即交易完成”的假设工作,结果对账失败、风控误判。这就是LSP的彻底崩塌:
AlipayPreAuthGateway
不能安全地替代
PaymentGateway
,因为它的行为契约已被破坏。
4.2 PHP中捍卫LSP的三大铁律
LSP不是理论,而是可验证的工程纪律。在PHP中,必须遵守:
铁律1:方法签名不能收缩,只能扩张
-
父类方法参数是
array $options,子类不能改为array $options = [](默认值不算收缩); -
但父类是
array $options = [],子类可改为array $options(移除默认值是收缩,禁止); -
返回类型同理:父类返回
?string,子类可返回string(更具体),但不能返回int(类型不兼容)。
铁律2:子类不能加强前置条件(Precondition)
父类方法文档说:“
$amount
必须大于0”,子类就不能额外要求“
$amount
必须是整数”。因为调用方按父类契约传入
19.99
,子类却报错,违反了“能替代”的前提。
铁律3:子类不能削弱后置条件(Postcondition)或不变量(Invariant)
这是最隐蔽的杀手。父类保证“
charge()
返回true后,
getTransactionId()
必返回非空字符串”,子类就必须100%遵守。若预授权场景下无法满足,正确方案不是重写
getTransactionId()
,而是:
-
重构基类:将
charge()拆分为preAuth()和confirmCharge(); -
或引入新接口:
PreAuthCapable,让预授权网关实现它,而非继承PaymentGateway。
4.3 PHP开发者如何主动检测LSP违规
静态分析工具(如PHPStan)能捕获部分类型违规,但契约层面需人工审查。我坚持三个检查点:
-
查看所有
@param和@return注释 :子类方法注释是否比父类更严格?如果是,立即重构; - 搜索父类方法的所有调用点 :假设此处传入的参数是父类契约允许的任意合法值,子类能否100%处理?
-
检查子类是否重写了父类的非抽象方法
:尤其是getter/setter和工具方法。重写
getTransactionId()这种关键契约方法,99%是LSP警报。
提示:当发现自己想在子类里
throw new \Exception()来拒绝父类允许的输入时,这不是子类的问题,而是继承关系设计错误。此时应放弃继承,改用组合(Composition)——让AlipayPreAuthGateway持有PaymentGateway实例,而非继承它。组合是LSP最可靠的防火墙。
5. ISP:接口隔离原则——别让PHP类背负它不需要的“功能债务”
ISP(Interface Segregation Principle)直击PHP开发者的痛点: 我们总在“方便”和“干净”间妥协,结果造出一堆“全能型”接口,让实现类背上沉重的功能债务 。在PHP中,ISP不是关于代码量,而是关于“依赖的精确度”——你依赖的接口越小、越专注,你的类就越轻、越可控。
5.1 PHP中ISP失效的典型症状:一个接口,三种命运
想象一个
UserServiceInterface
:
interface UserServiceInterface {
public function createUser(array $data): User;
public function updateUser(int $id, array $data): User;
public function deleteUser(int $id): void;
public function sendWelcomeEmail(int $id): void;
public function generateReport(string $type): string;
public function syncToCRM(int $id): void;
}
这个接口看似全面,却让所有实现者陷入困境:
-
命令行脚本
:只需
createUser(),却被迫实现sendWelcomeEmail()(空方法)、generateReport()(抛异常); -
API控制器
:需要
createUser()和sendWelcomeEmail(),但syncToCRM()在当前版本未接入,只能写// TODO: implement CRM sync; - 测试Mock :单元测试中要Mock这个接口,却得为6个方法提供桩,其中4个永远用不到。
这就是ISP的失败:
一个胖接口,强迫所有使用者承担它定义的全部责任,无论是否需要
。它把“可选功能”变成了“强制义务”,让代码充满
NotImplementedException
和
// TODO
注释。
5.2 PHP中的ISP实践:按角色切割,用组合组装
解决方案是“角色接口”(Role Interface):
// Domain/Contracts/UserCreationService.php
interface UserCreationService {
public function createUser(array $data): User;
}
// Domain/Contracts/UserEmailService.php
interface UserEmailService {
public function sendWelcomeEmail(int $id): void;
}
// Domain/Contracts/UserReportingService.php
interface UserReportingService {
public function generateReport(string $type): string;
}
// Domain/Contracts/UserCRMSyncService.php
interface UserCRMSyncService {
public function syncToCRM(int $id): void;
}
现在,每个接口只描述一个明确的角色:
-
命令行脚本只需依赖
UserCreationService; -
API控制器可同时依赖
UserCreationService和UserEmailService; -
报表服务单独依赖
UserReportingService。
关键在于: 客户端(调用方)按需索取接口,而非被动接受一个大礼包 。在Laravel中,这通过依赖注入自然实现:
class UserController {
public function __construct(
private UserCreationService $creationService,
private UserEmailService $emailService
) {}
public function store(Request $request) {
$user = $this->creationService->createUser($request->all());
$this->emailService->sendWelcomeEmail($user->id);
return response()->json($user);
}
}
UserController
只声明它真正需要的两个角色,对
UserReportingService
一无所知——这正是ISP追求的“精确依赖”。
5.3 PHP特有的ISP优化:Trait + 接口的协同防御
PHP的Trait可以辅助ISP落地,但必须谨慎。正确用法是: Trait只封装接口的通用实现,绝不定义新接口 。例如:
// Domain/Traits/EmailSenderTrait.php
trait EmailSenderTrait {
protected function sendEmail(string $to, string $subject, string $body): void {
// 通用邮件发送逻辑(SMTP配置、模板渲染)
Mail::to($to)->send(new WelcomeEmail($subject, $body));
}
}
// Domain/Contracts/UserEmailService.php
interface UserEmailService {
public function sendWelcomeEmail(int $id): void;
}
// Infrastructure/Services/DatabaseUserEmailService.php
class DatabaseUserEmailService implements UserEmailService {
use EmailSenderTrait; // 复用实现
public function sendWelcomeEmail(int $id): void {
$user = DB::table('users')->where('id', $id)->first();
$this->sendEmail($user->email, '欢迎', "Hi {$user->name}!");
}
}
这里,
EmailSenderTrait
是实现细节,
UserEmailService
是契约。Trait可以复用,但接口必须保持最小化。如果某天需要短信通知,就新增
UserSMSService
接口和
SMSSenderTrait
,而非在
UserEmailService
里加
sendWelcomeSMS()
方法——那会再次制造胖接口。
注意:ISP不是“越多接口越好”。过度切割会导致接口泛滥。我的经验法则是: 当一个接口的实现类中,超过1/3的方法是空实现或抛异常时,就是ISP警报;当两个接口总是被同一组类同时实现时,考虑合并 。平衡点在于“按变化频率分组”——变化频率相同的契约放一起,否则分开。
6. DIP:依赖倒置原则——PHP中解耦的终极武器,让框架为你打工
DIP(Dependency Inversion Principle)是SOLID的压轴,也是最颠覆PHP开发者直觉的一条。它说:“ 高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象 。”在PHP世界,这意味着: 你的业务逻辑(Domain)不该知道Laravel、MySQL、Redis、微信SDK的存在;它们都应该通过你定义的接口,向你的领域层“倒置”提供服务 。
6.1 为什么PHP项目尤其需要DIP:框架绑架的陷阱
PHP生态繁荣,Laravel、Symfony、ThinkPHP等框架极大提升了开发效率。但便利的代价是: 业务代码极易与框架深度耦合 。常见症状:
-
Controller里直接调用
DB::table()、Cache::get()、Storage::put(); -
Service类里
use Illuminate\Support\Facades\...; -
模型里写
$this->attributes['status'] = 'processed'; -
单元测试时,
phpunit启动整个Laravel应用,耗时30秒跑一个简单逻辑。
这导致:
-
框架升级恐惧症
:Laravel从8.x升到10.x,所有
DB::调用可能失效; -
技术栈锁定
:想把MySQL换成TiDB?得全局搜索替换
DB::; - 测试地狱 :不启动框架,业务逻辑无法运行,TDD成为空谈。
DIP正是破局之钥。它要求你: 在领域层(Domain)定义“我们需要什么服务”,在基础设施层(Infrastructure)实现“如何提供这些服务” 。框架不再是主人,而是仆人。
6.2 PHP中DIP的完整落地链条:从接口定义到容器绑定
以“用户注册”为例,DIP实施分四步:
Step 1:在Domain层定义抽象接口(不依赖任何框架)
// Domain/Contracts/UserRepositoryInterface.php
interface UserRepositoryInterface {
public function save(User $user): void;
public function findByEmail(string $email): ?User;
}
// Domain/Contracts/EmailServiceInterface.php
interface EmailServiceInterface {
public function send(EmailMessage $message): void;
}
Step 2:在Infrastructure层实现具体服务(依赖框架)
// Infrastructure/Repositories/EloquentUserRepository.php
class EloquentUserRepository implements UserRepositoryInterface {
public function save(User $user): void {
// 此处才使用Eloquent
User::create($user->toArray());
}
public function findByEmail(string $email): ?User {
return User::where('email', $email)->first();
}
}
// Infrastructure/Services/SmtpEmailService.php
class SmtpEmailService implements EmailServiceInterface {
public function send(EmailMessage $message): void {
// 此处才使用Laravel Mail
Mail::to($message->to)->send(new WelcomeMailable($message));
}
}
Step 3:在Application层协调(依赖抽象,不依赖实现)
// Application/UseCases/RegisterUserUseCase.php
class RegisterUserUseCase {
public function __construct(
private UserRepositoryInterface $userRepository,
private EmailServiceInterface $emailService
) {}
public function execute(array $data): User {
$user = new User($data);
$this->userRepository->save($user); // 依赖抽象
$this->emailService->send(new WelcomeEmail($user)); // 依赖抽象
return $user;
}
}
Step 4:在框架入口(如Laravel的ServiceProvider)绑定实现
// Providers/AppServiceProvider.php
public function register() {
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
$this->app->bind(EmailServiceInterface::class, SmtpEmailService::class);
}
至此,
RegisterUserUseCase
完全不知道Eloquent、SMTP、Laravel的存在。它只依赖自己定义的契约。想换Redis缓存用户?只需写
RedisUserRepository
实现
UserRepositoryInterface
,并在ServiceProvider里改一行绑定。想用SendGrid发邮件?写
SendGridEmailService
,改绑定。业务逻辑零修改。
6.3 PHP中DIP的实战红利:不只是解耦,更是生产力革命
践行DIP后,我收获的不仅是架构整洁:
-
单元测试速度提升10倍
:测试
RegisterUserUseCase时,直接注入Mock的UserRepositoryInterface和EmailServiceInterface,无需启动Laravel,100个测试用例2秒跑完; - 技术演进无痛 :项目从Laravel迁移到Symfony时,只重写了Infrastructure层的实现类,Domain和Application层代码100%复用;
- 新人上手加速 :新成员只需理解Domain层的接口契约,就能开始编写业务逻辑,无需先啃完Laravel文档;
-
故障隔离
:微信SDK挂了?
SmtpEmailService报错,但RegisterUserUseCase和UserRepository完全不受影响,用户注册照常进行。
DIP的本质,是把“技术决策”(用什么数据库、什么消息队列)从“业务决策”(用户注册要存哪些字段、发什么邮件)中剥离。在PHP这个快速迭代的生态里,这是保障长期可维护性的唯一可靠路径。
7. SOLID不是终点,而是PHP面向对象设计的起点:一个真实项目的演进复盘
讲完五个原则,你可能会想:“道理都懂,但我的遗留PHP项目一团乱麻,从哪下手?”这很真实。SOLID不是魔法咒语,而是渐进式重构的指南针。我以一个真实电商后台的演进为例,展示如何用SOLID思维“外科手术式”地改善现状。
7.1 项目初始状态:典型的PHP“意大利面”代码
一个
ProductController.php
文件,2100行:
-
index():列表页,含搜索、分页、多条件筛选(SQL拼接); -
show():详情页,查商品、查库存、查评论、查推荐商品; -
store():创建商品,含图片上传(move_uploaded_file())、规格解析(json_decode())、库存初始化; -
update():更新商品,逻辑与store()高度重复; -
destroy():软删除,同时清理关联的图片文件、缓存、ES索引; -
还有
exportExcel()、importCsv()、syncToThirdParty()等12个方法。
所有方法都直接调用
DB::table()
、
Storage::put()
、
File::delete()
、
Cache::forget()
,并充斥着
if ($request->has('xxx')) { ... }
的条件分支。
7.2 第一阶段:用SRP和ISP切出“职责边界”(2周)
目标:让Controller只做“协调”,不写业务逻辑。
-
新建
Application/UseCases/ProductListUseCase.php,封装列表查询逻辑; -
新建
Application/UseCases/ProductCreateUseCase.php,封装创建逻辑; -
新建
Domain/Contracts/ProductRepositoryInterface.php、ImageStorageInterface.php等6个细粒度接口; - 将Controller中所有业务逻辑剪切粘贴到UseCase中,并注入对应接口;
-
Controller剩余代码仅剩:接收Request、调用UseCase、返回Response。
效果:Controller降至300行,职责清晰;UseCase可独立测试;但UseCase内部仍耦合框架。
7.3 第二阶段:用DIP和OCP解耦基础设施(3周)
目标:让UseCase不依赖任何框架类。
-
在Domain层定义
ProductRepositoryInterface(含find(),save(),delete()); -
在Infrastructure层写
EloquentProductRepository实现它; -
将UseCase中所有
DB::table('products')替换为$this->productRepository->save($product); - 同样处理图片存储、缓存、第三方同步等;
-
为
ProductCreateUseCase添加策略:$config['image_storage'] = 'local' | 'oss' | 'cos',运行时选择LocalImageStorage或AliyunOSSStorage。
效果:UseCase完全脱离Laravel;更换云存储只需新增一个实现类和配置;单元测试不再需要RefreshDatabaseTrait。
7.4 第三阶段:用LSP和ISP加固契约(1周)
目标:确保所有实现类可安全互换。
-
审查所有接口实现,移除
NotImplementedException; -
为
ProductRepositoryInterface添加findBySku(string $sku): ?Product,因所有实现都支持SKU查询; -
将
ProductUpdateUseCase拆分为UpdateBasicInfoUseCase和UpdateInventoryUseCase,因它们的变更频率不同(基本信息很少变,库存天天变); -
为
ImageStorageInterface添加generateUrl(string $path): string,统一URL生成逻辑。
效果:接口契约更严谨;不同UseCase间复用率提升;新增“库存预警”功能时,直接复用UpdateInventoryUseCase的库存查询逻辑。
7.5 最终成果与关键心得
重构后,项目结构变为:
app/
├── Domain/ # 纯PHP,无框架依赖
│ ├── Contracts/ # 所有接口定义
│ └── Models/ # 核心领域模型(User, Product, Order)
├── Application/ # 用例协调层,依赖Domain
│ └── UseCases/
├── Infrastructure/ # 具体实现,依赖框架
│ ├── Repositories/
│ ├── Services/
│ └── Adapters/
└── Presentation/ # Controller/View,最薄一层
关键心得 :
- 不要追求一步到位 :先切出Controller的职责,再解耦UseCase,最后加固契约。每一步都有可见收益;
- 测试是重构的氧气 :在切出UseCase前,先为Controller写Feature Test;在解耦DIP前,为UseCase写Unit Test。没有测试,重构就是拆弹;
-
命名即设计
:
ProductListUseCase比ProductService更能表达意图;EloquentProductRepository比ProductRepository更明确实现方式;好名字是SOLID最好的注释; -
PHP的灵活性是优势,不是缺陷
:
__call()、__get()、eval()不是洪水猛兽,而是当你需要动态策略、运行时配置时的利器。SOLID不是限制PHP,而是让PHP的灵活性服务于可维护性。
SOLID的终极价值,不是写出“教科书式”的完美代码,而是让你在面对下一个需求变更时,能笑着打开编辑器,而不是盯着满屏红色报错,祈祷今晚不要上线。它是一套经过时间检验的“面向对象健康检查表”,而PHP,正是最适合实践它的语言之一——因为它的简洁,让原则的光芒更加刺眼;因为它的灵活,让原则的落地更具智慧。

1745

被折叠的 条评论
为什么被折叠?



