代码改变世界

较为理想的延迟代理的编写方式

2009-09-07 11:15 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

之前我谈到,在普通情况下我们可以很轻松地写出过一个代理类,用来处理延迟加载的情况。当时给出了一个很简单的做法,也就是指创建基类,覆盖它的一些属性实现,类似这种:

public class LazySomeClass : SomeClass
{
    public override int SomeID
    {
        get
        {
            return this.LazySomeID.Value;
        }
        set
        {
            this.LazySomeID.Value = value;
        }
    }
 
    public Lazy<int> LazySomeID { get; set; }
}

不过我当时也提到,这么做可能够用,但是也有一些缺点。例如,它破坏了SomeID属性中包含的业务逻辑。可能SomeID原本会包含一些验证逻辑,或和另外一个属性加以同步,或发起INotifyPropertyChanging/Changed中的事件。这也是我认为NHibernate的延迟加载方法欠妥的原因,至于其他还有一些缺陷有机会在讨论。

因此我又想了想,理想中的延迟加载方式应该是什么样的呢?例如,同样是个SomeClass类,其中部分属性允许“设置”延迟加载:

public class SomeClass
{
    public SomeClass() { }

    public SomeClass(int i) { }

    public virtual int LazyInt { get; set; }

    public virtual bool LazyBoolean { get; set; }

    public int EagerInt { get; set; }

    public bool EagerBoolean { get; set; }

    // some other members...
}

如果是一个较为合理的延迟代理类,我认为它的写法应该是这样的:

public class LazySomeClass : SomeClass
{
    public override int LazyInt
    {
        get
        {
            if (!this.m_lazyIntLoaded)
            {
                if (this.m_lazyIntLoader != null)
                {
                    base.LazyInt = this.m_lazyIntLoader();
                    this.m_lazyIntLoader = null;
                }

                this.m_lazyIntLoaded = true;
            }

            return base.LazyInt;
        }
        set
        {
            base.LazyInt = value;
            this.m_lazyIntLoaded = true;
            this.m_lazyIntLoader = null;
        }
    }

    private bool m_lazyIntLoaded = false;
    private Func<int> m_lazyIntLoader = null;
    public Func<int> LazyIntLoader
    {
        get
        {
            return this.m_lazyIntLoader;
        }
        set
        {
            this.m_lazyIntLoader = value;
            this.m_lazyIntLoaded = false;
        }
    }
}

如果我们需要为LazyInt属性设置延迟加载,那么可以设置LazyIntLoader属性,它是一个Func<int>委托对象。这种实现方式看上去复杂,不过它有一定的合理性:

  1. 每个Loader只执行一次,直到提供新的Loader。
  2. Loader执行后,会赋值给base.LazyInt,保持基类的业务逻辑。
  3. 从base.LazyInt读取,同样保持基类的业务逻辑。
  4. 如果不需要延迟加载,那么属性的行为保持不变。

其中第4点非常重要,这意味着这是一种可以“标准化”的延迟加载代理类的标准写法。我们可以在运行时使用Emit生成新的类型,继承目标类,为每个virtual属性在子类中重写一份。由于在默认情况下属性的行为不会改变,因此这样的代理类不会有问题。甚至,“辅助类库”的接口我也想好了:

var builder = LazyFactory.Create(() => new SomeClass(10)
{
    EagerInt = 10,
    EagerBoolean = true
});

SomeClass value = builder
    .Setup(c => c.LazyInt, () => GetLazyValue<int>())
    .Setup(c => c.LazyBoolean, () => GetLazyValue<bool>())
    .Create();

您有兴趣实现一下吗?