Posted on 2008-04-04 00:10
Jeffrey Zhao 阅读(10862)
评论(105) 编辑 收藏 所属分类:
.NET 3.5
尝试从缓存中获取数据,如果数据存在则返回,否则从数据源中获取数据,放入缓存,然后返回。
您是否熟悉上面这段逻辑说明?如果您的应用中大量使用了缓存,则上面这段逻辑很可能会出现许多次。例如:
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);
}
您接受这种做法了吗?
您善于使用匿名函数吗?
Feedback
@flyingchen
会慢慢成为主流的……要习惯,呵呵……
委托-->匿名方法-->Lambda
越来越简洁的变化
我以前写过一个简单的,方式和这个类似:
http://www.cnblogs.com/RChen/archive/2006/11/16/ASPnet_exception_handling.HTML
学习 jquery/ruby 之后就越来越喜欢这种用法了 :)
@木野狐(Neil Chen)
有了匿名函数,很多东西都方便很多,主要是因为避免了“封装”
@TimothyYe
匿名方法是个突破,Lambda表达式只是表现上的改进而已。
其实匿不匿名我倒不是太关心, 不匿名, 只要有这种委托, 不过是多声明一次罢了,倒也无所谓。
还有,我比较恨的是
for(var i;...) {
function() {
operate with i;
}
}
然后等到真正执行function的时候, 该死的i...。 无论是那种语言, 这种对付事的处理办法简直让人抓狂。
匿名方法 确定不错,刚才开发中,正在想怎么解决这个问题,刚好看到这个,学习了。谢谢!
@怪怪
其实说这个“匿名”,其实关键在“内联”,“内联”意味着能够使用外围方法的参数,局部变量等等,所以和多申明一个方法还是有很大不同的。
至于你说的for (var i)这种,其实只要不是“延迟执行”,问题倒都不大。
问题应该比较多是出在像setTimeout这种延迟执行的东西,还有就是比如现在有Expression Tree,在某些情况也会出现很麻烦的情况。
@Jeffrey Zhao
嗯, 你说的这个我想到了。 只是多申明一个方法, 虽然不能使用Scope内的变量了, 大不了转发一下嘛, 确实麻烦许多, 不过也不是啥大问题~
问题是延迟执行有时候不是性能要求或具体需求使然, 对我来说经常是一种设计要求使然。
所以我觉得比较混蛋, 他们工作少做一点, 我和无数其他人要做的无数工作的总量就多很多, 搞得我想开窗户朝外面嗷嗷叫,呵呵。
--引用--------------------------------------------------
Jeffrey Zhao: @XNet
这个很巧。
--------------------------------------------------------
还没睡觉啊,先顶一个,然后睡觉,最近我也正在学习LINQ,看看最近几天做个domo出来,现在LINQ资料还是少?
PS:我是北京.net俱乐部成员,上次VS2008北京发布(微软18层)会我好像见过您.
顺便问下:我安装了VS2008,怎么我IIS列表中 Framework3.5没有啊?我看是没有脚本映射,但是以前1.0,2.0那种重新注册的那个方法不行了,在V3.5里面找到一个VFXXX.exe(我忘记了,反正在最下面那个),我VS2008命令行注册Framework3.5,用VFXXX.exe/r 还是不行.其他几个命令我都试验过,还是不行.
请问怎么装啊?
@东哥
ASP.NET没有3.5的,还是2.0.XXXXX。
@怪怪
那么还是要包装一下了,想想以前没有匿名函数的做法可能会舒服一些,呵呵。
@Jeffrey Zhao
知道了,谢谢您,让您见笑了.
难怪我前几天看见有人说framework 3.5很有可能是一个过渡版本,微软会很快出framework 4.0的.估计也差不多.
@Jeffrey Zhao
我看见 添加删除程序里面 是 framework 3.5,怎么他不能脚本映射那?反正我一直不理解为什么在IIS里面 怎么就没有? 按照常理说:VS2003带framework1.1,VS2005 带framework2.0,VS2008 上次您在VS2008发布会(微软18层)说过不是带framework3.0,3.0里面有WCF,这次总应该是Framework3.5了吧? 疑惑中.相信很多人估计和我刚才一样疑惑.
内联后就让许多异步操作看起来不那么不直观了,比如
x.MethodX(para1,{
y.MethodY();
}};
从格式上显得“同步”了……
=======================
讲的有点乱,不过我还是很喜欢委托这种方式的……特别是用了点JavaScript后,看到它们的机会就更多了……
=======================
发现你在weblogs.asp.net上有blog了,哈哈,有意思啊,那个网站地段好噢~哈哈
@东哥
3.0 3.5中都是额外的功能的,但他们的编译后的代码还是2.0的,也就是类似于文章中有Lambda与过去使用的delegate的方式所编译成的目标代码都是基于2.0的,因为.NET框架是基于静态编译的代码,因此用2.0即可解释IL了……
@volnet(可以叫我大V)
明白了,谢谢解答,今天晚上终于能睡踏实了,冥思苦想了好几天,终于有您给解答了.
代码很有趣!讲解的很细致;
个人感觉 Lambda 表达式,和正则表达式一样都不是正常语意,如果写的太多看着很不舒服,感觉还是带 delegate 关键字匿名函数看着舒服点。
1,大牛们都是白天睡觉晚上工作?
2,大牛们压根就不睡觉?
你的解决方法有点像:将大量的子类变成大量的方法而已。个人第一感觉而已。-_-

