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






3.38/5 (5投票s)
严格来说这不是一种新的克隆技术,只是一种为了满足业务应用需求而编写代码的方式。
引言
最近,我在 C# 中寻找不同的对象克隆技术,以便在业务逻辑层实现一个简单的目标。我需要创建从数据库加载对象的快照以便进行修改。快照应该存储原始对象的所有属性(包括值类型和引用类型),并且应该是它们的副本,而不是任何引用。快照之后将由多个业务逻辑检查和验证使用,这些检查和验证需要比较当前值或用户提出的值(Proposed Value)与数据库中旧的或持久化的值(Persisted Value)。快速分析会发现这需要对象的深拷贝(Deep Copy),于是我开始搜索各种克隆和深拷贝技术。
我在这里找到了一篇关于不同克隆和拷贝技术的好分析比较。
尽管我发现二进制序列化技术非常诱人,并且正准备将其实现到我的业务逻辑层,但第二次思考迫使我从不同的角度看待问题。经过分析,我的目标如下:
- 在业务对象从数据库加载后,立即创建其深拷贝快照。
- 使用快照比较旧值(数据库持久化)与新值(用户提出)以进行验证和其他业务层用途。
- 代码应该易于理解,并且也易于初学者实现(我们有许多初级程序员)。
- 代码应该易于维护 - 我们不必担心在每次更改业务对象的属性时都要更新复制/克隆代码。
- 性能 - 任何克隆和/或复制机制都会对性能产生影响,所以这只是选择可接受的影响程度的问题。就我而言,我理想情况下会尽量避免以下情况,因为它们会影响性能:
- 多次从数据库获取数据
- 使用反射
- 使用序列化
看起来剩下的唯一方法是手动复制属性,但这将在每次向业务对象添加/更新属性时意味着需要精心维护复制代码。
考虑到以上所有方法的优缺点,我采用了一种*编码风格*,它帮助我以最小的开销实现了创建对象快照的目标。我再次强调,这只是一种编码风格,并非什么高深技术。只是想在适合您目标的情况下分享一下。
使用代码
在我的架构中,我拥有业务对象,它们基本上是数据容器,更像是 DTO(数据传输对象),还有业务对象控制器。DTOs 仅仅是数据容器,其中没有任何业务逻辑,而业务对象控制器类负责将数据加载/保存/验证进出 DTOs。所有业务逻辑都仅存在于各自的业务对象控制器中。坦率地说,我认为这种架构非常简单,但又足够成熟,能够满足典型的中小型业务应用目标,并且可以扩展到企业级。我将把架构讨论留到我下一篇文章的话题。
回到正题,业务对象控制器中的代码包含公共方法,如 New()
和 Get(id)
。New()
方法为数据输入创建一个空白的业务对象模板,而 Get(id)
方法从数据库返回具有指定 ID 值的一个对象。这些方法首先获取由数据访问层提供的相关数据集,然后将数据集中的值加载到相应的对象中。
然后,我向我的业务对象添加了两个属性,即 old
和 current
。'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; }
}
}
}
就是这样。底线是我在 GetUser
或 NewUser
方法中两次调用 LoadData
,一次加载数据到当前对象,第二次创建一个新对象并将其保留在当前对象本身的“old”属性中。尽管这似乎意味着即使我们不需要旧值快照,相同的代码(LoadData
)总是会运行两次,但在我的情况下,为了在业务逻辑层工作,几乎总会受益于在业务对象中同时拥有旧值和新值。
关于性能,我认为在进程内存中运行几行赋值代码(从数据集到对象)比直接与数据库进行检查(在需要时获取旧值)或使用反射或二进制序列化要便宜得多。
这样就达到了我的目的,并且仅仅存储对象的旧值快照是一种简单的*编码风格*,您可以使用。显然,可能还有其他更优雅的方法和解决方案,我很想了解更多。您可以通过 rajabasuroy@hotmail.com 联系我。