如何在类和应用层之间传递数据?

36
例如,如果我正在创建一个三层应用程序(数据/业务/Ul),并且数据层正在获取单个或多个记录。在将其发送到业务层之前,我是否将来自数据层的所有内容转换为通用列表/集合?发送数据表可以吗?那么向数据层发送信息呢?
如果我使用对象/列表,这些是数据层还是业务层的成员?我能够使用同一对象来在层之间传递吗?
以下是一些伪代码:
对象用户具有电子邮件/密码
在UI层中,用户输入电子邮件/密码。 UI层进行验证,然后我假设创建一个新的用户对象传递给业务层进行进一步验证,并将相同的对象传递给数据层以插入记录。 这样正确吗?
我是.NET的新手(来自8年ASP VBScript背景),正在努力了解“正确”处理事物的方法。

@开发者,看一下Mark Brittingham的代码,我认为那是最好的选择。我也喜欢Joel的方法,但Mark让我思考 :)。 - JonH
@JonH - 谢谢!我正在尽快学习 .net,但我仍然是一个新手。8年以上的ASP VBScript使学习变得更容易,但仍在挣扎。 - jpshook
8个回答

24

我更新了这个答案,因为Developr的评论似乎表明他希望有更多的细节。

简短回答你的问题是是的,你需要使用类实例(对象)来调解UI和业务逻辑层之间的接口。BLL和DAL将如下所述进行通信。您不应该在它们之间传递SqlDataTables或SqlDataReaders。

简单的原因是:对象是类型安全的,提供Intellisense支持,允许您在业务层中进行添加或修改,而这些在数据库中并不一定存在,并且让您有一些自由来断开应用程序与数据库之间的联系,以便您可以在数据库变化时保持一致的BLL接口(当然,在某种程度上)。这只是好的编程实践。

大局是,对于您UI中的任何页面,您都将拥有一个或多个要显示和交互的“模型”。对象是捕获模型当前状态的方法。在过程方面:UI将从业务逻辑层(BLL)请求模型(可能是单个对象或对象列表)。然后,BLL使用数据访问层(DAL)的工具创建并返回此模型。如果在UI中对模型进行更改,则UI将向BLL发送修订后的对象,并附带指示如何处理它们的说明(例如,插入、更新、删除)。

.NET非常适合这种关注点分离,因为通用容器类 - 特别是List<>类 - 对于这种工作非常完美。它们不仅允许您传递数据,而且还可以通过ObjectDataSource类轻松集成复杂的UI控件,例如网格、列表等。您可以使用ObjectDataSource实现需要开发UI的全套操作:“填充”具有参数的操作、CRUD操作、排序等。

由于这相当重要,让我快速转移一下,演示如何定义ObjectDataSource:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
    OldValuesParameterFormatString="original_{0}" 
    SelectMethod="GetArticles" 
    OnObjectCreating="OnObjectCreating"
    TypeName="MotivationBusinessModel.ContentPagesLogic">
    <SelectParameters>
        <asp:SessionParameter DefaultValue="News" Name="category" 
            SessionField="CurPageCategory" Type="String" />
    </SelectParameters>
</asp:ObjectDataSource>

这里,MotivationBusinessModel是BLL的命名空间,而ContentPagesLogic是实现内容页面逻辑的类。提取数据的方法是"GetArticles",它需要一个名为CurPageCategory的参数。在这种情况下,ObjectDataSource返回一个对象列表,然后由网格使用。请注意,我需要将会话状态信息传递给BLL类,因此在代码后台中,我有一个方法"OnObjectCreating",让我创建对象并传递参数。
public void OnObjectCreating(object sender, ObjectDataSourceEventArgs e)
{
    e.ObjectInstance = new ContentPagesLogic(sessionObj);
}

