【问题标题】:Is it good practice to use domain objects as keys?使用域对象作为键是一种好习惯吗?
【发布时间】:2011-03-06 12:06:53
【问题描述】:

使用域对象作为映射(或“get”方法)的键是一种好习惯,还是只使用域对象的 id 更好?

用一个例子来解释更简单。假设我有 Person 类、一个 Club 类和一个 Membership 类(连接另外两个)。即,

public class Person {
    private int id; // primary key
    private String name;
}

public class Club {
    private String name; // primary key
}

public class Membership {
    private Person person;
    private Club club;
    private Date expires;
}

或者类似的东西。现在,我想在 Club 中添加一个方法 getMembership。问题是,这个方法是否应该采用 Person 对象:

public Membership getMembership(Person person);

或者,一个人的id:

public Membership getMembership(int personId);

哪个最地道,哪个最方便,哪个最合适?

编辑:许多非常好的答案。我选择不公开 id,因为“Person”(您可能已经意识到,我的真实域与人和俱乐部没有任何关系......)实例很容易获得,但现在它在内部存储在在 id 上散列的 HashMap - 但至少我在界面中正确地公开了它。

【问题讨论】:

  • 避免将您的private 部分暴露给其他实体。
  • @DaveJarvis 生活的话。

标签: java oop domain-driven-design


【解决方案1】:

如其他人所述:使用对象。

我在一个系统上工作,我们有一些旧代码使用 int 来表示事务 ID。你猜怎么了?我们开始用完 id,因为我们使用了 int。

事实证明,更改为 long 或 BigNumber 很棘手,因为人们在命名方面变得非常有创意。有些用过

int tranNum

有些用过

int transactionNumber

一些使用

int trannNum

(包含拼写错误)。

有些人真的很有创造力……

这是一团糟,整理它是一场噩梦。我最终手动浏览了所有代码并转换为 TransactionNumber 对象。

尽可能隐藏细节。

