Files
test-repo/docs/.vitepress/theme/components/appendix/backend-layered-architecture/DomainModelDemo.vue
T

502 lines
13 KiB
Vue
Raw Normal View History

<template>
<div class="domain-model-demo">
<div class="demo-header">
<h4>📦 Domain 领域模型设计</h4>
<p class="subtitle">Domain 是业务概念的载体所有层的依赖基础</p>
</div>
<!-- 领域模型对比 -->
<div class="model-comparison">
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="comparison-content">
<!-- 贫血模型 vs 充血模型 -->
<div v-if="currentTab === 'comparison'" class="tab-panel">
<div class="model-cards">
<div class="model-card anemic">
<div class="card-header">
<span class="card-icon">📄</span>
<span class="card-title">贫血模型 (Anemic)</span>
<span class="card-badge">传统做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">Entity只有 getter/setter</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 只有 getter/setter,没有业务逻辑
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... 其他 getter/setter
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">Service所有业务逻辑都在这里</div>
<pre><code>@Service
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
// 贫血模型:业务逻辑散落在 Service 里
if (order.getStatus() == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (order.getStatus() == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
// 修改状态
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}</code></pre>
</div>
<div class="problems">
<div class="problem-title">😫 贫血模型的问题</div>
<ul>
<li><strong>违背面向对象</strong>对象只有数据没有行为变成了 "数据结构"</li>
<li><strong>逻辑分散</strong>同样的业务规则可能在多个 Service 重复</li>
<li><strong>难以维护</strong>改一个规则要找所有用到的地方</li>
</ul>
</div>
</div>
</div>
<div class="model-card rich">
<div class="card-header">
<span class="card-icon">🧠</span>
<span class="card-title">充血模型 (Rich Domain)</span>
<span class="card-badge tag-green">推荐做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">Entity包含业务逻辑</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 🎯 业务行为封装在实体里
/**
* 取消订单
*/
public void cancel() {
// 状态校验内聚在方法里
if (this.status == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (this.status == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
this.status = OrderStatus.CANCELLED;
// 可以触发领域事件
registerEvent(new OrderCancelledEvent(this.id));
}
/**
* 支付订单
*/
public void pay(Payment payment) {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不正确");
}
if (!payment.getAmount().equals(this.totalAmount)) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
this.paymentTime = LocalDateTime.now();
}
// ... 其他业务方法
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">Service只做协调不做业务判断</div>
<pre><code>@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final DomainEventPublisher eventPublisher;
@Transactional
public void cancelOrder(Long orderId) {
// 1. 加载聚合根
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// 2. 💡 调用领域对象的业务方法
// 业务规则封装在 Order 里,Service 只做协调
order.cancel();
// 3. 保存变更
orderRepository.save(order);
// 4. 发布领域事件
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}</code></pre>
</div>
<div class="benefits">
<div class="benefit-title">😊 充血模型的优势</div>
<ul>
<li><strong>符合面向对象</strong>数据和行为封装在一起是真正的 "对象"</li>
<li><strong>业务内聚</strong>规则跟着对象走改一处处处生效</li>
<li><strong>可复用可测试</strong>领域对象是纯内存对象单元测试不需要数据库</li>
<li><strong>表达力强</strong>order.cancel() orderService.cancel(order) 更自然</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 值对象 -->
<div v-else-if="currentTab === 'valueobject'" class="tab-panel">
<div class="value-object-content">
<div class="concept-intro">
<h5>💎 什么是值对象Value Object</h5>
<p>值对象是没有唯一标识不可变的对象它描述了某种特征或属性两个值对象如果所有属性相等就被认为是同一个对象</p>
</div>
<div class="vo-examples">
<div class="example-card">
<div class="example-title">📍 地址 Address</div>
<pre><code>// 值对象:不可变、无 ID
public record Address(
String province, // 省
String city, // 市
String district, // 区
String street, // 街道
String zipCode // 邮编
) {
// 值对象的方法通常是转换或计算
public String toDisplayString() {
return String.format("%s%s%s%s",
province, city, district, street);
}
// 校验逻辑
public boolean isValid() {
return StringUtils.isNotBlank(province)
&& StringUtils.isNotBlank(city);
}
}
// 使用:地址相等只要属性相同
Address addr1 = new Address("广东", "深圳", "南山", "科技园", "518000");
Address addr2 = new Address("广东", "深圳", "南山", "科技园", "518000");
System.out.println(addr1.equals(addr2)); // true - 值对象比较的是值</code></pre>
</div>
<div class="example-card">
<div class="example-title">💰 金钱 Money</div>
<pre><code>// 金钱是经典的值对象
public record Money(
BigDecimal amount,
Currency currency
) {
// 工厂方法
public static Money of(BigDecimal amount, String currencyCode) {
return new Money(amount, Currency.getInstance(currencyCode));
}
public static Money yuan(BigDecimal amount) {
return new Money(amount, Currency.getInstance("CNY"));
}
// 值对象的核心:运算返回新的值对象
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), currency);
}
public boolean isGreaterThan(Money other) {
return this.amount.compareTo(other.amount) > 0;
}
// 格式化显示
public String toDisplayString() {
return currency.getSymbol() + amount.setScale(2, RoundingMode.HALF_UP);
}
}
// 使用示例
Money price = Money.yuan(new BigDecimal("199.99"));
Money shipping = Money.yuan(new BigDecimal("10.00"));
Money discount = Money.yuan(new BigDecimal("20.00"));
Money total = price.add(shipping).add(discount.negate());
System.out.println(total.toDisplayString()); // ¥189.99</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentTab = ref('comparison')
const tabs = [
{ id: 'comparison', name: '贫血 vs 充血' },
{ id: 'valueobject', name: '值对象设计' }
]
</script>
<style scoped>
.domain-model-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.model-comparison {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.comparison-tabs {
display: flex;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn:hover {
color: #409eff;
}
.tab-btn.active {
color: #409eff;
background: white;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
}
.comparison-content {
padding: 20px;
}
.tab-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.model-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.model-card {
background: #f8f9fa;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e4e7ed;
}
.model-card.anemic {
border-left: 4px solid #ff4d4f;
}
.model-card.rich {
border-left: 4px solid #52c41a;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #e4e7ed;
}
.card-icon {
font-size: 20px;
}
.card-title {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.card-badge {
margin-left: auto;
padding: 2px 8px;
background: #ff4d4f;
color: white;
border-radius: 10px;
font-size: 11px;
}
.card-badge.tag-green {
background: #52c41a;
}
.card-content {
padding: 16px;
}
.code-section {
margin-bottom: 16px;
}
.code-label {
font-size: 11px;
color: #909399;
margin-bottom: 6px;
font-weight: 500;
}
.code-section pre {
background: #2d2d2d;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 0;
}
.code-section code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 10px;
line-height: 1.5;
}
.problems, .benefits {
padding: 12px;
border-radius: 6px;
margin-top: 12px;
}
.problems {
background: #fff1f0;
border-left: 3px solid #ff4d4f;
}
.benefits {
background: #f6ffed;
border-left: 3px solid #52c41a;
}
.problem-title, .benefit-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
}
.problem-title {
color: #cf1322;
}
.benefit-title {
color: #389e0d;
}
.problems ul, .benefits ul {
margin: 0;
padding-left: 16px;
}
.problems li, .benefits li {
margin: 6px 0;
font-size: 12px;
line-height: 1.5;
color: #595959;
}
@media (max-width: 1024px) {
.model-cards {
grid-template-columns: 1fr;
}
}
</style>