所以,它是如何工作的。但有一个非常重要的问题 - 模型/业务对象从哪里来?ORM(对象关系映射)如Linq to SQL和Subsonic提供代码生成器,让您为每个数据库表创建一个类。也就是说,这些工具认为模型类应该在您的数据访问层(DAL)中定义,并直接映射到数据库表。Linq to Entities允许您以与数据库布局完全不同的方式定义对象,但相应地更加复杂(这就是为什么Linq to SQL和Linq to Entities之间有区别)。实质上,它是BLL解决方案。Joel和我在本文的各个地方都说过,实际上,业务层通常是应该定义模型的地方(尽管我现实中使用了一种BLL和DAL对象的混合方法)。
一旦决定这样做,如何实现从模型到数据库的映射呢?好吧,您编写BLL中的类来拉取数据(使用您的DAL),并填充对象或对象列表。这是“业务逻辑”,因为映射通常伴随着附加逻辑来充实模型(例如,定义派生字段的值)。
Joel创建静态Factory类来实现模型到数据库的映射。这是一个很好的方法,因为它使用了一个众所周知的模式,并将映射放在要返回的对象的构造中。您总是知道要去哪里查看映射,整体方法简单明了。
我采取了不同的方法。在我的BLL中,我定义了“逻辑”类和“模型”类。这些通常以匹配对的形式定义,其中两个类在同一文件中定义,它们的名称通过其后缀区别(例如ClassModel和ClassLogic)。逻辑类知道如何使用模型类 - 执行如填充、保存(“Upsert”)、删除和为模型实例生成反馈等操作。
特别是,为了进行填充,我利用了在我的主DAL类中找到的方法(如下所示),让我可以获取任何类和任何SQL查询,并找到一种方法来使用查询返回的数据创建/填充类的实例(无论是单个实例还是列表)。也就是说,逻辑类只需获取模型类定义,定义SQL查询并将它们发送到DAL。结果是可以传递给UI的单个对象或对象列表。请注意,查询可能会返回来自一个表或多个连接在一起的多个表的字段。在映射级别,我真的不关心 - 我只想填充一些对象。
这是第一个函数。它将自动将任意类映射到从查询中提取的所有匹配字段。匹配是通过找到与类中属性匹配的字段来执行的。如果有额外的类字段(例如,您将使用业务逻辑填充的字段)或额外的查询字段,则会忽略它们。
    public List<T> ReturnList<T>() where T : new()
    {
        try
        {
            List<T> fdList = new List<T>();
            myCommand.CommandText = QueryString;
            SqlDataReader nwReader = myCommand.ExecuteReader();
            Type objectType = typeof (T);
            PropertyInfo[] typeFields = objectType.GetProperties();
            if (nwReader != null)
            {
                while (nwReader.Read())
                {
                    T obj = new T();
                    for (int i = 0; i < nwReader.FieldCount; i++)
                    {
                        foreach (PropertyInfo info in typeFields)
                        {
                            // Because the class may have fields that are *not* being filled, I don't use nwReader[info.Name] in this function.
                            if (info.Name == nwReader.GetName(i))
                            {
                                if (!nwReader[i].Equals(DBNull.Value)) 
                                    info.SetValue(obj, nwReader[i], null);
                                break;
                            }
                        }
                    }
                    fdList.Add(obj);
                }
                nwReader.Close();
            }
            return fdList;
        }
        catch
        {
            conn.Close();
            throw;
        }
    }

这是在我的数据访问层(DAL)中使用的,但你只需要在DAL类中拥有一个QueryString的变量、一个带有打开连接和任意参数的SqlCommand对象即可。关键是确保当调用时ExecuteReader函数能够正常工作。因此,我的业务逻辑层(BLL)通常会这样使用该函数:

return qry.Command("Select AttendDate, Count(*) as ClassAttendCount From ClassAttend")
          .Where("ClassID", classID)
          .ReturnList<AttendListDateModel>();

