构造函数中的复杂初始化



我有一种管理不同数据地图的模型。 我读过构造函数不应该包含业务逻辑,但我也读过构造函数可以自由地做他们需要做的事情来初始化对象的状态。 如果在构造函数中给定映射 A 和映射 B,我想合并这两个映射并在第三个字段中设置结果怎么办。 也许我也想做一些清理或其他事情。 这是不好的做法吗? 如果是这样,为什么?

public class MapManager {
private Map<String, Object> mapA;
private Map<String, Object> mapB;
private Map<String, Object> combinedMap;
public MapManager(Map<String, Object> mapA, Map<String, Object> mapB) {
this.mapA = mapA;
this.mapB = mapB;
this.combinedMaps = initCombinedMap(mapA, mapB);
}
public Map<String, Object> getMapA() {
return mapA;
}
public Map<String, Object> getMapB() {
return mapB;
}
public Map<String, Object> getCombinedMap() {
return combinedMap;
}
private static Map<String, Object> initCombinedMap(Map<String, Object> mapA, Map<String, Object> mapB) {
Map<String, Object> combinedMap = new HashMap<>(mapA);
if (mapB != null) {
mapB.forEach(combinedMap::putIfAbsent);
}
return combinedMap;
}
}

我读过构造函数不应该包含业务逻辑,但我也读过构造函数可以自由地做他们需要做的事情来初始化对象的状态。

这两种说法都是很好的建议——而且它们并不矛盾。

我认为你的问题实际上是关于什么算作"业务逻辑"。一般的想法是"业务逻辑"实现"业务规则",即软件的实际"业务客户端"(不是开发人员)要求或可能关心的行为。


例如,假设业务规则规定用户名中允许使用西里尔字母,除非(1) 用户名还包含拉丁字母,或 (2) 用户名中的每个西里尔字母都很容易被误认为是拉丁字母。(此业务规则有助于防止欺骗:我不能通过创建名为"Веn"的帐户来模拟名为"Ben"的帐户,也不能通过创建名为"Димa"的帐户来模拟名为"Дима"的帐户。

现在,您可能有一个业务域对象类,称为类似User的类,它表示应用程序用户;因此,您可能想将此业务规则实现为User类的不变量,这意味着实际上不可能有违反此业务规则的User实例。为此,您在构造函数中实现此规则(可能通过委托给某种validateUsername方法或其他方法)。

这样做的一个问题是,拥有违反此规则的用户名的用户并非完全不可能;如果一个重要的客户打电话要求用户名сор,那么有人可能需要进入数据库并手动创建该用户 - 在这种情况下,您不希望User类在您第一次尝试从数据库中加载该用户时爆炸。

这样做的另一个问题是,随着规则随着时间的推移变得越来越复杂,您最终可能会将相关数据提取到数据库或配置存储中,并且您显然不希望仅仅为了在单元测试中实例化User而需要数据库连接。

因此,更好的方法是区分User类的技术限制(例如,必须填充的字段,否则事情会爆炸,字段不能变异否则事情会爆炸)和用户帐户的业务限制。类本身应该严格执行前者,而不是后者。相反,您应该有一个单独的方法来验证用户,并在将用户添加到数据库之前调用该方法。


不幸的是,我不太了解你的MapManager类来评论它(我实际上有点怀疑它作为一个有凝聚力的类是否有意义),但如果它有帮助,这里有一些被视为"业务逻辑"的初始化逻辑示例:

  • 如果类表示一个数据结构,例如双向链表(java.util.LinkedList),哈希表(java.util.HashMap),红黑树(java.util.TreeMapjava.util.TreeSet),那么绝对可以预期它的构造函数将包含初始化数据结构的逻辑 - 特别是如果它有一个构造函数,该构造函数接受现有集合并复制其内容(因为这些示例都这样做)。这不是"业务逻辑",因为业务客户对他们没有意见;他们可能甚至不知道术语"链表"和"树"。关于这些类的所有内容都属于"技术"而不是"业务"类别。
  • 如果类有一个不应该为 null 的不可变字段,那么绝对期望它的构造函数会检查这一点并使用信息性消息引发java.lang.NullPointerException。在这里,业务规则可能会对技术实现产生一些影响——我们可以想象一个新的业务需求,它相当直接地转化为"这个字段现在必须为空"(尽管实际上可能会以不同的方式实现,例如通过将字段更改为java.util.Optional)——但这是一个技术规则,因为可能存在依赖于它的下游代码, 例如,在不检查 null 的情况下编写this.username.toLowerCase()之类的东西。(这是一个实用主义的问题;如果空用户名不太可能被允许,为什么要编写额外的代码来支持空用户名?最好只是预先验证。

我认为这很好,但取决于您使用它的原因,它可能有点过于复杂。例如,如果唯一一次将Map AMap B组合在构造函数中,则应删除initCombinedMap方法并在实际构造函数中进行组合。

您还可以使用包javafx.util中的Pair来为地图创建容器。然后,您的getter可以从该对中检索Key以获取mapA,并为mapBValue

这是您的班级在实现这些建议后的样子:

public class MapManager {
private Pair<Map<String, Object>, Map<String, Object>> maps;
private Map<String, Object> combinedMaps;
public MapManager(Map<String, Object> mapA, Map<String, Object> mapB) {
this.maps = new Pair<>(mapA, mapB);
this.combinedMaps = new HashMap<>(mapA);
if (mapB != null) {
mapB.forEach(combinedMaps::putIfAbsent);
}
}
public Map<String, Object> getMapA() {
return maps.getKey();
}
public Map<String, Object> getMapB() {
return maps.getValue();
}
public Map<String, Object> getCombinedMap() {
return combinedMaps;
}
}

另一个建议是通过替换为String来生成您的类,并用泛型类型KVObject。这不会完全简化您的类,但它将提供模块化,因为映射现在可以容纳用户在实例化新MapManager时指定的任何类型。

public class MapManager<K, V> {
private Pair<Map<K, V>, Map<K, V>> maps;
private Map<K, V> combinedMaps;
public MapManager(Map<K, V> mapA, Map<K, V> mapB) {
this.maps = new Pair<>(mapA, mapB);
this.combinedMaps = new HashMap<>(mapA);
if (mapB != null) {
mapB.forEach(combinedMaps::putIfAbsent);
}
}
public Map<K, V> getMapA() {
return maps.getKey();
}
public Map<K, V> getMapB() {
return maps.getValue();
}
public Map<K, V> getCombinedMap() {
return combinedMaps;
}
}

最新更新