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

业务对象简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.28/5 (18投票s)

2007年5月18日

CPOL

12分钟阅读

viewsIcon

70150

downloadIcon

1098

本文将介绍RockFord Lhotka撰写的《Expert C# 2005 Business Object》一书,该书面向业务应用程序开发人员。

下载源代码.zip - 319.1 KB

mainwindow.png

editwindow.png

引言

本文将介绍RockFord Lhotka撰写的《Expert C# 2005 Business Object》一书,该书面向业务应用程序开发人员。书中主要内容是关于业务对象(BO),以及如何在各种实际场景中实现它们。本书基于CSLA .NET框架,这是一个生产级的框架,涵盖了许多软件开发周期,包括系统架构设计(物理设计和逻辑设计)、编码和单元测试。书中揭示了框架内部每个类的工作原理,因此对新入门者来说学习曲线相当陡峭。在本系列文章中,我将从软件开发人员的角度介绍CSLA.NET框架。CSLA .NET框架是开源的,其许可证相当宽松。它允许您修改它来创建其他商业或业务软件;您只是不能将框架本身进行修改并作为产品出售。

什么是业务对象?

BO通常代表一组相关的业务功能,并且是一个根据实现了IBussinessObject接口(如下所示)的任何可序列化类实例化的对象。

public interface IBusinessObject
{
}  
正如您所见,任何类都可以成为BO类。但是,为了充分利用框架,您可能希望从更有用的类派生您的BO类,例如BusinessBase<T>、BusinessListBase<T,C>、ReadOnlyBase<T>等,具体取决于您要创建的BO类型。BusinessBase<T>是您将使用的最基本且最有用的BO类。一旦声明了BO类,您就需要声明对象的属性,设置业务规则(用于在将BO保存到数据存储之前验证BO是否有效),创建工厂方法来创建新的BO或检索现有的BO,创建CRUD操作来创建、检索、更新和删除自身,以及任何可能帮助BO消费者使用的附加辅助函数。仅此而已。然后框架将负责其余部分。我将在接下来的示例中更详细地展示。

框架的优势

每个人都会问,为什么我需要CSLA.NET框架,它为已经足够复杂的应用程序增加了额外的复杂性。答案很简单。它为您从每个业务应用程序都会遇到的繁琐业务需求中解脱出来。它们是
  • 集中验证
  • Authorization
  • 事务
  • 高可伸缩性和高性能
  • N级撤销
其他好处是
  • 单元测试
  • 数据绑定支持

集中验证

它允许集中验证业务对象。每个业务对象都负责验证其自身的状态以及它包含的子BO(如果有)。例如,在ASP.NET编程中,我们可以通过JavaScript在浏览器中放置任何用户输入的验证,例如地址行最多只能包含25个字符,并希望用户不要禁用JavaScript执行。或者,我们可以在ASPX页面的代码隐藏文件中放置验证以拒绝任何无效输入。或者,我们可以通过数据库中的存储过程来验证输入。或者XSLT文件。或者XSD文件。我曾在一个项目中看到验证代码散布在上述所有地方。当然,我们可以将所有这些结合起来,但这会导致代码重复并产生维护问题。一个更清晰的解决方案是使用CSLA .NET框架中的BO。BO类负责验证,并维护BO将根据其进行验证的规则列表。在我们的ASP.NET示例中,代码隐藏文件将读取业务规则列表,并通过验证控件设置JavaScript验证例程。如果JavaScript验证执行失败,也没关系,因为验证控件将进行服务器端检查,最终在保存到数据库之前将验证BO。CSLA.NET框架中的数据库仅作为永久数据存储库,不包含任何业务逻辑。

Authorization

您可以向BO添加基于字段/属性的授权。默认情况下,它是广泛开放的,这意味着BO的消费者将对BO的所有属性具有读/写访问权限。然而,在某些情况下,并非所有字段都对BO的所有消费者可用。可以通过AuthorizationRules属性授予访问权限。一旦为属性设置了AuthorizationRules,除授权的消费者外,该属性将不再对BO的所有消费者可用。授权也可以基于每个对象实例。