你也可以这样实现对匿名类的支持:
    public List<T> ReturnList<T>(T sample)
    {
        try
        {
            List<T> fdList = new List<T>();
            myCommand.CommandText = QueryString;
            SqlDataReader nwReader = myCommand.ExecuteReader();
            var properties = TypeDescriptor.GetProperties(sample);
            if (nwReader != null)
            {
                while (nwReader.Read())
                {
                    int objIdx = 0;
                    object[] objArray = new object[properties.Count];
                    for (int i = 0; i < nwReader.FieldCount; i++)
                    {
                        foreach (PropertyDescriptor info in properties) // FieldInfo info in typeFields)
                        {
                            if (info.Name == nwReader.GetName(i))
                            {
                                objArray[objIdx++] = nwReader[info.Name];
                                break;
                            }
                        }
                    }
                    fdList.Add((T)Activator.CreateInstance(sample.GetType(), objArray));
                }
                nwReader.Close();
            }
            return fdList;
        }
        catch
        {
            conn.Close();
            throw;
        }
    }

调用它的方式如下:
var qList = qry.Command("Select QueryDesc, UID, StaffID From Query")
               .Where("SiteID", sessionObj.siteID)
               .ReturnList(new { QueryDesc = "", UID = 0, StaffID=0 });

现在,qList是一个动态创建的类实例通用列表。假设你的业务逻辑层(BLL)中有一个函数,它以下拉列表作为参数,并请求使用数据填充该列表。以下是如何使用上述检索结果填充下拉列表的方法:
foreach (var queryObj in qList)
{
    pullDownList.Add(new ListItem(queryObj.QueryDesc, queryObj.UID.ToString()));
}

简而言之,我们可以动态定义匿名的业务模型类,并通过向数据访问层传递一些(动态生成的)SQL来填充它们。因此,响应UI不断变化的需求时,BLL非常容易更新。
最后注意一点:如果你担心定义和传递对象会浪费内存,你不必担心:如果你使用SqlDataReader来提取数据并将其放入列表中组成的对象中,你只会有一个内存副本(即列表),因为读取器以只读、单向的方式迭代。当然,如果你在数据访问层中使用DataAdapter和Table类等,则会产生不必要的开销(这就是为什么你不应该这样做)。

我确实看到一些我喜欢的东西,不过我会重新编写代码,使用一个可枚举的迭代器块,而不是一个列表。 - Joel Coehoorn
马克,我知道我已经给你点赞了,但是谢谢你做出的进一步改变,这非常酷。 - JonH
马克,感谢您详细的回复。我正在努力理解它。一旦我从我的DAL中使用了一个对象并需要将其发送回DAL(更新记录),那么我应该将其作为相同类型的对象(例如-列表)发送吗?我应该设置我的类文件,以便我有DAL、BLL、BO/DTO和UI来保持整洁和有序吗? - jpshook
@JonH - 不用谢。@Joel - 感谢您的评论,希望您不会将我在其他地方的评论视为侮辱:我喜欢您所做的。 我曾经经历过一个时期,想要创建最终的DAL,并最终采用了这种方法。当时我正在阅读Skeet的书籍,因此我尝试了各种酷炫的东西(这本书非常棒-如果您还没有购买,请购买)。关于使用IEnumerable而不是List<>-我在概念上同意,如果我有时间和需要,我会进行更改,但目前List运作良好,因此我几乎没有动力改变。 - Mark Brittingham
@developr - 在实践中,业务对象的列表(也称为模型)用于向用户界面提供来自BLL的数据以供表格/列表/网格使用。单个业务对象用于在仅显示一个记录的信息时向UI提供信息。在设计架构时,我总是考虑UI / BLL接口。我将找出如何根据需要获取BLL驱动DAL的方法(这更像是管道)。此外,虽然BO通常对应于来自表的记录,但并不总是这种情况。 - Mark Brittingham
显示剩余4条评论

13

总的来说,我认为发送对象比数据表更好。 通过对象,每个层都知道它正在接收什么(哪些具有什么属性的对象等)。 对象提供编译时安全性,您不会意外拼写属性名称等,并且它强制两个层之间存在内在契约。

Joshua也提出了一个很好的观点,通过使用自定义对象,还可以将其他层与数据层解耦。 您始终可以从另一个数据源填充自定义对象,而其他层则不会注意到。 对于SQL数据表,这可能不太容易。

