代码改变世界

您善于使用匿名函数吗?

2008-04-04 00:10 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

尝试从缓存中获取数据,如果数据存在则返回,否则从数据源中获取数据,放入缓存,然后返回。

您是否熟悉上面这段逻辑说明?如果您的应用中大量使用了缓存,则上面这段逻辑很可能会出现许多次。例如:

CacheManager cacheManager = new CacheManager();
 
public List<User> GetFriends(int userId)
{
    string cacheKey = "friends_of_user_" + userId;
 
    object objResult = cacheManager.Get(cacheKey);
    if (objResult != null) return (List<User>)objResult;
 
    List<User> result = new UserService().GetFriends(userId);
    cacheManager.Set(cacheKey, result);
 
    return result;
}

这段逻辑似乎比较简单,不过在实际应用中,从数据源中获取数据可能不是简单地调用一个方法,而是需要多个类之间的协作,事务控制等等,而缓存的读写可能也会比上面的示例来的复杂。因此,一个可读性高的做法是提供三个独立的方法(读取缓存,读取数据源,写入缓存),使得一个拥有缓存的方法只需要简单地实现上面所提到的读写逻辑即可。

正如文章开头所说,如果您的应用中大量使用了缓存,则上面这段逻辑很可能会出现许多次。在一定程度上这种重复也是多余的,违背了DRY原则。因此我们设法提供一个基类,把这段缓存读写逻辑封装起来:

public abstract class CacheReader<T>
{
    /// <summary>从缓存中获取数据</summary>
    /// <param name="data">从缓存中取得的数据</param>
    /// <returns>从缓存中成功取得数据则返回true,反之则false</returns>
    public abstract bool GetFromCache(out T data);
 
    /// <summary>从数据源获取数据</summary>
    /// <returns>从数据源取得的对象</returns>
    public abstract T ReadFromSource();
 
    /// <summary>将数据写入缓存</summary>
    /// <param name="data">将要写入缓存的数据</param>
    public abstract void SetToCache(T data);
 
    public T Read()
    {
        T data;
        if (this.GetFromCache(out data)) return data;
 
        data = this.ReadFromSource();
        this.SetToCache(data);
 
        return data;
    }
}

于是我们将这段缓存读写逻辑集中到了CacheReader类的Read方法中。而对于每个缓存读写操作,我们只要实现一个CacheReader类的子类,提供三个抽象方法的具体实现即可。如下:

private class GetFriendCacheReader : CacheReader<List<User>>
{
    private int m_userId;
    private string m_cacheKey;
    private CacheManager m_cacheManager;
 
    public GetFriendCacheReader(int userId, CacheManager cacheManager)
    {
        this.m_userId = userId;
        this.m_cacheKey = "friends_of_user_" + userId;
        this.m_cacheManager = cacheManager;
    }
 
    public override bool GetFromCache(out List<User> data)
    {
        object objData = this.m_cacheManager.Get(this.m_cacheKey);
        if (objData == null)
        {
            data = null;
            return false;
        }
 
        data = (List<User>)objData;
        return true;
    }
 
    public override List<User> ReadFromSource()
    {
        return new UserService().GetFriends(this.m_userId);
    }
 
    public override void SetToCache(List<User> data)
    {
        this.m_cacheManager.Set(this.m_cacheKey, data);
    }
}

于是我们的GetFriends方法就可以修改成如下模样:

public List<User> GetFriends(int userId)
{
    return new GetFriendCacheReader(userId, cacheManager).Read();
}

典型的“模板方法(Template Method)”模式的应用,真是……优雅?这是明显的“矫枉过正”!一个GetFriends方法需要开发一个类,而应用中少说也该有几十上百个这样的方法吧?于是乎,我们吭哧吭哧地开发了几十上百个CacheReader的子类,每个子类需要把所有用到的对象和数据封装进去,并且实现三个抽象方法——OMG,真可谓“OOP”到了极致!

还好,我们可以使用匿名方法。为此,我们写一个Helper方法:

public static class CacheHelper
{
    public delegate bool CacheGetter<TData>(out TData data);
 
    public static TData Get<TData>(
        CacheGetter<TData> cacheGetter,
        Func<TData> sourceGetter,
        Action<TData> cacheSetter)
    {
        TData data;
        if (cacheGetter(out data))
        {
            return data;
        }
 
        data = sourceGetter();
        cacheSetter(data);
 
        return data;
    }
}

委托是个好东西,可以作为方法的参数使用,而匿名方法的存在让这种方式变得尤其有用。例如,我们现在就可以:

public List<User> GetFriends(int userId)
{
    string cacheKey = "friends_of_user_" + userId;
 
    return CacheHelper.Get(
        delegate(out List<User> data) // cache getter
        {
            object objData = cacheManager.Get(cacheKey);
            data = (objData == null) ? null : (List<User>)objData;
 
            return objData != null;
        },
        () => // source getter
        {
            return new UserService().GetFriends(userId);
        },
        (data) => // cache setter
        {
            cacheManager.Set(cacheKey, data);
        });
 
}

看上去是不是有点古怪?其实习惯了就好。这种做法有好处还不少:

  • 可读性好:操作的逻辑被分割在不同block中。
  • 编程方便:能够直接使用方法的参数和外部对象,不会有封装的麻烦。
  • 调试方便:设置断点之后可以轻松看出“从缓存中读取”、“从数据源读取”和“写入缓存”的过程。

在出现匿名方法之后,这种将委托作为参数传入方法的做法其实已经非常普遍了。例如在微软推出的并行库中就能使用同样的调用方式:

void ParallelCalculate()
{
    double result = 0;
    object syncObj = new object();
 
    List<int> list = GetIntList();
 
    Parallel.For<double>(
        0,
        list.Count,
        () => 0,
        (index, ps) =>
        {
            int value = list[index];
            for (int n = 0; n < 100; n++)
            {
                ps.ThreadLocalState += Math.Sqrt(value) * Math.Sin(value);
            }
        },
        (threadResult) =>
        {
            lock (syncObj)
            {
                result += threadResult;
            }
        });
 
    Console.WriteLine("result = " + result);
}

您接受这种做法了吗?

您善于使用匿名函数吗?