事务

一个BO可以包含其他子BO。在在线购物车示例中,购物车是一个BO,其中包含许多商店商品,这些商品是子BO。在CSLA.NET的术语中,我们将购物车BO称为可编辑根对象,每个商店商品都是可编辑子对象。通常,可编辑子对象不能独立存在;它必须是可编辑根对象的一部分。由于我们知道BO作为与它们的消费者的一致性接口,因此可编辑根对象显然是支持事务以确保在保存BO及其子BO时保持一致状态的一个好选择。此设计的好处是,BO的消费者不知道事务的存在。它所知道的是,操作要么成功,要么完全失败,在这种情况下,什么也不会保存,并且会抛出异常。CSLA.NET框架不规定事务代码应如何实现或它可以与什么类型的数据源通信。这取决于程序员来实现它。

高可伸缩性和高性能

通过利用CSLA.NET框架,您的应用程序可以部署在客户端-服务器环境中,在这种环境中,您的应用程序直接与数据库或其他数据源通信。或者,无需更改一行代码,您就可以将应用程序部署到3层环境中。在3层环境中,您的应用程序与应用程序服务器通信,该服务器通过连接池机制与数据库或其他数据源通信来创建您的BO。这大大减少了对数据库服务器的争用。您甚至可以将您知道您的BO可能在下一次请求中使用的数据进行缓存,或在BO之间共享,以进一步提高应用程序的可伸缩性。在3层部署中,尽管对于任何单个请求,它可能需要经过更多的步骤并花费更多时间,但可伸缩性的提高带来了更高的吞吐量和更好的整体系统性能。

N级撤销

可编辑根对象支持无限级别的撤销,而无需从数据库重新加载数据。您可以获取BO的快照并进行任何更改,然后将其恢复到原始状态。此功能主要方便UI开发人员编辑复杂的BO。在我们的购物车示例中,在进入商店商品对象编辑页面之前,可以通过调用CopyState()函数获取订单BO的快照。用户可以修改商店商品BO,然后他们可以选择保存更改(在这种情况下将调用AcceptChanges())或取消更改(在这种情况下将调用UndoChanges())。这是1级撤销。为任何可编辑根对象添加N级撤销并非易事。幸运的是,该框架开箱即用地支持它。

单元测试

单元测试被认为是软件开发的最佳实践。然而,它常常被大多数实现所忽略。如果业务逻辑散布在代码库的各个地方,例如XSLT、Visual Basic、ASP、SQL存储过程、ASP.NET和WinForm应用程序,那么编写单元测试以确保应用程序符合业务需求几乎是不可能的。即使在这种情况下您能够编写单元测试,它通常也无法证明在编写任何代码之前所付出的努力是值得的。然而,如果您基于BO的概念构建应用程序,情况就会完全不同。由于BO具有明确定义的接口并封装了业务逻辑及其数据的所有信息,因此编写单元测试以确保BO满足业务需求是完全可能的。其中一些单元测试代码甚至可以复制粘贴到您的应用程序中,以使单元测试成为编码实践的一部分更具吸引力。

数据绑定支持

可编辑根对象内置了对Winform应用程序数据绑定的支持。正如我之前提到的,由于BO为每个属性都有验证规则,当某个字段不通过验证时,将会有视觉指示。不需要代码。您只需创建一个ErrorProvider控件,并将其DataSource属性指向BindingSource对象即可。如下图所示。对于ASP.NET应用程序,数据绑定仍然可以正常工作,但需要一些调整。有关更多信息,请参阅Rockford Lhotka的博客。http://www.lhotka.net/weblog/ASPNET20DataBindingFrustration.aspx

我们的第一个BO

我们先从用户BO开始(完整的源代码请参见zip文件)。

步骤1 声明类

初次看到上面的声明可能有些令人困惑。基类BusinessBase<T>的声明是为了允许基类的一些方法调用返回类型为T的对象。例如,如果您想克隆对象并检索Name属性,您可以编写代码,例如str = user.Clone().Name,而无需在代码中进行类型转换。Clone是从基类继承的,Name是派生类的属性。
    public class User : BusinessBase<User>
    {
    } 

