文章目录
零 引言:编程风格的重要性
- 在软件开发领域,编程风格的选择绝非仅仅是个人偏好的问题,而是直接影响代码质量、可维护性和系统架构的关键决策。如同建筑风格决定了建筑物的外观与功能布局,编程风格决定了软件系统的内部结构和外部行为。
- 本文将深入探讨领域驱动设计(DDD)中的编程风格选择,特别关注如何在面向对象与过程式风格之间找到平衡点。
一 领域对象与数据库的边界
1.1 严格解耦原则
- “领域对象不访问数据库” 这一原则是构建清晰架构的基础。想象一下,如果每个业务对象都直接与数据库对话,就像让公司每个部门的员工都直接操作财务系统一样混乱不堪。
- 在实际项目中,我们经常看到两种违反这一原则的反模式:
- 显式访问:在领域对象方法中直接编写SQL语句
// 反例:领域对象中直接执行SQL
public class Order {
public void save() {
String sql = "INSERT INTO orders (...) VALUES (...)";
// 执行SQL...
}
}
- 隐式访问:使用JPA等ORM框架的延迟加载特性
// 反例:JPA延迟加载导致的隐式数据库访问
@Entity
public class Order {
@OneToMany(fetch = FetchType.LAZY)
private List<OrderItem> items;
public double calculateTotal() {
// 当访问items时,会隐式触发数据库查询
return items.stream().mapToDouble(Item::getPrice).sum();
}
}
1.2 解耦带来的优势
保持领域对象与数据库解耦的主要好处包括:
- 可测试性:领域对象可以在不依赖数据库的情况下进行单元测试
- 可维护性:数据库 schema变更不会直接影响业务逻辑
- 清晰性:业务逻辑与技术实现分离,代码意图更明确
二 领域服务的职责边界
2.1 读写分离的哲学
- “领域服务只读,应用服务可读写” 这一原则体现了关注点分离的思想。领域服务专注于业务规则验证和决策,而应用服务协调工作流程和技术细节。
- 典型的领域服务结构:
public class OrderService {
private final OrderRepository orderRepository;
// 领域服务通常只读
public boolean canCancelOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId);
return order.canBeCancelled();
}
}
- 对应的应用服务可能如下:
public class OrderApplicationService {
private final OrderRepository orderRepository;
// 应用服务处理写操作
public void cancelOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId);
order.cancel();
orderRepository.save(order);
}
}
2.2 两种风格的权衡
在项目中,我们通常会遇到两种风格的选择:
- 薄应用服务:将更多逻辑放在领域服务中
- 优点:业务逻辑更集中
- 缺点:领域服务变得臃肿
- 厚应用服务:保持领域服务精简
- 优点:层次职责更清晰
- 缺点:业务逻辑可能分散
2.3 决策指南
考虑以下因素来决定风格选择:团队经验水平、项目复杂度、变更频率、性能要求
三 对象关联的艺术
3.1 ID引用 vs 对象导航
- “用ID表示关联” 这一选择体现了务实的设计哲学。在企业应用中,完全的对象导航可能导致:内存消耗过大,序列化问题,性能瓶颈
- 对比两种风格:
// 面向对象风格:直接引用对象
public class Order {
private Customer customer; // 直接持有对象引用
}
// 过程式风格:使用ID引用
public class Order {
private CustomerId customerId; // 仅持有ID
}
3.2 性能与表达的平衡
- 虽然ID引用更高效,但会损失一些表达力。为了弥补这一点,可以:
- 提供便捷方法获取完整对象
public class Order {
private CustomerId customerId;
public Customer getCustomer(CustomerRepository repo) {
return repo.findById(customerId);
}
}
- 使用DTO或视图对象组装完整数据
3.3 实践模式
根据场景灵活选择:
- 核心领域:优先考虑表达力
- 外围功能:优先考虑性能
- 高频操作:使用ID引用
- 复杂业务逻辑:考虑对象导航
四 领域对象与服务的协作
4.1 逻辑放置的决策
- “领域对象有自己的领域服务” 这一模式反映了过程式风格的特点。关键在于合理划分逻辑:
- 领域对象:封装自包含的状态和行为
public class Order { private OrderStatus status; public boolean canBeCancelled() { return status == OrderStatus.PENDING; } }
- 领域服务:处理跨对象的协调和外部依赖
public class OrderService { public boolean canRequestRefund(OrderId id, RefundPolicy policy) { Order order = orderRepository.findById(id); return order.isPaid() && policy.allowsRefund(order); } }
4.2 识别特性依恋
- 特性依恋(Feature Envy)是指一个类过度访问另一个类的数据。重构到表意接口(Intention-Revealing Interface)是解决这一问题的有效方法。
- 重构前:
// 反例:PaymentProcessor过度访问Order的细节
public class PaymentProcessor {
public void process(Order order) {
if (order.getItems().size() > 0 && order.getTotal() > 100) {
// 处理逻辑...
}
}
}
- 重构后:
// 正例:Order提供意图明确的接口
public class Order {
public boolean isEligibleForDiscount() {
return items.size() > 0 && total > 100;
}
}
public class PaymentProcessor {
public void process(Order order) {
if (order.isEligibleForDiscount()) {
// 处理逻辑...
}
}
}
五 封装与继承的明智使用
5.1 封装的艺术
- 即使在过程式风格中,良好的封装也能显著提升代码质量。关键在于: 隐藏实现细节、提供明确的接口
、控制修改入口
5.2 继承的谨慎使用
- 继承是一把双刃剑。在领域模型中,考虑:优先使用组合而非继承、仅当确实存在"is-a"关系时才使用继承、保持继承层次扁平
- 示例:谨慎的继承使用
// 基类:定义核心行为
public abstract class Payment {
protected abstract void executePayment();
}
// 派生类:实现特定支付方式
public class CreditCardPayment extends Payment {
@Override
protected void executePayment() {
// 信用卡支付实现
}
}
六 风格选择的实践指南
6.1 评估维度
选择编程风格时,考虑以下维度:
- 项目规模:
- 小型项目:更灵活的混合风格
- 大型项目:更严格的分层
- 团队组成:
- 新手较多:更明确的过程式风格
- 经验丰富:更深入的面向对象
- 性能需求:
- 高性能:偏向过程式
- 业务复杂:偏向面向对象
6.2 渐进式改进
不要试图一次性完美应用所有原则。建议:
- 从清晰的分层开始
- 识别核心领域
- 在核心领域应用更纯粹的面向对象
- 在非核心区域采用更实用的过程式风格
七 结论:平衡的艺术
编程风格的选择本质上是在多种因素间寻找平衡:
- 表达力 vs 性能
- 纯粹性 vs 实用性
- 灵活性 vs 严谨性
- 没有放之四海而皆准的最佳实践,关键在于理解每种选择的利弊,根据项目上下文做出明智决策。正如建筑大师密斯·凡·德·罗所说:“魔鬼在细节中”,优秀的软件设计同样体现在对这些风格细节的深思熟虑中。
- 记住,我们的目标不是追求理论上的完美,而是创建可维护、可扩展且高效的软件系统。希望你能在面向对象与过程式风格之间找到适合您项目的平衡点。