2004-10-3+ 学习PetShop3.0(6)实体模型
还记得用户注册时收集信息的方式吗?下面这几句:
//......
AddressInfo address = addr.Address;
//.....
AccountInfo accountInfo = new AccountInfo(userId, password, email, address, language, favCategory, showFavorites, showBanners);
//.....
ccountController.CreateAccount(accountInfo)
AddressInfo和AccountInfo都是瘦实体类,整个程序都用这种模型来完成信息传递。这些类的定义都在Model组件中。应该是整个petshop3.0中最重要的组件之一。
Model:瘦数据类或业务实体,在应用程序各层之间传递数据的瘦数据类。这些是用 C# 类实现的,每个字段都以属性的形式公开。 每个类都标记为“serializable”,启用进程间传输。
.net是面向对象的,这大家都知道,可以在具体实现的时候应该怎么做呢?总之我是完全的不明白。不过看了petshop3.0中Model组件的应用,算是又长了见识。
在程序中总有那么一些信息可以归为一类,比如商品信息,用户信息……拿用户信息来说,应该有用户名、密码、住址、电话、个人爱好等具体的信息。在petshop3.0,这些就可以总归为一个实体,而其中的信息都是这个类的属性,在其中住址、电话等信息又可以单独的归为一个实体,因为他们是描述个人具体信息的,并且是可变的(描述的不是很准确,可能有错误)。所以我们看到petshop3.0把这些信息归成一个AddressInfo实体,而该实体又成为AccountInfo实体的一个属性。具体应用的时候,这些实体都是作为一个整体使用,因为你总不能把一个人切开吧?再从c#的角度看,这些都是类型,可以看成用户的自定义类型,所以我们看到AddressUI这个用户控件把AddressInfo做为属性对外公开。
实体模型的使用贯穿了整个petshop3.0,从表面看,几乎每一层都引用了Model组件。还是以用户这块来说下去,表示层收集数据的时候,会把用户填的信息作为一个实体来收集,完成后用户接口处理组件会把信息实体做为参数传送给业务逻辑组件,业务逻辑部分再把信息实体做为参数经工厂传送给数据处理组件,在这里做最后处理,提交到数据库或返回错误。
在整个过程中,根本就不存在单独的值类型的传递。这样做的好处很明显,使逻辑操作更加透明和易于理解,便于构建复杂系统的模型,可以封装一些不需要向程序公开的信息,等等。
此类代码都处都是,一个比较明显的例子就是CartItemInfo,该实体模型表述放到购物车内的商品。该实体并不保存到数据库,完全是存在于进程中的。由于该实体的存在,大大简化了购物车这一抽象实体的组成。
实际上,在Model组件中根本就没有Cart这个实体。Cart的实现由BLL组件来完成。这个Cart并非和上面一样的瘦实体类,而是另外一种实体模型,因为它不仅包含着作为实体本身的信息,而且还有自己的行为。按照msdn上的叫法,这是“带有CRUD行为的业务实体”。我们可以把这个Cart看成一个智能机器人,他会自己挑选商品,并给出现在车内的信息。
通过类图来对比一下Cart和CartItemInfo。
CartItemInfo只有属性,而Cart则包括属性和方法。
要注意的是,Cart归属于业务逻辑组件,这是因为它的作用更多的是执行其行为,它的最主要的属性就是一个索引器,通过这个来获得CartItemInfo。
最后看一下CartItemInfo的代码,了解此类模型的基本构成。
/// <summary>
/// Business entity used to model items in a shopping cart
/// </summary>
[Serializable]
public class CartItemInfo {
private const string YES = "Yes";
private const string NO = "No";
// Internal member variables
private int _quantity = 1;
private bool _inStock = false;
private string _itemId = null;
private string _name;
private decimal _price;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="itemId">Every cart item requires an itemId</param>
public CartItemInfo(string itemId) {
this._itemId = itemId;
}
/// <summary>
/// Constructor with specified initial values
/// </summary>
/// <param name="itemId">Id of item to add to cart</param></param>
/// <param name="name">Name of item</param>
/// <param name="inStock">Is the item in stock</param>
/// <param name="qty">Quantity to purchase</param>
/// <param name="price">Price of item</param>
public CartItemInfo(string itemId, string name, bool inStock, int qty, decimal price) {
this._itemId = itemId;
this._name = name;
this._quantity = qty;
this._price = price;
this._inStock = inStock;
}
// Properties
public int Quantity {
get { return _quantity; }
set { _quantity = value; }
}
public bool InStock {
get { return _inStock; }
set { _inStock = value; }
}
public decimal Subtotal {
get { return (decimal)(this._quantity * this._price); }
}
public string ItemId {
get { return _itemId; }
}
public string Name {
get { return _name; }
}
public decimal Price {
get { return _price; }
}
}
下面是msdn上关于业务实体的一些资料。顺便转过来好了:)
----原文地址------
定义自定义业务实体组件
表示业务实体的自定义类通常包含以下成员:
- 用于在本地缓存业务实体的数据的专用字段。这些字段在数据访问逻辑组件从数据库检索数据时保存数据库数据的一个快照。
- 用于访问实体的状态和访问实体内数据的子集及层次结构的公共属性。这些属性的名称可以与数据库的列名称相同,但这并不是一个绝对要求。可以根据您的应用程序的需要选择属性名,而不必使用数据库中的名称。
- 用以使用实体组件中的数据执行本地化处理的方法和属性。
- 用以通知实体组件内部状态变化的事件。
图 9 所示为使用自定义实体类的方法。注意,实体类并不知道数据访问逻辑组件或基础数据库;所有数据库访问都由数据访问逻辑组件执行,以集中数据访问策略和业务逻辑。此外,在层间传递业务实体数据的方式与表示业务实体的格式也没有直接关系;例如,可以在本地将业务实体表示为对象,而用另一种方法(如标量值或 XML)将业务实体数据传递到其他层。
图 9:自定义业务实体组件的作用(单击缩略图以查看大图像)
定义自定义业务实体组件的建议
在实现自定义实体组件时,请考虑以下建议:
- 选择使用结构还是使用类。对于不包含分层数据或集合的简单业务实体,可以考虑定义一个结构来表示业务实体。对于复杂的业务实体或要求继承的业务实体,可将实体定义为类。有关结构和类这两种类型的比较,请参阅 Structures and Classes。
- 表示业务实体的状态。对于数字、字符串等简单值,可以使用等价的 .NET 数据类型来定义字段。有关说明如何定义自定义实体的代码示例,请参阅附录中的如何定义业务实体组件。
- 表示自定义业务实体组件中的子集合和层次结构。表示自定义实体中的数据的子集合和层次结构的方法有两种:
- .NET 集合(例如 ArrayList)。.NET 集合类为大小可调的集合提供了一个方便的编程模型,还为将数据绑定到用户界面控件提供了内置的支持。
- DataSet。DataSet 适合于存储来自关系数据库或 XML 文件的数据的集合和层次结构。此外,如果需要过滤、排序或绑定子集合,也应首选 DataSet。
有关说明如何表示自定义实体中数据的集合和层次结构的代码示例,请参阅附录中的如何表示自定义实体中数据的集合和层次结构。
- 支持用户界面客户端的数据绑定。如果自定义实体将要由用户界面使用并且希望利用自动数据绑定,可能需要在自定义实体中实现数据绑定。请考虑以下方案:
- Windows 窗体中的数据绑定。您可以将实体实例的数据绑定到控件而不必在自定义实体中实现数据绑定接口。也可以绑定实体的数组或 .NET 集合。
- Web 窗体中的数据绑定。如果不实现 IBindingList 接口,则不能将实体实例的数据绑定到 Web 窗体中的控件。然而,如果只想绑定集合,则可以使用数组或 .NET 集合而不必在自定义实体中实现 IBindingList 接口。
有关说明如何将自定义实体绑定到用户界面控件的代码示例,请参阅附录中的如何将业务实体组件绑定到用户界面控件。
- 公开内部数据更改的事件。公开事件可以获得丰富的客户端用户界面设计,因为它使得无论数据显示在哪里都可以对其进行刷新。事件应当只针对内部状态,而不是针对服务器上的数据更改。有关说明如何公开自定义实体类中的事件的代码示例,请参阅附录中的如何公开业务实体组件中的事件。
- 使业务实体可序列化。使业务实体可序列化可以将业务实体的状态保持在中间状态而不进行数据库交互。这样可以方便脱机应用程序的开发和复杂用户界面过程的设计,即在完成前不会影响业务数据。序列化有两种类型:
- 使用 XmlSerializer 类进行 XML 序列化。如果只需要把公共字段和公共读/写属性序列化为 XML,则可以使用 XML 序列化。注意,如果从 Web 服务返回业务实体数据,对象将通过 XML 序列化自动序列化为 XML。
您可以对业务实体执行 XML 序列化而无需在实体中实现任何附加代码。然而,只有对象中的公共字段和公共读/写属性被序列化为 XML。专用字段、索引生成器、专用属性、只读属性及对象图不会被序列化。您可以使用自定义实体中的属性控制结果 XML。有关将自定义实体组件序列化为 XML 格式的详细信息,请参阅附录中的如何将业务实体组件序列化为 XML 格式。
- 使用 BinaryFormatter 或 SoapFormatter 类进行格式序列化。如果需要序列化对象的所有公共字段、专用字段及对象图,或者需要与远程服务器之间传递实体组件,则可以使用格式序列化。
格式类将序列化对象的所有公共和专用字段及属性。BinaryFormatter 将对象序列化为二进制格式,SoapFormatter 将对象序列化为 SOAP 格式。使用 BinaryFormatter 的序列化比使用 SoapFormatter 的序列化速度要快。要使用任何一个格式类,都必须将实体类标记为 [Serializable] 属性。如果需要显式控制序列化格式,您的类还必须实现 ISerializable 接口。有关如何使用格式序列化的详细信息,请参阅附录中的如何将业务实体组件序列化为二进制格式及如何将业务实体组件序列化为 SOAP 格式。
注意:还原序列化某个对象时,不会调用默认的构造函数。对还原序列化添加这项约束,是出于性能方面的考虑。
- 使用 XmlSerializer 类进行 XML 序列化。如果只需要把公共字段和公共读/写属性序列化为 XML,则可以使用 XML 序列化。注意,如果从 Web 服务返回业务实体数据,对象将通过 XML 序列化自动序列化为 XML。
定义自定义实体的优点如下:
- 代码易读。要访问自定义实体类中的数据,可以使用有类型的方法和属性,如以下代码所示:
// 创建一个 ProductDALC 对象 ProductDALC dalcProduct = new ProductDALC(); // 使用该 ProductDALC 对象创建和填充一个 ProductEntity 对象。 // 此代码假设 ProductDALC 类有一个名为 GetProduct 的方法, // 该方法使用 Product ID 作参数(本例中为 21),并返回一个 // 包含该产品的所有数据的 ProductEntity 对象。 ProductEntity aProduct = dalcProduct.GetProduct(21); // 更改该产品的产品名称 aProduct.ProductName = "Roasted Coffee Beans";
在上述示例中,产品是一个名为 ProductEntity 的自定义实体类的一个实例。ProductDALC 类有一个名为 GetProduct 的方法,后者创建一个 ProductEntity 对象,将某个特定产品的数据填充到该对象,然后返回 ProductEntity 对象。调用应用程序可以使用 ProductName 等属性访问 ProductEntity 对象中的数据,并且可以调用方法以操作该对象。
- 封装。自定义实体可以包含方法以封装简单的业务规则。这些方法操作缓存在实体组件中的业务实体数据,而不是访问数据库中的实时数据。请考虑以下示例:
// 调用一个在 ProductEntity 类中定义的方法。 aProduct.IncreaseUnitPriceBy(1.50);
在上述示例中,调用应用程序对 ProductEntity 对象调用一个名为 IncreaseUnitPriceBy 的方法。在调用应用程序对 ProductDALC 对象调用相应方法,从而将 ProductEntity 对象保存到数据库之前,这一更改并不是永久性的。
- 构建复杂系统的模型。在构建复杂域问题(在不同业务实体之间存在很多交互)的模型时,可以定义自定义实体类,从而将复杂性隐藏在经过很好定义的类接口的后面。
- 本地化验证。自定义实体类可以在其属性存取器中执行简单的验证测试以检测无效的业务实体数据。有关详细信息,请参阅如何在业务实体组件的属性存取器中验证数据。
- 专用字段。您可以隐藏不希望向调用程序公开的信息。
定义自定义实体的缺点如下:
- 业务实体集合。自定义实体表示的是单个业务实体,而不是一个业务实体集合。要保存多个业务实体,调用应用程序必须创建一个数组或一个 .NET 集合。
- 序列化。您必须在自定义实体中实现自己的序列化机制。可以使用属性来控制实体组件的序列化方式,也可以通过实现 ISerializable 接口来控制自己的序列化。
- 表示业务实体中的复杂关系和层次结构。您必须在业务实体组件中实现自己的关系和层次结构表示机制。如前面所述,DataSet 通常是实现这一目的的最简单方式。
- 搜索和排序数据。您必须定义自己的机制来支持实体的搜索和排序。例如,可以通过实现 IComparable 接口以便将实体组件保存在一个 SortedList 集合或 Hashtable 集合中。
- 部署。您必须在所有物理层部署包含自定义实体的程序集。
- 支持企业服务 (COM+) 客户端。如果一个自定义实体将由 COM+ 客户端使用,则必须为包含该实体的程序集提供一个严格名称,并且必须在客户端计算机上注册。通常,该程序集安装在全局程序集缓存中。
- 可扩展性问题。如果修改了数据库架构,则可能需要修改自定义实体类并重新部署程序集。
定义带有 CRUD 行为的自定义业务实体组件
在定义一个自定义实体时,可以提供方法以完全封装对基础数据访问逻辑组件的 CRUD 操作。这是比较传统的面向对象的方法,可能适用于复杂的对象域。客户端应用程序不再直接访问数据访问逻辑组件类,而是创建一个实体组件并对该实体组件调用 CRUD 方法。这些方法将调用基础的数据访问逻辑组件。
图 10 所示为带有 CRUD 行为的自定义实体类的作用。
图 10:带有 CRUD 行为的自定义业务实体组件的作用(单击缩略图以查看大图像)
定义带有 CRUD 行为的自定义实体类的优点如下:
- 封装。自定义实体可以封装由基础数据访问逻辑组件定义的操作。
- 与调用程序的接口。调用程序必须只处理一个接口来保持业务实体数据。不必直接访问数据访问逻辑组件。
- 专用字段。您可以隐藏不希望向调用程序公开的信息。
定义带有 CRUD 行为的自定义实体类的缺点如下:
- 处理业务实体集合。自定义实体中的方法属于单个业务实体实例。要支持业务实体集合,可以定义静态方法以读取或返回一个数组或一个实体组件集合。
- 开发时间长。传统的面向对象方法通常比使用现有对象(如 DataSet)需要更多的设计和开发工作。