Joel也提出了一个很好的观点。 让数据层了解业务对象与让业务层和UI层了解数据层的具体情况一样不明智。


4
不仅如此,如果您使用对象,您可以允许将来更改数据层。 如果您传递SqlDataReader或类似对象,则会将数据层和业务层耦合在一起,并强制您的数据层始终使用SQL。 - Joshua
2
使用对象使更换数据层变得更加困难,而不是更容易,因为任何替代的数据层也必须了解您的对象。我更喜欢一种中间翻译层,它完全将知道业务对象的代码与知道数据访问的代码分离开来。 - Joel Coehoorn
2
如果你这样做,难道不是把问题转移到中间层,让它必须了解数据和业务吗?这不是完全相同的问题,只是在一个(可能)不同的程序集中吗? - Steven Evers
1
@SnOrfus - 其实并不是这样。尽管有关关注点分离的炒作,但只有业务和数据层仍然很正常,因为一个层面的变化可能会强制其他层面进行代码更改和重新编译。翻译层的目的是使数据层和业务层相互隔离。如果您的业务层发生变化,则可能会传播到翻译层,但不会进一步传播。数据层也是如此。而且翻译层通常可以由代码生成器编写。 - Joel Coehoorn
1
@developr - 你将使用数据层来填充BLL中的对象,然后将它们传递给UI。然后,你的BLL将从UI获取对象进行更新 - 它将使用DAL来完成这个过程。现在,你可以在BLL或DAL中完成大部分工作(例如低级别映射、保存等)。这取决于DAL的构建方式和映射的复杂性(类结构=数据库结构->在DAL中完成,类结构与数据库结构松散耦合->在BLL中完成)。我已经重新编写了我的答案,使其更加完整,并涵盖了所有这些内容-请看一下! - Mark Brittingham
显示剩余7条评论

7

在世界上的编程团队中,实现这个功能几乎有无数种“正确”的方法。尽管如此,我喜欢为我的每个业务对象构建一个工厂,它看起来像这样:

public static class SomeBusinessObjectFactory
{
   public static SomeBusinessObject FromDataRow(IDataRecord row)
   {
       return new SomeBusinessObject() { Property1 = row["Property1"], Property2 = row["Property2"] ... };
   }
}

我还有一种通用的翻译方法,我使用它来调用这些工厂:

public static IEnumerable<T> TranslateQuery(IEnumerable<IDatarecord> source, Func<IDatarecord, T> Factory)
{
    foreach (IDatarecord item in source)
        yield return Factory(item);
}

根据您的团队喜好、项目大小等因素,这些工厂对象和翻译器可以与业务层或数据层共存,甚至可以是额外的“翻译”程序集/层。

然后我的数据层将具有以下代码:

private SqlConnection GetConnection()
{
    var conn = new SqlConnection( /* connection string loaded from config file */ );
    conn.Open();
    return conn;
}

private static IEnumerable<IDataRecord> ExecuteEnumerable(this SqlCommand command)
{
    using (var rdr = command.ExecuteReader())
    { 
        while (rdr.Read())
        {
            yield return rdr;
        }
    }
}

public  IEnumerable<IDataRecord> SomeQuery(int SomeParameter)
{
    string sql = " .... ";

    using (var cn = GetConnection())
    using (var cmd = new SqlCommand(sql, cn))
    {
        cmd.Parameters.Add("@Someparameter", SqlDbType.Int).Value = SomeParameter;
        return cmd.ExecuteEnumerable();
    }
}

然后我可以像这样把它全部组合起来:

 SomeGridControl.DataSource = TranslateQuery(SomeQuery(5), SomeBusinessObjectFactory.FromDataRow);

