1. 概述
本文将深入探讨贫血领域模型与富领域模型的区别。首先明确富对象的定义,并与贫血对象进行对比。随后通过实际代码示例,逐步改进设计:通过封装数据并构建健壮的领域模型API。
2. 贫血对象与富对象
2.1 富对象
在OOP(面向对象编程)语境中,对象本质上是操作封装数据的函数集合。常见误区是将对象视为简单的元素集合,并通过直接操作字段来满足业务需求,这破坏了封装性。
要深入理解领域并构建富领域模型,必须封装数据。这样对象将成为自治实体,通过公共接口实现业务用例。
2.2 贫血对象
相反,贫血对象仅暴露一组数据元素,依赖外部组件进行操作。 典型例子是DTO(数据传输对象):它通过getter/setter暴露字段,但自身不具备任何操作能力。
本文代码示例假设我们正在开发网球比赛模拟应用。先看贫血领域模型:
public class Player {
private String name;
private int points;
// constructor, getters and setters
}
显然,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;
// constructor
}
}
3.1 质疑setter方法
首先分析setter:玩家名称通过构造函数传入且不可变,可移除对应setter。其次,玩家每次只能得1分,可用专门方法wonPoint()
替代setter:
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 质疑getter方法
原代码多次使用getPoints()
比较分数差。新增方法计算与对手的分差:
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
存储分数),不影响使用方代码。
5. 高内聚
富模型还能提升领域内聚性,符合单一职责原则。Player
负责管理自身得分,TennisGame
负责协调玩家并追踪比赛总分。
但需谨慎:将逻辑从用例实现移至模型时,只应迁移用例无关的函数以保持高内聚。
例如,可能想给Player
添加hasWonOver(Player opponent)
方法,但这仅在两人对战时有效。且违反用例无关原则:不同赛制(单打/双打/三局两胜等)的获胜条件可能不同。
6. 提升表现力
丰富领域模型的另一好处是降低领域服务/用例类的复杂度。 改进后的TennisGame
类更直观,开发者可专注业务规则而无需关注Player
细节。
质疑getter/setter并改进Player
公共API的过程,迫使我们深入理解领域模型的能力。 这常被忽视,因为IDE或Lombok自动生成getter/setter太方便了。
7. 结论
本文讨论了贫血对象的概念及富领域模型的优势,通过实际示例展示了如何封装数据并改进接口。最终发现该方法能提升表现力、增强内聚性并降低耦合。
完整代码见GitHub仓库。