摘要:利用delegate,可以在一个类的内部提取公共方法,减少代码重复。有时候,也会让代码变得更酷。
一、缘起,以及例子
最近参与一个项目的开发工作,项目的开发者中,有的经验丰富,有的经验比较丰富,也有新手。因此在开发的成果中,有不少质量可以提高的地方。在这里零星记下来,希望对项目有帮助。——这些博客的预期读者,主要是本项目的成员。
代码重复是最令人难以忍受的编码坏习惯之一。有很多手段可以减少代码重复,例如,提取公共方法,提取公共父类或共享的helper类等。在C#中,delegate也是一种可以考虑的手段。
看这样一个例子。我们要访问外部接口,获取系统所需的数据。为了提高效率,将从外部接口获取的数据缓存起来。我们先不考虑用memcached一类的技术,而仅仅是将其存放在自己的字典中。
一开始我们只获取数据A:
public class ServiceDataCacher
{
private Dictionary<string, object> cachedData = new Dictionary<string, object>();
public A GetA()
{
string key = "A";
if (!cachedData.ContainsKey(key))
{
A a = SeriviceClient.GetA();
cachedData.Add(key, a);
}
return (A)cachedData[key];
}
}
后来又需要获取数据B,这一次是根据参数获取:
public class ServiceDataCacher
{
private Dictionary<string, object> cachedData = new Dictionary<string, object>();
public A GetA()
{
...
}
public B GetB(long id)
{
string key = "B-" + id;
if (!cachedData.ContainsKey(key))
{
B b = SeriviceClient.GetB(id);
cachedData.Add(key, b);
}
return (B)cachedData[key];
}
}
事情看起来有点不妙了,因为出现了重复的代码:先判断是否已经缓存,如果没有缓存,就从外部服务中获取内容,将其缓存起来。
再后来,又要读取数据C,这一次是根据两个参数:
public class ServiceDataCacher
{
private Dictionary<string, object> cachedData = new Dictionary<string, object>();
public A GetA()
{
...
}
public B GetB(long id)
{
...
}
public C GetC(string type, string name)
{
string key = "C-" + type + "-" + name;
if (!cachedData.ContainsKey(key))
{
C c = ServiceClient.GetC(type, name);
cachedData.Add(key, c);
}
return (C)cachedData[key];
}
}
事情到了这个地步,就必须进行重构了,将“判断是否已经缓存-如果没有,就读取并缓存起来”这样的逻辑提取出来。实际上,在我们的项目中,这样的函数有几十甚至上百个。但怎样提取?提取公共方法有点难度,因为每个函数的框架是类似的,只是要调用的服务不同,而且参数也可能不同。另一种思路是提取公共父类,但为每一个方法定义一个子类的成本太高了:要定义上百个子类,谁受得了啊?
二、利用delegate
解决的思路是利用delegate。先考虑基本的做法:将公共的框架提取出来,而将服务调用定义为delegate。由于每个方法的参数不同,需要在delegate中使用动态参数。
private delegate object Caller(params object[] args);
提取公共方法,调用这个delegate来获取数据:
private object GetData(string key, Caller caller, params object[] args)
{
if (!cachedData.ContainsKey(key))
{
object a = caller(args);
cachedData.Add(key, a);
}
return cachedData[key];
}
这样就把“判断是否已经缓存-如果没有,就读取并缓存起来”这个逻辑提取出来了,每个方法定义好自己的delegate,调用这个方法即可。
重构后的类定义如下:
class ServiceDataCacher
{
private Dictionary<string, object> cachedData = new Dictionary<string, object>();
private delegate object Caller(params object[] args);
private object GetData(string key, Caller caller, params object[] args)
{
if (!cachedData.ContainsKey(key))
{
object a = caller(args);
cachedData.Add(key, a);
}
return cachedData[key];
}
public A GetA()
{
string key = "A";
return (A) GetData(key, ServiceGetA);
}
private object ServiceGetA(object[] args)
{
return ServiceClient.GetA();
}
public B GetB(long id)
{
string key = "B-" + id;
return (B) GetData(key, ServiceGetB, id);
}
private object ServiceGetB(object[] args)
{
return ServiceClient.GetB((long)args[0]);
}
public C GetC(string type, string name)
{
string key = "C-" + type + "-" + name;
return (C) GetData(type, ServiceGetC, name);
}
private object ServiceGetC(object[] args)
{
return ServiceClient.GetC((string)args[0], (string)args[1]);
}
}
三、利用匿名方法
经过上面的重构,相同的逻辑被提取出来并复用,减少了代码重复。但每一个方法在定义delegate时,都要搭配一个独立的方法。这样显得不够简练:原来的一个方法被分成了两个。另外,使用动态参数,也让人很闹心。
为此,考虑使用匿名方法来实现delegate。顾名思义,匿名方法只需要给出方法的内容,而不需要定义带有名字的、独立的具体方法。以GetC为例,使用匿名方法后,就变成了:
public C GetC(string type, string name)
{
string key = "C-" + type + "-" + name;
return (C) GetData(type, delegate
{
return ServiceClient.GetC(type, name);
});
}
其中delegate引导的部分就是一个匿名方法,我们定义了方法的内容后,就直接使用了。匿名方法有一个优势:能够感知当前上下文,可以使用其中的变量。这样,就不需要再把参数传递给匿名方法了。原来delegate定义中的参数也就不需要了。
delegate定义修改为:
private delegate object Caller();
公共方法修改为:
private object GetData(string key, Caller caller)
{
if (!cachedData.ContainsKey(key))
{
object a = caller();
cachedData.Add(key, a);
}
return cachedData[key];
}
经过这样的重构,类的定义变成了下面的样子:
class ServiceDataCacher
{
private Dictionary<string, object> cachedData = new Dictionary<string, object>();
private delegate object Caller();
private object GetData(string key, Caller caller)
{
if (!cachedData.ContainsKey(key))
{
object a = caller();
cachedData.Add(key, a);
}
return cachedData[key];
}
public A GetA()
{
string key = "A";
return (A) GetData(key, delegate
{
return ServiceClient.GetA();
});
}
public B GetB(long id)
{
string key = "B-" + id;
return (B) GetData(key, delegate
{
return ServiceClient.GetB(id);
});
}
public C GetC(string type, string name)
{
string key = "C-" + type + "-" + name;
return (C) GetData(key, delegate
{
return ServiceClient.GetC(type, name);
});
}
}
简单总结一下:缓存逻辑的集中,为调整带来了很大便利。例如,如果要考虑并发问题,需要对cachedData加锁,只需要修改一处即可。如果要考虑缓存的持久化或共享,例如使用memcached管理缓存,也只需要改造一处。
----------我是分隔线:主要内容介绍到此结束,以下的内容纯属耍酷 ----------
四、利用lambda表达式
C#中,可以用lambda运算符表示delegate,这样可以使代码显得更酷。
例如,GetC可以变成这个样子:
public C GetC(string type, string name)
{
string key = "C-" + type + "-" + name;
return (C) GetData(type, () =>
{
return ServiceClient.GetC(type, name);
});
}
更进一步,如果匿名方法中仅有一个return语句,这意味着整个方法体实际上就是一个表达式,可以用lambda表达式来表示这个方法。
例如,GetC可以变成:
public C GetC(string type, string name)
{
string key = "C-" + type + "-" + name;
return (C) GetData(key, () => ServiceClient.GetC(type, name); );
}
再进一步,如果lambda表达式中的方法调用没有参数,则可以用方法直接表示,省去lambda表达式。
例如,利用lambda表达式,GetA可以改造为:
public A GetA()
{
string key = "A";
return (A) GetData(key, () => ServiceClient.GetA());
}
利用方法,更可以进一步改造为:
public A GetA()
{
string key = "A";
return (A) GetData(key, ServiceClient.GetA);
}
整个类定义变为:
class ServiceDataCacher
{
private Dictionary<string, object> cachedData = new Dictionary<string, object>();
private delegate object Caller();
private object GetData(string key, Caller caller)
{
if (!cachedData.ContainsKey(key))
{
object a = caller();
cachedData.Add(key, a);
}
return cachedData[key];
}
public A GetA()
{
string key = "A";
return (A) GetData(key, ServiceClient.GetA);
}
public B GetB(long id)
{
string key = "B-" + id;
return (B) GetData(key, () => ServiceClient.GetB(id));
}
public C GetC(string type, string name)
{
string key = "C-" + type + "-" + name;
return (C) GetData(key, () => ServiceClient.GetC(type, name));
}
}
很酷,不是么?