@Joel - 也非常好 :). - JonH
@Joel Coehoorn - 你在UI界面的哪里执行了SomeGridControl.DataSource = TranslateQuery(SomeQuery(5), SomeBusinessObjectFactory.FromDataRow)这段代码?另外,你提到了SomeBusinessObject.Factory.FromDataRow,但是没有展示如何获取数据行。 - JonH
听起来是个好主意,但我认为我需要学习更多的C#概念,这样我才能完全理解这个解决方案。 - jpshook
@developr - 这些答案来自有着丰富的C#经验和编程经验的人。我建议你从简单的东西开始,所有这些代码都非常通用,如果你刚开始学习,可能想要坚持使用非通用的具体实现。我知道这听起来很奇怪,但这以及Mark的代码都相当强大,并需要C#经验。 - JonH
谢谢大家... 我最大的问题是我试图跳过初学者的内容。多年来,我一直在支持传统的ASP Vbscript应用程序,拖延了很长时间才开始接触.net。现在是将这些应用程序转换为ASP.net(一个具有许多子应用程序的大型电子商务网站)的时候了,我真的不知道从哪里开始。因此,我一直在学习有关设计模式和最佳实践的知识。使用UI上的sqldatasources(不想这样做)和完整的三层抽象和松散耦合之间很难找到中间地带。 - jpshook
显示剩余2条评论

1
我会添加一个新的层,ORM(对象关系映射),其责任是将数据从数据层转换为业务对象集合。我认为在您的业务模型中使用对象是最佳实践。

编写数据访问代码就是从客户那里偷取!这就是上述解决方案的问题所在。 - Robert

1
无论您使用什么方式在应用程序的各个层之间传递数据,都要确保每个层的实现细节不会泄漏到其他层。您应该能够更改关系数据库中存储的数据的方式,而无需修改业务对象层中的任何代码(当然除了序列化)。
业务对象设计与关系数据模型之间的紧密耦合非常恼人,也是浪费良好关系数据库管理系统的行为。

1

这里有很多非常好的答案,我只想补充一点,在您花费大量时间创建翻译层和工厂之前,了解应用程序的目的和未来是很重要的。

无论是在配置映射文件、工厂还是直接在数据/业务/UI层中,某个对象/文件/类等都必须了解每个层之间发生的事情。如果交换层是现实的,则创建翻译层很有用。其他时候,让某些层(通常是在业务层)了解所有接口(或至少足够来处理数据和UI之间的关系)是有意义的。

再次强调,这并不是说所有这些东西都是坏的,只是可能存在YAGNI的情况。有些DI和ORM框架使这种东西变得非常简单,所以不做这些操作就太傻了。如果您正在使用其中一个框架,则可能最好充分利用它。


非常好的表述,点赞。此外请注意,我的许多翻译代码可以在项目之间重复使用,而且大部分不可重用的代码也可以由代码生成器编写,因此设置类似这样的内容不再需要太多工作。 - Joel Coehoorn
我的一个项目是一个简单的内部网络,我将从头开始创建表格。为此,我可能会使用像Linq2SQL这样的ORM工具。我的主要项目是一个中等规模的电子商务网站,有50多个数据库表(非常混乱,未经过规范化处理,键很少),因此我正在尝试找出处理数据端的最佳方法。 - jpshook

0
我现在正在开发的应用程序相当老(在.NET术语中),并使用强类型数据集在数据层和业务层之间传递数据。在业务层中,数据集中的数据在传递到前端之前需要手动“映射”到业务对象中。
然而,这可能不是一个受欢迎的设计决策,因为强类型数据集一直存在争议。

是的,重构它们会更好 :-) - Robert

0
我强烈建议您使用对象来实现。其他方式可能会导致只有接口是公开的,而您的实现是内部的,并且通过对象的工厂公开您的方法,然后将您的工厂与外观耦合,最终获得单一和唯一的入口点到您的库。然后,只有数据对象通过您的外观,因此您始终知道在外观内部和外部可以期望什么。
这样,任何UI都可以调用您的库的外观,而唯一需要编写的就是您的UI。
这里有一个链接,我个人认为非常有趣,它概述了不同的设计模式:GoF .NET Design Patterns for C# and VBNET
如果您想要一个说明我所说内容的代码示例,请随时提出要求。

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