仓储,封装持久化数据
Repository(仓储)模式是一种设计模式,它用于将数据访问逻辑封装起来,使得领域层可以通过一个简单、一致的接口来访问聚合根或实体对象。这个模式的关键在于提供了一个抽象的接口,领域层通过这个接口与数据存储层进行交互,而不需要知道背后具体的实现细节。
特性
- 封装持久化操作:Repository负责封装所有与数据源交互的操作,如创建、读取、更新和删除(CRUD)操作。这样,领域层的代码就可以避免直接处理数据库或其他存储机制的复杂性。
- 领域对象的集合管理:Repository通常被视为领域对象的集合,提供了查询和过滤这些对象的方法,使得领域对象的获取和管理更加方便。
- 抽象接口:Repository定义了一个与持久化机制无关的接口,这使得领域层的代码可以在不同的持久化机制之间切换,而不需要修改业务逻辑。
用途
- 数据访问抽象:Repository为领域层提供了一个清晰的数据访问接口,使得领域对象可以专注于业务逻辑的实现,而不是数据访问的细节。
- 领域对象的查询和管理:Repository使得对领域对象的查询和管理变得更加方便和灵活,支持复杂的查询逻辑。
- 领域逻辑与数据存储分离:通过Repository模式,领域逻辑与数据存储逻辑分离,提高了领域模型的纯粹性和可测试性。
- 优化数据访问:Repository实现可以包含数据访问的优化策略,如缓存、批处理操作等,以提高应用程序的性能。
实现手段
在实践中,Repository模式通常通过以下方式实现:
- 定义Repository接口:在领域层定义一个或多个Repository接口,这些接口声明了所需的数据访问方法。
- 实现Repository接口:在基础设施层或数据访问层实现这些接口,具体实现可能是使用ORM(对象关系映射)框架,如MyBatis、Hibernate等,或者直接使用数据库访问API,如JDBC等。
- 依赖注入:在应用程序中使用依赖注入(DI)来将具体的Repository实现注入到需要它们的领域服务或应用服务中。这样做可以进一步解耦领域层和数据访问层,同时也便于单元测试。
- 使用规范模式(Specification Pattern):有时候,为了构建复杂的查询,可以结合使用规范模式,这是一种允许将业务规则封装为单独的业务逻辑单元的模式,这些单元可以被Repository用来构建查询。
案例
在这个例子中,我们将创建一个简单的用户管理系统,其中包含用户实体和用户仓储接口,以及一个基于内存的仓储实现。
首先,我们定义一个用户实体:
public class User { private Long id; private String username; private String email; // 构造函数、getter和setter省略 }接下来,我们定义用户仓储的接口:
public interface UserRepository { User findById(Long id); List<User> findAll(); void save(User user); void delete(User user); }然后,我们提供一个基于内存的仓储实现:
public class InMemoryUserRepository implements UserRepository { private Map<Long, User> database = new HashMap<>(); private AtomicLong idGenerator = new AtomicLong(); @Override public User findById(Long id) { return database.get(id); } @Override public List<User> findAll() { return new ArrayList<>(database.values()); } @Override public void save(User user) { if (user.getId() == null) { user.setId(idGenerator.incrementAndGet()); } database.put(user.getId(), user); } @Override public void delete(User user) { database.remove(user.getId()); } }最后,我们可以在应用服务中使用这个仓储:
public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User getUserById(Long id) { return userRepository.findById(id); } public void createUser(String username, String email) { User newUser = new User(); newUser.setUsername(username); newUser.setEmail(email); userRepository.save(newUser); } // 其他业务逻辑方法... }在实际应用中,我们通常会使用依赖注入框架(如Spring)来自动注入仓储的实现。这里为了简单起见,我们可以手动创建服务和仓储的实例:
public class Application { public static void main(String[] args) { UserRepository userRepository = new InMemoryUserRepository(); UserService userService = new UserService(userRepository); userService.createUser("XiaoFuGe", "xiaofuge@qq.com"); User user = userService.getUserById(1L); System.out.println("User found: " + user.getUsername()); } }适配(端口),调用外部接口
在领域驱动设计(DDD)的上下文中,适配器(Adapter)模式扮演着至关重要的角色。适配器模式允许将不兼容的接口转换为另一个预期的接口,从而使原本由于接口不兼容而不能一起工作的类可以协同工作。在DDD中,适配器通常与端口(Port)概念结合使用,形成"端口和适配器"(Ports and Adapters)架构,也称为"六边形架构"(Hexagonal Architecture)。这种架构风格旨在将应用程序的核心逻辑与外部世界的交互解耦。
概念
Port 在这种架构中代表了应用程序的一个入口或出口点。它定义了一个与外部世界交互的接口,但不关心具体的实现细节。端口可以是驱动端口(Driving Ports,通常是输入端口)或被驱动端口(Driven Ports,通常是输出端口)。
特性
- 抽象性:端口提供了服务行为的抽象描述,明确了服务的功能和外部依赖。
- 独立性:端口独立于具体实现,允许服务实现的灵活替换或扩展。
- 灵活性:可以为同一端口提供不同的适配器实现,以适应不同的运行环境或需求。
用途
- 标准定义:端口和适配器定义了服务的标准行为和外部依赖,提高了代码的可读性和可维护性。
- 隔离变化:当外部系统变化时,只需更换或修改适配器,无需改动核心业务逻辑。
- 促进测试:可以使用模拟适配器来测试核心逻辑,而不依赖真实的外部系统。
实现
实现端口和适配器架构通常涉及以下步骤:
- 定义端口:在领域层定义清晰的接口,这些接口代表了应用程序与外部世界的交互点。
- 创建适配器:在基础层或应用层实现适配器,这些适配器负责将端口的抽象操作转换为具体的外部调用。
- 依赖倒置:应用程序的核心逻辑依赖于端口接口,而不是适配器的具体实现。这样,适配器可以随时被替换,而不影响核心逻辑。
- 配置和组装:在应用程序启动时,根据需要将适配器与相应的端口连接起来。
案例
在这个例子中,我们将创建一个简单的支付系统,其中包含一个支付端口和一个适配器,该适配器负责调用外部支付服务的接口。
首先,我们定义一个支付端口(Port),它是一个接口,描述了支付服务应该提供的操作:
public interface PaymentPort { boolean processPayment(double amount); }接下来,我们创建一个适配器,它实现了支付端口,并负责调用外部支付服务的接口:
public class ExternalPaymentService { public boolean makePayment(double amount) { // 这里是外部支付服务的具体调用逻辑 System.out.println("Calling external payment service for amount: " + amount); // 假设支付总是成功 return true; } } public class PaymentAdapter implements PaymentPort { private ExternalPaymentService externalPaymentService; public PaymentAdapter(ExternalPaymentService externalPaymentService) { this.externalPaymentService = externalPaymentService; } @Override public boolean processPayment(double amount) { // 调用外部支付服务的接口 return externalPaymentService.makePayment(amount); } }现在,我们可以在应用程序的核心逻辑中使用支付端口,而不依赖于适配器的具体实现。这样,如果将来需要更换外部支付服务,我们只需提供一个新的适配器实现即可:
public class PaymentService { private PaymentPort paymentPort; public PaymentService(PaymentPort paymentPort) { this.paymentPort = paymentPort; } public void processUserPayment(double amount) { if (paymentPort.processPayment(amount)) { System.out.println("Payment processed successfully."); } else { System.out.println("Payment failed."); } } }最后,我们在应用程序的启动或配置阶段组装这些组件:
public class Application { public static void main(String[] args) { // 创建外部支付服务的实例 ExternalPaymentService externalPaymentService = new ExternalPaymentService(); // 创建适配器的实例,注入外部支付服务 PaymentAdapter paymentAdapter = new PaymentAdapter(externalPaymentService); // 创建支付服务的实例,注入适配器 PaymentService paymentService = new PaymentService(paymentAdapter); // 处理用户支付 paymentService.processUserPayment(100.0); } }在这个例子中,PaymentAdapter 负责调用外部的支付接口 ExternalPaymentService.makePayment。PaymentService 使用 PaymentPort 接口与外部世界交互,这样就实现了领域逻辑与外部服务之间的解耦。如果需要更换支付服务提供商,我们只需要实现一个新的 PaymentAdapter,而不需要修改 PaymentService 的代码。