贫血模型与丰富领域对象 | Baeldung
贫血模型与丰富领域对象 | Baeldung
在本文中,我们将探讨贫血和丰富领域模型之间的区别。我们将首先定义什么是丰富对象,并将其与贫血对象进行对比。从那里开始,我们将检查一个实际的代码示例,并通过封装数据和为我们的领域模型建立一个强大的API来逐步增强其设计。
让我们首先理解丰富和贫血对象是什么。在个人博客上,Robert C. Martin,《Clean Code》的作者,讨论了贫血对象的概念,称它们为“数据结构”。他通过声明:“类使函数可见,同时使数据隐含。数据结构使数据可见,同时使函数隐含。”来强调数据结构和对象之间的根本区别。
简单来说,丰富对象隐藏了其底层数据,并且只公开一组公共方法与之交互。相比之下,贫血对象和数据结构揭示了它们的数据,并依赖外部组件进行操作。
2.1. 丰富对象
在面向对象编程(OOP)的背景下,对象是一组操作封装数据的函数。一个常见的错误是将对象视为仅仅是元素的集合,并通过直接操作其字段来满足业务需求,从而破坏其封装性。
为了更深入地理解领域并构建一个丰富的领域模型,我们应该封装数据。结果,我们将把对象视为自治实体,专注于它们的公共接口以满足业务用例。
2.2. 贫血对象
相比之下,贫血对象只公开一组数据元素,这些数据元素旨在由隐式函数操作。例如,我们可以想到一个DTO(数据传输对象):它通过getter和setter公开其字段,但它不知道对它们执行任何操作。
对于本文的代码示例,让我们假设我们正在开发一个模拟网球比赛的应用程序。让我们看看这个应用程序的贫血领域模型可能是什么样子:
public class Player {
private String name;
private int points;
// 构造函数,getter和setter
}
正如我们所看到的,Player类没有提供任何有用的方法,并且通过getter和setter公开了它所有的字段。随着文章的进展,我们将逐步丰富我们的领域模型,封装其数据。
3. 封装
缺乏封装是贫血模型的一个主要症状。假设数据通过getter和setter公开。在这种情况下,我们冒着将与我们的模型相关的逻辑散布在整个应用程序并在不同的领域服务中潜在地复制它的风险。
因此,丰富Player模型的第一步将是质疑它的getter和setter。让我们看看Player类的简单用法,并了解这些数据是如何用于网球比赛的:
public class TennisGame {
private Player server;
private Player receiver;
public TennisGame(String serverName, String receiverName) {
this.server = new Player(serverName, 0);
this.receiver = new Player(receiverName, 0);
}
public void wonPoint(String playerName) {
if(server.getName().equals(playerName)) {
server.setPoints(server.getPoints() + 1)
} else {
receiver.setPoints(receiver.getPoints() + 1);
}
}
public String getScore() {
// 使用下面私有方法的一些逻辑
}
private boolean isScoreEqual() {
return server.getPoints() == receiver.getPoints();
}
private boolean isGameFinished() {
return leadingPlayer().getPoints() > Score.FORTY.points
&& Math.abs(server.getPoints() - receiver.getPoints()) >= 2;
}
private Player leadingPlayer() {
if (server.getPoints() - receiver.getPoints() > 0) {
return server;
}
return receiver;
}
public enum Score {
LOVE(0, "Love"),
FIFTEEN(1, "Fifteen"),
THIRTY(2, "Thirty"),
FORTY(3, "Forty");
private final int points;
private final String label;
// 构造函数
}
}
3.1. 质疑Setters
首先,让我们考虑代码中的setter方法。目前,球员的名字作为构造函数参数传递,并且在之后从不改变。因此,我们可以通过删除相应的setter方法,安全地使它们不可变。
接下来,我们观察到球员一次只能获得一分。因此,我们可以将现有的setter方法替换为一个更专业的_wonPoint()_方法,该方法将球员的得分增加一分:
public class Player {
private final String name;
private int points;
public Player(String name) {
this.name = name;
this.points = 0;
}
public void wonPoint() {
this.points++;
}
// getters
}
3.2. 质疑Getters
点数的getter被多次用于比较两个球员之间的得分差异。让我们引入一个方法,返回当前球员和他的对手之间的点数差异:
public int pointsDifference(Player opponent) {
return this.points - opponent.points;
}
为了检查一个球员是否处于“优势”或者是否赢得了比赛,我们需要一个额外的方法来检查一个球员的得分是否大于给定值:
public boolean hasScoreBiggerThan(Score score) {
return this.points > score.points();
}
现在,让我们删除getter并使用Player对象上的丰富接口:
private boolean isScoreEqual() {
return server.pointsDifference(receiver) == 0;
}
private Player leadingPlayer() {
if (server.pointsDifference(receiver) > 0) {
return server;
}
return receiver;
}
private boolean isGameFinished() {
return leadingPlayer().hasScoreBiggerThan(Score.FORTY)
&& Math.abs(server.pointsDifference(receiver)) >= 2;
}
private boolean isAdvantage() {
return leadingPlayer().hasScoreBiggerThan(Score.FORTY)
&& Math.abs(server.pointsDifference(receiver)) == 1;
}
4. 低耦合
**一个丰富的领域模型会导致设计具有低耦合。**通过移除_getPoints()_和_setPoints()_方法并增强对象的API,我们成功地隐藏了实现细节。让我们再次看看Player类:
public class Player {
private final String name;
private int points;
public Player(String name) {
this.name = name;
this.points = 0;
}
public void gainPoint() {
points++;
}
public boolean hasScoreBiggerThan(Score score) {
return this.points > score.points();
}
public int pointsDifference(Player other) {
return points - other.points;
}
public String name() {
return name;
}
public String score() {
return Score.from(points).label();
}
}
正如我们所看到的,我们可以轻松地更改内部数据结构。例如,我们可以创建一个新的类来存储球员的得分,而不是依赖于_int_原始类型,而不影响使用Player类的任何客户端。
5. 高内聚
一个丰富的模型也可以增加我们领域的内聚性,并符合单一职责原则。在这种情况下,Player实例负责管理它赢得的分数数量,而TennisGame类承担协调两名球员和跟踪比赛总得分的责任。
然而,当将这些小逻辑片段从用例实现移动到我们的模型中时,我们应该小心。作为经验法则,我们只应该移动与用例无关的函数以保持高内聚。
换句话说,我们可能被诱惑向Player类添加一个方法,如“hasWonOver(Player opponent)”,但这个规则只有在球员相互对抗时才有意义。此外,这不是一个与用例无关的规则:赢得比赛的条件可能根据比赛的格式(例如,当打单打、双打、三盘两胜制、五盘三胜制或其他格式)而有所不同。
6. 增加表达力
**丰富领域模型的另一个好处是,它允许我们减少领域服务或用例类的复杂性。**换句话说,TennisGame类现在将更具表达力,允许开发人员通过隐藏与Player相关的细节来专注于业务规则。
**质疑getter和setter的使用过程以及对Player类公共API的更改,挑战了我们对我们领域模型及其能力的更深入理解。**这是一个经常被忽视的重要步骤,因为使用像IDEs或Lombok这样的工具自动生成getter和setter的便利性。
7. 结论
在本文中,我们讨论了贫血对象的概念以及采用丰富领域模型的优势。随后,我们提供了一个实际示例,展示了如何封装对象的数据并提供改进的接口。最后,我们发现这种方法的许多好处,包括增加的表达力、提高的内聚性和降低的耦合。
像往常一样,完整的代码可以在GitHub上找到。