在企业级 Java 中应用领域驱动设计:一种行为驱动方法

了解如何结合 DDD 和 BDD 于企业级 Java 中,以创建能够模拟真实业务领域并通过可执行场景验证行为的软件。

在软件开发领域,最大的错误之一就是交付客户"精确"想要的东西。这听起来可能像陈词滥调,但即使在行业摸爬滚打数十年后,这个问题依然存在。一个更有效的方法是从关注业务需求开始测试。

行为驱动开发Behavior-driven development】(BDD )是一种强调行为和领域术语(也称为统一语言 )的软件开发方法论。它使用共享的自然语言,从用户的角度定义和测试软件行为。BDD 建立在测试驱动开发test-driven development】(TDD)的基础上,专注于与业务相关的场景。这些场景以纯语言规范的形式编写,可以自动化成测试,同时也充当活文档。

这种方法促进了技术和非技术利益相关者之间的共识,确保软件满足用户需求,并有助于减少返工和开发时间。在本文中,我们将进一步探讨这种方法论,并讨论如何使用 Oracle NoSQL 和 Java 来实现它。

BDD 与 DDD 如何协同工作

乍看之下,行为驱动开发(BDD)和领域驱动设计(DDD)似乎解决的是不同的问题------一个侧重于测试 ,另一个侧重于建模 。然而,它们共享相同的哲学基础:确保软件真实反映其所服务的业务领域

DDD ,由 Eric Evans 在其 2003 年具有开创性的著作*《领域驱动设计:软件核心复杂性的应对之道》*中提出,教导我们围绕业务概念(实体、值对象、聚合和限界上下文)来建模软件。其力量在于使用统一语言,这是一种连接开发人员和领域专家的共享词汇表。

BDD ,由 Dan North 在几年后提出,是这一思想自然而然的延伸。它将统一语言引入测试过程,将业务规则转化为可执行的规范。DDD 定义了系统应该表示什么 ,而 BDD 则根据该模型验证系统的行为方式

当结合使用时,DDD 和 BDD 形成了一个持续的反馈循环:

  • DDD 塑造了捕获业务逻辑的领域模型

  • BDD 确保系统行为随着时间的推移与该模型保持一致。

在实践中,这种协同作用意味着您可以编写与聚合(如 Room 和 Reservation)直接相关的特性场景------例如"当我预订一个 VIP 房间时,系统应将其标记为不可用"。这些测试成为开发人员和利益相关者的活文档,确保您的领域始终与真实的业务需求保持一致。

如果您想深入探索这种结合,我的著作《Domain-Driven Design with Java》详细阐述了这些原则。它展示了如何在现代 Java 应用程序中使用 Jakarta EE、Spring 和云技术应用 DDD 模式,为统一架构和行为提供了实践基础。

总之,DDD 和 BDD 共同弥合了理解业务与证明其可行之间的差距------将软件从技术制品转变为领域本身的忠实表达。

代码实现

在本示例中,我们将使用企业级 Java 和 Oracle NoSQL 数据库生成一个简单的酒店管理应用程序。

第一步是创建项目。由于我们使用的是 Java SE,我们可以使用以下 Maven 命令生成它:

shell 复制代码
mvn archetype:generate \

"-DarchetypeGroupId=io.cucumber" \

"-DarchetypeArtifactId=cucumber-archetype" \

"-DarchetypeVersion=7.30.0" \

"-DgroupId=org.soujava.demos.hotel" \

"-DartifactId=behavior-driven-development" \

"-Dpackage=org.soujava.demos" \

"-Dversion=1.0.0-SNAPSHOT" \

"-DinteractiveMode=false"

下一步是引入 Eclipse JNoSQLOracle NoSQL ,以及 Jakarta EE 组件的实现:CDI、JSON 和 Eclipse MicroProfile 实现。

您可以找到完整的 pom.xml 文件。

初始项目准备就绪后,我们将从创建测试开始。

请记住,BDD 是 TDD 的扩展,它包含了统一语言------领域和业务之间的共享词汇。

textile 复制代码
功能: 管理酒店房间

  


场景: 注册一个新房间

