如何为两个域创建通用存储库以避免添加依赖项?
How to make generic repository for two domains to avoid adding dependencies?
我正在制作一个应用程序,它有两个独立的域,其中包含代表不同概念的模型:
现在,我想创建通用存储库,它允许我对数据集进行操作,并且我希望两个 DataAccess 项目都使用它,因为 DRY。问题是,我会以使用实体 id 的方式编写这些存储库,因此我需要基本类型或至少需要允许我对这些进行操作的接口。我会使用 BaseEntity
创建通用存储库,其中包含这样的 ID:
public class GenericRepository<T> where T: BaseEntity
{
public T SomeActionThatRequiresId(int id)
{
// something that requires ID from BaseEntity
}
}
问题是我不知道放在哪里BaseEntity
。
总是有人说领域项目不应该有依赖关系,所以我不想创建单独的“基础”项目来引用,以跟上这个想法。
如果我想遵守上述规则,将它放在一个域项目中然后在其他域项目中引用它甚至都不算数。
我不知道如何正确执行此操作。
有人能帮忙吗?
在不了解您的项目细节的情况下,我们很难找到最佳解决方案。
但我会尽量给你一些选择:
- 使用 BaseEntity 定义扩展您的核心库。好处:根据小图,这似乎遵循了您的基本设计,但也意味着您的 repo 模块将依赖于核心库。
- 引入定义 BaseEntity 的新中间层。如果由于某种原因它不属于现有的 Core 库,您可以创建一个新的库,该库对两个域都是通用的并且从 Core 派生,但在中间库上存在相同的依赖性问题。
- 您不创建 BaseEntity,而是 Repo 模块以纯函数形式提供必要的接口,例如IRepoObject { getId(); } 并且您的域对象实现该接口。这具有清晰的关注点分离。 (并且域模块需要 repo 模块不是问题,因为它们现在提供 repo 功能)
- 函数式编程方法:如果 repo 模块需要的数据结构不是过于复杂,您只需将它们作为原语传递给函数调用,例如doRepoStuff(String id, Byte[] blob, ...)。因此,repo 模块对于任何特定的 类 或接口实现都是不可知的。
根据我的经验,简单的解决方案通常比具有深度继承和大量引用的复杂的面向对象设计更可取,因此我倾向于根据交换数据的复杂性使用解决方案 3 或 4。
我正在写这个答案,因为评论的字符数有限。 :)
据我了解,您并不是真的在寻找实现接口的通用方法。我认为人们对 DRY
存在普遍的误解。如果您创建两个以类似方式实现的存储库(具有与 GetById
相同的方法),这不是 DRY
。 DRY
是当您 copy-pasting
代码在同一上下文中做完全相同的事情时。
您有两个完全不同的上下文,其中一个是 Configuration
,第二个是 Core
。在那种情况下,您将打破 Common Closure Principle
,因为存储库将共享相同的界面,而它们可能具有不同的变化轴。
根据您的描述,我希望您确认您正在正确使用 UML(Core
项目正在被 DataAccess
继承,或者您想要显示参考方向? 所以 Core
被 DataAccess
引用了?)
我不知道这是否最适合您,但 Data Access Layer
应该引用 Domain Layer
。由于大多数时候 Data Access
仅用于保留 Domain
对象或检索 Domain
对象,不应被任何其他地方引用。
在那种情况下,它看起来像这样:
箭头代表什么在引用什么。在那种情况下,您将 IConfigRepo
放入 Config.Domain
项目,并且 Repository
的实施在 Config.DataAccessLayer
中完成,这可以确保您的域提供合同并知道如何允许获取数据但不知道它们是如何存储的。所以您可以将它们存储在 SQL 或 No-SQL 数据库中。
了解您的 Core
项目只是委托工作来纠正 Domain
项目。无论如何都可以完成,但您确定 Buisness logic
存储在特定的 Domain
项目中。
关于使用抽象存储库来获取记录的想法。这样做有什么好处?您确定要在 Config.DataAccess
中实现的存储库 Core.Domain
中允许多态执行吗?
不要对此提出质疑,我认为您可能高估了 DRY 作为主体的重要性,并且过早地试图定义共同的基础。 DRY 的一个主要限制是当它导致依赖性问题时,您似乎在很早的设计阶段就遇到了。
如果 Config 和 Core 确实是独立的域,那么它们的数据访问模式似乎很可能会随着时间的推移而不同。在开发的早期至少重复一次,然后等着看他们是否有分歧,这没有错。如果它们总是需要同步更改,您可以稍后将它们合并到一个通用存储库中。
同样,您的陈述:“总是有人说域项目不应在其中具有依赖性”应被视为指南,而不是硬性规定。
如果您希望所有域实体都具有公共属性,也许除了 int id 字段之外还有审计字段,那么公共基础实体库是显而易见的解决方案,不应该被排除在外。
我的回答分为两部分...
第 1 部分:将 BaseEntity 放在哪里
放在单独的独立地方。
这样各个域都可以依赖它,而不是相互依赖。
您可以在下面看到一个多层架构,其中适当分离了通常的嫌疑对象(UI、逻辑、数据访问)。他们需要分享的概念位于 Common
层中,该层位于旁边。它被大多数其他 projects/assemblies 引用,但这样做是安全的,因为 Common 中的代码相对更稳定。
例如Poco 是哑数据结构,与使用它们的(例如)逻辑代码相比,更改的可能性较小。如果 Poco 确实需要更改,很可能您已经想要更改其他层(例如,由功能更改驱动)。
因此,Common
(或一些类似的独立域的地方)你应该把你的 BaseEntity
.
第 2 部分:您是否解决了正确的问题?
我的感觉(阅读此主题的其他人也有同感)是您 可能 关注了错误的问题。如果是这样的话——没关系。这很容易做到,你不应该为此自责——或者更糟的是,陷入 sunk cost fallacy.
问题是,一旦您拥有一个被广泛使用的实现(例如您的通用存储库),它就会变得受限。使用基础 类 和继承(尤其是与接口对比时)是这个普遍问题的一个很好的例子。
在架构层面,可以通过几种方式实现重用,一种是通过模式。在您的情况下,您需要做的就是确定一种模式(或模式组合),在满足您的需求和优先级(例如代码可维护性、满足功能需求的能力、灵活性 - 等等)方面非常适合您。
重用这些模式意味着您具有一致性和清晰度,您可能有重复代码这一事实并不需要担心,因为模式的好处超过了重复代码的任何负面影响。
然后是 Single Responsibility Principle (SRP)。拥有一个单一的通用回购协议会破坏 SRP,因为它试图跨所有领域工作——每个领域都有不同的设计和需求力量,以不同的方式处理它们,这将与通用回购理念相冲突。
通用存储库看起来只做一件事(数据访问)——但那一件事通常太大太复杂。说通用回购是 'doing SRP' 是一种错觉。
我自己尝试了类似的路径(并在项目中观察了它们),您会发现数据访问 边缘情况 通用回购想法会成为阻碍- 例如当您有跨域活动时,尤其是当这些活动的运行时性能需要特别高时。
DRY 的使用很好,但它只是众多工具中的一种,并不是它本身的目的,它的使用需要与其他考虑因素相平衡。
我正在制作一个应用程序,它有两个独立的域,其中包含代表不同概念的模型:
现在,我想创建通用存储库,它允许我对数据集进行操作,并且我希望两个 DataAccess 项目都使用它,因为 DRY。问题是,我会以使用实体 id 的方式编写这些存储库,因此我需要基本类型或至少需要允许我对这些进行操作的接口。我会使用 BaseEntity
创建通用存储库,其中包含这样的 ID:
public class GenericRepository<T> where T: BaseEntity
{
public T SomeActionThatRequiresId(int id)
{
// something that requires ID from BaseEntity
}
}
问题是我不知道放在哪里BaseEntity
。
总是有人说领域项目不应该有依赖关系,所以我不想创建单独的“基础”项目来引用,以跟上这个想法。
如果我想遵守上述规则,将它放在一个域项目中然后在其他域项目中引用它甚至都不算数。
我不知道如何正确执行此操作。
有人能帮忙吗?
在不了解您的项目细节的情况下,我们很难找到最佳解决方案。
但我会尽量给你一些选择:
- 使用 BaseEntity 定义扩展您的核心库。好处:根据小图,这似乎遵循了您的基本设计,但也意味着您的 repo 模块将依赖于核心库。
- 引入定义 BaseEntity 的新中间层。如果由于某种原因它不属于现有的 Core 库,您可以创建一个新的库,该库对两个域都是通用的并且从 Core 派生,但在中间库上存在相同的依赖性问题。
- 您不创建 BaseEntity,而是 Repo 模块以纯函数形式提供必要的接口,例如IRepoObject { getId(); } 并且您的域对象实现该接口。这具有清晰的关注点分离。 (并且域模块需要 repo 模块不是问题,因为它们现在提供 repo 功能)
- 函数式编程方法:如果 repo 模块需要的数据结构不是过于复杂,您只需将它们作为原语传递给函数调用,例如doRepoStuff(String id, Byte[] blob, ...)。因此,repo 模块对于任何特定的 类 或接口实现都是不可知的。
根据我的经验,简单的解决方案通常比具有深度继承和大量引用的复杂的面向对象设计更可取,因此我倾向于根据交换数据的复杂性使用解决方案 3 或 4。
我正在写这个答案,因为评论的字符数有限。 :)
据我了解,您并不是真的在寻找实现接口的通用方法。我认为人们对 DRY
存在普遍的误解。如果您创建两个以类似方式实现的存储库(具有与 GetById
相同的方法),这不是 DRY
。 DRY
是当您 copy-pasting
代码在同一上下文中做完全相同的事情时。
您有两个完全不同的上下文,其中一个是 Configuration
,第二个是 Core
。在那种情况下,您将打破 Common Closure Principle
,因为存储库将共享相同的界面,而它们可能具有不同的变化轴。
根据您的描述,我希望您确认您正在正确使用 UML(Core
项目正在被 DataAccess
继承,或者您想要显示参考方向? 所以 Core
被 DataAccess
引用了?)
我不知道这是否最适合您,但 Data Access Layer
应该引用 Domain Layer
。由于大多数时候 Data Access
仅用于保留 Domain
对象或检索 Domain
对象,不应被任何其他地方引用。
在那种情况下,它看起来像这样:
箭头代表什么在引用什么。在那种情况下,您将 IConfigRepo
放入 Config.Domain
项目,并且 Repository
的实施在 Config.DataAccessLayer
中完成,这可以确保您的域提供合同并知道如何允许获取数据但不知道它们是如何存储的。所以您可以将它们存储在 SQL 或 No-SQL 数据库中。
了解您的 Core
项目只是委托工作来纠正 Domain
项目。无论如何都可以完成,但您确定 Buisness logic
存储在特定的 Domain
项目中。
关于使用抽象存储库来获取记录的想法。这样做有什么好处?您确定要在 Config.DataAccess
中实现的存储库 Core.Domain
中允许多态执行吗?
不要对此提出质疑,我认为您可能高估了 DRY 作为主体的重要性,并且过早地试图定义共同的基础。 DRY 的一个主要限制是当它导致依赖性问题时,您似乎在很早的设计阶段就遇到了。
如果 Config 和 Core 确实是独立的域,那么它们的数据访问模式似乎很可能会随着时间的推移而不同。在开发的早期至少重复一次,然后等着看他们是否有分歧,这没有错。如果它们总是需要同步更改,您可以稍后将它们合并到一个通用存储库中。
同样,您的陈述:“总是有人说域项目不应在其中具有依赖性”应被视为指南,而不是硬性规定。
如果您希望所有域实体都具有公共属性,也许除了 int id 字段之外还有审计字段,那么公共基础实体库是显而易见的解决方案,不应该被排除在外。
我的回答分为两部分...
第 1 部分:将 BaseEntity 放在哪里
放在单独的独立地方。
这样各个域都可以依赖它,而不是相互依赖。
您可以在下面看到一个多层架构,其中适当分离了通常的嫌疑对象(UI、逻辑、数据访问)。他们需要分享的概念位于 Common
层中,该层位于旁边。它被大多数其他 projects/assemblies 引用,但这样做是安全的,因为 Common 中的代码相对更稳定。
例如Poco 是哑数据结构,与使用它们的(例如)逻辑代码相比,更改的可能性较小。如果 Poco 确实需要更改,很可能您已经想要更改其他层(例如,由功能更改驱动)。
因此,Common
(或一些类似的独立域的地方)你应该把你的 BaseEntity
.
第 2 部分:您是否解决了正确的问题?
我的感觉(阅读此主题的其他人也有同感)是您 可能 关注了错误的问题。如果是这样的话——没关系。这很容易做到,你不应该为此自责——或者更糟的是,陷入 sunk cost fallacy.
问题是,一旦您拥有一个被广泛使用的实现(例如您的通用存储库),它就会变得受限。使用基础 类 和继承(尤其是与接口对比时)是这个普遍问题的一个很好的例子。
在架构层面,可以通过几种方式实现重用,一种是通过模式。在您的情况下,您需要做的就是确定一种模式(或模式组合),在满足您的需求和优先级(例如代码可维护性、满足功能需求的能力、灵活性 - 等等)方面非常适合您。
重用这些模式意味着您具有一致性和清晰度,您可能有重复代码这一事实并不需要担心,因为模式的好处超过了重复代码的任何负面影响。
然后是 Single Responsibility Principle (SRP)。拥有一个单一的通用回购协议会破坏 SRP,因为它试图跨所有领域工作——每个领域都有不同的设计和需求力量,以不同的方式处理它们,这将与通用回购理念相冲突。
通用存储库看起来只做一件事(数据访问)——但那一件事通常太大太复杂。说通用回购是 'doing SRP' 是一种错觉。
我自己尝试了类似的路径(并在项目中观察了它们),您会发现数据访问 边缘情况 通用回购想法会成为阻碍- 例如当您有跨域活动时,尤其是当这些活动的运行时性能需要特别高时。
DRY 的使用很好,但它只是众多工具中的一种,并不是它本身的目的,它的使用需要与其他考虑因素相平衡。