企业对象/数据库访问层的架构设计

7
出于各种原因,我们正在编写一个新的业务对象/数据存储库。该层的要求之一是将业务规则的逻辑与实际数据存储层分离。
可以有多个数据存储层来实现对同一个对象的访问 - 例如,一个主要的“数据库”数据存储源来实现大多数对象,另一个“ldap”源来实现用户对象。在这种情况下,用户可以选择来自LDAP源,可能具有略微不同的功能(例如,无法保存/更新用户对象),但否则应用程序将以相同的方式使用它。另一种数据存储类型可能是Web服务或外部数据库。
我们正在考虑两种主要的实现方式,我和我的同事在基本层面上存在分歧,希望能够得到一些建议,哪种方法最好使用。我将尽力保持对每种方法的描述中立,因为我正在寻找一些客观的观点。
  • 业务对象是基类,数据存储对象继承自业务对象。客户端代码处理数据存储对象。

    在这种情况下,每个数据存储对象都继承了通用的业务规则,直接由客户端代码使用数据存储对象。这意味着客户端代码确定为给定对象使用哪种数据存储方法,因为它必须显式声明一个该类型的对象实例。客户端代码需要显式地知道每种使用的数据存储类型的连接信息。

    如果数据存储层为给定对象实现了不同的功能,则客户端代码在编译时显式知道它,因为对象看起来不同。如果更改了数据存储方法,则必须更新客户端代码。

  • 业务对象封装数据存储对象。

    在这种情况下,业务对象直接由客户端应用程序使用。客户端应用程序将基本连接信息传递到业务层。关于给定对象使用哪种数据存储方法的决策由业务对象代码进行。连接信息将是从配置文件中获取的一块数据(客户端应用程序实际上并不知道/关心其详细信息),其中可能是一个数据库的单个连接字符串或多个不同数据存储类型的连接字符串的几个部分。还可以从另一个位置读取其他数据存储连接类型,例如指定各种 Web 服务的 URL 的配置表中。

    这里的好处是,如果为现有对象添加了新的数据存储方法,则可以在运行时设置配置设置以确定使用哪种方法,并且对客户端应用程序完全透明。如果给定对象的数据存储方法发生更改,则不需要修改客户端应用程序。

  • 业务对象是基类,数据源对象继承自业务对象。客户端代码主要处理基类。

    这与第一种方法类似,但客户端代码声明基本业务对象类型的变量,并且业务对象上的 Load()/Create()/etc 静态方法返回相应的数据源类型的对象。

    这种解决方案的架构类似于第一种方法,但主要区别在于关于为给定业务对象使用哪个数据存储对象的决策由业务层而不是客户端代码进行。

我知道已经存在提供某些功能的 ORM 库,但请暂时忽略它们(可能使用其中一个 ORM 库实现数据存储层)-还请注意,我故意没有告诉您正在使用的语言,除了它是强类型的。

我在寻求一些通用建议,关于哪种方法更好使用(或随意提出其他建议),以及为什么。

7个回答

12

我建议另一个替代方案,可能具有更好的解耦性:业务对象使用数据对象,而数据对象实现存储对象。这样可以将业务规则保留在业务对象中,但不依赖于存储源或格式,同时允许数据对象支持所需的任何操作,包括动态更改存储对象(例如用于在线/离线操作)。

这属于上述第二类(业务对象封装数据存储对象),但更清晰地分离了数据语义和存储机制。


1

你也可以使用门面模式来避免客户端直接调用业务逻辑,同时它还可以创建通用的业务入口。

正如所说,你的业务应该只暴露DTO和门面。

是的,你的客户端可以处理DTO。这是在应用程序中传递数据的理想方式。


