使用领域对象作为键是一个好的实践吗?

17

在使用映射(或“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,因为“人员”(正如您可能意识到的,我的实际领域与人和俱乐部无关...)实例很容易获得,但现在它在哈希映射中内部存储ID - 但至少我在接口中正确地公开它。

2
避免将您的“私有”部分暴露给其他实体。 - Dave Jarvis
@DaveJarvis 人生的箴言。 - Random Human
13个回答

5
不要使用id,这是一个坏主意的所有原因。你会把自己锁在设计中。让我给你举个例子。现在你将成员身份定义为俱乐部与人之间的映射。正确的做法是,你的成员应该是俱乐部和“成员”的映射,但你假设所有的成员都是人,并且由于所有人的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()方法! - waxwing

4
假设这是一个仅用于索引(而不是像社会安全号码一样的东西)的数据库ID,那么在理想情况下,ID的存在是实现细节。
作为一个实现细节,我希望将其隐藏在其他域对象的接口中。因此,成员资格基本上涉及个人而不是数字。
当然,我会确保我实现了hashCode和equals()并很好地记录它们的含义。
在这种情况下,我会明确说明两个Person对象的相等性仅基于ID确定。这有点冒险,但如果你能确保它,可以使代码更易读。我感到更舒服的是在我的对象是不可变的时候做出这个决定,所以在程序的生命周期内,我不会真正得到具有相同ID但不同名称的两个Person对象。

仅靠不可变性并不能确保没有具有不同值的多个实例。它只能确保一旦创建,实例的属性将不会更改。 - Angelo Genovese
@Angelo:我同意。不过,我假设所有的领域对象都是从数据库加载或者创建后立即保存的。 - Uri
在相等性和哈希码上加1。对于更新,您更关心是否具有来自后备存储(数据库/缓存)的相同“对象”,而不是对象的数据是否相同。实际上,您可能希望数据不同。对于插入,拥有一个具有无效ID的“新”对象很有帮助,这样您就知道该对象是新的并且实际上需要插入。 - AngerClown

4
我认为第一种情况更“纯粹”,因为getMembership方法可能需要比id更具体的个人数据(假设您不知道getMembership方法的内部,尽管这没有太大意义,因为它很可能在同一领域内)。如果实际上需要来自Person实体的数据,则不需要DAO或工厂来处理相关人员。如果您的语言和/或ORM允许使用代理对象(并且您有方便的方法来创建这些代理),则可以轻松调用此方法。但是,说实话。如果您正在查询某个人的成员资格,那么在调用此方法时,您很可能已经拥有该Person实例的内存。在“基础架构领域”的后续发展中,Uri在我撰写此答案时已经提到了实现细节的概念(该怎么办,这家伙真快!)。具体而言,如果您决定该“Person”概念在底层数据库中具有复合主键/标识符,那么现在会使用标识符类吗?也许使用我们谈论的代理?

使用ID在短期内确实更容易,但如果您已经使用了可靠的ORM,则没有理由不使用代理或其他方式来表达实体的面向对象标识,而不泄露实现细节。


1
你如何获取一个人?在某些时候,您可能需要一个getPerson(int personId)函数[而非getPerson(String name)]。这并不是反对“纯粹”的投票,只是一个提示:难以避免地要在某处暴露实现细节。 - AngerClown
正如我所说,你很可能拥有某种代理提供者或注入的对象来作为其依赖项。在我看来,这已经超出了这个问题的范围。 - Denis 'Alpheus' Cahuk

3
如果你真正在实践面向对象设计,那么你需要运用信息隐藏的思想。一旦你开始在成员资格对象的公共接口方法中悬挂人员对象的内部字段类型,你就开始强制外部开发人员(用户)学习关于人员对象的各种信息,以及它是如何存储的,以及它有什么样的ID。
更好的做法是,由于一个人可以拥有会员资格,为什么不将“getMemberships”方法挂载到person类上呢?询问一个人他们拥有哪些会员资格似乎比询问“membership”一个给定的人可能属于哪个俱乐部更合理...
编辑-由于OP已经更新了他感兴趣的是会员本身,而不仅仅是作为Person和Club之间的关系,我更新了我的答案。
长话短说,“Club”类你现在要定义,你现在要求它表现为“club roster”。俱乐部有一个名单,它不是名单。花名册可以有几个特点,包括通过加入日期等方式查找属于俱乐部的人员。除了按俱乐部ID查找人员之外,您可能还想按SSN、姓名、加入日期等查找他们。对我来说,这意味着类“Club”上有一个名为getRoster()的方法,它返回一个数据结构,可以查找俱乐部中的所有人员。称之为集合。问题变成了,你是否可以使用预定义的集合类方法来满足你到目前为止定义的需求,或者你需要创建自定义集合子类来提供适当的方法来查找会员记录。
由于您的类层次结构很可能由数据库支持,并且您可能正在谈论从数据库加载信息,并且不一定想要获取整个集合只是为了获取一个会员资格,因此您可能需要创建一个新类。这个类可以像我说的那样叫做“Roster”。您将从“club”类的getRoster()调用中获取它的实例。您将根据任何关于人的“公开可用”的搜索标准向该类添加“搜索”方法。姓名、俱乐部ID、人员ID、SSN等...
我的原始答案仅适用于“membership”纯粹是指示哪些俱乐部属于哪些人的关系的情况。

准确地说,不要让你的类的用户必须理解键是什么,ID是什么等等。如果会员逻辑上属于一个人,那么这就是你的方法接口应该是什么。 - matt b
可能更适合于Person类。但是假设一个人通常是数百个俱乐部的成员,那么你希望在Person类上有一个getMembership(Club club)方法,然后你就会遇到同样的问题,只不过是相反的。 - waxwing
我现在明白你的问题所在了。会适当修改回答。 - Zak
如果你在Person对象中添加“getMemberships”,那么你的Person类现在与用于会员资料的数据源耦合了。 - Frank Schwieterman
这是一个不错的想法,但并不准确 - 使用面向对象的语言特性并不能使您的代码“更加面向对象”。 - Frank Schwieterman
@Frank:关于在Person中添加getMemberships方法...什么?!关于使用面向对象的特性使其“更加面向对象”,好吧,无论你想怎么称呼它,就叫它那个。重点是你应该提供一个清晰的接口并隐藏实现细节。 - Zak

1

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

我在一个系统上工作,我们有一些旧代码使用int表示交易ID。猜猜怎么着?因为我们使用了int,我们开始用尽了ID。

更改为long或BigNumber变得棘手,因为人们在命名方面变得非常有创意。有些人使用了

int tranNum

一些已使用的

int transactionNumber

一些使用过的

int trannNum

有些人真的很有创意...

这是一团糟,整理起来真是噩梦。最终我不得不手动浏览所有代码,并将其转换为TransactionNumber对象。

尽可能隐藏细节。


1

在我看来,这非常取决于应用程序的流程 - 当您想要获取Membership详细信息时,是否有Person可用? 如果是,请使用:
public Membership getMembership(Person person);

此外,我不认为Club不能基于Person的ID而不是实际对象跟踪会员资格 - 我认为这意味着您不需要实现hashCode()equals()方法。(虽然这始终是一个很好的最佳实践)。

正如Uri所说,您应该记录声明,如果两个Person对象的ID相等,则它们相等。


1
哇,慢下来。 getMembership() 方法不属于 Club。它属于所有会员的集合,而您尚未实现该集合。

1

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

如果我有一个ID,获取Person需要多少工作量?可能很“容易”,但它确实需要访问数据存储,这是缓慢的...

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


0

实际上,我会通过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;
}

0

在编程中,我通常奉行简约至上的原则。调用方法所需的信息越少越好。如果您知道ID,则只需要ID。

如果需要,可以提供接受额外参数(例如整个类)的重载方法。


为什么要点踩?我很好奇你认为我的建议有何不妥之处? - Nate
一个简单的ID通常也不是类型安全的。没有任何强制规定只有Person的ID被添加到映射中。原始类型没有上下文信息。我没有给你点踩,但它不像面向对象,并且它没有为该类提供相等性意义的细节抽象。 - Matt H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接