PHP 开发中常见的继承与组合选择困惑
本文探讨了PHP开发中继承与组合的选择困惑。首先分析了二者的核心差异:继承体现"is-a"关系,耦合度高;组合体现"has-a"关系,耦合度低。接着提出了决策模型和设计原则(里氏替换原则和组合复用原则),强调应根据关系本质选择合适方式。通过UI组件、支付系统和电商产品三个实战场景,展示了组合模式的灵活性优势,如使用trait组合特性、策略模式实现支付方式切换
·
目录
PHP 开发中常见的继承与组合选择困惑
一、问题本质解析
1.1 继承与组合的核心差异
继承(Inheritance):
- 关系类型:
is-a关系 - 代码复用方式:通过扩展父类
- 耦合度:高(编译时绑定)
- PHP示例:
class Vehicle {
public function move() {
return "Moving...";
}
}
class Car extends Vehicle {
// 自动获得move()方法
}
组合(Composition):
- 关系类型:
has-a关系 - 代码复用方式:通过对象引用
- 耦合度:低(运行时绑定)
- PHP示例:
class Engine {
public function start() {
return "Engine started";
}
}
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
1.2 常见困惑场景
- 代码复用困惑:什么时候应该用继承复用代码,什么时候用组合?
- 设计困惑:如何判断类之间的关系本质?
- 扩展困惑:未来需求变化时,哪种方式更灵活?
二、决策模型与原则
2.1 决策流程图
开始
↓
判断关系类型
├── 是"is-a"关系且符合里氏替换原则? → 考虑继承
│ ↓
│ 子类是否需要父类的所有方法?
│ ├── 是 → 继承可能合适
│ └── 否 → 考虑组合或接口
│
└── 是"has-a"关系或功能组合? → 优先组合
↓
组件是否需要独立变化?
├── 是 → 组合 + 依赖注入
└── 否 → 简单组合
2.2 四大设计原则
原则1:里氏替换原则(LSP)
// ✅ 正确示例:子类可完全替换父类
class Bird {
public function eat() {
return "Eating...";
}
}
class Sparrow extends Bird {
// 没有破坏父类行为
}
// ❌ 错误示例:违反了LSP
class Penguin extends Bird {
public function fly() {
throw new Exception("Penguins can't fly!");
}
}
原则2:组合/聚合复用原则(CARP)
// ✅ 使用组合而非继承
interface Logger {
public function log($message);
}
class DatabaseLogger implements Logger {
public function log($message) {
// 数据库日志
}
}
class UserService {
private $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function createUser($data) {
// 创建用户
$this->logger->log("User created");
}
}
三、实战解决方案
3.1 场景1:UI组件设计
问题:按钮、输入框、选择框都需要样式和事件处理
// ❌ 继承方式(容易造成类爆炸)
class UIElement {
protected $styles = [];
public function setStyle($key, $value) {
$this->styles[$key] = $value;
}
}
class Button extends UIElement {
public function onClick($handler) {
// 按钮点击事件
}
}
class Input extends UIElement {
// 还需要onChange等特定方法
}
// ✅ 组合方式(更灵活)
trait StyleTrait {
protected $styles = [];
public function setStyle($key, $value) {
$this->styles[$key] = $value;
}
}
trait EventTrait {
protected $events = [];
public function on($event, $handler) {
$this->events[$event][] = $handler;
}
}
class Button {
use StyleTrait, EventTrait;
public function __construct() {
$this->on('click', function() {
// 默认点击处理
});
}
}
class Input {
use StyleTrait, EventTrait;
// 可以自由组合需要的特性
}
3.2 场景2:支付系统设计
// ✅ 使用策略模式(组合的典型应用)
interface PaymentStrategy {
public function pay($amount);
}
class CreditCardPayment implements PaymentStrategy {
public function pay($amount) {
return "Paid $amount via Credit Card";
}
}
class PayPalPayment implements PaymentStrategy {
public function pay($amount) {
return "Paid $amount via PayPal";
}
}
class PaymentProcessor {
private $paymentMethod;
public function setPaymentMethod(PaymentStrategy $method) {
$this->paymentMethod = $method;
}
public function process($amount) {
return $this->paymentMethod->pay($amount);
}
}
// 使用
$processor = new PaymentProcessor();
$processor->setPaymentMethod(new PayPalPayment());
echo $processor->process(100);
3.3 场景3:电子商务产品模型
// ✅ 使用组合构建复杂对象
abstract class Product {
protected $id;
protected $name;
protected $price;
public function __construct($id, $name, $price) {
$this->id = $id;
$this->name = $name;
$this->price = $price;
}
abstract public function getDescription();
}
class PhysicalProduct extends Product {
private $dimensions;
private $weight;
public function setDimensions($dimensions) {
$this->dimensions = $dimensions;
return $this;
}
public function getDescription() {
return "Physical product: {$this->name}";
}
}
class DigitalProduct extends Product {
private $downloadLink;
private $fileSize;
public function setDownloadLink($link) {
$this->downloadLink = $link;
return $this;
}
public function getDescription() {
return "Digital product: {$this->name}";
}
}
// 使用装饰器模式添加功能(组合的变体)
class ProductDecorator {
protected $product;
public function __construct(Product $product) {
$this->product = $product;
}
}
class DiscountedProduct extends ProductDecorator {
private $discountRate;
public function __construct(Product $product, $discountRate) {
parent::__construct($product);
$this->discountRate = $discountRate;
}
public function getPrice() {
return $this->product->getPrice() * (1 - $this->discountRate);
}
}
四、PHP特有考虑因素
4.1 Trait的使用(多重继承的替代方案)
// Trait适合横向代码复用
trait Loggable {
public function log($message) {
echo "[LOG]: $message\n";
}
}
trait Cacheable {
protected $cache = [];
public function cache($key, $value) {
$this->cache[$key] = $value;
}
}
class UserRepository {
use Loggable, Cacheable;
public function find($id) {
$this->log("Finding user $id");
if (isset($this->cache["user_$id"])) {
return $this->cache["user_$id"];
}
// 数据库查询...
}
}
4.2 魔术方法的组合使用
class ComposableObject {
private $components = [];
public function addComponent($name, $component) {
$this->components[$name] = $component;
}
// 使用__call实现方法委托
public function __call($method, $args) {
foreach ($this->components as $component) {
if (method_exists($component, $method)) {
return call_user_func_array([$component, $method], $args);
}
}
throw new BadMethodCallException("Method $method not found");
}
// 使用__get实现属性委托
public function __get($property) {
foreach ($this->components as $component) {
if (property_exists($component, $property)) {
return $component->$property;
}
}
return null;
}
}
五、最佳实践总结
5.1 选择继承的情况
- 真正的"is-a"关系:子类是父类的特殊类型
- 需要多态行为:通过父类接口操作子类对象
- 框架/库扩展点:设计用于被继承的类
- 模板方法模式:定义算法骨架,子类实现步骤
// 继承适用场景:框架基类
abstract class Controller {
public function beforeAction() {}
public function afterAction() {}
public function runAction($action) {
$this->beforeAction();
$this->$action();
$this->afterAction();
}
}
class UserController extends Controller {
public function beforeAction() {
// 检查用户登录
}
public function indexAction() {
// 用户列表
}
}
5.2 选择组合的情况
- 功能复用:多个类需要相同功能
- 运行时行为变化:需要动态改变对象行为
- 避免深层次继承:超过2层的继承链应考虑组合
- 跨领域功能:如日志、缓存、验证等横切关注点
5.3 实用检查清单
在做决定前问自己这些问题:
1. 子类是否需要父类的所有方法和属性?
□ 是 → 继承可能合适
□ 否 → 考虑组合
2. 未来是否会添加更多类似子类?
□ 是,且变化在垂直方向 → 继承
□ 是,且变化在水平方向 → 组合
3. 是否需要替换或修改父类行为?
□ 少量修改 → 继承 + 重写
□ 大量修改或完全不同 → 组合
4. 是否需要在运行时改变行为?
□ 是 → 必须用组合
□ 否 → 两者都可
5. 是否违反里氏替换原则?
□ 是 → 用组合
□ 否 → 继承可能可行
5.4 混合使用策略
// 实际项目中往往是混合使用
abstract class BaseService {
// 基础功能使用继承
}
class UserService extends BaseService {
use LoggableTrait, CacheableTrait; // 横切关注点使用Trait
private $validator; // 功能组件使用组合
public function __construct(Validator $validator) {
$this->validator = $validator;
}
// 使用策略模式处理不同场景
public function setNotificationStrategy(NotificationStrategy $strategy) {
$this->notificationStrategy = $strategy;
}
}
六、重构示例:从继承到组合
重构前(继承导致的问题):
class Report {
public function generate() {
$data = $this->fetchData();
return $this->format($data);
}
protected function fetchData() {
// 获取数据
}
protected function format($data) {
// 格式化数据
}
}
class PDFReport extends Report {
protected function format($data) {
// 生成PDF格式
}
}
class ExcelReport extends Report {
protected function format($data) {
// 生成Excel格式
}
protected function fetchData() {
// 需要不同的数据源,但被强制使用父类的数据源
}
}
重构后(组合提供灵活性):
interface DataFetcher {
public function fetch();
}
interface ReportFormatter {
public function format($data);
}
class Report {
private $dataFetcher;
private $formatter;
public function __construct(DataFetcher $fetcher, ReportFormatter $formatter) {
$this->dataFetcher = $fetcher;
$this->formatter = $formatter;
}
public function generate() {
$data = $this->dataFetcher->fetch();
return $this->formatter->format($data);
}
}
// 可以自由组合
$pdfReport = new Report(new DatabaseFetcher(), new PDFFormatter());
$excelReport = new Report(new APIFetcher(), new ExcelFormatter());
结论
继承与组合的选择不是非此即彼的二元选择,而是基于具体场景的权衡。在PHP开发中:
- 优先使用组合:组合提供了更好的灵活性、可测试性和低耦合
- 谨慎使用继承:只在真正的"is-a"关系且符合LSP时使用
- 善用PHP特性:Trait、接口、魔术方法等可以帮助实现更优雅的设计
- 考虑变化方向:垂直变化考虑继承,水平变化考虑组合
记住这个经验法则:当你犹豫不决时,选择组合。当组合变得复杂时,重新评估是否需要继承。
更多推荐


所有评论(0)