65.9K
CodeProject 正在变化。 阅读更多。
Home

快速、灵活、易用的 N 层软件架构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (10投票s)

2010年3月11日

CPOL

9分钟阅读

viewsIcon

33466

downloadIcon

608

快速、灵活、易用的 N 层软件架构

引言

我开发该架构的最大目标是能够在不损失性能的情况下,简化数据库和应用程序之间的整个映射过程。正如我们所知,对数据集可以进行的四项操作是:

  • 选择过滤后的数据 - 根据过滤条件选择多行
  • 按 ID 选择一行(表的唯一标识符),保存数据,这可以分为两类:
    • 将数据插入表中(例如,如果 ID 小于或等于零)
    • 更新表中的数据(如果行的 ID 大于零)
  • 按 ID 删除数据

无论应用程序中的业务逻辑多么复杂,这四项操作都是对表或视图可以且必须进行的操作。下图说明了系统的 N 层架构。

让我们从 BusinessEntities 项目开始。它是所有项目中分离最彻底的,但也最容易理解。

BusinessEntities 项目

图 1:BusinessEntities 项目的类图

BusinessEntities 项目是一个类库项目。它是 N 层架构中中间层的一部分。它包含容器类,这些类将从数据库中填充数据。这类类仅用于对从数据库返回的数据进行编程表示。通常,这样的类代表一个表或视图,在常见场景下,它代表一个从数据库获取数据列表的查询。类的名称对应于表名;类属性的名称对应于列名。这些对应项通过属性设置。这在 EntityAttributes.cs 文件中完成。MapField 类继承自 .NET 基类 Attribute。此属性用于将类的属性映射到数据库的列。例如:

[MapField("Title")]
public string Title
{
   get { return _title; }
   set { _title = value; }
}

类中名为 'Title' 的属性对应于根据 MapField 属性从数据库返回的名为 'Title' 的列。'SpNames' 类也继承自 .NET 基类 attribute。它用于为四项主要操作设置存储过程的名称——“select”、“select one”、“insert or update”和“delete”。例如:

[SPNames("pContactList", "pContactSelect", "pContactSave", "pContactDelete")]
public class ContactEntity : BaseEntity, IContactEntity
{

类 'SpNames' 的构造函数已被重写,如果需要,可以对其进行初始化,使其包含一个或多个存储过程的名称。这些自定义属性的使用方式将在稍后详细解释。正如您所见,有一个 'BaseEntity' 类。它用于存储所有实体类中的两个主要属性——'ID' 和 'ReturnMessage'。每个表都有一个 ID;实体类中的返回消息用于存储存储过程的返回消息。'IBaseEntity' 接口是所有实体的基接口。它使得 'ID' 和 'ReturnMessage' 属性成为必需的。如果所有实体类都有一个对应的接口来描述所有属性,那将是很好的,但这并非强制要求。

DBAccess 项目

此项目负责与数据库建立连接。它也是一个类库项目。其类图如图 2 所示。

图 2:DBAccess 项目的类图

名为 Reflection.cs 的文件包含一个名为 Mapper 的类。顾名思义,此类包含将实体类及其属性映射到表名和列名的方法。这是通过 .NET Framework 中的反射机制读取属性来实现的。'GetMapFields' 方法是此类中实现的其中一个方法。通过对其的解释,您将能够理解整个反射机制。

private static void GetMapFields
	(Type type, DbDataReader reader, ref Dictionary<int> fields)
{
            PropertyInfo[] pInfo = type.GetProperties();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                foreach (PropertyInfo pi in pInfo)
                {
                    object[] attributes = 
			pi.GetCustomAttributes(typeof(MapField), false);
                    if (attributes.Length > 0)
                    {
                        MapField field = attributes[0] as MapField;
                        if (field != null && field.DbField == 
			reader.GetName(i) && !fields.ContainsKey(i))
                        {
                            fields.Add(i, pi);
                            break;
                        }
                    }
                }
            }
            if (type.BaseType != null && type.BaseType.IsClass)
                Mapper.GetMapFields(type.BaseType, reader, ref fields);
}

方法 'GetProperties()' 返回类型的所有属性。由于 Type 类是所有类型的基类,因此可以将 'BusinessEntities' 项目中的所有实体类传递给它。在返回属性后,每个属性都必须收集由开发人员定义的属性,然后检查它们是否与 'DbDataReader' 对象返回的数据匹配。之后,匹配项将保存在 Dictionary 集合中。通过此代码:

if (type.BaseType != null && type.BaseType.IsClass)
                Mapper.GetMapFields(type.BaseType, reader, ref fields);