步骤2 定义属性

这很简单。您需要做的就是调用PropertyHasChanged()方法,告知基类该值已被更新。它将仅触发对属性的验证。
        protected int _userID = 0;
        protected string _name = string.Empty;

        public  int UserID
        {
            get
            {
                return _userID;
            }
        }

        public  string Name
        {
            get
            {
                return _name;
            }
            set
            {
                if (value == null) value = string.Empty;
                if (!_name.Equals(value))
                {
                    _name = value;
                    PropertyHasChanged("Name");
                }
            }
        }
        //... more properties declaration 

步骤3 创建业务规则

您可以使用下面的一些预建规则。或者您可以创建自己的规则。添加规则后,您始终可以使用IsValid属性来确定BO是否处于有效状态。CSLA.NET框架内部也使用此标志来确定BO是否可以保存。在我们的演示程序中,我们使用此属性来启用/禁用“保存”按钮。
        protected override void AddBusinessRules()
        {
            ValidationRules.AddRule(CommonRules.StringRequired, "Name");
            ValidationRules.AddRule(CommonRules.StringRequired, "FullName");
            ValidationRules.AddRule(CommonRules.StringMaxLength, new CommonRules.MaxLengthRuleArgs("FullName", 15));
        } 

步骤4 创建工厂方法

工厂方法通常会调用DataPortal.XYZ方法来检索对象。CSLA.NET框架明确反对使用类构造函数。所有BO都通过工厂方法创建。DataPortal.XYZ方法提供了创建对象的独立于位置的方式。在后台,框架可能会在远程服务器(通常是应用程序服务器)上创建对象,将其序列化,传输,然后在本地机器上反序列化。我们知道,当您通过MS DotNet库反序列化对象时,它将返回一个对象,并且没有有效的方法可以反序列化到现有对象。这就是为什么不使用类构造函数的原因。
        public static User GetUser(int userID )
        {
            return DataPortal.Fetch<User>(new Criteria(userID));
        }

        public static User NewUser()
        {
            return DataPortal.Create<User>();
        } 

步骤5 创建DataPortal_XYZ方法

根据您要创建的BO类型,您可以选择实现一些DataPortal_XYZ方法。总共有5种,分别是DataPortal_Create、DataPortal_Fetch、DataPortal_Update、DataPortal_Insert和DataPortal_Delete。顾名思义,DataPart_XYZ方法提供了一种从数据源创建、获取/检索、更新、插入和删除当前对象的方式。所有DataPortal_XYZ方法仅由CSLA.NET框架调用,具体取决于您调用的基类方法以及BO的状态。场景1。您正在创建新的BO,将调用DataPortal_Create。场景2。您正在创建新的BO并调用保存方法,将调用DataPortal_Insert来保存当前BO。场景3。您正在从数据源检索BO,将调用DataPortal_Fetch。检索BO后,您更改属性值并保存,将调用DataPortal_Update。场景4。如果您正在从数据源检索BO,调用删除方法并保存BO,将调用DataPortal_Delete。这里的很多代码都是样板代码。所有DataPortal_XYZ方法的实现,请参见附加的zip文件。我只想在此展示DataPortal_Fetch方法。有一点需要注意的是名为criteria的函数参数,它通常存储BO的主键。在工厂方法GetUser中,DataPortal.Fetch<User>(new Criteria(userID))的参数与DataPortal_Fetch方法的参数是同一个对象(按值传递)。
private void DataPortal_Fetch(Criteria criteria)
        {
            using (SqlConnection cn = new SqlConnection(Database.Connection))
            {
                cn.Open();

                ExecuteFetch(cn, criteria);
            }//using
        }

        protected void ExecuteFetch(SqlConnection cn, Criteria criteria)
        {
            using (SqlCommand cm = cn.CreateCommand())
            {

                cm.CommandType = CommandType.Text;
                cm.CommandText = "Select UserID, Name, FullName, Description from UserTable where @UserID=UserID;";
                cm.Parameters.AddWithValue("@UserID", criteria.UserID);

                using (SafeDataReader dr = new SafeDataReader(cm.ExecuteReader()))
                {
                    FetchObject(dr);
                    ValidationRules.CheckRules();
                }
            }//using
        }

        protected void FetchObject(SafeDataReader dr)
        {
            dr.Read();
            _userID = dr.GetInt32("UserID");
            _name = dr.GetString("Name").TrimEnd();
            _fullName = dr.GetString("FullName").TrimEnd();
            _description = dr.GetString("Description").TrimEnd();
        } 

