关于架构:不同逻辑层上的接口

关于架构:不同逻辑层上的接口

Interfaces on different logic layers

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

这是您第一次编写UI代码时的绝佳选择,因为您拥有一个可以信任的简洁定义的界面。

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

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

我希望我已经足够清楚地解释了这个问题。如何防止接口腐蚀,保持信息隐藏和封装,同时又适应不同层之间的不同接口需求?


如果我正确理解了这个问题,则说明您已经创建了一个域模型,并且希望编写一个对象关系映射器来映射数据库中的记录与域对象之间的映射。但是,您担心要使用"管道"代码来污染域模型,而这对于读取和写入对象的字段是必不可少的。

退后一步,您基本上有两种选择,可以将数据映射代码放置在何处-在域类本身内还是在外部映射类内。
第一个选项通常称为Active Record模式,其优点是每个对象都知道如何持久保存自身,并具有对其内部结构的充分访问权限,以使其能够执行映射而无需公开与业务无关的字段。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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的用户。我们不想直接设置状态,也许是因为我们要检查更改是否是有效的状态转换,所以我们没有设置器。幸运的是,GetById和Save静态方法中的映射代码可以完全访问对象的名称和状态字段。

第二个选项是拥有第二个类,该类负责映射。这样做的好处是可以分离出业务逻辑和持久性的不同方面,这可以使您的设计更具可测试性和灵活性。这种方法的挑战在于如何将名称和状态字段公开给外部类。一些选项是:
1.使用反射(对于深入探究对象的私有部分没有约束)
2.提供专门命名的公共二传手(例如,在其前加上"私人"一词),希望没有人会意外使用它们。
3.如果您的语言支持,请将设置器设为内部,但授予您的数据映射器模块访问权限。例如。在.NET 2.0及更高版本中使用InternalsVisibleToAttribute或在C

中使用好友功能

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

不过,警告一下,在继续编写自己的映射器之前,我强烈建议您考虑使用第三方对象关系映射器(ORM)工具,例如nHibernate或Microsoft的Entity框架。我从事过四个不同的项目,由于各种原因,我们编写了自己的mapper,很容易浪费很多时间来维护和扩展mapper,而不是编写提供最终用户价值的代码。到目前为止,我已经在一个项目上使用了nHibernate,尽管最初它的学习曲线相当陡峭,但您早期投入的投资却能获得可观的回报。


这是一个经典问题-将域模型与数据库模型分离。有几种方法可以攻击它,我认为这实际上取决于项目的大小。您可以像其他人所说的那样使用存储库模式。如果您使用的是.net或Java,则可以使用NHibernate或Hibernate。

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

ps问题标题有点误导人


@冰^^热度:

What do you mean by that the data tier should not be aware of the business logic tier? How would you fill an business object with data?

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

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

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

UI负责将用户输入的数据与对象进行映射,数据层负责将db中的数据与对象进行映射。业务层保持纯粹的业务。 :)


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

我认为许多开发人员将数据库视为对象的简单持久层,并且只关心那些对象所需的CRUD操作。对象模型和关系模型之间的"阻抗不匹配"花费了很多精力。这是一个主意:停止尝试。

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

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

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

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


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

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

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

1
public class BusinessObject : IView, IData

这样,在您的数据层中,您仅需要查看IData的接口实现,而在您的UI中,您仅需要查看IView的接口实现。

您可能要使用的另一种策略是在UI或Data层中组合对象,以使它们仅被这些层消耗,例如,

1
2
3
4
5
public class BusinessObject : DomainObject

public class ViewManager< T > where T : DomainObject

public class DataManager< T > where T : DomainObject

这又使您的业务对象对UI / View层和数据层均一无所知。


因此问题在于业务层需要向数据层公开更多功能,而添加此功能意味着向UI层公开太多功能?如果我正确地理解了您的问题,这听起来像是您在尝试通过单个界面满足太多的要求,这只会使它变得混乱。为什么没有两个接口连接到业务层?一个可能是用于UI层的简单,安全的界面。另一个将是数据层的较低级接口。

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

1
2
3
4
5
public class BusinessLayer : ISimpleBusiness
{}

public class Some3LayerObject : ISimpleSome3LayerObject
{}

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

我经常这样做:

1
2
3
4
5
6
7
8
9
10
namespace Data
{
    public class BusinessObjectDataManager
    {
         public void SaveObject(BusinessObject object)
         {
                // Exec stored procedure
         {
    }
}

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

1
2
3
public class BusinessObjectRecord : BusinessObject
{
}

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

  • 很多小型接口(例如ICreateRepository,IReadRepository,IReadListRepsitory ..列表还在继续,并且它们中的大多数严重依赖于泛型)
  • 从IReadRepository继承的许多具体接口(例如IPersonRepository),您就可以理解这一点。
    您无法使用较小的接口来描述的所有内容都放入了具体的接口中。
    只要使用IPersonRepository声明对象,您就可以使用一个干净,一致的接口。但更重要的是,您还可以创建一个采用f.x的类。在其构造函数中创建一个ICreateRepository,因此该代码最终将非常容易执行某些非常时髦的工作。这里在业务层中也有用于服务的接口。
  • 最后,我将所有领域对象都放入了额外的程序集中,只是为了使代码库本身更简洁,耦合更松散。这些对象没有任何逻辑,它们只是描述所有3层数据的常用方式。

顺便说一句。为什么要在业务逻辑层中定义方法以容纳数据层?
数据层应该没有理由甚至没有业务层。.


推荐阅读