复杂实体转map
在使用我们的一种内部工具时,我决定做一个很小的包含而不遵循我自己的建议。 我们正在构建一个迷你CRM工具,最初的要求是:
- 维护有关我们正在处理的公司的信息;
- 维护每个公司的联系人列表;
- 维护每个公司的参与清单(项目,培训,咨询)。
注意:为了使本文简单,我将省略代码,属性等的详细信息。
从小开始,在为公司建立CRUD的同时,我得到了一个公司实体,看起来像这样:
class Company {
+ id: CompanyId
+ name: String
}
一切都很好。 然后,我需要编写代码以维护每个公司的联系人列表。 我得出以下结论:
class Contact {
+ id: ContactId;
+ companyId: CompanyId;
+ name: String
+ email: String;
}
class Company {
+ id: CompanyId;
+ name: String;
+ contacts: List[Contact];
}
那是问题的开始。 对于“查看公司”页面,我需要显示与公司及其所有联系人有关的数据。 对于仅处理公司数据的页面(列出所有公司的页面,用于编辑/删除公司的页面),我不需要联系信息。 每次我加载公司时都应该加载联系人吗? 我不应该加载它们吗? 在某些情况下不加载联系人的问题是,随着代码的发展,我不知道Company内部的联系人列表是否为空,因为该公司没有联系人或未加载他们。 令人困惑。 由于在此应用程序中性能不是问题,因此我决定每次需要公司时都加载联系人列表。 问题解决了。
在下一个功能中,我必须保持公司的参与度(CRUD)。 遵循与联系人相同的方法,最终得到以下实体:
class Engagement {
+ id: EngagementId;
+ companyId: CompanyId;
+ name: String
+ startDate: Date;
+ endDate: Date;
+ description: String;
}
class Company {
+ id: CompanyId;
+ name: String;
+ contacts: List[Contact];
+ engagements: List[Engagement];
}
在这一点上,事情变得非常混乱。 我的页面需要公司及其联系方式和参与度。 仅需要公司和参与的页面,仅需要公司和联系的页面。 但是问题不仅与加载内容和加载位置有关。 我有很多依赖于Company结构的代码。
该应用程序是一个Web应用程序,其前端使用AngularJS,而JSON则进入浏览器并返回到应用程序中。 为此,我有JSON转换器,可以将JSON与对象之间进行转换。 我还对API和内部层进行了大量测试,这些测试将使用构建器来组装数据。 总之,为了满足所有功能,有很多代码将依赖于Company实体的结构。 此代码“必须知道”何时加载和不加载联系人和参与。 当然,在我们决定每页需要多少信息时,这种情况一直在变化。
随着功能的稳定以及我在代码中进行了一些其他更改,一切正常。
涟漪效应
当我们认为准备开始构建其他功能(仪表板,财务信息,预测,注释,提醒,跟进操作等)时,我们意识到我们错过了一些重要的东西。
我们的某些项目来自合作伙伴(其他公司)。 这意味着一项业务可能涉及多个公司。 这可能会使“公司”与“参与”之间的关系有所不同。 也许公司与敬业度之间的关系不再是一对多的了。 这可能是很多很多 。
class Engagement {
+ id: EngagementId;
+ companies: List[Company];
+ name: String
+ startDate: Date;
+ endDate: Date;
+ description: String;
}
class Company {
+ id: CompanyId;
+ name: String;
+ contacts: List[Contact];
+ engagements: List[Engagement];
}
我以为这是一个容易的更改,但是我很惊讶地看到它在我的代码中产生了巨大的连锁React。 测试数据,构建器,JSON解析器和API结构的负载会受到影响,这并不是一种好感觉。 老实说,我对自己感到很失望,很生气。
遵循我自己的建议
几年前,我遇到了CQS ,后来遇到了CQRS 。 刚开始时,我并没有给予CQS太多的关注,而只有CQRS才使我真正理解了另一种设计软件的方式。 从那时起,我一直坚决主张将用于写入的数据结构与用于读取的数据结构分开(是的,我将实体视为数据结构)。 我不是在谈论可独立部署的读/写模型,不同的数据库,事件,消息等。我只是在谈论使用不同的对象来读写数据。
解决问题(第一种解决方案)
经过另一次讨论,我们认为参与始终是公司的事,但它可能是通过合作伙伴来进行的。 那又改变了事情。 因此,我决定执行以下操作:从所有实体中删除所有依赖项(包含其他实体或实体列表的属性)。 然后像这样结束:
class Company {
+ id: CompanyId
+ name: String
}
class Contact {
+ id: ContactId;
+ companyId: CompanyId;
+ name: String
+ email: String;
}
class Engagement {
+ id: EngagementId;
+ companyId: CompanyId;
+ partnerId: Optional[CompanyId];
+ name: String
+ startDate: Date;
+ endDate: Date;
+ description: String;
}
通过这种方法,实体将仅包含需要持久化的数据。
但是我仍然有需要解决的查询,其中许多查询将需要这些实体的组合。 为此,我创建了“读取”对象,该对象将完全包含每个查询所需的数据。 其中一些看起来像这样:
class CompanyWithContacts {
+ company: Company;
+ contacts: List[Contacts]
}
class CompanyWithContactsAndEngagements {
+ company: Company;
+ contacts: List[Contacts];
+ engagements: List[Engagements];
}
class EngagementWithCompanies {
+ engagement: Engagement;
+ client: Company;
+ partner: Optional[Company];
}
使用这种方法,每个查询将返回传递机制(网站上的页面)所请求的数据组合。 我的实体之间相互关系的更改不再引起更改的连锁React,因为只有特定的查询才会中断。 懒加载/渴望获取不再有问题。 毫无疑问,因为没有属性了,所以没有空属性。 可选的那些可以很容易地标记为可选(感谢Java 8)。
解决问题(第二个解决方案)
经过上述修复后,我感到很高兴,因为当实体关系发生更改时,我能够本地化更改。 但是还有更多的东西。 从积极的方面来说,他们使我可以从需要数据组合的页面中进行单个呼叫。 消极的一面,性能并不是我真正关心的问题,我也不希望这些多余的对象带有奇怪的名字。 我仍然必须编写代码来填充它们并将它们转换为JSON。
然后,我决定从我的页面打多个电话。 如果页面需要公司,联系人列表和与该公司相关的业务列表,我将在页面中打三个电话。 这个决定使所有“读取”对象消失了,并且仍然使我的代码非常简单。
结论
使您的实体彼此分离,并专注于从客户端实施简单查询。 如果性能确实被证明是一个问题,只需移至单个查询即可。
不要使用ORM。 ORM使我的更改更加糟糕,因为我必须保持实体和数据库同步。 可以自由使用所需的任何查询从数据库中获取记录集,并以所需的方式填充对象,这非常好。
查询数据更改的方式比持久化数据的方式要频繁得多,并且这些更改可以采用许多不同的方式对数据进行切片和分割。 将您的实体绑定在一起只会使满足所有查询变得更加困难,并且只会给假定运行业务逻辑和存储数据的代码带来不必要的压力。
翻译自: https://www.javacodegeeks.com/2015/08/increasing-complexity-one-entity-at-a-time.html
复杂实体转map