假设 酒店管理系统正在运行

当 我注册一个号码为 203 的房间

那么 号码为 203 的房间应该出现在房间列表中

  


场景: 注册多个房间

假设 酒店管理系统正在运行

当 我注册以下房间:

| number | type | status | cleanStatus |

| 101 | STANDARD | AVAILABLE | CLEAN |

| 102 | SUITE | RESERVED | DIRTY |

| 103 | VIP_SUITE | UNDER_MAINTENANCE | CLEAN |

那么 系统中应该有 3 个可用房间

  


场景: 更改房间状态

假设 酒店管理系统正在运行

并且 一个号码为 101 的房间已注册为 AVAILABLE

当 我将房间 101 标记为 OUT_OF_SERVICE

那么 房间 101 应被标记为 OUT_OF_SERVICE

Maven 项目完成后,让我们进入下一步,即创建建模和存储库。如前所述,我们将专注于房间管理。因此,我们的下一个目标是确保之前定义的 BDD 测试通过。让我们从实现领域模型和存储库开始:

java 复制代码
public enum CleanStatus {

CLEAN, // 清洁

DIRTY, // 脏污

INSPECTION_NEEDED // 需要检查

}

  


public enum RoomStatus {

AVAILABLE, // 可用

RESERVED, // 已预订

UNDER_MAINTENANCE, // 维护中

OUT_OF_SERVICE // 停止服务

}

  


public enum RoomType {

STANDARD, // 标准间

DELUXE, // 豪华间

SUITE, // 套房

VIP_SUITE // VIP套房

}

  


@Entity

public class Room {

  


@Id

private String id;

  


@Column

private int number; // 房间号

  


@Column

private RoomType type; // 房间类型

  


@Column

private RoomStatus status; // 房间状态

  


@Column

private CleanStatus cleanStatus; // 清洁状态

  


@Column

private boolean smokingAllowed; // 允许吸烟

  


@Column

private boolean underMaintenance; // 处于维护状态

  


}

有了模型,下一步是创建企业级 Java 与作为非关系型数据库的 Oracle NoSQL 之间的桥梁。我们可以使用 Jakarta Data 非常轻松地完成,它只有一个存储库接口,所以我们不需要担心实现。

java 复制代码
@Repository

public interface RoomRepository {

  


@Query("FROM Room")

List<Room> findAll();

  


@Save

Room save(Room room);

  


void deleteBy();

  


Optional<Room> findByNumber(Integer number);

}

项目完成后,下一步是准备测试环境,首先提供一个数据库实例用于测试。多亏了 Testcontainers,我们可以轻松启动一个隔离的 Oracle NoSQL 实例来运行我们的测试。

java 复制代码
public enum DatabaseContainer {

  


INSTANCE;

  


private final GenericContainer<?> container = new GenericContainer<>

(DockerImageName.parse("ghcr.io/oracle/nosql:latest-ce"))

.withExposedPorts(8080);

  


{

container.start();

}

public DatabaseManager get(String database) {

DatabaseManagerFactory factory = managerFactory();

return factory.apply(database);

}

  


public DatabaseManagerFactory managerFactory() {

var configuration = DatabaseConfiguration.getConfiguration();

Settings settings = Settings.builder()

.put(OracleNoSQLConfigurations.HOST, host())

.build();

return configuration.apply(settings);

}

  


public String host() {

return "http://" + container.getHost() + ":" + container.getFirstMappedPort();

}

}

之后,我们将创建一个与 @Alternative CDI 注解集成的生产者。此配置指导 CDI 如何提供数据库实例------在本例中是由 Testcontainers 管理的实例:

java 复制代码
@ApplicationScoped

@Alternative

@Priority(Interceptor.Priority.APPLICATION)

public class ManagerSupplier implements Supplier<DatabaseManager> {

  


@Produces

@Database(DatabaseType.DOCUMENT)

@Default

public DatabaseManager get() {

return DatabaseContainer.INSTANCE.get("hotel");

}

  


}