【讨论】:

    【解决方案2】:

    不要使用 id 的 man,由于上述所有原因,这只是一个坏主意。你会把自己锁在一个设计中。举个例子吧。

    现在,您将 Membership 定义为俱乐部与人员之间的映射。正确地,您的会员资格应该是俱乐部到“会员”的地图,但是您假设所有会员都是人,并且由于所有人的 ID 都是唯一的,您认为您可以只使用 ID。

    但是,如果将来您想将成员资格概念扩展到“家庭成员资格”,您可以为此创建一个 Family 表和一个 Family 类。在良好的 OO 方式中,您提取了名为 Member 的 Family 和 Person 的接口。只要两个类都正确地实现了 equals 和 hashCode 方法,就不需要触及其他代码。就个人而言,我会在前面定义 Member 接口。

    public interface Member {
    }
    
    public class Person implements Member {
        private int id; // primary key
        private String name;
    }
    
    public class Family implements Member {
       private int id;
       private String name;
    }
    
    public class Club {
        private String name; // primary key
    }
    
    public class Membership {
       private Member member;
       private Club club;
       private Date expires;
    }
    

    如果您在界面中使用了 ID,您将需要强制键值的跨表唯一性,或者维护两个单独的 Map 并放弃漂亮的多态界面的东西。

    相信我,除非您正在编写一次性的一次性应用程序,否则您希望避免在界面中使用 ID。

    【讨论】:

    • 这是一个非常有说服力的论点。此外,遵循这种做法意味着 Person 根本不需要公开 getId() 方法!
    【解决方案3】:

    其实我会用id来调用它,但是对原来的设计进行一点重构:

    public class Person {
        private int id; // primary key
        private String name;
    }
    
    public class Club {
        private String name; // primary key
        private Collection<Membership> memberships;
        public Membership getMembershipByPersonId(int id);
    }
    
    public class Membership {
        private Date expires;
        private Person person;
    }
    

    public class Person {
        private int id; // primary key
        private String name;
        private Membership membership;
        public Membership getMembership();
    }
    
    public class Club {
        private String name; // primary key
        private Collection<Person> persons;
        public Person getPersonById(int id);
    }
    
    public class Membership {
        private Date expires;
    }
    

    【讨论】:

      【解决方案4】:

      我可能会使用 ID。为什么?通过获取 ID,我可以对调用者做出更安全的假设。

      如果我有 ID,获取 Person 需要做多少工作?可能很“简单”,但它确实需要访问数据存储,这很慢......

      如果我有 Person 对象,获取 ID 需要做多少工作?简单的会员访问。快速且可用。

      【讨论】:

        【解决方案5】:

        除非在其他地方有显着的好处,否则可以说 map 中的键应该是单值的东西,如果可能的话。也就是说,通过注意 equals() 和 hashCode(),您可以使任何对象作为键工作,但是 equals() 和 hashCode() 并不是非常值得关注的事情。你会更乐意坚持使用 ID 作为键。

        【讨论】:

          【解决方案6】:

          如果您真的在练习面向对象的设计,那么您想调用信息隐藏的想法。一旦您开始在成员对象方法的公共接口中挂起人员对象的内部字段类型,您就开始强制您的对象的外部开发人员(用户)开始学习有关人员对象是什么以及如何使用的各种信息它被存储了,它有什么样的ID。

          更好的是,既然一个人可以拥有会员资格,为什么不直接将“getMemberships”方法挂在 person 类上。询问一个人他们拥有哪些会员资格似乎比询问某个人可能属于哪些俱乐部的“会员资格”更合乎逻辑......

          编辑 - 由于 OP 已更新以表明他感兴趣的是会员本身,而不仅仅是用作 Person 和 Club 之间的关系,所以我正在更新我的答案。

          长话短说,您正在定义的“俱乐部”课程,您现在要求表现得像“俱乐部花名册”。一个俱乐部有一个名册,它不是​​是一个名册。名册可以具有多种功能,包括查找属于该俱乐部的人员的方法。除了通过俱乐部 ID 查找一个人之外,您可能还想通过 SSN、姓名、加入日期等来查找他们。对我来说,这表示“俱乐部”类上有一个名为 getRoster() 的方法,它返回一个可以查找俱乐部中所有人员的数据结构。称之为集合。那么问题就变成了,您是否可以使用预先存在的集合类上的方法来满足您迄今为止定义的需求,或者您是否需要创建自定义集合子类以提供适当的方法来查找成员记录。

          由于您的类层次结构很可能由数据库支持,并且您可能正在考虑从数据库中加载信息,并且不一定想要获取整个集合只是为了获得一个成员资格,您可能想要创建一个新的班级。正如我所说的“名册”,这门课可以称为“名册”。您可以从“club”类的 getRoster() 调用中获得它的实例。您可以根据您想要的任何搜索条件向类添加“搜索”方法,这些条件是有关人员的“公开可用”信息......姓名、俱乐部 ID、个人 ID、SSN 等......

          我的原始答案仅适用于“会员资格”纯粹是表明哪些人属于哪些俱乐部的关系。

          【讨论】:

          • 正是这个。不要让您的类的用户必须了解键是什么、ID 是什么等。如果一个成员在逻辑上属于一个人,那么这就是您的方法接口应该是什么。
          • 可能更适合 Person。但是假设一个人通常是数百个俱乐部的成员,那么您想要在 Person 上的 getMembership(Club club) 代替,并且您遇到了同样的问题,只是颠倒了。
          • 我明白你现在问的重点。适当地编辑答案。
          • 如果您将“getMemberships”添加到 Person 对象,现在您的 Person 类与用于成员资格的数据源相结合。
          • 这是个好主意,但不是真的——使用面向对象的语言特性不会让你的代码“更加面向对象”。
          【解决方案7】:

          首先,我会将任何此类性质的吸气剂放入 DAO 中(而不是模型上)。然后我将实体本身用作参数,方法内部发生的事情是实现细节。

          【讨论】:

          • 在我的例子中,Membership 对象与 Club 对象同时从数据库中加载。因此调用 getMembership(..) 不会向数据库发出任何内容 - 它只会访问 Club 中的内部状态。
          【解决方案8】:

          哇。在这里备份一秒钟。 getMembership() 方法不属于 Club。它属于您尚未实施的所有成员资格的集合。

          【讨论】:

            【解决方案9】:

            如果您已经拥有该对象,则没有理由提取 ID 来获取哈希键。

            只要 ID 始终唯一,实现 hashCode() 以返回 ID,同时实现 equals()。

            很可能每次您需要 Membership 时,您就已经拥有 Person,因此以后可以节省代码和混乱。

            【讨论】:

            • 当对象从瞬态变为持久时,实现哈希码返回 id 是有问题的。它会破坏 hashcode/equals 合约,因为它会突然返回不同的结果。如果此时它位于集合中,则可能会导致问题。
            【解决方案10】:

            我认为第一种情况会被认为是“更纯粹的”,因为 getMembership 方法可能需要来自人本身而不是其 id 的更具体的数据(假设您不知道 getMembership 方法的内部结构,即使这没有什么意义,因为它很可能在同一个域中)。

            如果事实证明它实际上需要来自 Person 实体的数据,那么它就不需要相关人员的 DAO 或工厂。

            如果您的语言和/或 ORM 允许您使用代理对象(并且如果您有一种方便的方法来创建这些代理),则可以轻松调用它。

            但是说实话。如果您要查询某个人的某些成员资格,那么当您调用此方法时,您很可能已经在内存中拥有此 Person 实例。

            在“基础设施领域”的道路上,还有一个关于实施细节的概念,Uri 在我写这个答案时已经提到过(该死的,这太快了兄弟!)。具体来说,如果你决定这个“人”概念突然在底层数据库中有一个复合主键/标识符……你现在会使用标识符类吗?也许使用我们正在谈论的那个代理?

            TL;DR 版本

            从短期来看,使用 ID 确实更容易,但如果您已经在使用可靠的 ORM,我认为没有理由不使用代理或其他方式来表达不会泄漏的实体的面向对象身份实现细节。

            【讨论】:

            • 如何获得一个人?在某些时候,您可能需要一个 getPerson(int personId) 函数 [vs getPerson(String name)]。不是反对“纯粹”的投票,而只是说明很难不在某处公开实现细节。
            • 正如我所说,您很可能会在需要它作为依赖项的对象中使用某种代理提供程序或注入 Person。在我看来,这超出了这个问题的范围。
            【解决方案11】:

            IMO,我认为这在很大程度上取决于应用程序的流程 - 当您想要获取 Membership 详细信息时,您是否有可用的 Person?如果是,请使用:
            public Membership getMembership(Person person);

            另外,我看不出Club 无法根据Person 的ID 而不是实际对象跟踪成员资格的任何原因 - 我认为这意味着您不需要实现hashCode()equals() 方法。 (尽管这始终是一个很好的最佳实践)。

            正如 Uri 所说,如果两个 Person 对象的 ID 相等,则您应该记录其减速。

            【讨论】:

              【解决方案12】:

              我通常会坚持少即是多。调用您的方法所需的信息越少越好。如果您知道 ID,则只需要 ID。

              如果你愿意,可以提供额外的重载来接受额外的参数,比如整个类。

              【讨论】:

              • 为什么要投反对票?我很好奇你为什么认为我的建议不好?
              • 简单的 ID 通常也不是类型安全的。没有必要强制只将 Person 的 ID 添加到地图中。基元没有上下文信息。我没有对你投反对票,但它不是面向对象的,也没有提供关于平等对那个类意味着什么细节的抽象。
              【解决方案13】:

              假设这是一个数据库 ID 或仅用于索引的东西(而不是像 SSN 之类的东西),那么在理想的系统中,ID 的存在是一个实现细节。

              作为实现细节,我更愿意将其隐藏在其他领域对象的接口中。因此,从根本上说,成员资格涉及个人而不是人数。

              当然,我会确保我实现了 hashCodeequals() 并详细记录了它们的含义。

              在这种情况下,我会明确说明两个 Person 对象的相等性仅根据 ID 确定。这是一个有点冒险的提议,但如果你能确保它使代码更具可读性。当我的对象是不可变的时,我觉得这样做更舒服,所以在程序的生命周期中,我实际上不会得到两个 ID 相同但名称不同的 Person 对象。

              【讨论】:

              • 单靠不变性并不能确保缺少具有不同值的多个实例。它只确保实例的属性一旦创建就不会改变。
              • @Angelo:我同意。但是,我假设所有域对象要么从数据库加载,要么创建并立即保存。
              • +1 相等和哈希码。对于更新,您更关心后备存储(数据库/缓存)中是否具有相同的“对象”,而不是对象的数据是否相同。事实上,您可能希望数据有所不同。对于插入,拥有一个具有无效 id 的“新”对象很有帮助,这样您就知道该对象是新的并且实际上需要插入。
              猜你喜欢
              • 1970-01-01
              • 2020-02-11
              • 2018-05-05
              • 2013-03-28
              • 1970-01-01
              • 1970-01-01
              • 2013-08-03
              • 2016-06-01
              • 2020-03-17
              相关资源
              最近更新 更多