1
我通常更喜欢“业务对象封装数据对象/存储”最佳实践。然而,在短期内,您可能会发现数据对象和业务对象之间存在高度冗余,这似乎不值得。特别是如果您选择ORM作为数据访问层(DAL)的基础,情况就更加如此。但是,从长远来看,真正的回报在于应用程序生命周期。正如所示,“数据”通常来自一个或多个存储子系统(不限于RDBMS),特别是随着云计算的出现,并且在分布式系统中通常是这种情况。例如,您可能有一些数据来自Restful服务,另一块或对象来自RDBMS,另一块来自XML文件,LDAP等等。由此可见,非常好的封装数据访问对业务至关重要。请注意通过c-tors和属性公开的依赖项(DI)。

话虽如此,我一直在考虑的方法是将架构的“核心”放在业务控制器中。将现代数据访问视为资源而不是传统思维,控制器接受URI或其他形式的元数据,以便知道它必须管理哪些数据资源以供业务对象使用。然后,业务对象本身不封装数据访问;相反,控制器会这样做。这使得您的业务对象轻巧且具体,并允许您的控制器提供优化、可组合性、事务氛围等功能。请注意,您的控制器将“托管”您的业务对象集合,就像许多ORM的控制器部分一样。

此外,还要考虑业务规则管理。如果你仔细看你的UML图(或者像我一样在脑海中构建模型:D),你会发现你的业务规则模型实际上是另一个模型,有时甚至是持久化的(例如,如果你使用业务规则引擎)。我建议让业务控制器也实际控制你的规则子系统,并让你的业务对象通过控制器引用规则。原因是,不可避免地,规则实现经常需要执行查找和交叉检查,以确定有效性。通常,它可能需要水化的业务对象查找,以及后端数据库查找。例如,考虑检测重复实体,其中只有“新”实体被水化。将规则管理留给业务控制器,你就可以做任何你需要的事情,而不会牺牲你的“领域模型”中的良好抽象。
伪代码如下:
using(MyConcreteBusinessContext ctx = new MyConcreteBusinessContext("datares://model1?DataSource=myserver;Catalog=mydatabase;Trusted_Connection=True ruleres://someruleresource?type=StaticRules&handler=My.Org.Business.Model.RuleManager")) {

User user = ctx.GetUserById("SZE543");
user.IsLogonActive = false;
ctx.Save();
}

//a business object
class User : BusinessBase {
  public User(BusinessContext ctx) : base(ctx) {}

  public bool Validate() {
    IValidator v = ctx.GetValidator(this);
    return v.Validate();
  }
}

// a validator
class UserValidator : BaseValidator, IValidator {
 User userInstance;
 public UserValidator(User user) {
  userInstance = user;
 }

 public bool Validate() {
   // actual validation code here
   return true;
 }
}

0
客户端不应直接处理存储对象。他们可以直接处理DTO,但任何具有与存储相关的逻辑且未包装在业务对象中的对象都不应由客户端直接调用。

0

请查看Rocky Lhotka的CSLA.net网站。


0

0

嗨,我就是同事Greg提到的那个人。

Greg非常准确地描述了我们一直在考虑的备选方案。我只想在情况说明中添加一些额外的考虑。

客户端代码可能不知道业务对象存储在哪里,但这可能发生在只有一个数据存储的情况下,或者在同一业务对象类型(用户存储在本地数据库和外部LDAP中)有多个数据存储的情况下,但客户端不创建这些业务对象。从系统分析的角度来看,这意味着不应该存在任何用例,其中同一类型的两个对象的两个数据存储的存在会影响用例流程。

一旦需要区分在不同数据存储中创建的对象,客户端组件必须意识到其宇宙中数据存储的多样性,并且将不可避免地负责决定在对象创建时(以及我认为从数据存储加载对象时)使用哪个数据存储。业务层可以假装自己做出这些决策,但是决策制定的算法将基于来自客户端组件的信息类型和内容,使客户端有效地对决策负责。

这种责任可以通过多种方式实现:可以是每个数据存储的特定类型的连接对象;可以是调用以创建新BO实例等的分隔方法。

敬礼,

迈克尔


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