实体类的动态生成(一)

前言

在应用开发中,一般都会涉及各类 POJO/POCO 实体类(DO, DTO, BO, VO)的编写,有时这些实体类还须要实现 INotifyPropertyChanged 接口以支持属性变动通知,通常咱们都会手写这些代码或者经过工具根据数据库表定义抑或别的什么模板、映射文件之类的来生成它们。html

可是,在业务实现中每每伴随着诸如“如何简单且高效的获取某个实体实例有哪些属性发生过变动?”、“变动后的值是什么?”这样的问题,而大体的解决方法有:git

  1. 由实体容器来跟踪实例的属性变动;
  2. 改造实体类(譬如继承特定实体基类,在基类中实现这些基础构造)。

方法(1)须要配合一整套架构设计来提供支撑,也不是专为解决上述实体类的问题而设,而且实现和使用也都不够简单高效,故此略过不表。接下来我将经过几篇文章来详细阐述这些问题的来由以及解决方案,并给出完整的代码实现以及性能比对测试。github

关于源码

下面将要介绍的全部代码均位于咱们的开源系列项目(地址:https://github.com/Zongsoft),项目主要采用 LGPL 2.1受权协议,欢迎你们参与并使用(请遵守受权协议)。而本文相关的源码位于其中 Zongsoft.CoreLibrary 项目的 feature-data 分支(https://github.com/Zongsoft/Zongsoft.CoreLibrary/tree/feature-data)及其中的 /samples/Zongsoft.Samples.Entities 范例项目,因为目前我正在忙着造 Zongsoft.Data 数据引擎这个轮子,不排除后面介绍到的代码会有一些调整,待该项目完成后这些代码亦会合并到 master 分支中,敬请留意。数据库

基础版本

万里长城也是从第一块砖头开始磊起来的,就让咱们来搬第一块砖吧:性能优化

public class User
{
    private uint _userId;
    private string _name;

    // 传统写法
    public uint UserId
    {
        get {
            return _userId;
        }
        set {
            _userId = value;
        }
    }

    // C# 7.0 语法
    public string Name
    {
        get => _name;
        set => _name = value;
    }

    // 懒汉写法:仅限不须要操做成员字段的场景
    public string Namespace
    {
        get;
        set;
    }
}

以上代码特意用了三种编码方式,它们被C#编译器生成的IL没有模式上的不一样,故而性能没有任何区别,你们根据本身的口味采用某种便可,由于咱们的源码因为历史缘由可能会有一些混写,在此一并作个展现而已。架构

因为业务须要,咱们但愿实体类能支持属性变动通知,即让它支持 INotifyPropertyChanged 接口,这么简单的需求固然不在话下:工具

public class User : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private uint _userId;
    private string _name;

    public uint UserId
    {
        get => _userId;
        set {
            if(_userId == value)
                return;

            _userId = value;
            this.OnPropertyChanged("UserId"); // 传统写法
        }
    }

    public string Name
    {
        get => _name;
        set {
            if(_name == value)
                return;

            _name = value;
            this.OnPropertyChanged(nameof(Name)); // nameof 为 C# 7.0 新增操做符
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        // 注意 ?. 为 C# 7.0 新增操做符
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

一切看起来是那么完美,可是,当咱们写了几个这样的实体类,尤为是有些实体类的属性还很多时,体验就有点糟糕了。天然咱们会想到写个实体基类来实现属性变动通知的基础构造,固然,在某些特定场景也能够经过工具来生成相似上面这样的C#实体类文件,但工具生成的方式有必定局限性而且不易维护(譬如须要在生成的代码基础上进行特定改造),在此再也不赘述。性能

实体基类

在进行基础类库或API设计的时候,我有个建议:__从应用场景开始__。具体的做法是,先尝试编写使用这些API的应用代码,待各类应用场景的使用代码基本都完成后,API接口也就天然而然的肯定了。譬如,在咱们这个需求中我但愿这么去使用实体基类:测试

public class User : ModelBase
{
    private uint _userId;
    private string _name;

    public uint UserId
    {
        get => _userId;
        set => this.SetPropertyValue(nameof(UserId), ref _userId, value);
    }

    public string Name
    {
        get => _name;
        set => this.SetPropertyValue(nameof(Name), ref _name, value);
    }
}

有了这样的实体基类后,加强了功能后代码依然如第一块砖的“基础版本”同样简洁,真是高兴啊!但这就够了么,能不能把具体实体类里面的成员字段也省了,交给基类来处理呢?嗯,有点意思,试着写下应用场景代码:优化

public class User : ModelBase
{
    public uint UserId
    {
        get => (uint)this.GetPropertyValue(nameof(UserId));
        set => this.SetPropertyValue(nameof(UserId), value);
    }
}

看起来棒极了,代码变得更简洁了,真是天才啊!淡定,丧心病狂的 C# 设计者彷佛看到了这种广泛的需求,因而在 C# 5 中增长了 System.Runtime.CompilerServices.CallerMemberNameAttribute 自定义标记,C# 编译器将自动把调用者名字生成出来传递给加注了该标记的参数,所以这样的代码还能够继续简化:

public class User : ModelBase
{
    public uint UserId
    {
        get => (uint)this.GetPropertyValue();
        set => this.SetPropertyValue(value);
    }
}

可是,属性的 getter 里面的那个类型强制转换,怎么看都像是一朵“乌云”啊,能不能把它也去掉呢?嗯,利用C#的泛型类型推断能够完美解决它,继续强势进化:

public class User : ModelBase
{
    public uint UserId
    {
        get => this.GetPropertyValue(() => this.UserId);
        set => this.SetPropertyValue(() => this.UserId, value);
    }
}

哇喔,有点小崇拜本身了,这代码漂亮的一批!至此,实体基类的API接口基本肯定,已经火烧眉毛想要去实现它了。

提示:因为采用 CallerMemberNameAttribute 自定义标记的参数会致使 C# 编译器要求该参数必需有默认值,所以有些 SetPropertyValue(...) 方法重载版本中 propertyName 参数须要位于参数集的最后,为了与上面的范例代码对应就省略了这些参数的标记,并保持与原有范例相同的签名设计。

using System;
using System.Linq.Expressions;

public class ModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected object GetPropertyValue([CallerMemberName]string propertyName = null);
    protected T GetPropertyValue<T>(Expression<Func<T>> property);

    protected void SetPropertyValue<T>(string propertyName, ref T field, T value);
    protected void SetPropertyValue<T>(string propertyName, T value);
    protected void SetPropertyValue<T>(Expression<Func<T>> property, T value);
}

实体基类的实现主要思路就是采用字典来记录各属性的变动值,有了这个基础,要继续增长诸如“获取哪些属性发生过变动”之类的需求天然就很容易了:

public class ModelBase : INotifyPropertyChanged
{
    // other members

    public bool HasChanges(params string[] propertyNames);
    public IDictionary<string, object> GetChangedPropertys();
}

具体的代码就不在这里贴出了,有兴趣的能够参考:https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/master/src/Common/ModelBase.cs,从功能角度上看,目前的设计仍是不错的。可是,某些方法的设计有严重性能缺陷的,主要有如下几点:

  1. 每次读写属性都会解析Lambda 表达式的操做会产生巨大的性能损耗;
  2. 采用字典来保存实体属性值的设计机制,会致使值类型的属性读写反复被装箱(Boxing)、拆箱(Unboxing);
  3. 字典的读写效率也远低于直接操做成员字段的语言原语方式。

综上所述,虽然目前方案有性能缺陷,但应对通常场景实际上是没有问题的,并且功能和易用性方面都是很好的;可是,性能对于后台程序猿而言犹如悬在头顶的 达摩克利斯之剑,这正是这个系列文章要最终解决的问题。在此以前,若是你们有关于这个问题的性能优化方案,欢迎关注咱们的公众号(Zongsoft)留言讨论。

敬请期待更精彩的下篇,关注咱们的公众号能够第一时间看到哦!
wechat

提示

本文可能会更新,请阅读原文: https://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-1,以免因内容陈旧而致使的谬误,同时亦有更好的阅读体验。


知识共享许可协议

本做品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议<span data-type="color" style="color:rgb(79, 79, 79)"> </span>进行许可。欢迎转载、使用、从新发布,但必须保留本文的署名 钟峰(包含连接:http://zongsoft.github.io),不得用于商业目的,基于本文修改后的做品务必以相同的许可发布。若有任何疑问或受权方面的协商,请致信给我 (zongsoft@qq.com)。

相关文章
相关标签/搜索