提取属性以及基类型的属性,并在匹配时将它们添加到 Dictionary 集合中。'GetMapFields()' 方法有不同的实现。有些方法传入类型和输出集合,有些传入类型,还有些传入类型和 DbDataReader 对象。GetEntitiesPaging 类将在我的下一篇文章中进行解释,届时我将详细介绍返回分页数据的内容。Filter.cs 文件包含 'FilterInfo' 类,该类用于在使用过滤参数通过存储过程获取数据时传递参数。(过滤参数是限制结果或按某些标准对结果进行排序的参数)。DBAccess 类包含创建命令、创建命令参数以及返回 'DbDataReader' 和 'DataTable' 类型对象的各种方法。这是 DataTransaction 类的基类。在 'DataTransaction' 类中,执行数据库连接的打开和关闭,并且还有用于处理事务的附加方法。'Paging' 类存储分页结果时所需的属性。'DataSource' 类继承了 ICollectionIEnumerable 接口。它包含 'Paging' 属性,该属性的类型为 'Paging'(前述类),用于分页,还包含记录总数、记录列表和来自数据库的消息的属性。'Enumerator' 类继承了 'IEnumerator' 接口,还有一个属性返回当前元素,以及用于在集合中导航的方法。另一个非常重要的类是 'Utils' 类。它包含支持应用程序与数据库工作的各种方法。'GetDbType':这是一个方法,通过该方法,输入类型检查其名称并返回数据库中类型的表示。'GetPropertyValue' 是一个方法,通过该方法,输入类型和属性名称可以返回属性的值。

public static DbType GetDbType(Type pType)
{
DbType ret;
switch (pType.ToString())
{
    case "System.Int16":
        ret = DbType.Int16;
        break;
    case "System.Int32":
        ret = DbType.Int32;
        break;
    case "System.Int64":
        ret = DbType.Int64;
        break;

方法 'GetProcedureName' 通过输入类型和过程类型(enum 类型)返回过程的名称。这也通过反射机制实现。有一个名为 'ProcedureType' 的嵌套枚举类型,用于所有类型的存储过程,以及 EntityException 类。该类以 'Exception' 类作为基类,用于在类属性未定义存储过程时抛出异常。'DBAccessBase' 类位于 EntityMethods.cs 文件中。它是整个类库应用程序的主要类。它包含许多数据处理方法,这些方法分为几个区域。

  • 'GetEntities' 区域由 static 方法组成,这些方法返回 DataSource 类型的对象。泛型用于传递对象的类型。所有方法名称都相同,都是 'GetEntities',并且根据其输入参数重载了多次。
  • 'GetEntity' 区域由返回单个对象的方法组成,对象的类型由泛型定义。此返回类型必须继承自 'IBaseEntity' 接口。这意味着它将是 BusinessEntities 项目的类型。
  • 'SaveEntity' 区域用于保存 'BusinessEntities' 项目中的类型。它传递了与之前未定义类型相同的对象——再次使用泛型机制。
  • 'DeleteEntity/DeleteEntities' 区域包含根据过滤器删除单个或多个元素的方法。
  • Filters 区域只包含一个 static 方法,用于添加此类过滤器。
  • 在 Other 区域中添加的代码会打破系统的架构,但在某些情况下非常有用。它可以接受存储过程的名称然后执行它。当需要除了实体类四个主要属性之外的其他过程时,可以使用此选项。这使得架构不是那么一致,但总比创建一个必须继承具有与类方法结果相同类型的新实体类要好。

Business 项目

此项目也像我之前解释的两个项目一样,是一个类库项目。这是实现业务规则的地方。它只包含一个 'BLBase' 类。此类包含直接调用 'DBAccess' 类库项目的方法。

图 3:使用 Microsoft Visual Studio 2008 类设计器制作的 Business 项目类图

从类图可以看出,其中包含对数据库进行操作的非常重要的方法。'DeleteEntity' 方法根据元素的 ID 删除一个元素。'DeleteEntities' 方法根据过滤器条件(FilterInfo 类型对象数组)删除多个元素。'ExecuteProcedure' 方法执行存储过程。存储过程的名称作为参数传递。与方法通过反射获取名称不同。'GetEntities' 方法返回 DataSource 类型的对象。根据输入参数,有几种这种方法的实现。GetEntitiesPaging 方法也返回 DataSource 对象,但其中包含由 Paging 类型输入参数确定的记录。通过此参数传递开始记录、最后记录和记录数。有了这些输入数据,存储过程就可以只返回必要的记录。'GetEntity' 方法根据 ID 返回特定类型的一个元素。'SaveEntity' 方法根据传递对象的 ID 属性保存特定对象的更改或创建一个新对象。它返回创建的对象或更新的对象。

BusinessActions 项目

这是另一层。它不是必需的,但很有用,因为它将数据返回方法与 aspx 页面或项目中其他演示文稿文件的业务逻辑分离开来。

图 4:BusinessActions 项目

ConsoleTestApplication 项目

图 5:ConsoleTestApplication 项目

这是一个简单的控制台应用程序,仅用于调用 'BusinessActions 项目' 中的方法。

结论

在我看来,这是一个非常好的分层架构,非常易于使用。唯一的性能损失在于对象创建时。这是因为反射机制需要时间来获取类的属性、存储过程名称并进行匹配。我做了一些关于其性能的测试,结果表明,只有当从数据库返回大量对象时,这才会成为一个问题,但在实际情况中不太可能发生。如果您有任何疑问,请随时提出。我乐于接受批评。在下一篇文章中,我将向您展示一个用于对数据进行排序和过滤的存储过程。获取此类过程中数据的 C# 代码已包含在代码中,因此如果您不理解,请不用担心。(例如:Reflection.cs 文件中的 GetEntitiesPaging 过程)

© . All rights reserved.