1. 继承与组合的本质差异
在PHP开发中,继承(Inheritance)和组合(Composition)是两种完全不同的代码复用方式。继承体现的是"is-a"关系,比如"Student is a Person";而组合体现的是"has-a"关系,比如"Car has an Engine"。
继承通过extends关键字实现:
php复制class Person {
protected $name;
public function __construct($name) {
$this->name = $name;
}
}
class Student extends Person {
private $studentId;
public function __construct($name, $studentId) {
parent::__construct($name);
$this->studentId = $studentId;
}
}
组合则是通过对象属性实现:
php复制class Engine {
public function start() {
echo "Engine started";
}
}
class Car {
private $engine;
public function __construct() {
$this->engine = new Engine();
}
public function start() {
$this->engine->start();
}
}
关键区别:继承会暴露父类的所有公共和保护成员给子类,而组合可以精确控制外部对象的访问权限。
2. 何时选择继承
2.1 适合使用继承的场景
-
严格的层级关系:当两个类确实存在"是一种"的关系时,比如:
- File => ImageFile
- User => AdminUser
- Payment => CreditCardPayment
-
需要多态行为:当需要通过基类接口操作不同子类时:
php复制abstract class Shape {
abstract public function area();
}
class Circle extends Shape {
public function area() {
// 实现圆形面积计算
}
}
class Square extends Shape {
public function area() {
// 实现正方形面积计算
}
}
function printArea(Shape $shape) {
echo $shape->area();
}
- 框架扩展点:许多PHP框架(如Laravel)使用继承作为扩展机制:
php复制class UserController extends Controller {
// 可以重写父类方法
}
2.2 继承的潜在问题
- 脆弱的基类问题:父类的修改可能意外破坏子类
- 多重继承限制:PHP不支持多重继承
- 过度暴露:子类会继承父类所有非私有成员
3. 何时选择组合
3.1 适合使用组合的场景
- 共享功能而非身份:当需要复用代码但不满足"is-a"关系时:
php复制class Logger {
public function log($message) {
file_put_contents('app.log', $message, FILE_APPEND);
}
}
class UserService {
private $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function createUser($userData) {
// 创建用户逻辑
$this->logger->log("User created");
}
}
- 运行时行为变更:组合允许在运行时替换组件:
php复制interface PaymentMethod {
public function pay($amount);
}
class CreditCardPayment implements PaymentMethod {
public function pay($amount) { /*...*/ }
}
class PayPalPayment implements PaymentMethod {
public function pay($amount) { /*...*/ }
}
class Order {
private $paymentMethod;
public function setPaymentMethod(PaymentMethod $method) {
$this->paymentMethod = $method;
}
public function checkout() {
$this->paymentMethod->pay($this->total);
}
}
- 避免深度继承链:当继承层次超过3层时,应考虑使用组合。
3.2 组合的实现模式
- 依赖注入:
php复制class Database {
public function query($sql) { /*...*/ }
}
class UserRepository {
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
}
- 特征(Trait)(PHP特有的折中方案):
php复制trait Loggable {
public function log($message) {
echo $message;
}
}
class Order {
use Loggable;
public function process() {
$this->log("Processing order");
}
}
4. 实际项目中的决策框架
4.1 决策流程图
code复制是否需要多态行为?
是 => 考虑继承
否 => 是否需要代码复用?
是 => 是否存在"is-a"关系?
是 => 考虑继承
否 => 使用组合
否 => 不需要复用机制
4.2 关键考量因素
-
关系类型:
- 继承:描述对象是什么
- 组合:描述对象有什么
-
变化频率:
- 频繁变化的特性适合用组合
- 稳定的核心特性可以考虑继承
-
测试难度:
- 组合的类更容易单独测试
- 继承的类可能需要构造复杂的父类
-
PHP版本特性:
- PHP 8.0+的属性提升特性让组合更简洁:
php复制class UserService { public function __construct(private Logger $logger) {} }
5. 常见误区与最佳实践
5.1 典型错误案例
- 滥用继承作为复用工具:
php复制// 错误示范 - 只是为了复用validate方法而继承
class UserValidator {
public function validate($data) { /*...*/ }
}
class ProductValidator extends UserValidator {
// 产品验证逻辑
}
应改为:
php复制trait Validator {
public function validate($data) { /*...*/ }
}
class ProductValidator {
use Validator;
}
- 过度深度的继承链:
code复制Controller => BaseController => AuthController => AdminController
超过3层的继承通常意味着设计问题。
5.2 最佳实践建议
- 优先考虑组合:大多数情况下组合是更安全的选择
- 遵守LSP原则:如果使用继承,子类必须能替换父类
- 使用接口定义契约:
php复制interface Cacheable {
public function cache();
public function getFromCache();
}
class ProductRepository implements Cacheable {
// 必须实现接口方法
}
- 考虑使用依赖注入容器:
php复制// 使用PHP-DI等容器
$container = new Container();
$container->set('logger', function() {
return new Logger();
});
class UserService {
public function __construct(protected Logger $logger) {}
}
$userService = $container->get('UserService');
6. PHP 8+的新特性影响
6.1 构造器属性提升
php复制// PHP 8之前
class OrderService {
private $paymentGateway;
private $logger;
public function __construct(PaymentGateway $pg, Logger $logger) {
$this->paymentGateway = $pg;
$this->logger = $logger;
}
}
// PHP 8+
class OrderService {
public function __construct(
private PaymentGateway $paymentGateway,
private Logger $logger
) {}
}
6.2 只读属性
php复制class Configuration {
public function __construct(
public readonly string $env,
public readonly array $settings
) {}
}
6.3 交集类型
php复制interface Loggable {
public function log(string $message): void;
}
interface Cacheable {
public function cache(): void;
}
function process(Loggable&Cacheable $service) {
// 必须同时实现两个接口
}
7. 性能考量
虽然现代PHP版本中继承和组合的性能差异可以忽略不计,但在极端情况下:
-
继承:
- 方法调用略快(直接使用父类方法表)
- 内存占用略少(共享父类方法)
-
组合:
- 更灵活但可能有轻微间接调用开销
- 对象图更复杂时可能增加内存使用
实际项目中,设计清晰度应优先于微小的性能差异。
8. 重构策略
8.1 从继承重构为组合
原始代码:
php复制class File {
public function read() { /*...*/ }
}
class CsvFile extends File {
public function read() {
$data = parent::read();
return $this->parseCsv($data);
}
}
重构后:
php复制class FileReader {
public function read() { /*...*/ }
}
class CsvParser {
public function __construct(private FileReader $reader) {}
public function read() {
$data = $this->reader->read();
return $this->parseCsv($data);
}
}
8.2 使用策略模式替换条件逻辑
重构前:
php复制class Payment {
public function pay($type) {
if ($type === 'credit') {
// 信用卡逻辑
} elseif ($type === 'paypal') {
// PayPal逻辑
}
}
}
重构后:
php复制interface PaymentMethod {
public function pay();
}
class CreditCardPayment implements PaymentMethod {
public function pay() { /*...*/ }
}
class Payment {
public function __construct(private PaymentMethod $method) {}
public function process() {
$this->method->pay();
}
}
9. 框架中的实践差异
9.1 Laravel的继承倾向
php复制class UserController extends Controller {
// 大量框架方法通过继承获得
}
// 但服务类通常使用组合
class UserService {
public function __construct(
private UserRepository $users,
private Mailer $mailer
) {}
}
9.2 Symfony的组合倾向
php复制class MailService {
private $mailer;
private $logger;
public function __construct(MailerInterface $mailer, LoggerInterface $logger) {
$this->mailer = $mailer;
$this->logger = $logger;
}
}
10. 团队协作建议
- 制定明确规范:在项目早期确定继承使用边界
- 文档化设计决策:记录为什么选择某种方式
- 代码审查重点:特别关注新的继承关系
- 定期重构:警惕逐渐恶化的继承层次
经验法则:当你犹豫该用继承还是组合时,组合通常是更安全的选择。只有在明确符合"is-a"关系且需要多态行为时,才考虑使用继承。