单元测试

由于User BO具有明确定义的接口并封装了所有数据,因此编写单元测试变得非常简单。这是创建新用户的测试。
       [TestMethod()]
        public void UserNewTest()
        {
            User u;

            u = UserGroupLibrary.User.NewUser();
        } 
测试将一个新的用户BO插入到数据源中。
        [TestMethod()]
        public void UserInsertTest()
        {
            User u;
            u = GetValidNewUserObject();
            u.Save();
        } 
单元测试业务规则也同样简单。
        [TestMethod()]
        public void UserBussinessRuleTest()
        {
            User u;

            // initial validation, as a starting point
            u = GetValidNewUserObject();
            Assert.IsTrue(u.IsValid, "Valid user BO failed to pass test!");

            // Name field is required
            u = GetValidNewUserObject();
            u.Name = null; 
            Assert.IsFalse(u.IsValid, "Business rule (Name field is required)for User BO failed to pass test!");

            // FullName field is requried
            u = GetValidNewUserObject();
            u.FullName = null;
            Assert.IsFalse(u.IsValid, "Business rule (FullName field is required)for User BO failed to pass test!");
            
            // FullName field can not exceed 15 chars, test both positive and negative tests
            u = GetValidNewUserObject();
            u.FullName = string.Empty.PadLeft(15, 'x'); 
            Assert.IsTrue(u.IsValid, "Business rule (FullName field can not exceed 15 chars)for User BO failed to pass test!");
            u.FullName = string.Empty.PadLeft(16, 'x');
            Assert.IsFalse(u.IsValid, "Business rule (FullName field can not exceed 15 chars)for User BO failed to pass test!");
        } 

与WinForm应用程序的数据绑定

一旦用户BO创建并经过单元测试,我们就可以在WinForm应用程序中使用它。以下步骤演示了如何创建一个用户控件来封装用户BO编辑屏幕。

步骤1
创建一个空的自定义控件

步骤2
创建一个BindSource对象并指向我们的User BO

Screenshot - bindsource.png

步骤3
创建显示BO属性的控件。下图显示了将文本框控件绑定到数据BindingSource。 截图 - textboxbinding.png

步骤4
(可选)创建一个ErrorProvider对象,并将其DataSource属性设置为步骤1中创建的BindSource对象。它提供了任何验证错误的视觉指示。
截图 - errorproviderbinding.png

步骤5
在运行时分配BO。这在自定义控件的构造函数中完成,在该控件显示任何内容之前。
     UserBindingSource.DataSource = user;

步骤6
在主WinForm类中挂钩它。只需一行代码即可使用自定义控件。是不是没费什么功夫?
new UserEdit(User.GetUser(Int16.Parse(textBox1.Text)), this);
我们刚刚创建的WinForm应用程序具有CSLA.NET应用程序的所有特征。它具有高度的可伸缩性,组织良好(这转化为低维护成本)并且经过单元测试。

结论

在本文中,我演示了如何使用CSLA.NET框架创建简单的程序。我希望我能有时间写一篇续集,介绍CSLA.NET中的其他有用类以及用于创建即用型BO的代码生成技术。事实上,此项目中的许多代码都从名为CSLAcontrib的代码生成程序中提取出来,以便新用户能够理解。希望您到目前为止还喜欢。

我还要借此机会感谢Rogerio Caceres在撰写本文中的支持。
© . All rights reserved.