终结DbHelper鬼画符1:Scrum实战演示
这个系列,记录实际工作中基础类库开发的思维方式、类设计过程。额外的,也会提及实战中如何使用Scrum类似的过程来组织自己的工作,以及在这类工作中如何使用Tdd方式。
这个世界充斥着一种叫做DbHelper的东西,微软的Entlib已经坚持不懈更新多年了,满世界都能够搜索到不知是谁发布的各类DbHelper类的代码,许多程序员都有自己的具备完全或不完全自主知识产权版本。这其实是件非常痛苦的事情,因为过多的选择总会令人迷茫。
于是在某一天,我决定终结这种鬼画符。
我首先思考,为什么要用Ado.net,为什么要用DbHelper?其实大家有许多选择,Linq To Sql、Aef是一层包裹,社区有许多Orm工具,不过大多数程序员仍然使用原生的Ado.net。原因有两种:性能问题,编程复杂度问题。从这个角度而言,说明微软自己的Entlib、Linq To Sql和Ado.net Entity,这类努力并没有带来合用的东西。
进一步,为什么需要DbHelper?想少写一些代码也就是复用代码,同时希望在多种数据库之间无缝切换,这两点是原生的动力。不过,这不是回答,真正的回答是,Ado.net本身的设计比较混乱。
一个良好的设计,应保证用户,也就是使用这个体系的程序员,能够非常简易的使用。这体现在两个方面,第一、概念要尽可能的少。第二、代码要尽可能的少。理解这一点其实也是非常容易的,程序员为最终用户提供的应用,需要易于理解、需要操作的步骤尽可能的少,这是对应的用户体验问题。
框架的设计和开发,最终用户是程序员,也应该遵循同样的原则。
我想做的事情,包括两个层面,第一、易于理解:程序员只需要理解连接、命令、DbReader三者,就能够便捷的应用Ado.net开发,这需要屏蔽DbProviderFactory、DbProviderFactories、DbDataAdapter和DataSet、DataTable之类概念。第二、简单的定义实体和实体集合对象,从而在大多数情况下无需直接写数据库访问代码。
当然,这里的前提,是维持Ado.net的性能水平。
幸运的是,这项工作远没有想象中那样困难。几个月前,经过5个工作日,相对比较轻松的达成了上述目标。两个地球人都没有想到的方式,第一、自己定义一个继承于DbConnection的类,解决消灭DbProviderFactory的问题;第二,使用索引器实现实体对象和DataRow的互相转换,从而避免使用反射,确保性能。
结合代码生成器自行创建实体类,多数情况下,无需编写任何数据访问代码。
我们遇到的第一个问题,是DbProviderFactory的问题。
Db系列的类基本上是抽象类,举例来说,SqlConnection和OleDbConnection都是继承于DbConnection的,为了尽可能容易的在不同数据库中切换,我们需要使用Db系列的类。由于是抽象类,当然无法自行实例化,你只能通过DbProviderFactory创建,无论是连接、适配器甚至参数,都需要通过它来创建。熟悉设计模式的朋友应该知道,这是提供者模式和工厂模式的混血儿,微软那些年轻的小学究们还真是煞费苦心。
DbProviderFactory通过DbProviderFactories.GetDbFactory(“提供者名称”)获取。这样,你需要在代码中维持一个DbProviderFactory对象,然后,比较笨拙的做法是,在每个地方,创建Db系列的对象时,都用DbProviderFactory.CreateXXX之类的方式去做。我非常讨厌这样子,为什么讨厌,你懂的:
这样不是很好吗?DbConnection connection= new DbConnection(数据库类型,连接字符串);甚至,你的系统中如果不涉及到同时使用Oracle、SqlServer的情形,你可以将数据库类型和连接字符串保存在配置文件中,这样创建连接的时候连参数也无需考虑。
既不需要理解DbProviderFactory,也不需要理解DbProviderFactories,更不需要理解“提供者名称”,我相信这些东西很难用自然的中文,来表达它是神马意思。每次实例化的时候代码也少了许多,这显然符合我们概念少、做事少的目标。
目的明确,那么,如同阿Q先生所说,我们革命吧
我选择的项目管理工具,比较懒惰。使用Team Foundation Server,使用微软的Scrum 1.0模版,我们基于这个模版创建一个团队项目,命名为Faster。按照惯例,应该先提出项目愿景、列出Product Backlog,这实际上就是Story、用例、用户情景的集合,这些翻译我不满意,因此我简单的翻译成“项目目标”和“功能清单”。
项目目标:以最简单的方式处理数据访问,消灭绝大部分与数据访问相关的代码编写任务。
1.简化DbConnection的创建方式:工作量评估2 优先级1000
2.获取数据库中的元数据:工作量评估5 优先级2000
3.自动生成Crud命令:工作量评估3 优先级3000
4.实现数据访问泛型类:工作量评估8 优先级4000
5.制作代码生成器:工作量评估3 优先级5000
6.通过外键处理一对多关系:工作量评估2 优先级6000
项目目标定义清晰是非常重要的,两句话,解决“我们这个项目要做什么”的问题。
功能清单,必须使用用户语言,每一项功能都是满足用户的一项特定的需求。这里非常简单的只列出标题,实际工作中,对于每一项功能,我仅仅是写一段两百字以内的介绍,绝不愿意书写非常庞大的文本,那既没有必要、程序员也不会真正用心去看。注意标题的书写格式,没有主语,动宾结构,形如“做--什么”,其中优先级应由用户决定,工作量评估则是团队程序员通过讨论决定。这里工作量评估的单位是一个相对的单位,你可以将其看成“理想工作日”,只是衡量每项功能相互之间的大小。我首先在功能列表中找出最小的第一项,将其定义为2,第二项我认为它的工作量应是第一项的2倍到3倍之间,定义为5。
然后,我们将这些功能的“迭代”也就是我翻译的阶段,设置为第一个版本的第一个Sprint,嗯,有些拗口。我们简单些:我认为这些任务在一个阶段就能够完成,无需划分为多个阶段。如果是比较大的项目,此时应根据优先级和工作量评估,将其划分成多个阶段,保持每个阶段的工作量总和大体均衡。微软Scrum模版中的版本概念不需要考虑。
现在,打开Product Backlog,什么都没有了。
面对你的第一个Sprint,也就是第一个阶段,刚刚那些功能都转到这里了。
接下来,我们为每一项功能划分“任务”,举例来说,对于第一项功能,简化DbConnection的创建工作,我将其划分为如下的工作任务:
1.创建测试数据库 优先级1005 需要1小时
2.继承DbConnection,创建Db类 优先级1010 需要2小时
3.创建连接:优先级1015 需要1小时
4.使用配置文件中的默认连接字符串,创建连接:优先级1020,需要1小时
5.实现其他2种创建连接方式:优先级1025,需要1小时
总体需要6小时的时间,一天的工作量。可以看到每项工作任务的状态,都是To do…分配给谁,还是空白。
那么,我先将第一项任务,分配给自己。当然如果是多人工作的话,为每个人分配一项任务。然后,我准备开始做第一项工作了,将其状态设置为in progress。
必须注意,团队中每一个程序员,在任何时刻只面对一项任务,这是关注点的问题。
那么,开始创建测试数据库。
首先,在这项工作任务的描述栏目中,已经有任务内容的简要描述。那么,先按照这样的格式写一句话:10月3日 8:50开始,预期1小时。之后记录工作内容,结束后加上结束时间和中途中断的时间。这只是个人的工作习惯。
首先建立一个Sql Server数据库,我创建了三个表格,Post、Tag、PostForTag,看起来很眼熟,嗯,这是一个简易的微博数据库,只处理发布微薄,没有用户系统也没有评论、转发之类的功能。我们为Post增加一个Image字段,这当然是为了测试方便,但这也让Post表格编程一个可以分类处理相片的东西。
然后,我们为三个表格定义外键关系,由于Post是数据量比较大的表格,那么同时也为其中几个经常会用于查询的字段定义索引。
为这个数据库创建脚本,将这个脚本加入到测试项目,这样同时也能够对数据库脚本进行版本管理。
另外我们加入一个简单的Excel文件,这个也是有必要的,测试OleDb,我们至少需要验证两种不同的Db对象,工作是否正常。
既然有创建测试数据库这项工作任务,这说明我们在写单元测试的时候,没有使用Mock对象。一向很排斥这个,因为Mock实际上增加了单元测试工作的工作量、逻辑也变得复杂,而带来的好处相当有限,这与“简单”的宗旨是不合拍的。
这项工作如期结束,将任务状态改为“Done“,将第二项任务的状态改为in progress…
最初我企图用扩展方法解决问题,不过,由于扩展方法的语法不支持属性和字段,我决定使用原始的方法。定义一个类Db,继承于DbConnection。当然,要实现这个类需要覆盖抽象类的许多方法和属性,这是比较艰难的工作。不过,我用一种很奇怪的方式规避了这类工作。我在这个类里,加入一个私有的DbConnection类型的字段connection,所有需要覆盖的方法、属性和事件,都传递给这个connection去做,这就如同抄书,几分钟就搞定了一个自定义的DbConnection类,当然,这不是一个抽象类。在这个类里,同时加入了一个DbProviderFactory类型的私有字段,私有,所以使用该类的程序员是不能感觉到DbPRoviderFactory的存在的。
嗯,这就是所谓的封装。所谓面向对象,其实百分之七十以上的场景,都只是运用“封装”。屏蔽内部细节,提供简易的服务接口。
不过,我实际上做的工作,是消除Ado.net中的提供者模式和工厂模式,换句话说,是在替微软的Ado.net打补丁,将他们高深的设计模式知识粉碎掉。由于工作性质龌龊,工作内容低级趣味,所以在这里谈及面向对象还是多少有些惭愧的。
第二项任务就这么忽悠过去,没有必要写任何单元测试,这只是抄写的工作。
开始做第三项任务,先从全局归纳一下,我们需要为Db类提供三种构造方法,用于创建连接:
Db():使用配置文件中默认的提供者和连接字符串创建连接,我们简单的将这个配置项命名为"ApplicationServices",因为Asp.net 和Asp.net Mvc项目中都包含这个配置项。
Db(连接字符串,提供者名称)
Db(配置项名称):根据配置文件中某个配置项,获取连接字符串和提供者名称,创建连接。
另外,桌面应用中,我们常常在程序运行的整个周期,使用唯一的一个连接,当然,我是指类似Sqlite一类的桌面数据库。那么,我们为其提供一个静态属性Default,这个属性返回默认的Db对象。
这样,多数工作场景,你可以通过如下两个步骤,使用这个连接,
第一、在配置文件的ConnectionString节,定义连接字符串和数据库类型,保持此前同样的语法:
<add name="ApplicationServices" connectionString="Data Source=.;Initial Catalog=MiniBlog;Integrated Security=True;"
providerName="System.Data.SqlClient" />
<add name="RunTime" connectionString="Data Source=.;Initial Catalog=MiniBlog;Integrated Security=True;"
providerName="System.Data.SqlClient" />
第二个RunTime,是访问Excel文件的用到的连接字符串。大家应该看出,这是在单元测试项目的App.config中使用的。
这里要关注一下,在Vs2010结合Tfs的情形下,怎样写单元测试是最轻松的。
一般的流程是:先写单元测试、让代码编译通过、写代码实现让测试通过、写下一个单元测试。这会导致在写单元测试的时候,代码语法自动提示是无法工作的。
我这么做:先在类图中添加方法,然后在这个方法中“创建单元测试”,再写单元测试,再实现。然后下一个方法或属性。这个过程中,永远不要对私有成员进行单元测试,虽然Vs2010也能够通过创建访问器来测试私有成员,但那个毫无意义。对public成员的测试必然会覆盖私有成员,如果私有成员逻辑很复杂,那就需要重构。
下一篇将描述第三项工作任务的具体过程,用Step By Step的方式讲述Tdd的工作方式。 类图如下: