在N层架构中实现数据库功能对象?

5
我正在为我们的网站添加功能,使用MSMQ异步执行长时间运行的进程。 然而,这样做意味着我们需要在请求完成时通知用户。 我使用命令模式创建了一个名为INotify的接口*,并将其组合到消息类中,因此消息处理类可以简单地在消息的INotify对象上调用GiveNotice()。 第一种实现EmailNotify比预期更困难,因为我惊讶地发现MailMessage不可序列化,但最终解决了这个问题。
现在我正在开发一个新的具体通知程序DBNotify,它将调用某种存储过程并更新主事务数据库中的状态。 我遇到的问题是,我希望重用我们已经创建的DAL架构,但INotify是Model项目的成员,而该项目比DAL更基础。
我们的层次结构如下所示: Common> Model> DAL> BAL
以下是有关层的更多详细信息。请记住,我从下面继承了这个层次结构: Common负责所有“实用”功能,这些功能在应用程序的许多地方使用,例如访问配置设置,解析字符串,与业务无关的功能。
Model是商业对象,有些人称之为数据传输对象,即获取器和设置器的集合。 我在这个层次上添加了一些“智能”,但只有该对象内部的业务规则,例如“项目名称必须以字母数字字符开头”。
DAL是数据访问层,理论上,所有在此处发生的事情都是将模型对象移入和移出数据库。
BAL是业务层; 理论上,执行管理对象交互的业务规则(即,“表单必须至少具有两个项目。”)。
因此,INotify接口被定义为允许通知方法独立变化(即电子邮件,TXT,twitter等)的抽象。它对系统很重要,所以我在Model层创建了它,该层与DAL层无关。然而,我正在创建一个新的INotify具体实现,其通知方法是调用数据库中的SP。
有人处理过旨在与数据库交互的业务对象吗?您如何安排在N层架构中?
在你告诉我使用Linq to Sql之前,非常感谢。这不是技术问题(我该怎么做),而是设计问题(我应该怎么做)。
我认为有一个StackExchange网站更专注于这些语言无关的设计问题,所以我打算将它复制到那里。

仅在名称上使用接口,因为我实际上想要序列化这些对象,所以我不得不将其设为抽象类。 - Michael Blackburn
1
我对你的层次结构感到困惑。Model和BAL分别负责什么?我认为BAL代表Business Access Layer,但是Model似乎不太合适。在代码中,我看到Model通常是最抽象的,位于(使用)或是业务层的一部分...此外,如果Model比DAL更基础,那么DAL使用Model的类并调用其方法有什么问题呢? - Marjan Venema
哦,如果你不知道的话,你不需要在注释中添加信息,你可以直接编辑你的问题... - Marjan Venema
6个回答

3
也许并不是你问题的答案,但是仍有值得思考的地方。
我对于你在组件层次结构中放置数据访问的位置持有异议。我不会把它放在两个功能域层之间,甚至不会放在单个领域模型类的“上面”。数据访问或持久性不是任何领域类的关注点。它应该只是可以对它们进行操作的东西,而不是它们所做的事情。
尽管我最初编写了像TClient.Save和TClient.Load这样的代码,但现在我得出的结论是并不是客户端决定需要保存,而是用户交互决定何时需要加载领域实例的数据,以及是否应该持久化客户端的数据。因此,我现在支持在GUI中(更具体地说是控制器中)编写DataStore.Load(ClientInstance)和DataStore.Save(ClientInstance)等内容。然后由数据访问层来确定如何执行此操作。它可以使用C#中的反射或Delphi中的新RTTI来迭代所有客户端属性,以便将它们发送到数据库。
虽然分层是一个非常好的概念,可以分离关注点,并通过简单地遵循“您可以向下调用但不能向上调用”来防止将东西放置在各个地方,但是当涉及日志记录、异常处理、通知和所有其他有趣的交叉关注点时,它并没有帮助太多。
此外,由于公共层是一个实用程序层,因此确实应该对所有其他层可访问。
为了将所有内容放在一张图片中(其中我保留了你在简单领域类、模型和跨类业务规则之间所做的区分):
+---+   +-------------+
| C |<--| Data Access |<--------------------------+
| o |   +-------------+                           |
| m |         |                                   |
| m |         |                                   |
| o |         v                                   |
| n |   +-------------+   +----------------+   +-----+
|   |<--| Model       +<--| Cross class    |<--| GUI |
|   |   +-------------+   | business rules |   |     |
|   |                     |                |   |     |
|   |<--------------------|                |   |     |
|   |                     +----------------+   |     |
|   |                                          |     |
|   |<-----------------------------------------|     |
+---+                                          +-----+

当前调用数据库的INotify实现位于模型中,如上图所示,它并不直接调用数据访问层,而是被数据访问层调用或者说被查询。
问题在于,INotify应该放在“模型”中,即域层的一部分,还是应该作为通用接口,并且应该有一个单独的“通知”层/组件,可以从域和GUI访问。这个新组件不仅可以关注通知,还可以关注许多其他交叉问题,例如日志记录。它可以访问公共(当然)和数据访问组件以及GUI,至少以某种回调方式。
在下面的图片中,我试图将其可视化,但我不太擅长可视化,并且总是遇到那些讨厌的交叉问题。这就是为什么从域层到交叉问题没有调用箭头,尽管域层当然应该能够访问例如“Logger”接口。也许我试图过于区分公共和交叉组件,可以提出将它们合并,并将它们可视化为“实用程序”层/组件内的单独块。
        +--------------------------------------------+
  +-----| Cross cutting concerns                     |
  |     +--------------------------------------------+
  v           v^                                    ^
+---+   +-------------+                             |
| C |<--| Data Access |<--------------------------+ |
| o |   +-------------+                           | |
| m |         |                                   | |
| m |         |                                   | |
| o |         v                                   | v
| n |   +-------------+   +----------------+   +-----+
|   |<--| Model       +<--| Cross class    |<--| GUI |
|   |   +-------------+   | business rules |   |     |
|   |                     |                |   |     |
|   |<--------------------|                |   |     |
|   |                     +----------------+   |     |
|   |                                          |     |
|   |<-----------------------------------------|     |
+---+                                          +-----+

2
只是因为你的 Model 层中有 INotify 接口并不意味着所有具体实现都需要在那里。它应该是一个接口,接口的目的是为了实现抽象化,而不是基类,基类的目的是为了实现共享功能。所以,无论在哪个层次上有这种类型的属性或参数,都应该声明为 INotify。在你的 BAL(Business Logic Layer) 中,你可以决定要使用哪种具体类型来处理 INotify 的实例。根据通知的复杂程度,你可以在 BLL 中定义具体实现,并让它使用 DAL 中的帮助类来执行对存储过程的调用,或者你可以直接在 DAL 中将其定义为一个类,因为它与数据库交互;这真的是一个基于类的责任判断,无论哪种方式,它都应该在顶层可访问。
“有人处理过一个业务对象吗?它的目的是与数据库交互,你如何将其放置在你的 N 层架构中?”
按照你项目的结构方式,每个层的逻辑职责应该如下:
Common: 没有依赖于项目中其他任何东西的共享工具方法
Model: 定义系统中实体的结构,也称为 DTO 或数据传输对象,这意味着它们可以在层之间传输。它们只是存储数据并执行基本验证。
DAL: 负责从 Model 层创建类的实例,并根据存储在存储库(如数据库)中的值设置属性。还负责跟踪 Model 实体的更改,并将这些更改保存(持久化)回存储库。
BAL/BLL: 使用其他层中定义的类来实现有用的功能,并验证是否遵循业务要求。
你可以使用各种技术来实现这一点,甚至可以使用相同的技术,但你的具体实现将取决于你的工作方式。像 Linq2Sql 或 Entity Framework 等技术会模糊你的 Model 和 DAL 之间的界限;它们希望在同一个项目中定义两者。但是,Entity Framework 更灵活,通过一些工作,你可以将实体模型和“上下文”(使用 Entity Framework 术语)的定义分离成不同的项目。你可以编辑 T4 模板或在网上找到可以从实体模型生成实体和上下文类定义以支持 Repository 和 Unit of Work 设计模式的模板,其中你永远不会直接引用实体上下文,而是让它实现 IRepository 接口,这使得你的代码更易于测试。我个人没有使用过 NHibernate,但我的理解是,它能够做到同样的事情(并且可能当前做得更好)。