嗯,一步一地看,好不容明白了那个“优雅”的模板,刚叫完好!

~~~~~结果又被小老赵当头一棒,打晕了。
我等还没明白过神来的时候,只见小老赵祭起一件奇怪的法器,顿时光芒满地,立即扫除一切妖魔鬼怪。
随即,小老赵淡淡地说:这就是匿名函数!
然后,留下两个问题,又消失于无影无踪...

您接受这种做法了吗?
您善于使用匿名函数吗?
我等凡夫俗子,慢慢领悟吧......
建议用IList<User>代替List<User>
@东哥
.NET版本推出往往会带有新的语言版本,目前框架和语言双方什么风声都没有,所以一般不会很快推出.NET 4.0
@Klesh Wong
良好的做法是,参数尽量用抽象类型,返回值尽量用具体类型。
这里是返回值,所以List比IList好。
老赵把代码都贴出来啊...
Func 与 Action 是哪个函数或方法..CacheManager也没找到.
对于初学者学起来就更难了
@黑虫
我随便乱写的,就一个Get和Set方法,就当是最普通的缓存容器。
@sand
Func和Action都是.NET Framework里的委托,查一下MSDN吧。
写JavaScript写多了就肯定会有写lamda的倾向……
今天的《参考消息》里面有一篇文章讲每天睡眠时间不足的人非常容易诱发肥胖,想起老赵每天都很晚睡,于是上来回复一帖跟主题无关的,老赵一定要注意休息啊,身体是革命的本钱,特别是能够传播革命火种的人更是要保养好
老赵,你好,这个问题我MAIL过你,不过不知道你收到没有。
有一个具体的问题想请教一下,我在一个页面中使用TIMER,向服务器发出请求,通常会执行较长时间,在此期间,我点击已经出来的结果里的链接,即向本应用程序中的另一个页面发出请求,这个请求会被阻塞,直至父页面的TIMER长任务完全完成。请问这个问题怎么解决。
多谢!
另,我在UpdateProgress中放置BUTTON,用javascript中断了Timer后,的确中断了TIMER,可当页面再次回发时(比如GridView翻页),这个TIMER控件会被重新激活。请问如何能够彻底关掉这个Timer.
如有了解这方面的,还请不吝赐教.
@吉林哥163
看过大文件上传并且进度条显示百分比的实现吗,没有柱塞哦,那应该可以解决你的问题吧
看看效果吧 www.aspnetupload.com
第二个问题可以判断翻页的url参数让Timer不工作
其实匿名方法在java世界早主流了,我在学校里面整天碰到的的就是java的匿名方法或者内部类唉
@私家侦探
感谢你的回复。。
第一个问题,那个大文件上传的例子似乎不太好使,并且我中间点击CANCEL时出错,关键是它的程序逻辑就不是并行的,感觉应该是传一部分,作一下回显,再接着传,类似TIMER。可是我说的情况是手动的在一个回传正在进行时,发起另一个请求。
第二个问题,看似可以解决此问题,但其实不然,因为我的TIMER是要TICK过N次(N不确定)的,在客户端没有中止它的时候,我翻页也不应该中止这个TIMER,但是客户端中止它,似乎无法通知到服务器端。
lambda方便是方便,只是多行就不好调试了,这个应该是编译器一个可以改进的地方
暂时还不习惯这样的写法,呵呵,只能慢慢习惯了
不过这篇文章写的恶很好,支持下
--引用--------------------------------------------------
私家侦探: 其实匿名方法在java世界早主流了,我在学校里面整天碰到的的就是java的匿名方法或者内部类唉
--------------------------------------------------------
java中没有匿名方法,不过有内部类,所以可以使用内联的写法实现一个接口,得到我这篇文章中类似的做法。
--引用--------------------------------------------------
fox23: lambda方便是方便,只是多行就不好调试了,这个应该是编译器一个可以改进的地方
--------------------------------------------------------
lambda是一行代码,的确不容易调试。
我倒不期望这个,不过很希望调试器能够在Watch这种地方使用lambda表达式,呵呵。
@吉林哥163
Timer?是哪里的Timer?setTimeout?如果要去除的话用clearTimeout就可以了。
--引用--------------------------------------------------
助燃: 今天的《参考消息》里面有一篇文章讲每天睡眠时间不足的人非常容易诱发肥胖,想起老赵每天都很晚睡,于是上来回复一帖跟主题无关的,老赵一定要注意休息啊,身体是革命的本钱,特别是能够传播革命火种的人更是要保养好
--------------------------------------------------------
最近瘦了十多斤……聊胜于无……
---------引用-----------------------------
@私家侦探
感谢你的回复。。
第一个问题,那个大文件上传的例子似乎不太好使,并且我中间点击CANCEL时出错,关键是它的程序逻辑就不是并行的,感觉应该是传一部分,作一下回显,再接着传,类似TIMER。可是我说的情况是手动的在一个回传正在进行时,发起另一个请求。
第二个问题,看似可以解决此问题,但其实不然,因为我的TIMER是要TICK过N次(N不确定)的,在客户端没有中止它的时候,我翻页也不应该中止这个TIMER,但是客户端中止它,似乎无法通知到服务器端。
---------------------引用----------------------
老赵,你好,这个问题我MAIL过你,不过不知道你收到没有。
有一个具体的问题想请教一下,我在一个页面中使用TIMER,向服务器发出请求,通常会执行较长时间,在此期间,我点击已经出来的结果里的链接,即向本应用程序中的另一个页面发出请求,这个请求会被阻塞,直至父页面的TIMER长任务完全完成。请问这个问题怎么解决。
-----------------------------------------------
你是说:服务器能够一边计算一边显示部分结果?我很怀疑,除非用了特殊技术.在提交过程中(注意:还在提交噢)服务器是不可能把部分结果显示到同一页面的任何地方,即使在该页面嵌入一个iframe,然后不断的刷新去获取放在session的部分计算结果值,即会发生你说的"柱塞".
如果非要在提交时获取部分结果值,使用的特殊技术httpHandlers,可以web.config配置文件里面配置.把获取部分结果的页面配置为另外一个http处理者去处理,这样就变成有人在计算结果有人在旁边看结果,可以把"结果"暂时放会话中,这样就不会"柱塞"拉,哈哈畅通!!
以上仅供参考,嘿嘿!
@老赵
java确实没有匿名方法,应该是匿名类啊,在java的c/s事件编程即awt或swing,不想用都不行
@Jeffrey Zhao
Timer是指Asp.net Ajax中的Timer服务器控件.
我是用这种方式中止它的。
function CancelSearch()
{
if (prm.get_isInAsyncPostBack())
{
prm.abortPostBack();
var timer = $find("<%=this.Timer1.ClientID %>");
timer.set_enabled(false);
timer._stopTimer();
}
}
个人感觉,那句set_enabled(false)其实没起到什么作用。
@私家侦探
具体描述一下,我作一个长查询,Timer每TICK一次,就多得到一些数据,然后把数据绑定到GridView中(绑定前,我会把这个DataTable放到Session中),在GridView里最后一列是“详情”链接,点击它,就开个新IE窗口,根据传递过去的ID,查看这个数据的详情。全部详情数据都来自该条记录(在Session中的DataTable里)。这样,问题就出现了,由于是异步提交,我在一次TIMER TICK过程中,可以点击“详情”,期望的结果是它应该立即显示出来,可事实就是,它一定要等父窗体的异步提交执行完成后,才会执行子窗体(经过设断点跟踪发现,直到父窗口的异步提交完成,子窗口的Page_Load才开始执行)。这就是我说的阻塞。
CacheHelper.Get后面的<>里不写是因为可以推断出来吗?
@吉林哥163
你确定你已经实现同一页面一边提交一边显示数据吗?
那我真的很崇拜你。据我猜测Asp.net Ajax中的Timer的底层应该也是js的settimeout一类的东西,或许你的意思可能一小段时间的定时提交吧,比如两秒提交一次,然后结果数据绑定到页面,然后再提交,再绑定,一直到定时器终止,对吧。如果是这样的话,可能是定时器设置间隔得太快了,来不及反应,而不是柱塞。你这个需求到很奇怪啊,呵呵,你们经理该大pp了。
或者用另外一种方案吧,即拦截器(java中很常见的):
在web.config配置httpHandlers节点:
--------------------------
<httpHandlers>
<add verb="*" path="你那个详情链接的网址" type="MyHandler.NewHandler,MyHandler"/>
</httpHandlers>
--------------------------
* 代表拦截所有提交提交,path代表拦截的网址,type代表被拦截到的网址的处理类
这样就不会柱塞了,因为“详情”页面是用另外一个http请求处理器处理的
你再搜索httpHandlers的资料看看吧,需要一点功底的
@私家侦探
首先感谢!
如你所述,我的要求恰好就是在提交过程中显示,因此TIMER间隔越短,越能模拟我的要求,可惜最短就是1ms,呵呵。
我就是要在Page1的异步回发过程中,点击Page1中的Link1(a href="Page2.aspx?id=XXX"),然后在Page2里显示id为xxx的数据。
如果如你所说,Timer时间设置太短,“来不及反应”,这不就相当于是同一个会话不能并行提交多个请求吗?而我想要的就是两者互不干扰(像多线程的感觉一样)。我想请教的就是这个问题。
@吉林哥163
你的问题很糟糕,嘿嘿.
你试着把间隔时间拉长点,再试试.
我已经试过在同一页面中是不可能提交未结束时把数据显示出来的,不管是使用本页面或者iframe或者模式对话框来显示那些"部分结果",都是会柱塞的,边提交边显示都做不到了更别提你还想点击那些"部分结果"中的"详情"链接.
没有在网页上做过多线程.好像实现不了,页面提交的过程整个页面的元素就被"封"住了呀,唉
@吉林哥163
把你的案例再说清楚点,比如查询前页面有哪些元素,要实现什么样的效果
或许我能用httpHandler的并行技术处理你的问题,其实page的基类就是一个httpHandler,所以每个页面都能处理http请求,当然你也可以自己写http请求处理类,在特殊地方就是要这么干,很多ajax控件就是这么做的,进度条也是这么做的
@私家侦探
你说的并行httpHandler我感觉大概可能会解决我的问题,虽然我并不了解它的具体细节。我去了解一下先。
你看一下www.cooseek.com(比较购物搜索引擎),就明白我的意思了。
尝试从缓存中获取数据,如果数据存在则返回,否则从数据源中获取数据,放入缓存,然后返回
这个所说的缓存 到底能缓存多长时间呢?
@私家侦探
我想这应该是asp.net执行模型的默认行为,N年前,我试验过用Page中启动一个线程(无论是否显式的设置为后台线程或非后台线程),如果想导航到另外一个页面(在t.Start()之后Response.Redirect(newPage)),它都一定要等那个线程执行完毕后,才可能导航过去,这说明asp.net下如此使用多线程是不可行的,后来我用分进程的方式(把长任务放到一个web service中,在web service里再启动一个线程)来实现的。
试想一下,如果在Page1的后台代码还在执行的过程中,同一个session又请求page2,势必有可能引起混乱,比如对公共的session,application,全局变量等的读写会引起难以预期的错误。可现在我的确需要这样做,还请多指教。haitaohua@163.com,泡泡(常在线),MSN都是这个账号
--引用--------------------------------------------------
peace: 尝试从缓存中获取数据,如果数据存在则返回,否则从数据源中获取数据,放入缓存,然后返回
这个所说的缓存 到底能缓存多长时间呢?
--------------------------------------------------------
缓存的“功能”其实很简单,就是把一段内容保留一段时间,这个时间自然是根据业务性质来决定的。当然在内存不够时,缓存容器也会去除一些它认为不太重要的内容。
--引用--------------------------------------------------
fff: CacheHelper.Get后面的<>里不写是因为可以推断出来吗?
--------------------------------------------------------
编译器会负责确定所有这些。
@吉林哥163
发现你们一会儿再谈浏览器的线程,一会儿再说asp.net的线程,很乱阿……
@Jeffrey Zhao
能说的稍微详细点儿吗?谢谢:)
我们好象没谈浏览器的线程啊..
HttpContext.Current.Cache.Insert("ReportByTemp", dtUsers, null, DateTime.Now.AddMinutes(10), TimeSpan.Zero); 我之前的缓存是这样用的,这样是指定10分钟 可老赵那没看到你缓存有时间之类的参数啊