12

假设您有一个分为 3 层的应用程序:GUI、业务逻辑和数据访问。在您的业务逻辑层中,您已经描述了您的业务对象:getter、setter、accessors 等等……您明白了。业务逻辑层的接口保证了业务逻辑的安全使用,所以你调用的所有方法和访问器都会验证输入。

这在您第一次编写 UI 代码时非常棒,因为您有一个可以信赖的定义整齐的界面。

但是棘手的部分来了,当您开始编写数据访问层时,业务逻辑的接口无法满足您的需求。您需要更多的访问器和获取器来设置隐藏/曾经隐藏的字段。现在你不得不侵蚀你的业务逻辑的接口;现在可以从 UI 层设置字段,UI 层没有业务设置。

由于数据访问层所需的更改,业务逻辑的接口已经被侵蚀到甚至可以使用无效数据设置业务逻辑的程度。因此,该接口不再保证安全使用。

我希望我足够清楚地解释了这个问题。您如何防止接口侵蚀,维护信息隐藏和封装,同时仍然适应不同层之间的不同接口需求?

4

9 回答 9

7

如果我正确理解了这个问题,那么您已经创建了一个域模型,并且您想编写一个对象关系映射器来映射数据库中的记录和域对象。但是,您担心使用读取和写入对象字段所必需的“管道”代码污染您的域模型。

退后一步,您基本上有两种选择将数据映射代码放置在何处 - 在域类本身或外部映射类中。第一个选项通常称为 Active Record 模式,它的优点是每个对象都知道如何持久化自己,并对其内部结构有足够的访问权限,以允许它执行映射而无需暴露与业务无关的字段。

例如

public class User
{
    private string name;
    private AccountStatus status;

    private User()
    {
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public AccountStatus Status
    {
        get { return status; }
    }

    public void Activate()
    {
        status = AccountStatus.Active;
    }

    public void Suspend()
    {
        status = AccountStatus.Suspended;
    }

    public static User GetById(int id)
    {
        User fetchedUser = new User();

        // Lots of database and error-checking code
        // omitted for clarity
        // ...

        fetchedUser.name = (string) reader["Name"];
        fetchedUser.status = (int)reader["statusCode"] == 0 ? AccountStatus.Suspended : AccountStatus.Active;

        return fetchedUser;
    }

    public static void Save(User user)
    {
        // Code to save User's internal structure to database
        // ...
    }
}

在此示例中,我们有一个对象,该对象表示具有名称和 AccountStatus 的用户。我们不想让 Status 直接被设置,可能是因为我们想检查更改是否是有效的状态转换,所以我们没有设置器。幸运的是,GetById 和 Save 静态方法中的映射代码可以完全访问对象的名称和状态字段。

第二个选项是有一个负责映射的第二个类。这样做的好处是将业务逻辑和持久性的不同关注点分开,这可以使您的设计更具可测试性和灵活性。这种方法的挑战是如何将名称和状态字段公开给外部类。一些选项是: 1. 使用反射(它不会对深入挖掘对象的私有部分感到不安) 2. 提供特殊命名的公共设置器(例如,在它们前面加上“Private”这个词)并希望没有人意外使用它们 3 . 如果您的语言支持它,请将设置器设置为内部但授予您的数据映射器模块访问权限。例如,在 .NET 2.0 及更高版本中使用 InternalsVisibleToAttribute 或在 C++ 中使用友元函数

有关更多信息,我推荐 Martin Fowler 的经典著作《企业架构模式》

但是,作为警告,在开始编写自己的映射器之前,我强烈建议使用 3rd-party 对象关系映射器 (ORM) 工具,例如 nHibernate 或 Microsoft 的实体框架。我参与了四个不同的项目,出于各种原因,我们编写了自己的映射器,很容易浪费大量时间维护和扩展映射器,而不是编写为最终用户提供价值的代码。到目前为止,我已经在一个项目中使用了 nHibernate,虽然它最初的学习曲线相当陡峭,但您在早期投入的投资获得了可观的回报。

于 2008-08-13T06:08:24.597 回答
5

这是一个经典问题——将域模型与数据库模型分开。有几种方法可以攻击它,在我看来这真的取决于你的项目的大小。您可以像其他人所说的那样使用存储库模式。如果您使用的是 .net 或 java,您可以使用NHibernateHibernate

我所做的是使用测试驱动开发,所以我首先编写我的 UI 和模型层,然后模拟数据层,所以 UI 和模型是围绕特定领域的对象构建的,然后我将这些对象映射到我正在使用的任何技术数据层。让数据库决定您的应用程序的设计,先编写应用程序,然后再考虑数据,这是一个非常糟糕的主意。

ps问题的标题有点误导

于 2008-08-12T21:28:09.823 回答
1

@冰^^热:

数据层不应该知道业务逻辑层是什么意思?您将如何用数据填充业务对象?

UI 向业务层中的 ServiceClass 询问服务,即获取由具有所需参数数据的对象过滤的对象列表。
然后,ServiceClass 在数据层中创建一个存储库类的实例,并调用 GetList(ParameterType filters)。
然后数据层访问数据库,提取数据,并将其映射到“域”程序集中定义的通用格式。
BL 不再需要处理这些数据,因此将其输出到 UI。

然后 UI 想要编辑项目 X。它将项目(或业务对象)发送到业务层中的服务。业务层对对象进行验证,如果OK,则将其发送到数据层进行存储。

UI 知道业务层中的服务,而业务层又知道数据层。

UI 负责将用户数据输入映射到对象和从对象映射,数据层负责将数据库中的数据映射到对象和从对象映射。业务层保持纯粹的业务。:)

