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

C# 对象克隆 - 适用于业务应用程序

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.38/5 (5投票s)

2009 年 3 月 28 日

CPOL

4分钟阅读

viewsIcon

23385

严格来说这不是一种新的克隆技术,只是一种为了满足业务应用需求而编写代码的方式。

引言

最近,我在 C# 中寻找不同的对象克隆技术,以便在业务逻辑层实现一个简单的目标。我需要创建从数据库加载对象的快照以便进行修改。快照应该存储原始对象的所有属性(包括值类型和引用类型),并且应该是它们的副本,而不是任何引用。快照之后将由多个业务逻辑检查和验证使用,这些检查和验证需要比较当前值或用户提出的值(Proposed Value)与数据库中旧的或持久化的值(Persisted Value)。快速分析会发现这需要对象的深拷贝(Deep Copy),于是我开始搜索各种克隆和深拷贝技术。

我在这里找到了一篇关于不同克隆和拷贝技术的好分析比较。

尽管我发现二进制序列化技术非常诱人,并且正准备将其实现到我的业务逻辑层,但第二次思考迫使我从不同的角度看待问题。经过分析,我的目标如下:

  • 在业务对象从数据库加载后,立即创建其深拷贝快照。
  • 使用快照比较旧值(数据库持久化)与新值(用户提出)以进行验证和其他业务层用途。
  • 代码应该易于理解,并且也易于初学者实现(我们有许多初级程序员)。
  • 代码应该易于维护 - 我们不必担心在每次更改业务对象的属性时都要更新复制/克隆代码。
  • 性能 - 任何克隆和/或复制机制都会对性能产生影响,所以这只是选择可接受的影响程度的问题。就我而言,我理想情况下会尽量避免以下情况,因为它们会影响性能:
    1. 多次从数据库获取数据
    2. 使用反射
    3. 使用序列化

看起来剩下的唯一方法是手动复制属性,但这将在每次向业务对象添加/更新属性时意味着需要精心维护复制代码。

考虑到以上所有方法的优缺点,我采用了一种*编码风格*,它帮助我以最小的开销实现了创建对象快照的目标。我再次强调,这只是一种编码风格,并非什么高深技术。只是想在适合您目标的情况下分享一下。

使用代码

在我的架构中,我拥有业务对象,它们基本上是数据容器,更像是 DTO(数据传输对象),还有业务对象控制器。DTOs 仅仅是数据容器,其中没有任何业务逻辑,而业务对象控制器类负责将数据加载/保存/验证进出 DTOs。所有业务逻辑都仅存在于各自的业务对象控制器中。坦率地说,我认为这种架构非常简单,但又足够成熟,能够满足典型的中小型业务应用目标,并且可以扩展到企业级。我将把架构讨论留到我下一篇文章的话题。

回到正题,业务对象控制器中的代码包含公共方法,如 New()Get(id)New() 方法为数据输入创建一个空白的业务对象模板,而 Get(id) 方法从数据库返回具有指定 ID 值的一个对象。这些方法首先获取由数据访问层提供的相关数据集,然后将数据集中的值加载到相应的对象中。

然后,我向我的业务对象添加了两个属性,即 oldcurrent。'current' 属性将包含当前对象的引用,而 'old' 属性将包含同一对象的另一个副本的引用。那个“另一个”副本就是字面意思,它是一个不同的副本,位于不同的内存位置,即使当前副本被编辑也不会被触及。因此,每当需要旧值引用进行比较时,我都可以通过 *myobject.old.myproperty* 获取;当前对象属性可以作为 *myobject.current.myproperty* 或简单地通过 *myobject.myproperty* 来引用。

以下是一个以“User”业务对象为例的示例。

namespace TestApp.Security.BLL.User
{
    public class UserController : BusinessController
    {
        ...
        ...
        // Public access methods on the controller
        public UserData New()
        {
            return this.NewUser();
        }
        public UserData Get(int userId)
        {
            return this.GetUser();
        }
        ...
        ...
        ...
        // Private methods, mainly work as helpers to public methods
        // 'da' is the Data Access Layer object implemented
        // with Enterprise Library Data Access
        private UserData NewUser()
        {
            // Creates a new user (returns a blank UserData object for data entry)
            // Note that the da.NewUser executes
            // a "SELECT * FROM Users WHERE 1 = 2" to get the 
            // schema and then appends a blank row
            // lke DataSet.Tables[0].Rows.Add(). So this NewUser
            // method in business logic layer gets blank row to load in the object.

            UserData current, old;
            DataSet dsUser = da.NewUser();
            DataSet dsUserRoles = da.GetUserRoles(0);
            current = this.LoadData(dsUser, dsUserRoles);
            old = this.LoadData(dsUser, dsUserRoles);
            current._old = old;
            return current;
        }
        // Gets an existing user from the database
        private UserData GetUser(int userId)
        {
             UserData current, old;
             DataSet dsUser = da.GetUser(userId);
             DataSet dsUserRoles = da.GetUserRoles(userId);
             current = this.LoadData(dsUser, dsUserRoles);
             old = this.LoadData(dsUser, dsUserRoles);
             current._old = old;
             return current;
        }
        ...
        ...
        private UserData LoadData(DataSet dsUser, DataSet dsUserRoles)
        {
            // Get an empty business object (DTO) first
            UserData user = new UserData();

            // Get the row
            DataRow drUser = dsUser.Tables[0].Rows[0];
            // Load the DTO (loading directly to internal field variables)
            user._id = (int)ConvertDBNullToString(drUser["Id"], 0);
            user._name = (String)ConvertDBNullToString(drUser["Name"], "");
            user._fullName = (String)ConvertDBNullToString(drUser["FullName"], "");
            ...
            ...
            return user;
        }
    }
 
    // The UserData class is implemented as 
    public class UserData
    {
        // Fields and variables
        internal int _id;
        internal string _name, _fullname;
        internal UserData _old;

        // Public properties
        public UserData Old
        {
            get { return _old; }
            internal set
            {
                _old = value;
            }
        }
        public UserData Current
        {
            get { return this; }
        }
    }
}

就是这样。底线是我在 GetUserNewUser 方法中两次调用 LoadData,一次加载数据到当前对象,第二次创建一个新对象并将其保留在当前对象本身的“old”属性中。尽管这似乎意味着即使我们不需要旧值快照,相同的代码(LoadData)总是会运行两次,但在我的情况下,为了在业务逻辑层工作,几乎总会受益于在业务对象中同时拥有旧值和新值。

关于性能,我认为在进程内存中运行几行赋值代码(从数据集到对象)比直接与数据库进行检查(在需要时获取旧值)或使用反射或二进制序列化要便宜得多。

这样就达到了我的目的,并且仅仅存储对象的旧值快照是一种简单的*编码风格*,您可以使用。显然,可能还有其他更优雅的方法和解决方案,我很想了解更多。您可以通过 rajabasuroy@hotmail.com 联系我。

© . All rights reserved.