借助 Cucumber,我们可以定义一个将类注入到 Cucumber 测试上下文中的 ObjectFactory。由于我们使用 CDI 并以 Weld 作为实现,我们将创建一个自定义的 WeldCucumberObjectFactory 来无缝集成这两种技术。

java 复制代码
public class WeldCucumberObjectFactory implements ObjectFactory {

  


private Weld weld;

private WeldContainer container;

  


@Override

public void start() {

weld = new Weld();

container = weld.initialize();

}

  


@Override

public void stop() {

if (weld != null) {

weld.shutdown();

}

}

  


@Override

public boolean addClass(Class<?> stepClass) {

return true;

}

  


@Override

public <T> T getInstance(Class<T> type) {

return (T) container.select(type).get();

}

}

一个重要提示:此设置作为 SPI(服务提供者接口)工作。因此,您必须创建以下文件:

src/test/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory

内容如下:

shell 复制代码
org.soujava.demos.hotels.config.WeldCucumberObjectFactory

我们将让 Mapper 将我们的数据表转换为所有模型中的 Room 对象。

java 复制代码
@ApplicationScoped

public class RoomDataTableMapper {

  


@DataTableType

public Room roomEntry(Map<String, String> entry) {

return Room.builder()

.number(Integer.parseInt(entry.get("number")))

.type(RoomType.valueOf(entry.get("type")))

.status(RoomStatus.valueOf(entry.get("status")))

.cleanStatus(CleanStatus.valueOf(entry.get("cleanStatus")))

.build();

}

}

整个测试基础设施完成后,下一步是设计包含我们实际测试的 Step 测试类。

java 复制代码
@ApplicationScoped

public class HotelRoomSteps {

  


@Inject

private RoomRepository repository;

  


@Before

public void cleanDatabase() {

repository.deleteBy();

}

  


@Given("the hotel management system is operational")

public void theHotelManagementSystemIsOperational() {

Assertions.assertThat(repository).as("RoomRepository 应该已初始化").isNotNull();

}

  


@When("I register a room with number {int}")

public void iRegisterARoomWithNumber(Integer number) {

Room room = Room.builder()

.number(number)

.type(RoomType.STANDARD)

.status(RoomStatus.AVAILABLE)

.cleanStatus(CleanStatus.CLEAN)

.build();

repository.save(room);

}

  


@Then("the room with number {int} should appear in the room list")

public void theRoomWithNumberShouldAppearInTheRoomList(Integer number) {

List<Room> rooms = repository.findAll();

Assertions.assertThat(rooms)

.extracting(Room::getNumber)

.contains(number);

}

  


@When("I register the following rooms:")

public void iRegisterTheFollowingRooms(List<Room> rooms) {

rooms.forEach(repository::save);

}

  


@Then("there should be {int} rooms available in the system")

public void thereShouldBeRoomsAvailableInTheSystem(int expectedCount) {

List<Room> rooms = repository.findAll();

Assertions.assertThat(rooms).hasSize(expectedCount);

}

  


@Given("a room with number {int} is registered as {word}")

public void aRoomWithNumberIsRegisteredAs(Integer number, String statusName) {

RoomStatus status = RoomStatus.valueOf(statusName);

Room room = Room.builder()

.number(number)

.type(RoomType.STANDARD)

.status(status)

.cleanStatus(CleanStatus.CLEAN)

.build();

repository.save(room);

}

  


@When("I mark the room {int} as {word}")

public void iMarkTheRoomAs(Integer number, String newStatusName) {

RoomStatus newStatus = RoomStatus.valueOf(newStatusName);

Optional<Room> roomOpt = repository.findByNumber(number);

  


Assertions.assertThat(roomOpt)

.as("房间 %s 应该存在", number)

.isPresent();

  


Room updatedRoom = roomOpt.orElseThrow();

updatedRoom.update(newStatus); // 假设 Room 类有 update 方法

  


repository.save(updatedRoom);

}

  


@Then("the room {int} should be marked as {word}")

public void theRoomShouldBeMarkedAs(Integer number, String expectedStatusName) {

RoomStatus expectedStatus = RoomStatus.valueOf(expectedStatusName);

Optional<Room> roomOpt = repository.findByNumber(number);

  


Assertions.assertThat(roomOpt)

.as("房间 %s 应该存在", number)

.isPresent()

.get()

.extracting(Room::getStatus)

.isEqualTo(expectedStatus);

}

}

