在PHP领域,模型设计一直存在两种主流范式:贫血模型(Anemic Domain Model)和充血模型(Rich Domain Model)。这两种模式代表着完全不同的设计哲学,也直接影响着代码的组织结构和维护成本。
贫血模型的特点是对象仅包含数据属性和简单的getter/setter方法,业务逻辑被剥离到Service层。这种模式在早期PHP框架中非常普遍,比如:
php复制class User {
private $id;
private $name;
public function getId() { return $this->id; }
public function getName() { return $this->name; }
// 只有数据存取,没有业务逻辑
}
而充血模型则强调将数据和相关行为封装在一起,对象不仅包含状态还包含业务逻辑。典型的充血模型实现如下:
php复制class User {
private $id;
private $name;
private $balance;
public function transferMoney(User $to, float $amount) {
if ($this->balance < $amount) {
throw new \Exception('余额不足');
}
$this->balance -= $amount;
$to->balance += $amount;
}
}
贫血模型遵循"数据与行为分离"的原则,符合传统的分层架构思想。它的优势在于:
而充血模型则体现了面向对象设计的本质 - "将数据和行为绑定在一起"。它的特点包括:
在实际项目中,两种模型的性能表现也有差异:
| 对比维度 | 贫血模型 | 充血模型 |
|---|---|---|
| 内存占用 | 较低 | 较高 |
| 方法调用开销 | 分散在多个Service | 集中在对象内部 |
| 序列化效率 | 高 | 可能较低 |
| 缓存友好度 | 很好 | 一般 |
在Laravel等现代框架中,贫血模型通常这样实现:
php复制class UserService {
public function transferMoney($fromId, $toId, $amount) {
DB::transaction(function() use ($fromId, $toId, $amount) {
$from = User::find($fromId);
$to = User::find($toId);
if ($from->balance < $amount) {
throw new \Exception('余额不足');
}
$from->balance -= $amount;
$to->balance += $amount;
$from->save();
$to->save();
});
}
}
实现充血模型时需要注意:
php复制class Order {
private $items = [];
public function addItem(Product $product, int $quantity) {
if ($quantity <= 0) {
throw new \InvalidArgumentException('数量必须大于0');
}
$this->items[] = new OrderItem($product, $quantity);
}
}
php复制class Account {
public function withdraw($amount) {
if ($this->balance < $amount) {
throw new \Exception('余额不足');
}
$this->balance -= $amount;
Event::dispatch(new MoneyWithdrawn($this->id, $amount));
}
}
在实际项目中,可以灵活组合两种模式:
php复制class Product {
// 基础属性存取
private $price;
// 核心业务逻辑
public function applyDiscount(Discount $discount) {
$this->price = $discount->applyTo($this->price);
}
}
class ProductService {
// 跨实体的协调逻辑
public function applyCampaign(Campaign $campaign, array $products) {
foreach ($products as $product) {
$product->applyDiscount($campaign->getDiscount());
$product->save();
}
}
}
问题表现:
解决方案:
问题表现:
解决方案:
典型错误:
php复制class Order {
public function save() {
DB::table('orders')->insert([...]); // 直接耦合数据库操作
}
}
正确做法:
php复制class Order {
// 只关注领域逻辑
}
class OrderRepository {
public function save(Order $order) {
DB::table('orders')->insert([...]);
}
}
php复制class ReportService {
public function generateUserReport(DateTime $start, DateTime $end) {
return User::whereBetween('created_at', [$start, $end])
->chunk(1000, function($users) {
// 分批处理避免内存溢出
});
}
}
php复制class UserDTO {
public $id;
public $name;
// 仅包含需要传输的数据
}
class UserController {
public function index() {
$users = User::all();
return array_map(function($user) {
return new UserDTO($user);
}, $users);
}
}
php复制class Order {
private $customer;
public function getCustomer() {
if ($this->customer === null) {
$this->customer = CustomerRepository::find($this->customerId);
}
return $this->customer;
}
}
php复制class ShoppingCart {
private $items;
private $totalCache;
public function getTotal() {
if ($this->totalCache === null) {
$this->totalCache = array_reduce($this->items,
fn($sum, $item) => $sum + $item->getPrice(), 0);
}
return $this->totalCache;
}
}
示例:
php复制class UserServiceTest {
public function testTransferMoney() {
$mockUserRepo = $this->createMock(UserRepository::class);
$service = new UserService($mockUserRepo);
// 测试业务逻辑
}
}
示例:
php复制class AccountTest {
public function testWithdraw() {
$account = new Account(100);
$account->withdraw(50);
$this->assertEquals(50, $account->getBalance());
$this->expectException(\Exception::class);
$account->withdraw(100);
}
}
重构步骤:
重构示例:
php复制// 重构前
class OrderService {
public function placeOrder($data) {
// 验证逻辑
// 计算逻辑
// 保存逻辑
}
}
// 重构后
class Order {
public function place() {
$this->validate();
$this->calculateTotal();
DomainEvent::dispatch(new OrderPlaced($this));
}
}
在维护遗留代码时的建议:
对于Laravel项目:
示例:
php复制class Order extends Model {
use HandlesOrderState;
public function cancel() {
$this->status = 'cancelled';
$this->save();
}
}
trait HandlesOrderState {
public function markAsPaid() {
if ($this->status !== 'pending') {
throw new OrderStateException('只有待支付订单可标记为已支付');
}
$this->status = 'paid';
}
}
在Symfony中:
示例:
php复制class Product {
/**
* @ORM\Column(type="decimal")
*/
private $price;
public function changePrice(Money $newPrice) {
if ($newPrice->lessThan(new Money(0))) {
throw new \DomainException('价格不能为负');
}
$this->price = $newPrice->getAmount();
$this->recordThat(new PriceChanged($this->id, $newPrice));
}
}
建立团队共识:
审查充血模型时关注:
审查贫血模型时检查:
正确设计聚合的要点:
示例:
php复制class Order {
private $id;
private $customerId; // 通过ID引用
private $items;
public function addItem(ProductId $productId, $quantity) {
// 维护聚合内部一致性
if ($this->isPaid()) {
throw new \DomainException('已支付订单不能修改');
}
$this->items[] = new OrderItem($productId, $quantity);
}
}
实现领域事件的典型方案:
示例:
php复制class Account {
public function withdraw($amount) {
// ...
$this->recordThat(new MoneyWithdrawn($this->id, $amount));
}
}
class SendWithdrawalNotification {
public function handle(MoneyWithdrawn $event) {
// 发送通知邮件
}
}
适合场景:
实现模式:
设计要点:
示例服务划分:
code复制- 订单服务(充血模型,复杂业务规则)
- 支付服务(事务脚本,与第三方集成)
- 库存服务(事件溯源,高一致性要求)
贫血模型实现:
php复制class OrderService {
public function createOrder($userId, $items) {
$order = new Order();
$order->user_id = $userId;
$order->status = 'pending';
$total = 0;
foreach ($items as $item) {
$product = Product::find($item['id']);
$total += $product->price * $item['qty'];
$order->items()->create([...]);
}
$order->total = $total;
$order->save();
return $order;
}
}
充血模型实现:
php复制class Order {
public static function create(UserId $userId, array $items) {
$order = new self();
$order->userId = $userId;
$order->status = OrderStatus::PENDING();
foreach ($items as $item) {
$order->addItem($item['productId'], $item['quantity']);
}
DomainEvents::dispatch(new OrderCreated($order));
return $order;
}
public function addItem(ProductId $productId, $quantity) {
$product = ProductRepository::find($productId);
$this->items[] = new OrderItem($product, $quantity);
$this->total = $this->calculateTotal();
}
}
贫血模型方案:
php复制class AuthService {
public function checkPermission($userId, $permission) {
$user = User::with('roles.permissions')->find($userId);
return $user->roles->flatMap->permissions->contains('name', $permission);
}
}
充血模型方案:
php复制class User {
public function hasPermission($permissionName) {
return $this->roles->exists(function($role) use ($permissionName) {
return $role->hasPermission($permissionName);
});
}
}
class Role {
public function hasPermission($permissionName) {
return $this->permissions->contains('name', $permissionName);
}
}
典型演进过程:
评估维度:
混合使用时的考量:
php复制class UserImportService {
public function importUsers($csvFile) {
$batch = [];
foreach ($csvFile as $row) {
$batch[] = new User($row);
if (count($batch) >= 1000) {
User::insert($batch);
$batch = [];
}
}
}
}
php复制class ReportGenerator {
public function generate() {
return Order::with(['customer', 'items.product'])
->chunk(500, function($orders) {
// 处理每批数据
});
}
}
php复制class Order {
private $customer;
public function getCustomer() {
if ($this->customer === null) {
$this->customer = CustomerRepository::find($this->customerId);
}
return $this->customer;
}
}
php复制class ProductCatalog {
private $products;
private $cache;
public function findProduct($id) {
if (!isset($this->cache[$id])) {
$this->cache[$id] = $this->products->find($id);
}
return $this->cache[$id];
}
}
典型测试结构:
测试示例:
php复制class UserServiceTest {
public function testRegisterUser() {
$mockRepo = $this->createMock(UserRepository::class);
$service = new UserService($mockRepo);
$user = $service->register('test@example.com', 'password');
$this->assertEquals('test@example.com', $user->email);
}
}
测试关注点:
测试示例:
php复制class AccountTest {
public function testTransferBetweenAccounts() {
$account1 = new Account(100);
$account2 = new Account(50);
$account1->transferTo($account2, 30);
$this->assertEquals(70, $account1->getBalance());
$this->assertEquals(80, $account2->getBalance());
}
}
重构信号:
重构示例:
php复制// 重构前
class OrderService {
public function cancelOrder($orderId) {
$order = Order::find($orderId);
if ($order->status !== 'pending') {
throw new \Exception('只能取消待处理订单');
}
$order->status = 'cancelled';
$order->save();
}
}
// 重构后
class Order {
public function cancel() {
if (!$this->canBeCancelled()) {
throw new \DomainException('只能取消待处理订单');
}
$this->status = OrderStatus::CANCELLED();
}
private function canBeCancelled() {
return $this->status->equals(OrderStatus::PENDING());
}
}
贫血模型规范:
充血模型规范:
领域模型文档应包含:
php复制class Order extends Model {
// 使用标准Eloquent特性
}
trait HandlesOrderFulfillment {
// 添加领域行为
public function fulfill() {
if ($this->status !== 'paid') {
throw new OrderException('只有已支付订单可以发货');
}
$this->status = 'fulfilled';
}
}
php复制class OrderFulfilledListener {
public function handle(OrderFulfilled $event) {
// 发送物流通知等
}
}
php复制/**
* @Entity
*/
class Product {
/**
* @Column(type="decimal", precision=10, scale=2)
*/
private $price;
public function changePrice(Money $newPrice) {
$this->price = $newPrice->getAmount();
}
}
php复制class Order {
public function cancel() {
// ...
$this->domainEvents[] = new OrderCancelled($this->id);
}
public function releaseEvents(): array {
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
贫血模型方案:
php复制class OrderProcessingService {
public function processOrder($orderId) {
DB::transaction(function() use ($orderId) {
$order = Order::find($orderId);
$this->paymentService->charge($order);
$this->inventoryService->reserveItems($order);
$this->shippingService->scheduleDelivery($order);
$order->status = 'processed';
$order->save();
});
}
}
充血模型方案:
php复制class Order {
public function process(PaymentService $payment, Inventory $inventory, Shipping $shipping) {
$payment->charge($this);
$inventory->reserveFor($this);
$shipping->scheduleFor($this);
$this->status = OrderStatus::PROCESSED();
}
}
解决方案:
示例:
php复制class OrderFulfillmentService {
public function fulfillOrder($orderId) {
$order = $this->orderRepo->find($orderId);
$inventory = $this->inventoryRepo->forProduct($order->productId);
if ($inventory->canFulfill($order->quantity)) {
$order->fulfill();
$inventory->allocate($order->quantity);
}
}
}
安全变更步骤:
php复制class Account {
private $balance;
public function withdraw($amount) {
if ($amount <= 0) {
throw new \DomainException('金额必须大于零');
}
// ...
}
}
php复制class MedicalRecord {
public function access(User $user) {
if (!$user->hasRole('doctor') && !$this->isOwner($user)) {
throw new AccessDeniedException();
}
}
}
典型实现:
php复制class UserService {
public function register($data, $locale) {
$validator = Validator::make($data, [
'name' => 'required',
], $this->getMessages($locale));
// ...
}
}
领域对象中的实现:
php复制class Product {
public function rename($name, Language $language) {
$this->translations[$language->code] = $name;
}
public function getName(Language $language) {
return $this->translations[$language->code] ?? $this->defaultName;
}
}
Swagger注解示例:
php复制/**
* @OA\Post(
* path="/api/users",
* tags={"User"},
* @OA\Response(response="200", description="创建用户")
* )
*/
class UserController {
public function store(UserCreateRequest $request) {
return UserService::create($request->validated());
}
}
DTO转换示例:
php复制class UserController {
public function update(UserUpdateRequest $request, $id) {
$user = UserRepository::find($id);
$user->changeEmail($request->email);
return new UserResource($user);
}
}
class UserResource extends JsonResource {
public function toArray($request) {
return [
'id' => $this->id,
'name' => $this->name,
// ...
];
}
}
示例:
php复制class Order {
final public function apply(OrderEvent $event): self {
$new = clone $this;
switch (true) {
case $event instanceof OrderPlaced:
$new->status = 'placed';
break;
// ...
}
return $new;
}
}
模型设计调整:
在实际项目中,我发现没有绝对的"正确"选择。经过多个项目的实践,我总结出以下经验:
对于大多数PHP项目,我建议采用渐进式策略:初期使用贫血模型快速迭代,在识别出复杂核心领域后,逐步引入充血模型概念。特别是在处理复杂业务规则、状态转换和多实体交互的场景下,充血模型能够显著提高代码的可维护性和表达力。
最后提醒一点:不要为了使用充血模型而过度设计。评估项目的实际需求和团队的接受能力,选择最适合当前阶段的模型风格,并保持开放心态,随着业务发展不断调整和演进你的设计。