于 2008-08-12T21:34:47.127 回答
0

我总是创建一个单独的程序集,其中包含:

  • 很多小的接口(想想 ICreateRepository、IReadRepository、IReadListRepsitory .. 列表还在继续,其中大多数严重依赖泛型)
  • 很多具体的接口,比如 IPersonRepository,从 IReadRepository 继承,你明白了。
    任何你不能用更小的接口描述的东西,你都放在具体的接口中。
    只要您使用 IPersonRepository 来声明您的对象,您就可以获得一个干净、一致的接口来使用。但更重要的是,您还可以创建一个在其构造函数中使用 fx 的 ICreateRepository 的类,因此代码最终将很容易做一些非常时髦的事情。这里还有业务层中的服务接口。
  • 最后,我将所有域对象都粘贴到额外的程序集中,只是为了使代码库本身更干净,更松散耦合。这些对象没有任何逻辑,它们只是描述所有 3+ 层数据的常用方式。

顺便提一句。为什么要在业务逻辑层中定义方法来适应数据层?
数据层应该没有理由甚至知道有业务层..

于 2008-08-12T21:16:50.987 回答
0

这可能是一个解决方案,因为它不会侵蚀界面。我想你可以有这样的课程:

public class BusinessObjectRecord : BusinessObject
{
}
于 2008-08-12T21:18:44.650 回答
0

数据层不应该知道业务逻辑层是什么意思?您将如何用数据填充业务对象?

我经常这样做:

namespace Data
{
    public class BusinessObjectDataManager
    {
         public void SaveObject(BusinessObject object)
         {
                // Exec stored procedure
         {
    }
}
于 2008-08-12T21:28:05.753 回答
0

那么问题来了,业务层需要向数据层暴露更多的功能,而增加这个功能就意味着向UI层暴露了太多?如果我正确理解了您的问题,那么听起来您试图通过单个界面满足太多,而这只会导致它变得混乱。为什么没有两个接口进入业务层?一个是用于 UI 层的简单、安全的界面。另一个是数据层的低级接口。

您也可以将这种双接口方法应用于需要同时传递给 UI 和数据层的任何对象。

public class BusinessLayer : ISimpleBusiness
{}

public class Some3LayerObject : ISimpleSome3LayerObject
{}
于 2008-08-12T21:52:01.570 回答
0

您可能希望将接口分为两种类型,即:

  • 查看界面——这些界面指定了您与 UI 的交互,以及
  • 数据接口——允许您指定与数据交互的接口

可以继承和实现这两组接口,例如:

public class BusinessObject : IView, IData

这样,在你的数据层你只需要看到 IData 的接口实现,而在你的 UI 中你只需要看到 IView 的接口实现。

您可能想要使用的另一种策略是在 UI 或数据层中组合您的对象,以便它们仅由这些层使用,例如,

public class BusinessObject : DomainObject

public class ViewManager<T> where T : DomainObject

public class DataManager<T> where T : DomainObject

这反过来又允许您的业务对象保持对 UI/View 层和数据层的无知。

于 2008-08-13T01:00:12.783 回答
0

我将继续我违背常规的习惯,并说你应该质疑为什么要构建所有这些极其复杂的对象层。

我认为许多开发人员将数据库视为其对象的简单持久层,并且只关心这些对象所需的 CRUD 操作。在对象和关系模型之间的“阻抗不匹配”上投入了太多精力。这是一个想法:停止尝试。

编写存储过程来封装您的数据。根据需要从代码中使用结果集、DataSet、DataTable、SqlCommand(或 java/php/任何等效项)与数据库交互。你不需要这些对象。一个很好的例子是将 SqlDataSource 嵌入到 .ASPX 页面中。

您不应该试图向任何人隐藏您的数据。开发人员需要准确了解他们与物理数据存储交互的方式和时间。

对象关系映射器是魔鬼。停止使用它们。

构建企业应用程序通常是一种管理复杂性的练习。您必须使事情尽可能简单,否则您将拥有一个绝对不可维护的系统。如果您愿意允许一些耦合(无论如何这在任何应用程序中都是固有的),那么您可以取消业务逻辑层和数据访问层(用存储过程替换它们),并且您不需要任何这些接口。

于 2008-08-13T22:45:03.680 回答