[转载]面向对象类设计的五大原则(一)单一职责原则Single Responsibility Principle

mikel阅读(761)

[转载]面向对象类设计的五大原则(一)单一职责原则Single Responsibility Principle – 软件设计开发 – 博客园.

引言

面向对象类设计,或者说是面向对象设计,有五大原则:

简介

今天我们要接触的是SRP,单一职责原则。

我觉得这些原则的目的就是要实现类的高内聚、低耦合。高内聚high cohision和低耦合low couple在很多时候都是我们的目标,不管我们引入设计模式,进行架构分析、设计、提高复用性,解耦decouple是我们共同的目标。

There should never be more than one reason for a class to change.

类的改变不应该由一个以上的原因导致,也就是说不应该有一个以上的原因可以导致类的改变。

如果类的职责超过一个,这些职责之间就会产生耦合。改变一个职责,可能会影响和妨碍类为其他人服务的功能。这种类型的耦合将会导致脆弱的设计, 在修改的时候可能会引入未逾期的问题。

如上图所示,Rectangle类有两个方法:一个是在屏幕上画矩形,一个是计算矩形的面积。两个不同的应用会使用 Rectangle类,一个应用使用Rectangle来帮助计算面积,它从来不会在屏幕画矩形。另外一个是图形界面的应用,也可能会计算面积,但是肯定 会在屏幕画矩形。

当前的设计就违反了SRP原则,Rectangle类有两个职责,一个是为数学模型提供几何的矩形,计算面积;另外 一个职责是为图形用户界面提供矩形的显示。

这个违反SRP原则的设计会导致很多严重的问题。首先,在计算几何应用中必须包含GUI。因为计算几何应用可能就是 一个控制台程序,不需要用户界面,但是由于需要使用这个违反SRP原则的类,就必须要包含GUI的类库,在编译的时候要同时编译,在部署的时候还增加了部 署的内容,而且这些内容可能永远都不会被用到。

其次,如果图形应用的变化,需要修改Rectangle类的话。我们的计算几何应用也必须要重新编译和部署,因为它 也引用了相同的Rectangle类,如果不那么做的话,可能会发生意外的错误(因为Rectangle类被修改了,不知道有没有什么影响)。

好一点的做法是将Rectangle分解为两个类,如下图所示

将计算面积部分从Rectangle类中分离出来,产生一个GeometricRectangle类,专门用来 计算面积。这样的话,对于画矩形方法的修改就不会影响到计算几何应用了。也不用在部署多余的内容。
什么是职责?
在上文中,我们将单一职责定义为“一个改变的原因”。如果你想出超过一个改变类的动机,就说明类具有多个职责。 这很难以发现。我们习惯于按照组来思考职责。例如,考虑下面的Modem接口。
interface IModem
{
public void Dial(string no);
public void Hangup();
public void Send(char c);
public char Reveive();
}

我们中的大多数人认为上面的接口已经设计的不错了,四个功能也都是modem的功能。

但是,这里其实是两个职责。一个职责是连接管理,另外一个是数据通信。Dial和Hangup方法管理 moden的链接,Send和Receive管理modem的数据通信。
这两个职责是否应该分开呢?毫无疑问是应该的。这两个职责是没有关系的。他们改变的原因不尽相同。而且,他们会 被应用的不同部分调用,这两个部分也会因为不同的原因变化。
上图的做法可能更好,将接口分为两个。然后有一个类来实现这两个接口,代表一个modem。
结论
SRP原则是最简单的原则之一,也是最难做好的原则之一。我们会很自然的将职责连接在一起。找到并且分离这些职 责是软件设计需要达到的目的。

【Blog】http://virusswb.cnblogs.com/

【MSN】jorden008@hotmail.com

[转载]微软的jQuery国际化插件

mikel阅读(1570)

[转载]微软的jQuery国际化插件-Scott Guthrie 博客中文版.

原文发表地址] jQuery Globalization Plugin from Microsoft

[原文发表时间] Thursday, June 10, 2010 10:34 PM

上个月,我写了一篇关于微软如何在向 jQuery贡献代码的文章,也谈到了在第一批贡献的代码中的一些功能:jQuery 模板和数据链接支持.

今天,我们发布了一个新的JQuery国际化插件的原型,你可以在你的JavaScript程序中引用这 个插件添加国际化功能。插件涵盖了超过350种文化的国际化信息,从苏格兰盖尔语、佛里斯兰语、匈牙利语、日语到加拿大英语等。插件将以开源的形式发布。

你可以从我们的Github代码库下载插件的原型版本:

http://github.com/nje/jquery-glob

另外也可以在这里下 载一些示例了解简单的用法。

理解国际化

JQuery国际化插件可以让你在 JavaScript代码中轻松地处理不同文化之间的数字、货币和日期格式。例如,你可以使用这个插件正确地显示一个国家的货币符号:

image

你也可以用这个插件编排日期格式,这样日期和月份 会以正确的顺序显示,而日期和月份的名称也能够得到正确的翻译:

image

请注意上图阿拉伯的日期格式中,年份显示成了 1431。这是因为年份已经按照回历的方式转换过了。

一些文化差异比较明显,例如使用不同的货币或者不同的月份名称等。但另外一些差异 却细微而又出人意料。例如,在有一些文化里,数字不是以均衡的方式分组的。在语言标记为“te-IN”的文化中(印度东部的泰卢固语),数字先按3位数字 分组,然后再每2位数字分组。因此象数字一百万(1000000)就会被写成“10,00,000”。而有些文化就根本不对数字进行分组。所有这些细微的 差异都能够被jQuery国际化插件自动处理。

要得到正确的日期可能非常需要技巧。不同的文化采用不同的历法,比如公历和回历。同一种 文化也可能会采用多个历法,比如日本就同时采用公历和以天皇年号推算的纪年历。插件提供了函数将日期在所有这些历法中相互转换。

使 用语言标记