是时候执行测试了:

shell 复制代码
mvn clean test

您可以看到结果:

shell 复制代码
INFO: Connecting to Oracle NoSQL database at http://localhost:61325 using ON_PREMISES deployment type

✔ Given the hotel management system is operational # org.soujava.demos.hotels.HotelRoomSteps.theHotelManagementSystemIsOperational()

✔ And a room with number 101 is registered as AVAILABLE # org.soujava.demos.hotels.HotelRoomSteps.aRoomWithNumberIsRegisteredAs(java.lang.Integer,java.lang.String)

✔ When I mark the room 101 as OUT_OF_SERVICE # org.soujava.demos.hotels.HotelRoomSteps.iMarkTheRoomAs(java.lang.Integer,java.lang.String)

✔ Then the room 101 should be marked as OUT_OF_SERVICE # org.soujava.demos.hotels.HotelRoomSteps.theRoomShouldBeMarkedAs(java.lang.Integer,java.lang.String)

Oct 21, 2025 6:18:43 PM org.jboss.weld.environment.se.WeldContainer shutdown

INFO: WELD-ENV-002001: Weld SE container fc4b3b51-fba8-4ea6-9cef-42bcee97d220 shut down

[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.231 s -- in org.soujava.demos.hotels.RunCucumberTest

[INFO] Running org.soujava.demos.hotels.MongoDBTest

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.007 s -- in org.soujava.demos.hotels.MongoDBTest

[INFO]

[INFO] Results:

[INFO]

[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0

[INFO]

结论

通过结合领域驱动设计(DDD)和行为驱动开发(BDD),开发人员可以超越技术正确性,构建真正反映业务意图的软件。DDD 为领域提供了结构,确保模型精确地捕捉现实世界的概念,而 BDD 则通过用业务本身的语言编写的清晰、可测试的场景,确保这些模型按预期运行。

在本文中,您学习了如何使用 Oracle NoSQL、Eclipse JNoSQL 和 Jakarta EE 连接这两个世界------从定义您的领域到运行由 Cucumber 和 CDI 支持的真实行为测试。这种协同作用将测试转化为活文档,弥合了工程师和利益相关者之间的差距,并确保您的系统在演进过程中始终与业务目标保持一致。

您可以深入探索并将 DDD 与 BDD 结合起来。在《Domain-Driven Design with Java》这本书中,您可以找到一个很好的起点来理解为什么 DDD 对我们仍然很重要。它扩展了这里分享的想法,展示了 DDD 和 BDD 如何共同带来更简单、更易维护且以业务为中心的软件。这种软件交付的是超越需求的实际价值。


【注】本文译自:Applying Domain-Driven Design With Enterprise Java: A Behavior-Driven Approach

相关推荐
kevinzeng5 天前
MVC 和 DDD
后端·领域驱动设计
canonical_entropy21 天前
Nop平台到底有什么独特之处,它能用在什么场景?
java·后端·领域驱动设计
canonical-entropy1 个月前
范式重构:可逆计算如何颠覆DDD的经典模式
低代码·重构·ddd·领域驱动设计·可逆计算·nop平台
于过1 个月前
我为什么不喜欢DDD
架构·领域驱动设计
canonical_entropy1 个月前
领域驱动设计(DDD)中聚合根的最主要职责真的是维护一致性吗?
后端·架构·领域驱动设计
canonical_entropy1 个月前
领域驱动设计(DDD)领域对象一定要讲究充血模型吗?
后端·领域驱动设计·graphql
canonical_entropy1 个月前
Nop平台架构白皮书:一个基于广义可逆计算理论的软件构造体系评估
后端·低代码·领域驱动设计
canonical_entropy1 个月前
范式重构:可逆计算如何颠覆DDD的经典模式
后端·低代码·领域驱动设计
canonical_entropy1 个月前
告别经验主义:DDD的数学基础与工程实现
后端·架构·领域驱动设计