不错的努力,这里有一个建议:SO上的答案往往都是简短明了的。要么尝试缩短它,要么将其分成更多段落并添加标题。 - jgauffin
INotify在功能上是一个接口,但你不能序列化一个接口,所以我将它变成了一个抽象类。这限制了灵活性(如果它是一个接口,我可以从其他类继承并实现INotify)。 - Michael Blackburn
是的,BAL == BLL。只是使用我继承的名称。 - Michael Blackburn

1
如果您的模型类是您的DTO(有些人可能称之为数据结构或数据类型),它们应该(很可能)横跨其他层并为所有层所知。
根据您所说的,您可能有一个MessageProcessing类,它位于BAL中,并从BAL或DAL的其他部分接收消息,然后通知任何正在监听的人(UI或BAL的其他感兴趣的成员)。

消息进入MSMQ并由作为服务运行的代码处理,因此在Web应用程序中创建MessageProcessing类没有帮助。但是你的答案给了我一个想法。 - Michael Blackburn

1

如果你的数据实体是POCOs,那么你可以在项目中使用它们。否则,我会像你所做的那样创建单独的模型。但请将它们放在一个单独的程序集中(而不是DataAccess项目中)。

在我看来,人们过度使用层次结构。大多数应用程序并不需要很多层次。我的当前客户为他们所有的应用程序都采用了类似于你的架构。问题是,只有数据访问层和表示层中有逻辑,其他层只是从下一层获取数据,对其进行转换,并将其发送到上一层。

我首先告诉他们放弃所有层次,改用类似于以下结构的方式(需要IoC容器):

  • 核心(包含业务规则和通过ORM进行数据访问)
  • 规范(分离接口模式。包含服务接口和模型)
  • 用户界面(可能是Web服务、WinForms或Web应用程序)

这对大多数应用程序都适用。如果你发现核心变得越来越庞大,难以处理,你可以将其拆分,而不影响任何用户界面。

你已经在使用ORM,是否考虑过使用验证模块(FluentValidation或DataAnnotations)进行验证?这样可以轻松地在所有层次中验证你的模型。


0

使用在多个层次中的类让我感到担忧。

特别是当它们还与数据模型/基础/层绑定时。

一旦这些类发生变化,你可能会遇到所有层次的重新编码。换句话说,你缺少抽象的有益作用。

话虽如此,维护转换代码(从层到层)也不是很有趣,但总体上工作量较小。

一个折中的解决方案可能是使用接口/角色:为每个层定义对象应该扮演的接口/角色,并使用该接口传递到层中。然后,一个(共享的)类应该实现一个角色(或者多个)。这将提供一个更松散耦合的系统。

我从这个关于DCI(数据,协作和交互)的精彩讲座中学到了很多。


0

感谢大家的建议,我计划实施一些改进措施,虽然没有一个直接回答我所提出的问题。

我将这个问题发布到了Programmers上,那里可能更适合这种问题,并得到了一些有用的想法。如果您感兴趣,可以在这里查看帖子:Programmers thread on this issue。不可否认的是,当我发布帖子时,我添加了基于自己研究的依赖注入的“提示”,因此问题可能更清晰明了。

这是一个伟大而有用的社区,我很自豪能够参与其中。


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