jQuery国际化插件依据RFC 4646和RFC 5646标准里定义的语言标记来识别文化(参见 http://tools.ietf.org/html/rfc5646), 语言标记通常由连字符将多个辅标签组合而成,比如:

语言标记 语言名称 (中文)
en-AU 英文(澳大 利亚)
en-BZ 英文(伯利兹)
en-CA 英文(加拿 大)
Id 印度尼西亚文
zh-CHS 中文(简体) — 标记不再使用
Zu 祖鲁文

请注意单一语言例如英语可以同时有多个语言标记。同是英语国家,加拿大采用的数字、货币和日期格式就不同于澳大利亚或者美国。你可以使用下面 的语言辅标签查找工具来获取某个文化的语言标记:

http://rishida.net/utils/subtags/

jQuery国际化插件里一个叫做globinfo的文件夹包含了这350种文化的信息。实际上,这个文件夹里有700多个文 件,因为它包括了每个文件的最小化(minify)和原始版本。

比如说,在globinfo文件夹,文件 jQuery.glob.en-AU.js对应英文(澳大利亚),jQuery.glob.id.js对应印度尼西亚文,而 jQuery.glob.zh-CHS对应简体中文。

示例:指定某种文化

假如你要创建 一个德文的网站,并且希望在客户端的JavaScript脚本里使用德语习惯显示所有的日期、货币符号和数字。那页面的源代码也许与下面的HTML类似:

image

注意上面的span标签,它们标识了所有需要用国 际化插件进行格式化的区域:商品的价格、上架的日期以及库存量。

要在网页里使用jQuery国际化插件,需要添加三个 JavaScript文件:jQuery库,jQuery国际化插件和指定的文化习惯信息:

image

在上例中,我将包含了德语文化习惯的 jQuery.glob.de-DE.js文件静态地加入到网页中,”de-DE”是语言标记,表示在德国使用的德语。

既然已经引入了 必要的脚本文件,就可以使用下面的客户端JavaScript代码调用插件来显示价格、上架的日期以及库存量等值:

image

jQuery国际化插件为jQuery函数库添加 了一些新的函数—包括preferCulture()和format()等新函数。你可以通过preferCulture()函数设置插件其他函数调用时 使用的默认文化。preferCulture()函数接受一个语言标记作为参数,这个函数会使用最接近这个语言标记的文化。

$.format() 函数才是真正用来编排货币、日期和数字格式的。$.format()函数的第二个参数是格式分类符。例如,传入“c”表示按货币的样式编排参数值。在 githum的ReadMe文件里详细描述了所有格式分类符代表的意思:http://github.com/nje/jquery-glob

当我们在浏览器打开这个页面的时候,所有的东西都是按照德语习惯正确显示。货币符号采用的是欧元符号,日期采用德语的日期和月份名称,并且,数 字分隔符用的是点号(.),而不是逗号(,)。

image

你可以通过运行示例下 载中的3_GermanSite.htm文件来了解上述方法。

示例:用户动态选择一个区域设置

在前面的例子中,我们显式指定使用德语文化习惯(通过引用jQuery.glob.de-DE.js文件)。我们现在来看看动态设置文化习惯的 几个例子。

比如说我们打算在页面上加一个包含所有350种文化的下拉框,当用户在下拉框里选择了一个文化,页面上所有的日期都按照这个 文化的习惯显示。

image

这是网页的HTML源代码:

image

请留意所有包含了日期的<span> 标签都有一个data-date属性(data-*属性是HTML 5里的新功能,同时在老的浏览器上也可以使用)。当用户在下拉框里选择了一个文化信息后,我们的代码将会格式化data-date属性里保存的日期。

为了显示所有可能的文化信息,我们要象下面这样添加一个叫jQuery.glob.all.js 的文件。

image

jQuery国际化插件里有一 个叫做jQuery.glob.all.js的文件,这个文件包含了所有插件支持的350多种文化的国际化信息。即使经过最 小化处理,还是有367k字节。由于文件很大,除非你的确需要一次性使用所有这些文化信息,我们建议你只添加所使用到的文化信息对应的 JavaScript文件。下一个例子中,我会演示如何在网页中动态地加载某个语言对应的JavaScript文件。

下一步,把所有支持的文化名称添加到下拉框的列表里。可以用$.cultures这个属性获取它们:

image

最后,再写几行代码把每个span标签里的 data-date属性的日期都抓出来并编排格式:

image

jQuery国际化插件里面的 parseDate()函数将一个字符串的日期转换成JavaScript日期,而format()函数则用来编排这些日期的格式。“D”这个格式分类符 说明使用长日期格式编排日期。

现在,只要页面浏览者选择这350种语言其中之一,内容都会被正确的国际化。你可以通过运行示例下 载中的4_SelectCulture.htm文件来了解上述方法。

示例:动态加载国际化文件

在上一节说过,你应该尽量避免在页面中引用jQuery.glob.all.js文件, 因为它实在是太大了。相反,你可以动态地加载你所需要的国际化信息。

比如说,你要创建一个下拉列表显示下面这些语言:

image

下面的代码会在用户从下拉框中选择一个新的语言后 执行。这段代码会先判断选中的语言对应的国际化脚本文件是否已经加载了。如果还没有加载,则使用jQuery的$.getScript()函数动态加载国 际化脚本文件。

image

当国际化脚本文件被浏览器加载以后,就会调用 globalizePage()函数,继而执行客户端的代码完成国际化。

这个方法的优点是可以让你避免加载整个 jQuery.glob.all.js文件—只要加载使用到的文件,而且只加载一次。

示例下 载中的5_Dynamic.htm文件演示了这个方法。

示例:自动检测用户的常用语言设置

大多数网站通过检测用户的浏览器设置来得到其常用语言,然后根据这种语言文化习惯来国际化网站内容。用户可以在浏览器中设置一个常用语言。这 样,用户在请求页面时,常用语言设置会包含在请求Http消息头的Accept-Language字段中。

如果使用的是微软的IE浏览 器,可以通过以下步骤设置你常用的语言:

1. 在菜单栏里选择工具Internet 选项。

2. 选择常规标签。

3. 在外观里点击语言按钮。

4. 点击添加在语言列表里增加新的语言。

5. 然后把你最常用的语言放在列表的最顶端。

image

你可以在这个对话框中设置多门常用语言。所设 置语言的顺序关系将会体现在HTTP请求的Accept-Language字段中,如:

Accept-Language:fr- FR,id-ID;q=0.7,en-US;q=0.3

比较奇怪的是,用户不能够通过客户端的JavaScript脚本得到 Accept-Language的内容。微软的IE和Firefox浏览器都支持一系列语言相关的属性,这些属性可以通过 Window.navigator进行访问,例如windows.navigator.browserLanguage和 window.navigator.language,不过这些属性中的语言一般指的是操作系统中设置的语言或者是浏览器的语言版本。你不能通过这些属性 来得到用户设置的常用语言。

得到用户语言偏好(也就是Accept-Language字段中的内容)的唯一可靠方法就是编写服务器代 码。例如,下面的ASP.NET页面就利用了服务器的Request.UserLanguages属性把用户的语言偏好设置赋给了客户端 JavaScript的acceptLanguage变量(然后就可以通过客户端JavaScipt访问这些值了):

image

为了使这段代码能够正常工作,页面中必须包含与 acceptLanguage对应的文化信息。例如如果你的常用语言是fr-FR(法语-法国),那就必须在这个页面中引用 jQuery.glob.fr-FR.js或者jQuery.glob.all.js这两个JavaScript脚本之一,否则没办法得到文化信息。关于 这个方法的应用可以示例下 载中的”6_AcceptLanguages.aspx”。

如果对于页面中用户的常用语言没有对应的脚本包含其文化信息,那 么$.preferCulture()方法就会引用中立(neutral)的文化信息(例如,使用jQuery.glob.fr.js,而不是 jQuery.glob.fr-FR.js)。如果中立文化信息也不可用,那么$.preferCulture()方法就会回滚到使用默认文化(英语)。

示例:利用国际化插件使JQuery UI DatePicker实现国际化

国际化插件的目的之一就是使开发能适应各种不同文化的jQuery小工具的变得更为简单。

我们希望确保jQuery的国际化 插件能够与现有的jQuery的UI插件如DatePicker共存并正常工作。为了实现这个目标,我们创建了DatePicker插件的一个整合版本, 使它可以在渲染日历的时候利用我们的插件来提供国际化支持。下图演示了把jQuery的国际化插件和整合后的DatePicker插件一起加入到页面并选 择印度尼西亚语的效果。

image

注意:一周中每一天的名称都以印度尼西亚语的缩写 形式显示。 此外,月份的名字也以印度尼西亚语显示。

你可以从我们的github网站下载整合版的JQuery UI DatePicker。你也可以使用示例下 载中文件7_DatePicker.htm所使用的版本。

总结

很高兴我们能够 为jQuery社区提供持续的贡献。这个国际化插件已经是我们发布的第三个插件了。

感谢大家对我们年初发布jQuery模板 (Templating)和数据链接(data-linking)的原型提供的很多非常有价值的反馈和设计方面的建议。最后我想趁这个机会感谢 jQuery和jQuery UI团队在创建这些插件时所提供的帮助。

希望这能对您有所帮助。

[转载]Spb内部资料:为什么选择mvc?

mikel阅读(999)

[转载]Spb内部资料:为什么选择mvc? – SPB代言人 – Spacebuilder.

前言

本资料是2008年7月份左右我们筹划开发Spacebuilder V2.0之前进行的调研,详细的分析了Spacebuilder的问题,以及分析了为什么选择mvc,在后续的文章中我们会继续放出内部在使用mvc开发 SPB 2.0中遇到和解决的问题,以及mvc为Spacebuilder所带来的改变。

一、SPB表现层当前存在的问题

1.表现层整体运行效率低

由于全部采用的自定义控件,是靠在运行时动态加载skin文件实现换肤的,导致以下性能问题:

*动态加载skin导致全部采用FindControl方式导致浪费很多服务器资源;

*在加载skin文件时首先都要检查文件是否存在,也浪费了服务器资源;

*不利于前端优化,当前不好控制css、JavaScript的链接位置及JavaScript的执行位置;

*即便很多功能不依靠ViewState但是为了减小页面大小还需要对ViewState进行处理;

2.ajax效率低

当前spb的ajax主要使用的是ASP.NET ajax的UpdatePanel,使用UpdatePanel并不是真正的ajax解决办法,其实只是一个防止页面刷新的障眼法而已,页面的传输流量没 有减少而且会经历完整的页面生命周期。

3.url rewrite不够灵活

*当前实现二级域名形式(http://mazq.spacebuilder.cn)url rewrite,要借助外部工具;

*无法重写实现类似目录的url重写,例如:http://spacebuilder.cn/mazq/blog/2008/8/10/19

4.SEO困难

*css、js文件的链接及加载位置难以控制,页面内容不利于SEO

*当前url rewrite规则不利于SEO

二、WebForm与MVC比较

1.WebForm与MVC表现层模式比较

mvp

View不能重用
P与V关系密切

View可以完全交给界面设计人员

mvc

View可以重用

C与V关系不紧密

View完全交给界面设计人员有一定难度

2.WebForm优缺点分析

优点:快速上手、快速开发、强大的扩展机制

缺点:复杂的引擎、对于开发高性能的站点反而降低开发效率(解决ViewState、控件ID、换肤功能、SEO)

3.ASP.NET mvc优缺点分析

优点:

原生态url routing,便于url rewrite
Control与View完全分离,利于换肤且没有性能损失
便于对输出的html做完全的控制,利于精简代码及SEO
表现层的性能可以优化到极致
应用asp.net的master及去除控件的运行时特性,使用vs开发时将可以使用设计视图

缺点:

开发人员需要花时间熟悉这个新技术
现有代码移植到mvc需要一定时间
开发人员需要熟悉html以及css、JavaScript
开发人员需要摆脱在WebForm开发时对服务器控件种种依赖

三、ASP.NET MVC介绍

1.mvc运行图

mvc运行图

2.mvc详细请求流程

mvc详细请求流程

(1)用户发起一个url请求

(2)ASP.NET MVC framework通过url roueing rules找到一个处理该请求的Controller及Action
(3)Controller调用Model加载View需要的数据
(4)Model从数据库获取数据
(5)Controller把从Model取出的数据传输到View,然后由View负责对外呈现

四.使用mvc注意事项

1.aspx、ascx、master依然可用,但是不再有postback模型,亦不会有页面生命周期及ViewState;

2.asp.net mvc框架将完全支持象forms/windows身份认证,URL授权,成员/角色,输出和数据缓存,session/profile状态管理,健康检 测,配置系统,以及provider架构等现有的ASP.NET特性;

3.SBContext将不复存在;

4.XxxUrls Url集中管理类将不复存在;

5.所有依赖ViewState的控件将重新考虑设计或直接去除;

6.ResourceManager及相关控件需要调整

附 件:SPB表现层技术选型-asp.net mvc.pps

[转载]网络阅读,为什么人会浮躁?

mikel阅读(876)

[转载]网络阅读,为什么人会浮躁? – 项目管理 – develop – JavaEye论坛.

这篇文章放到这个版面,因为我认为它属于管理的范畴:个人管理(时间管理、知识管理)。

是不是大家也有这种体会:

  • 网站注册越来越没耐心,看到页面全是文本框、下拉框,就心烦,咔一下关了。
  • 文章超过两屏的,就没勇气往下看。
  • 看到文章的相关链接,没完没了,两小时后,脑袋一片混乱,真想凉水冲冲。
  • Google Reader的未读项又是1000+了,看吧,压力太大,不看吧,有种挫败感,干脆,全部设置为已读。

焦虑、挫败、恐惧,什么感觉都来了,唯独没有愉悦感。

以上种种问题我都体验过,08年尤其突出,现在已经适应了,能够平静地进行网络阅读。

其实,阅读时浮躁,可能反映你对知识的渴求。比如你对IT技术很感兴趣,上JavaEye一篇篇读啊读,恨不 得一晚看100篇,于是导致每篇都是蜻蜓点水。其实,没必要这么大压力,就说我吧,以前写技术文章,如123,一篇就是一整 天,你一个晚上看10篇,收获已经很大了。如果你上猫扑,我估计你不会这么大压力,因为那些无聊文章你可看可不看。
是不是你会有种自我安慰?不过我还是要打击打击你:看理解类文章别太贪婪了,看了解类文章才可以略读。
打个现实的比方,如果有三个mm都在追你,除了让你心花怒放,会不会更让你心神不安患得患失,如果是你追求一个魂牵梦绕的mm,你是不是会很专 注?

浮躁往往是因为自己不能驾驭导致的。比如keso做donews主编时,订阅的RSS是1500,每天还写一 篇精品博客,因为他扫一眼就知道文章大概。你现在去读初中语文,就会理解我说的意思。
而不能驾驭,往往是因为对该专业不精通。记得以前自己通读过dojo框架源码,并做过小改进,后来有什么dojo新闻,我扫一眼就知道文章的含 量,但对于财经、房地产类文章,我至今还很容易被人忽悠,即使咬文嚼字地读。

判断力是你决定一篇文章是否值得阅读的条件,它是资讯的过滤器。专业能力决定了你的判断力水 平
浮躁在看资讯类文章时很明显,特别是你关注的领域,比如我现在很关注电子商务,这类文章会带给我压力。但我看企业管理类文章时,就会轻松得多,因 为我大致知道企业管理有哪些方面,哪些方面我深入思考过,比如沟通、决策、激励,哪些方面我现在很想了解。

为什么网站注册时会很浮躁呢?也许根源于是你的期望值,更确切地说,是你的投入产出比,如果只是因为想下载一 篇文章而不得不注册,你肯定会浮躁。如果你在携程上订打折机票,相信你不会浮躁。当你觉得一个网站分量很重时,它复杂的注册流程,可能会更赢得你的信赖, 比如招商银行网站。

那么,怎么改掉浮躁的坏习惯呢?
我上面其实都给出了答案,虽然是我的主观分析。
我从阅读工具说起,因为这比上面可操作性强。
就从iPhone展开吧。
我用iPhone近两年了,最近半年对它爱不释手,当然不是iPhone控。我以前用我的ThinkPad T60看Google Reader时,我会很烦,现在用iPhone看就不烦了。用过iPhone的,对iPhone不支持多任务咬牙切齿,后来我终于明白乔布斯这个洁癖和偏 执狂,为什么不让iPhone支持多任务了,从技术角度,iPhone是基于Unix内核,支持多任务很简单。乔布斯想让iPhone提 供了一种浸入式阅读体验。浸入式体验,我的理解是:只专注于一件事。比如iPhone上看天气、看股票。
那为什么PC不能提供浸入式呢?因为PC的多任务,我们会一会儿和QQ好友搭讪两句,一会儿看看股票,一会儿看看文章。打个比方,当你看到一位时 尚美女的衣领处若隐若现时,你会不心动吗?虽然你可能不会行动。
而iPhone剥夺了你选择的权利。你用iPhone上douban,看新浪微博,你会很专注的。注意,我说的这两个是iPhone APP,不是手机网站。

互联网文章,之所以让我们浮躁,我认为互联网的本质属性超链接为罪魁祸首,超链接会让人在网站之间频繁跳 转。我上面说的iPhone APP,如douban和微博是没有超链接的。没有超链接怎么可能遨游豆瓣的信息海洋?试试iPhone就知道了。iPhone这种没有超链接的导航模 式,会顺应你的意图高效阅读,而不是漫无目的的跳转。
iPad之所以是一款颠覆性产品,可能就是因为它的纸质阅读风格,对电子阅读体验做了很大提升,很多人说它是报刊和杂志的救星。那么,它一定会想 办法提升它的深阅读体验,让人尽量不浮躁。iPad也不提供多任务哦。
有时想,为什么我07年前看书不浮躁,那时候我啃过很多本技术类书籍,如《Core Java》 第一册我啃了三遍。也许,是因为选择权太少。如果你可以把新华书店几个橱柜搬到你的客厅,你试试看,你还能否静下心来阅读一本书。所以我建议那些电子书搜 集狂,该收敛一下了。

阅读环境,我发现对降低浮躁很有帮助。比如我喜欢去咖啡厅上网,在河边草坪上用iPhone阅读RSS。
以前回家我喜欢把客厅房间卫生间厨房等全部打开,然后坐在沙发上看书。现在晚上我喜欢把所有灯灭了,把路由器关了,静心认真阅读几个优秀的 PPT。
另外,我特地买了一个上网本HP Mini 210,专门为了上网。而没有在上面装任何开发或工作相关软件。

控制阅读量也非常重要。有段时间,我iPhone上的微博实在多得看不过来,根本没法思考。后来,我仔细分析 了一下每天的几个空闲时间段,如中午20分钟,睡前25分钟等。如果我只有45分钟时间,我该订阅多少微博?因为每看一篇有思想的微博,我都会问自己四个 问题,而这个思考时间不能省:这篇微博说了什么?怎么说的?说得对吗?对我有用吗?总之,我会心里想着给作者一个回复。
我看有很知名的博主也受不了微博的骚扰,比如工作时频繁MSN弹出。我只想告诉他:可能你利用微博的方式不对。

[转载][原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(后篇)

mikel阅读(723)

[转载][原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(后篇) – ASP.NET 架构 – 博客园.

3. 再次借鉴.NET Framework设计思想

自己实现其实不难, 关键看怎么做了。在实现的时候,如果自己单独去搞一套方式,如果设计的不好,可能到后来别人不好理解,甚至连自己也忘记当初自己为什么这样设计。所以,要 充分的借鉴已有的好的实现思想。分析了IQueryable,其实最大的区别就是,我们不希望去立刻操作数据源,但是在实现 IQueryable过程中是操作数据源的。除此之外,如记录对于实现了IQueryable接口的类的上的操作,这是我们需要的,也就是说我们自己的实 现的条件对象也要记录对它对象的操作,然后把这些操作在服务器那边解析执行。

所以,条件对象的接 口实现如下:

代码

/// <summary>
/// 所有的 查询对象都要从这个接口继承
/// </summary>
public interface ICriteria
{
#region Property

Type ObjectType { get; }

ICriteriaProvider Provider { get; }

#endregion

}

代码

/// <summary>
/// 泛型版 的条件对象
/// </summary>
/// <typeparam name=”TSource”></typeparam>
public interface ICriteria<T> : ICriteria
{
Dictionary
<string, List<Expression>> ExpressionDictionary { get; }

ICriteria<T> Where(Expression<Func<T, bool>> predicate);

ICriteria<T> OrderBy<K>(Expression<Func<T, K>> predicate);
ICriteria
<T> OrderByDescending<K>(Expression<Func<T, K>> predicate);

ICriteria<T> ThenBy<K>(Expression<Func<T, K>> predicate);
ICriteria
<T> ThenByDescending<K>(Expression<Func<T, K>> predicate);

ICriteria<T> Skip(int count);
ICriteria
<T> Take(int count);

ICriteria<T> First();
ICriteria
<T> First(Expression<Func<T, bool>> predicate);

ICriteria<T> Distinct<K>(Expression<Func<T, K>> predicate);
ICriteria
<T> All(Expression<Func<T, bool>> predicate);
ICriteria
<T> Any(Expression<Func<T, bool>> predicate);

ICriteria<T> GroupBy<K>(Expression<Func<T, K>> predicate);

ICriteria<T> Max<K>(Expression<Func<T, K>> predicate);
ICriteria
<T> Min<K>(Expression<Func<T, K>> predicate);
}

public interface ICriteriaProvider
{
object Execute<T>(ICriteria<T> condition);
}

大家可以看到,上面 的接口的声明和IQueryable合QueryProvider的声明很相似。

大家看到了,在上面 的ICritera接口声明中,有一些方法,如Where,First等等,其实这些方法在IQueryable也是有的,只不过是以扩 展方法的形式出现了。我们这里就直接写在这里了,如果以后要加入更多的方法,我们也可以利用扩展方法的方式。

IQueryable 中,使用 Expression Tree表达式树来记录操作的,那么我们这里也采用这种方式。

还有一点比较重要: 在上面的自己的实现linq to SQL的例子中,或者微软官方实现的linq to SQL中,操作的都是数 据实体和ADO.NET对象之间的关系,例如下面的代码:

代码

public bool MoveNext() {
if (this.reader.Read()) {
if (this.fieldLookup == null) {
this.InitFieldLookup();
}
T instance
= new T();
for (int i = 0, n = this.fields.Length; i < n; i++) {
int index = this.fieldLookup[i];
if (index >= 0) {
FieldInfo fi
= this.fields[i];
if (this.reader.IsDBNull(index)) {
fi.SetValue(instance,
null);
}
else {
fi.SetValue(instance,
this.reader.GetValue(index));
}
}
}
this.current = instance;
return true;
}
return false;
}

从代码中可以看出,在解析表达式树后,执 行返回的数据实体的属性的名字和查询出的表的字段名字是相同的。

但是:我们在客户端 构造出来的条件对象中使用的字段是业务类的属性,业务类中的属性名字和数据库表字段的名字可能不是一样的,甚至有可能业务类中的一个属性的值是几个表中字 段计算而来的。如Age>3,其中在Age属性的值,可能来自己数据库中UserDetail表中age字段。所以,对于 在业务类上生成的条件对象,通过解析最后要生成对应表的查询,例如在User类上生成的条件对象,最后生成的sql语句要是:select * from UserDetail where age>3 ,而不是select * from User where Age>3. 也就是说,User业务类要清楚的知道自己的数据来自哪个数据库表,而且User类中的属性也要清楚的知道自己的数据到底来自数据表中的哪个字段。

下面就来讲述如何解 决上面提到的问题。

Mapping属性

代码

public static readonly PropertyInfo<int> UserIdProperty = RegisterProperty<User>(
new PropertyInfo<int>(UserId,typeof(M_Product),Id));

public string UserId
{

get { return ReadProperty(UserIdProperty ); }
set { LoadProperty(UserIdProperty , value); }
}

从代码中可以看出,业务类的属性的声明和之前有点不一样了。可能一直以来,业务类中的属性都是像下面这样声明的:

public string UserId { get; set; }

但是,为了解决上面的问题:即每个属性都要清楚的知道自己对应数据表中的那个字段, 那么每个属性就要保存一些数据库中表的字段的信息,这样也便于条件对象最后生成正确的sql语句。

在框架中,有一个接 口的声明:

代码

public interface IPropertyInfo
{
/// <summary>
/// Gets the property name value.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the type of the property.
/// </summary>
Type Type { get; }

/// <summary>
/// Gets the friendly display name
/// for the property.
/// </summary>
string FriendlyName { get; }

/// <summary>
/// Mapping data entity type
/// </summary>
Type DataEntityTyoe { get; set; }

/// <summary>
/// Mapping to data entity name
/// </summary>
string DataEntityPropertyName { get; set; }

/// <summary>
/// Gets the default initial value for the property.
/// </summary>
/// <remarks>
/// This value is used to initialize the property’s
/// value, and is returned from a property get
/// if the user is not authorized to
/// read the property.
/// </remarks>
object DefaultValue { get; }
}

这个接口就为是为用来描述业务类中每一个属性的信息的,例如,属性对应哪个数据表中哪个字段。

框架中存在一个全局的字典,用来保存所有业务类的一些属性的信息:如下

Dictionary<Type,List<IPropertyInfo>> propertyInfomationDictionary;

在条件对象中的表达式树遍历解析的时候,就会使用这个字典中保存的信息来生成正确的sql语句。看看下面条件对象解释器的代码:大家只看 Translate方法就行了。

代码

public class CriteriaTranslator : YYT.Core.ExpressionVisitor
{
#region Fields

StringBuilder sb;
Dictionary<string, string> resultDictionary = null;
private Type objectType;

#endregion

#region Constructor

internal CriteriaTranslator(Type type)
{
objectType
= type;
}

#endregion

#region Main Methods

internal Dictionary<string, string> Translate(Dictionary<string, List<Expression>> expressionDictionary)
{
resultDictionary
= new Dictionary<string, string>();

foreach (var expressionKeyValePair in expressionDictionary)
{
this.sb = new StringBuilder();
foreach (Expression expression in expressionKeyValePair.Value)
{
this.Visit(Evaluator.PartialEval(expression));
}
AddExpressionResult(expressionKeyValePair.Key, sb.ToString());

}
return resultDictionary;
}

#endregion

#region Override Methods

protected override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(string))
{
switch (m.Method.Name)
{
case StartsWith:
sb.Append(
();
this.Visit(m.Object);
return m;
case Contains:
sb.Append(
();
this.Visit(m.Object);
sb.Append(
LIKE ‘%’ + );
this.Visit(m.Arguments[0]);
sb.Append(
+ ‘%’));
return m;
}
}

throw new NotSupportedException(string.Format(The method ‘{0}’ is not supported, m.Method.Name));
}

protected override Expression VisitUnary(UnaryExpression u)
{
switch (u.NodeType)
{
case ExpressionType.Not:
sb.Append(
NOT );
this.Visit(u.Operand);
break;
default:
throw new NotSupportedException(string.Format(The unary operator ‘{0}’ is not supported, u.NodeType));
}
return u;
}

protected override Expression VisitMemberAccess(MemberExpression m)
{
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
{
if (this.objectType.IsAssignableFrom(typeof(BusinessBase)))
{
List
<IPropertyInfo> propertyInfoList = PropertyInfoManager.GetRegisteredProperties(this.objectType);
var property
= propertyInfoList.Where(u => u.Name == m.Member.Name).SingleOrDefault();
if (property != null)
{
var dataEntityName
= property.DataEntityPropertyName;
sb.Append(dataEntityName);
AddExpressionResult(
Type, property.DataEntityTyoe.Name);
}
}
return m;
}
else
throw new NotSupportedException(string.Format(The member ‘{0}’ is not supported, m.Member.Name));

}

protected override Expression VisitBinary(BinaryExpression b)
{
sb.Append(
();
this.Visit(b.Left);
switch (b.NodeType)
{
case ExpressionType.And:
case ExpressionType.AndAlso:
sb.Append(
AND );
break;
case ExpressionType.Or:
case ExpressionType.OrElse:
sb.Append(
OR);
break;
case ExpressionType.Equal:
sb.Append(
= );
break;
case ExpressionType.NotEqual:
sb.Append(
<> );
break;
case ExpressionType.LessThan:
sb.Append(
< );
break;
case ExpressionType.LessThanOrEqual:
sb.Append(
<= );
break;
case ExpressionType.GreaterThan:
sb.Append(
> );
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(
>= );
break;
default:
throw new NotSupportedException(string.Format(The binary operator ‘{0}’ is not supported, b.NodeType));
}
this.Visit(b.Right);
sb.Append(
));
return b;
}

protected override Expression VisitConstant(ConstantExpression c)
{
IQueryable q
= c.Value as IQueryable;
if (q != null)
{
// assume constant nodes w/ IQueryables are table references
sb.Append(SELECT * FROM );
sb.Append(q.ElementType.Name);
}
else if (c.Value == null)
{
sb.Append(
NULL);
}
else
{
switch (Type.GetTypeCode(c.Value.GetType()))
{
case TypeCode.Boolean:
sb.Append(((
bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append(
);
sb.Append(c.Value);
sb.Append(
);
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format(The constant for ‘{0}’ is not supported, c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}

#endregion

#region Assistant Methods

private static Expression StripQuotes(Expression e)
{
while (e.NodeType == ExpressionType.Quote)
{
e
= ((UnaryExpression)e).Operand;
}
return e;
}

private void AddExpressionResult(string key, string value)
{
string tempValue = string.Empty;
if (!resultDictionary.TryGetValue(key, out tempValue))
{
lock (resultDictionary)
{
if (!resultDictionary.TryGetValue(key, out tempValue))
{
resultDictionary.Add(key, value);
}
}
}
}

#endregion
}

VisitMemberAccess方法就是用来把业务属性的 名字换成对应数据库中表字段的名字的。

代码

protected override Expression VisitMemberAccess(MemberExpression m)
{
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
{
if (this.objectType.IsAssignableFrom(typeof(BusinessBase)))
{
List
<IPropertyInfo> propertyInfoList = PropertyInfoManager.GetRegisteredProperties(this.objectType);
var property
= propertyInfoList.Where(u => u.Name == m.Member.Name).SingleOrDefault();
if (property != null)
{
var dataEntityName
= property.DataEntityPropertyName;
sb.Append(dataEntityName);
AddExpressionResult(
Type, property.DataEntityTyoe.Name);
}
}
return m;
}
else
throw new NotSupportedException(string.Format(The member ‘{0}’ is not supported, m.Member.Name));

}

上面的代码中,并没有立刻就把所有操作拼接成sql语句,而且把操作分类的记录下来在一个字典中,这个字典最后会被数据层那里使用。例如,字典中可能保存 的值如下:

操作

生成的sql语句

Where

age>18 and name like ’%xiaoyang%’

OrderBy

age

GroupBy

age

……………

…………………….

最后这个字典会在数 据层那边被使用:ICriteria包含CriteriaProvider,然后DAL代码调用 Icriteria.CriteriaProvider.Execute(),获得字典,然后执行真正的数据库操作。

下面就看看数据层的 一些接口声明就清楚了。

代码


/// <summary>
/// 数据提 供者要实现的借口
/// </summary>
public interface IDataProvider
{
DataResult
<TEntity> Add<TEntity>(TEntity entity) where TEntity : IDataEntity;
DataResult
<TEntity> Add<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;

DataResult<TEntity> Update<TEntity>(TEntity entity) where TEntity : IDataEntity;
DataResult
<TEntity> Update<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;
bool Update(ICriteria condiftion, object value);

DataResult<TEntity> Delete<TEntity>(TEntity entity) where TEntity : IDataEntity;
DataResult
<TEntity> Delete<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;
bool Delete(ICriteria condiftion);

int GetCount(ICriteria condition);

DataResult<TEntity> GetOne<TEntity>(ICriteria condition) where TEntity : IDataEntity;
DataResult
<TEntity> GetList<TEntity>(ICriteria condition) where TEntity : IDataEntity;
DataResult
<TEntity> GetPageData<TEntity>(ICriteria condition, int pageIndex, int pageSize, ref int entityCount) where TEntity : IDataEntity;
List
<object> GetCustomData(ICriteria condiftion);
}

4. 水到渠成

到这里,一切就很清 楚了。如果有不明白的地方或者需要讨论的,大家可以给我留言。我会及时的解答。

5. 代码版本的说明

大家之后可以下载到 代码的 V1.0版本,下载代码之后可以看到,这个V1.0的版本的代码中存在着CSLA的影子,确实是这样的。本框架在V1.0中确实融合了很多的开源框架,所 以不要奇怪。对CSLA不懂,也是没有问题的。

下面我就讲述一下以 后版本的一些特性和本系列文章的走向:

V1.0: 存在CLSA的影子。

V2.0:去掉 CLSA的影子,重构,并且使用VS开发出DSL工具,图形化的业务类开发和代码的生成

V3.0: 设计和开发出一种基于领域的语言和这个语言的编译器。

V4.0: 设计和开发领域语言的代码IDE。

V5.0: 使用领域语言开发项目

其中使用领域的语言 开发时 目标:例如,可能最后的代码是这样的写的:

Define a use who name is “xiaoyang”and email is “yangyang4502@yahoo.com.cn

然后我们的领域语言 编译器把上面的语句解释为代码,然后进行操作,最终的结果就是在数据库中的UserDetail表中添加了一条记录。

也就是我们梦寐以求 的那种“自然语言编程”,是的业务人员来写程序。

文章以后的难度和深 度会越来越大,我们一起加油,也希望大家及时的反馈。

呵呵,现在比较的兴 奋,说句”胡话”:咱们搞开发的,不仅仅只是懂得使用别人的开源框架和开源技术,咱们自己也可以成为技术的创建者。共勉

版权为小洋和博客园所有,欢迎转载,转载请标明出处给作者。

http://www.cnblogs.com/yanyangtian

Code

[转载][原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇)

mikel阅读(922)

[转载][原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇) – ASP.NET 架构 – 博客园.

.NET 业务框架开发实战 之十 第一阶段总结,深入浅出,水到渠成(前篇)

前言:这个系 列有段时间没有动了。主要是针对大家的反馈在修改代码。在修改的过程中,也有了一些新的体会,这里和大家分享一下,同时也发布一下业务框架的第一个版本。 在本篇文章中,学习到的不是仅仅只是代码,而是设计的思想和实现这种思想的方法。在写本篇时有个感触:把一个东西彻底的讲清楚,不容易。希望大家

多提意见。而且在写本篇的时候,我个人也是很兴奋的,至于原因 相信大家在看完之后就知道了。J

本篇的议题如下:

1. 打通业务层和数据层

2. 打通方法的选择和实现

3. 再次借鉴.NET Framework设计思想

4. 水到渠成

5. 代码的版本说明

1. 打通业务层和数据层

首先,回顾之前的文章一直讨论的问题:

1. 如何使得数据层” 以不变应万变”。

2. 条件对象如何实现

3. 如何在业务层和数 据层之间mapping数据

本篇就告诉大家,如何切切实实的解决上面三个问题。

首先,从一个图示开始讲述。

从上面的图中可以看出,架起在BLL和DAL之前的桥梁的就是中间的那个条 件对象。正是因为有了这个,所以上面的提出的问题才得以解决。

下面先从操作上总体来讲述一下这个图的具体流程:

a. 在业务类中创建一个 方法。例如在业务类User中,定义如下:

现在只看GetUserByAge这个方法:在方法中构造出一个条件对象, 大家第一眼能看出来:是Linq的实现。其实最后的实现只是借用了Linq的思想,仅此而已

b. 解析条件对象。在上面的构造的 条件对象中,Age,Name等,都是业务类User的字段,这些字段的值肯定最终是从数据库中的表字段中获取的(或者是通过数据库中的值算出来的),所 以在解析条件对象的时候,就要知道这些业务属性对应数据库中哪个表的哪个字段。因此在业务类中声明每个属性的时候就要同时保留它所对应的数据库字段的信 息。一旦业务类的属性中保存了这些信息

c. 数据层操作SQL语 句。在解析条件对象的时候,就会最终得到相对应的SQL语句,然后在数据层中执行这些SQL语句。

可能上面讲的比较的抽象,因为本篇的要讲述的东西确实比较的多(Word中 写了超过了10页),上面讲述的三个步骤也是先按大家有个印象。不是很懂也没有关系。

2. 打通方法的选择和实现

接下来就是方法的 探索和思考,以及实现的过程。我是想带着个大家跟着一起看看,为什么最后会采用这个解决方案的。

首先,就从条件对象 开始看起。

在实现条件对象(条 件对象和查询对象的区别之前讲过,这里重述一下:查询对象只是条件对象的一个子集,查询对象用来在查询的使用构造查询条件;条件对象不仅仅在查询时构造条 件,而且在增加,删除,,修改时候也使用,例如:只修改Name=”admin”的数据,在修改数据库的时候也用了一定的条件。所以条件对象>查询 对象)的时候,也是参看了其他开源框架的一些实现(Nhibernate中查询对象的,CSLA)。

同时要明白一点:设 计出来的框架是给开发人员使用的,所以要考虑如何使得开发人员最快最好的使用框架,所以要从开发人员的角度看(这一点也是很重要的)。例如在 Nhibernate中查询对象,使用的方法如下(仅仅是简单的举例而已):

代码
其实本系列中的业务框架之前的条件对象的构造也是参看了Nhibernate中查询对象的方法来实现和使用的,如下:

ICriteria condition=CriteriaFactory.Create(typeof(ProductBL).Where(ProductName, Operation.Equal,book);

因 为现在.NET中的开发人员对Linq的一些操作比较的熟悉,而且如果把条件对象的使用方式改为下面的方式:

ICriteria<ProductBL> condition=CriteriaFactory.Create<ProductBL>(o => o.ProductName == book);

那么开发人员的学习成本就几乎为零(因为他们熟悉Linq,如果条件对象也采用这种比较统一的方法实现,他们就可以采用”以此类推”的思想来使 用框架),更多的好处我就不说了,大家可以自己体会或者参看本系列之前的文章。

接下来就探索实现条件对象的方法(Linq to XXX篇)

熟悉Linq的朋友可以看出:可以使条件对象实现IQueryable接 口,然后采用实现linq to XXX的方法,实现自己的Linq Provider。这个方法是否可行,下面,我们就深入linq to XXX的来看一看,看完之后,结果就很清楚了。

首先来看下面两个接口:

代码

public interface IQueryable : IEnumerable {
Type ElementType {
get; }
Expression Expression {
get; }
IQueryProvider Provider {
get; }
}

public interface IQueryable<T> : IEnumerable<T>, IQueryable, IEnumerable {}
public interface IQueryProvider {

IQueryable CreateQuery(Expression expression);

IQueryable<TElement> CreateQuery<TElement>(Expression expression);

object Execute(Expression expression);

TResult Execute<TResult>(Expression expression);

}

Linq的出现,使得很多东 西都和linq扯上了关系:Linq to sql, Linq to Google, Linq to JavaScript, Linq to Nhibernate…… 所列出来的这些,我们统称为linq to XXX。这些Linq to XXX都是实现了上面两个接口。

下面就通过自己实现linq to sql来举例分析。

从总体来看:linq to sql的本质就是:把操作转换为sql语句,然后用ADO.NET执行,最后把结果转换为实体返回。

其实下面列出了这么多的代码,其中最关键的其实就是 QueryProvider中的Execute方法:这个方法负责把你的操作进行解析,其实真正负责解析的QueryTranslator。

代码

public class Query<T> : IQueryable<T>, IQueryable, IEnumerable<T>, IEnumerable, IOrderedQueryable<T>, IOrderedQueryable {
QueryProvider provider;
Expression expression;

public Query(QueryProvider provider) {
if (provider == null) {
throw new ArgumentNullException(provider);
}
this.provider = provider;
this.expression = Expression.Constant(this);
}

public Query(QueryProvider provider, Expression expression) {
if (provider == null) {
throw new ArgumentNullException(provider);
}
if (expression == null) {
throw new ArgumentNullException(expression);
}
if (!typeof(IQueryable<T>).IsAssignableFrom(expression.Type)) {
throw new ArgumentOutOfRangeException(expression);
}
this.provider = provider;
this.expression = expression;
}

Expression IQueryable.Expression {
get { return this.expression; }
}

Type IQueryable.ElementType {
get { return typeof(T); }
}

IQueryProvider IQueryable.Provider {
get { return this.provider; }
}

public IEnumerator<T> GetEnumerator() {
return ((IEnumerable<T>)this.provider.Execute(this.expression)).GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator() {
return ((IEnumerable)this.provider.Execute(this.expression)).GetEnumerator();
}

public override string ToString() {
return this.provider.GetQueryText(this.expression);
}
}

代码

public abstract class QueryProvider : IQueryProvider {
protected QueryProvider() {
}

IQueryable<S> IQueryProvider.CreateQuery<S>(Expression expression) {
return new Query<S>(this, expression);
}

IQueryable IQueryProvider.CreateQuery(Expression expression) {
Type elementType = TypeSystem.GetElementType(expression.Type);
try {
return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType), new object[] { this, expression });
}
catch (TargetInvocationException tie) {
throw tie.InnerException;
}
}

S IQueryProvider.Execute<S>(Expression expression) {
return (S)this.Execute(expression);
}

object IQueryProvider.Execute(Expression expression) {
return this.Execute(expression);
}

public abstract string GetQueryText(Expression expression);
public abstract object Execute(Expression expression);
}

代码

internal class QueryTranslator : ExpressionVisitor {
StringBuilder sb;

internal QueryTranslator() {
}

internal string Translate(Expression expression) {
this.sb = new StringBuilder();
this.Visit(expression);
return this.sb.ToString();
}

private static Expression StripQuotes(Expression e) {
while (e.NodeType == ExpressionType.Quote) {
e
= ((UnaryExpression)e).Operand;
}
return e;
}

protected override Expression VisitMethodCall(MethodCallExpression m) {
if (m.Method.DeclaringType == typeof(Queryable) && m.Method.Name == Where) {
sb.Append(
SELECT * FROM ();
this.Visit(m.Arguments[0]);
sb.Append(
) AS T WHERE );
LambdaExpression lambda
= (LambdaExpression)StripQuotes(m.Arguments[1]);
this.Visit(lambda.Body);
return m;
}
throw new NotSupportedException(string.Format(The method ‘{0}’ is not supported, m.Method.Name));
}

protected override Expression VisitUnary(UnaryExpression u) {
switch (u.NodeType) {
case ExpressionType.Not:
sb.Append(
NOT );
this.Visit(u.Operand);
break;
default:
throw new NotSupportedException(string.Format(The unary operator ‘{0}’ is not supported, u.NodeType));
}
return u;
}

protected override Expression VisitBinary(BinaryExpression b) {
sb.Append(
();
this.Visit(b.Left);
switch (b.NodeType) {
case ExpressionType.And:
sb.Append(
AND );
break;
case ExpressionType.Or:
sb.Append(
OR);
break;
case ExpressionType.Equal:
sb.Append(
= );
break;
case ExpressionType.NotEqual:
sb.Append(
<> );
break;
case ExpressionType.LessThan:
sb.Append(
< );
break;
case ExpressionType.LessThanOrEqual:
sb.Append(
<= );
break;
case ExpressionType.GreaterThan:
sb.Append(
> );
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(
>= );
break;
default:
throw new NotSupportedException(string.Format(The binary operator ‘{0}’ is not supported, b.NodeType));
}
this.Visit(b.Right);
sb.Append(
));
return b;
}

protected override Expression VisitConstant(ConstantExpression c) {
IQueryable q
= c.Value as IQueryable;
if (q != null) {
// assume constant nodes w/ IQueryables are table references
sb.Append(SELECT * FROM );
sb.Append(q.ElementType.Name);
}
else if (c.Value == null) {
sb.Append(
NULL);
}
else {
switch (Type.GetTypeCode(c.Value.GetType())) {
case TypeCode.Boolean:
sb.Append(((
bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append(
);
sb.Append(c.Value);
sb.Append(
);
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format(The constant for ‘{0}’ is not supported, c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}

protected override Expression VisitMemberAccess(MemberExpression m) {
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter) {
sb.Append(m.Member.Name);
return m;
}
throw new NotSupportedException(string.Format(The member ‘{0}’ is not supported, m.Member.Name));
}
}

对于实现了IQueryable的对象,在他们进行的每一个操作(也就是调 用实现这个接口的类上的方法)其实都没有立刻去执行,而且把进行的操作记录下来了,放在一个称为Expression Tree表达式数的数据结构中,然后再真正执行的时候(就是调用Execute来执行), QueryTranslator对象就遍历表达式树,对操作进行解析,例如linq to sql就是把表达式树中操作解析为对数据库进行的操作,以sql语句的形式体现出来.

当把操作解析为了 sql语句之后,就是用ADO.NET的方法来执行SQL操作,然后通过反射,把ADO.NET执行的结果转换为数据实体。如下:

代码

internal class ObjectReader<T> : IEnumerable<T>, IEnumerable where T : class, new() {
Enumerator enumerator;

internal ObjectReader(DbDataReader reader) {
this.enumerator = new Enumerator(reader);
}

public IEnumerator<T> GetEnumerator() {
Enumerator e
= this.enumerator;
if (e == null) {
throw new InvalidOperationException(Cannot enumerate more than once);
}
this.enumerator = null;
return e;
}

IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}

class Enumerator : IEnumerator<T>, IEnumerator, IDisposable {
DbDataReader reader;
FieldInfo[] fields;
int[] fieldLookup;
T current;

internal Enumerator(DbDataReader reader) {
this.reader = reader;
this.fields = typeof(T).GetFields();
}

public T Current {
get { return this.current; }
}

object IEnumerator.Current {
get { return this.current; }
}

public bool MoveNext() {
if (this.reader.Read()) {
if (this.fieldLookup == null) {
this.InitFieldLookup();
}
T instance
= new T();
for (int i = 0, n = this.fields.Length; i < n; i++) {
int index = this.fieldLookup[i];
if (index >= 0) {
FieldInfo fi
= this.fields[i];
if (this.reader.IsDBNull(index)) {
fi.SetValue(instance,
null);
}
else {
fi.SetValue(instance,
this.reader.GetValue(index));
}
}
}
this.current = instance;
return true;
}
return false;
}

public void Reset() {
}

public void Dispose() {
this.reader.Dispose();
}

private void InitFieldLookup() {
Dictionary
<string, int> map = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
for (int i = 0, n = this.reader.FieldCount; i < n; i++) {
map.Add(
this.reader.GetName(i), i);
}
this.fieldLookup = new int[this.fields.Length];
for (int i = 0, n = this.fields.Length; i < n; i++) {
int index;
if (map.TryGetValue(this.fields[i].Name, out index)) {
this.fieldLookup[i] = index;
}
else {
this.fieldLookup[i] = 1;
}
}
}
}
}

上面简单的介绍了如何实现linq to sql具体的实现代码,大家可以自己过后慢慢的看或者参看我的另外的一个linq系列,现在我们继续后面的话题。

现在我们回到之前的 话题:条件对象是否可以采用这种方式实现

大家看到上面的代码 Query<T>其中的T,和Execute方法的返回值。在上面的代码中,如果T是User类,即,Query<User>, 那么最后的Execute方法返回的一定会是User的集合。也就是说:Execute方法已经对数据源(这里是数据库)进行了操作,并且把结果以数据实 体的形式已经返回了,并且返回的数据实体的类型就是T,例如User。

但是我们这里的要实 现的条件对象只是想把条件构造出来,不是立刻去执行。至于具体的执行数据操作者(DataProvider)可以任意 选择的:使用ADO.NET方法,还是EF的方法,还是Nhibernate,都是可以配置的。如下:

代码

public List<User> GetUserByAge()
{
ICriteria
<User> conditionPerson =
CriteriaFactory.Create
<User>().Where(u => u.Age < this.Age).OrderBy<string>(u => u.Name).Skip(8).Take(8);
return DataPortal.Query(conditionPerson);
}

上面的代码中,Create方法就是实例化一个 ICriteria<User>,此时我们想做的仅仅只是一件事:把在 ICriteria<User>上的操作记录下来而已。然后把记录下来的结果解析,解析的最终结果就是一条sql命令,然后再给不同的 DataProvider去执行。也就是说,在DataPortal内部可以配置用什么方法来执行数据操作:是直接使用ADO.NET执行sql命令,还 是把sql命令给Entity Framework…通过配置决定。如果ICriteria<T>是从IQueryable接口进行了继承,那么在ICriteria实 现这个结果的过程中就必须要去数据库中进行执行,因为Execute方法返回的是T的集合,而不是sql命令(字符串)。

大家可能想到:那就 在Execute方法中去实现不同的DataProvider,例如之前的例子在ObjectReader用ADO.NET实现了,那么也可以在 ObjectReader中用EF实现数据操作。这个方法确实可以,也很不错。但是这个方法在分布式开发中(特别是在WCF中)有一点的局限性。例如你有 一个界面,上面可以有很多的选项,如下:

在服务接口那边,你肯定不想定义N多差不多的接口方法:如

GetUserByName(string username);

GetUserByEmail(string email);

或者

GetUserByCondition(string username,string password,string email …..);

这样都是很不灵活的,如果User的属性减少了或者增多了,那么如果要 在服务器那边暴露的接口的方法也要修改,这样终究是不好。如下采用下面的方法:

GetUserByCondition(Critera condition);

其中,Critera是条件对象。那么我们在客户端就可以任意构造条件 对象,这个条件对象就把在它上面进行的操作记录下来,然后统一的交给GetUserByCondition方法去服务器解释并执行。此时,这个条件对象就 是在客户端生成的,而且这个条件对象此时是不用去数据库中去执行的。如果条件对象是从IQueryable接口继承的,那么在客户端构造完条件对象之后, 就要去数据库中执行了,如果再在ObjectReader搞个分布式调用,难度不说,也很别扭,这不是我们所要的。

所以,综合上面的一 些考虑,那么可以确定:条件对象不继承IQueryable接口。但是我们又希望采用类似linq的操作,那么只有自己实现了。

本篇就暂时写到这里,因为太长了,所以分为前篇和后篇发布,因为博客园 不能在一小时内发两篇,所以后篇将会在9点左右发布。希望大家见谅。

版权为小洋和博客园所有,,欢迎转载,转载请 标明出处给作者。

http://www.cnblogs.com/yanyangtian

[转载]打印样式表(UE之可用性)

mikel阅读(1041)

[转载]打印样式表(UE之可用性) – sohighthesky – 博客园.

介绍

一个打印样式表设置网页的样式,使页面在打印的时候,呈现更友好的样式。打印样式表已存在数年了,然而,现在很少有网站使用到它,这意味着我们让并 不是很适合的网页打印到纸上。(比如yahoo : http://developer.yahoo.com/performance/rules.html,这个页面打印后,会显示右边的边栏,而通常我们 只是想要这篇文章的内容,而且显示右边后,导致内容宽度太小,字体超小 ,所以如果你打印过个页面,你就会意识到本文的必要性)

  • 打印样式表极大的提高了可用性,尤其是有很多内容的页面(比如本页面)
  • 它可以简单迅速的设置
一些网站也提供了打印版本的链接,但是很明显这还需要建立和维护。它还需要用户在页面里找到这个链接,然后打开打开这个页面,然后再打印。然而人们通常是 直接“文件-打印“或者 直接按ctrl+p.

如何设置你的打印样式表

打印样式表与我们通常使用的样式表很像,但是它只会在打印的时候调用。要使用一个打印样式表,只需要将下面的代码放到head里

<link rel="stylesheet" href="print.css" type="text/css" media="print" />
print.css就是打印样式表文件,media="print"意味着这个样式表只有在打印的时候被调用。属性 media有很多可选项(比如handheld,tv,screen),查看完整列表
删除不需要的项目

通常我们只要想要logo和页面的内容在出现在打印版本里,所以通常要移除头部和底部(或者 移除左边和右边的边栏),也许还有其它单独的元素不想显示,只需要在html里加上class="noprint",要移除这 些内容,只需要设置:display:none,这样样式表打概可以写成这样:

#header, #nav, .noprint {display: none;}
设置页面格式

确保页面内容涵盖了充分的宽度(防止像上面yahoo那页面一样打印出来宽度和字体太小,无法阅读的杯具)

#container, #container2, #content {width: 100%; margin: 0; float: none;}
链接样式
a:link, a:visited {color: #781351}

链接打印出来后看不到地址了,我们也可以让其地址显示在链接后面

.printlink a:after{
content:" <" attr(href) ">";
color:#FF0000;font-style:italic;
}

制作打印样式表

在制作打印样式表时,可以直接将样式写在主样式 表后面,检查页面在屏幕上显示的内容,直到满意,然后可以剪切到print.css中,给所有的页面使用

[转载]PHP三层结构(上)

mikel阅读(1073)

[转载]PHP三层结构(上) – Net.AfritXia – 博客园.

我们以一个简单的留言板代码为例,先来看一个最简单的三层结构代码:

// 代码 1 // 外观层类 class LWordHomePage { // 添加留言 public function append($newLWord) { // 调用中间服务层 $serv = new LWordServiceCore(); $serv->append($newLWord); } }; // 中间服务层 class LWordServiceCore { // 添加留言 public function append($newLWord) { // 调用数据访问层 $dbTask = new LWordDBTask(); $dbTask->append($newLWord); } }; // 数据访问层 class LWordDBTask { // 添加留言 public function append($newLWord) { // 数据层代码(省略) } };

执行时序图,如图1所示:

(图1),简单三层结构时序图

从代码和时序图中可以直观看出三层结构的调用顺序。但实际开发中这种简单三层结构并不能满足需求!我们先从外观层与中间服务层这两层的代码开始讨 论。在外观层LWordHomePage类中直接使用new关键字创建并调用中间服务类LWordServiceCore属于一种硬编码的方式。在实际项目开发过程中,外观层和中间服 务层可能会由不同的人员来开发,即一个功能模块由多个人共同完成。而外观层LWordHomePage类的开发进度是不可能等到 LWordServiceCore类完全开发完成之后才开始(换句话来说就是,外观层不可能等到中间服务层完全开发完成之后才开始),这样的协作效率非常低!为了使项目可以由多人同时开发,所 以我们要切割代码设计。我们可以组织一个临时的中间服务类来满足外观层的开发进度。等到中间服务层完全开发完成之后,替换一下就可以了……如图2所示:

(图2),外观层在不同的服务间切换

显然,要实现这样的需求,在外观层中直接使用new关键字创建和调用LWordServiceCore类是非常不灵活的!这很难做到灵活的随意的切 换!!我们可以创建TempService类充当中间服务层的临时实现。我们还需要分析一下TempService和LWordServiceCore这 两个类,它们都具备相同的append函数用于添加留言,只不过一个是临时的而另外一个是真实的。既然TempService和 LWordServiceCore这两个类都有公共函数,那么就应该可以有一个公共的父类。考虑到对这个公共的上级类没有别的成员和属性,所以将这个公共 的上级类定义为接口,即ILWordService!UML类图如图3所示:

(图3)定义和实现ILWordService接口

在LWordHomePage类中并不直接创建TempService或者LWordServiceCore类对象,创建过程会交给一个工厂类 MyServiceFactory(简单工厂模式)。这样一来, 外观层中的LWordHomePage类只需要知道ILWordService接口即可,外观层代码并不关心具体的中间服务代码是什么,这样就极好的实现了 外观层与具体服务代码的分离。

这相当于什么呢?就好比两个硬件工程师,一个是制造计算机显卡的,一个是制造计算机主板的。制造显卡的工程师可以把显卡插到到一块测试电路中,来测 试显卡是否可以正常工作?同样,制造主板的工程师也可以把主板插入到另一块测试电路中,来测试主板是否可以正常工作?等到这两位工程师都各自完成工作之 后,将他俩的工作成果对接在一起就可以了。这是一种并行开发方式,几乎可以省掉一半的时间。从软件工程的角度来讲,我们在设计接口代码的时候也应该考虑是 否需要支持多人同时开发,从而提高生产效率。

依照UML类图(如图3所示),我们修改PHP代码如下:

// 代码2, 通过工厂创建留言服务并调用 // 外观层类 class LWordHomePage { // 添加留言 public function append($newLWord) { // 调用中间服务 $serv = MyServiceFactory::create(); // 注意此时是操作 ILWordService 接口, 而非 LWordService 类 $serv->append($newLWord); } }; // 留言服务接口 interface ILWordService { public function append($newLWord); }; // 服务工厂类 class MyServiceFactory { // 创建留言服务 public static function create() { if (1) { // 返回中间服务层 return new LWordServiceCore(); } else { // 返回临时实现 return new TempService(); } } } // 临时服务类 class TempService implements ILWordService { // 添加留言 public function append($newLWord) { // 临时代码(省略) } }; // 中间服务层 class LWordServiceCore implements ILWordService { // 添加留言 public function append($newLWord) { // 调用数据访问层 $dbTask = new LWordDBTask(); $dbTask->append($newLWord); } }; // 数据访问层 class LWordDBTask { // 添加留言 public function append($newLWord) { // 数据层代码(省略) } };

时序图如图4所示:

(图4)通过工厂类创建留言服务

[转载]有关Lucene的问题(8):用Lucene构建实时索引的文档更新问题

mikel阅读(848)

[转载]有关Lucene的问题(8):用Lucene构建实时索引的文档更新问题 – 觉先 – 博客园.

在有关Lucene的问题(7),讨论了使用Lucene内存索引和硬盘索引构建实时索引的问题。

然而有的读者提到,如果涉及到文 档的删除及更新,那么如何构建实时的索引呢?本节来讨论这个问题。

1、Lucene删除文档的几种方式

  • IndexReader.deleteDocument(int docID)是用 IndexReader 按文档号删除。
  • IndexReader.deleteDocuments(Term  term)是用 IndexReader 删除包含此词(Term)的文档。
  • IndexWriter.deleteDocuments(Term  term)是用 IndexWriter 删除包含此词(Term)的文档。
  • IndexWriter.deleteDocuments(Term[]  terms)是用 IndexWriter 删除包含这些词(Term)的文档。
  • IndexWriter.deleteDocuments(Query  query)是用 IndexWriter 删除能满足此查询(Query)的文档。
  • IndexWriter.deleteDocuments(Query[] queries)是用 IndexWriter 删除能满足这些查询(Query)的文档。

删除文档既可以用 reader进行删除,也可以用writer进行删除,不同的是,reader进行删除后,此reader马上能够生效,而用writer删除后,会被缓 存,只有写入到索引文件中,当reader再次打开的时候,才能够看到。

2、Lucene文档更新的几个问题

2.1、使用IndexReader还是IndexWriter进行删除

既 然IndexReader和IndexWriter都能够进行文档删除,那么到底是应该用哪个来进行删除呢?

本文的建议是,用 IndexWriter来进行删除。

因为用IndexReader可能存在以下的问题:

(1) 当有一个IndexWriter打开的时候,IndexReader的删除操作是不能够进行的,否则会报 LockObtainFailedException

(2) 当IndexReader被多个线程使用的时候,一个线程用其进行删除,会使得另一个线程看到的索引有所改变,使得另一个线程的结果带有不确定性。

(3) 对于更新操作,在Lucene中是先删除,再添加的,然而删除的被立刻看到的,而添加却不能够立刻看到,造成了数据的不一致性。

(4) 即便以上问题可以通过锁来解决,然而背后的操作影响到了搜索的速度,是我们不想看到的。

2.2、如何 在内存中缓存文档的删除

在上一节中,为了能够做到实时性,我们使用内存中的索引,而硬盘上的索引则不经常打开,即便 打开也在背后线程中打开。

而要删除的文档如果在硬盘索引中,如果不重新打开则看不到新的删除,则需要将删除的文档缓存到内存中。

那如何将缓存在内存中的文档删除在不重新打开IndexReader的情况下应用于硬盘上的索引呢?

在Lucene中,有一种 IndexReader为FilterIndexReader,可以对一个IndexReader进行封装,我们可以实现一个自己的 FilterIndexReader来过滤掉删除的文档。

一个例子如下:

public class MyFilterIndexReader extends FilterIndexReader {

OpenBitSet dels;

public MyFilterIndexReader(IndexReader in) {

super(in);

dels = new OpenBitSet(in.maxDoc());

}

public MyFilterIndexReader(IndexReader in, List<String> idToDelete) throws IOException {

super(in);

dels = new OpenBitSet(in.maxDoc());

for(String id : idToDelete){

TermDocs td = in.termDocs(new Term(“id”, id)); //如果能在内存中Cache从 Lucene的ID到应用的ID的映射,Reader的生成将快得多。

if(td.next()){

dels.set(td.doc());

}

}

}

@Override

public int numDocs() {

return in.numDocs() – (int) dels.cardinality();

}

@Override

public TermDocs termDocs(Term term) throws IOException {

return new FilterTermDocs(in.termDocs(term)) {

@Override

public boolean next() throws IOException {

boolean res;

while ((res = super.next())) {

if (!dels.get(doc())) {

break;

}

}

return res;

}

};

}

@Override

public TermDocs termDocs() throws IOException {

return new FilterTermDocs(in.termDocs()) {

@Override

public boolean next() throws IOException {

boolean res;

while ((res = super.next())) {

if (!dels.get(doc())) {

break;

}

}

return res;

}

};

}

}

2.3、 文档更新的顺序性问题

Lucene的文档更新其实是删除旧的文档,然后添加新的文档。如上所述,删除的文档是缓存在 内存中的,并通过FilterIndexReader应用于硬盘上的索引,然而新的文档也是以相同的id加入到索引中去的,这就需要保证缓存的删除不会将 新的文档也过滤掉,将缓存的删除合并到索引中的时候不会将新的文档也删除掉。

Lucene的两次更新一定要后一次覆盖前一次,而不能让前 一次覆盖后一次。

所以内存中已经硬盘中的多个索引是要被保持一个顺序的,哪个是老的索引,哪个是新的索引,缓存的删除自然是应该应用于所 有比他老的索引的,而不应该应用于他自己以及比他新的索引。

3、具有更新功能的Lucene实时索引方案

3.1、初始化

首先假设我们硬盘上已经有一个索引FileSystemIndex,被事 先打开的,其中包含文档1,2,3,4,5,6。

我们在内存中有一个索引MemoryIndex,新来的文档全部索引到内存索引中,并且 是索引完IndexWriter就commit,IndexReader就重新打开,其中包含文档7,8。

绘图8

3.2、 更新文档5

这时候来一个新的更新文档5, 需要首先将文档5删除,然后加入新的文档5。

需要做的事 情是:

  • 首先在内存索引中删除文档5,当然没有文档5,删除无效。
  • 其次将对文档5的删除放入内存文档删 除列表,并与硬盘的IndexReader组成FilterIndexReader
  • 最后,将新的文档5加入内存索引,这时候,用户 可以看到的就是新的文档5了。
  • 将文档5放入删除列表以及将文档5提交到内存索引两者应该是一个原子操作,好在这两者都是比较块的。

注:此处对硬盘上的索引,也可以进行对文档5的删除,由于IndexReader没有重新打开,此删除是删不掉的,我们之所以没有这样做,是想保 持此次更新要么全部在内存中,要么全部在硬盘中,而非删除部分已经应用到硬盘中,而新文档却在内存中,此时,如果系统crash,则新的文档5丢失了,而 旧的文档5也已经在硬盘上被删除。我们将硬盘上对文档5的删除放到从内存索引向硬盘索引的合并过程。

更新文档5

如果再有一次对文档5的更新,则首先将内存索引 中的文档5删除,添加新的文档5,然后将文档5加入删除列表,发现已经存在,则不必删除。

3.3、合并索引

然而经过一段时间,内存中的索引需要合并到硬盘上。

在合并的过程中,需要重新建立一个空的内存索引,用于合并阶段索引新的文档, 而合并中的索引的IndexReader以及硬盘索引和删除列表所组成的FilterIndexReader仍然保持打开,对外提供服务,而合并阶段从后 台进行。

后台的合并包括以下几步:

  • 将删除列表应用到硬盘索引中。
  • 将内存索引合并到硬 盘索引中。
  • IndexWriter提交。

合并

3.4、合并的过程中更新文 档5

在合并的过程中,如果还有更新那怎么办呢?

  • 首先将合并中索引的文档5删除,此删除 不会影响合并,因为合并之前,合并中索引的IndexReader已经打开,索引合并中索引的文档5还是会合并到硬盘中去的。此删除影响的是此后的查询在 合并中索引是看不到文档5的。
  • 然后将文档5的删除放入删除列表,并同合并中索引的删除列表,已经硬盘索引一起构成 FilterIndexReader。
  • 将新的文档5添加到内存中索引。
  • 提交在合并中索引对文档5的删除,将 文档5添加到删除列表,提交在内存索引中对文档5的添加三者应该是一个原子操作,好在三者也是很快的。

合并时更新

3.5、重新打开硬盘索引的 IndexReader

当合并中索引合并到硬盘中的时候,是时候重新打开硬盘上的索引了,新打开的 IndexReader是可以看到文档5的删除的。

如果这个时候有新的更新,也是添加到内存索引和删除列表的,比如我们更新文档6.

重新打开

3.6、替代 IndexReader

当IndexReader被重新打开后,则需要删除合并中的索引及其删除列表,将硬盘索引 原来的IndexReader关闭,使用新的IndexReader。

替换IndexReader