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 常见困惑场景

  1. 代码复用困惑:什么时候应该用继承复用代码,什么时候用组合?
  2. 设计困惑:如何判断类之间的关系本质?
  3. 扩展困惑:未来需求变化时,哪种方式更灵活?

二、决策模型与原则

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 选择继承的情况

  1. 真正的"is-a"关系:子类是父类的特殊类型
  2. 需要多态行为:通过父类接口操作子类对象
  3. 框架/库扩展点:设计用于被继承的类
  4. 模板方法模式:定义算法骨架,子类实现步骤
// 继承适用场景:框架基类
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 选择组合的情况

  1. 功能复用:多个类需要相同功能
  2. 运行时行为变化:需要动态改变对象行为
  3. 避免深层次继承:超过2层的继承链应考虑组合
  4. 跨领域功能:如日志、缓存、验证等横切关注点

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开发中:

  1. 优先使用组合:组合提供了更好的灵活性、可测试性和低耦合
  2. 谨慎使用继承:只在真正的"is-a"关系且符合LSP时使用
  3. 善用PHP特性:Trait、接口、魔术方法等可以帮助实现更优雅的设计
  4. 考虑变化方向:垂直变化考虑继承,水平变化考虑组合

记住这个经验法则:当你犹豫不决时,选择组合。当组合变得复杂时,重新评估是否需要继承。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