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仓库