.NET框架设计—常被忽视的C#设计技巧
.NET框架设计—常被忽视的C#设计技巧
阅读目录:
- 1.开篇介绍
- 2.尽量使用Lambda匿名函数调用代替反射调用(走进声明式设计)
- 3.被忽视的特性(Attribute)设计方式
- 4.扩展方法让你的对象如虎添翼(要学会使用扩展方法的设计思想)
- 5.别怕Static属性(很多人都怕Static在Service模式下的设计,其实要学会使用线程本地存储(ThreadStatic))
- 6.泛型的协变与逆变(设计架构接口(Interface)时要时刻注意对象的协变、逆变)
- 7.使用泛型的类型推断(还在为参数类型烦恼吗)
- 8.链式编程(设计符合大脑思维习惯的处理流程)
- 8.1.链式编程(多条件(方法碎片化)调用
- 9.部分类、部分方法的使用(扩大设计范围)
1.】开篇介绍
本文中的内容都是我无意中发现觉得有必要分享一下的设计经验,没有什么高深的技术,只是平时我们可能会忽视的一些设计技巧;为什么有这种想法是因为之前跟一些同事交流技术的时候会发现很多设计思维被固化了,比如之前我在做客户端框架开发的时候会去设计一些关于Validator、DTO Transfer等常用的Common function,但是发现在讨论某一些技术实现的时候会被弄的云里雾里的,会自我郁闷半天,不会及时的明白对方在说的问题;
后来发现他们一是没有把概念分清楚,比如.NETFramework、C#、VisualStudio,这三者之间的关系;二是没有理解.NET中各个对象的本质含义,比如这里的特性(Attribute),大部分人都认为它是被用来作为代码说明、标识使用的,而没有突破这个思维限制,所以在设计一些东西的时候会绕很多弯路;还有一点是很多人对C#中的语法特性分不清版本,当然我们要大概的了解一下哪些特性或者语法是C#2的哪些是C#3的,这样在我们设计东西的时候不会由于项目的版本问题而导致你无法使用设计技巧,比如扩展方法就无法使用在低于.NET3.0版本中,LINQ也无法在低于.NET3.O的版本中使用;
.NETFramework的版本不断的在升级,目前差不多5.0都快面世了;.NETFramework的升级跟C#的升级没有必然的关系,这个要搞清楚;C#是为了更好的与.NET平台交互,它提供给我们的都是语法糖,最后都是.NETCTS中的类型;就比如大家都在写着LINQ,其实到最后LINQ也就被自动解析成对方法的直接调用;
2.】尽量使用委托调用代替反射调用
委托相信大家都玩的很熟,委托的发展到目前为止是相当不错的,从原本很繁琐的每次使用委托的时候都需要定义一个相应的方法用来实例化委托,这点在后来的C#2中得到了改进,支持匿名委托delegate{…}的方式使用,再到现在的C#3那就更方便了,直接使用面向函数式的Lambda表达式;那么这样还需要反射调用对象的方法吗?(当然特殊场合我们这里不考虑,只考虑常用的场景;)当然反射不是不好,只是反射需要考虑很多性能优化方面的东西,增加了代码的复杂性,也让框架变的很重(现在都是在追求轻量级,只有在DomainModel中需要将平面化的数据抽象;),所以何不使用简单方便的委托调用呢;
注:如果你是初学者,这里的委托可以理解成是我们平时常用的Lambda表达式,也可以将它与Expression<T>结合起来使用,Expression<T>是委托在运行时的数据结构,而非代码执行路径;(兴趣的朋友可以查看本人的:LINQ系列文章)
下面我们来看一下演示代码:
这是一个订单领域实体,它里面引用了一个Item的商品类型;
上面代码应该没有问题,基本的订单领域模型大家都太熟了;为了保证上面的代码是绝对的正确,以免程序错误造成阅读者的不爽,所以都会有100%的单元测试覆盖率;这里我们主要使用的是Order类中的SumPrices方法,所以它的UnitTest是100%覆盖;
图1:
Order中的SumPrices方法的UnitTest代码:
在以往我基本上不写单元测试的,但是最近工作上基本上都需要写每个方法的单元测试,而且要求是100%覆盖,只有这样才能保证代码的正确性;也建议大家以后多写单元测试,确实很有好处,我们应该把单元测试做起来;下面我们言归正传;
由于我们的Order是在DomainModel Layer中,现在有一个需求就是在Infrastructure Layer 加入一个动态计算Order中指定Item.ItemUsingType的所有Prices的功能,其实也就是说需要将我们的一些关键数据通过这个功能发送给远程的Service之类的;这个功能是属于Infrastructure中的Common部分也就是说它是完全独立与项目的,在任何地方都可以通过它将DomainModel中的某些领域数据发送出去,那么这样的需求也算是合情合理,这里我是为了演示所以只在Order中加了一个SumPrices的方法,可能还会存在其他一些DomainModel对象,然后这些对象都有一些关键的业务数据需要在通过Infrastructure的时候将它们发送出去,比如发送给配送部门的Service Interface;
那么常规设计可能需要将扩展点配置出来放在指定的配置文件里面,然后当对象经过Infrastructure Layer中的指定Component时触发事件路由,然后从缓存中读取出配置的信息执行,那么配置文件可能大概是这样的一个结构:DomainEntity名称、触发动作、方法名称、参数,DomainEntity名称是确定聚合根,触发动作是对应Infrastructure中的组件,当然你也可以放在DomainModel中;这里只关心方法名称、参数;
当然这里只演示跟方法调用相关的代码,其他的不在代码中考虑;我们来看一下相关代码:
这是业务调用接口;
这里简单实现IBusinessService接口,其实代码很简单,第一个方法使用反射的方式调用代码,而第二个方法则使用委托调用;在实现类里面还包含了一个简单的接口;
目的是为了方便单元测试,我们来看一下单元测试代码;
在第二个单元测试方法里面我们将使用Lambda方式将逻辑直接注入进BusinessService中,好就好这里;可以将Lambda封进Expression<T>然后直接存储在Cache中或者配置中间,彻底告别反射调用吧,就好比委托一样没有人会在使用委托在定义个没用的方法;(所以函数式编程越来越讨人喜欢了,可以关注一下F#;)总之使用泛型解决类型不确定问题,使用Lambda解决代码逻辑注入;大胆的尝试吧,将声明与实现彻底分离;
(对.NET单元测试有兴趣的朋友后面一篇文章会详细的讲解一下如何做单元测试,包括Mock框架的使用;)
3】被忽视的特性(Attribute)设计方式
大部分人对特性的定义是代码的“数据注释”,就是可以在运行时读取这个特性用来做类型的附加属性用的;通常在一些框架中对DomainModel中的Entity进行逻辑上的关联用的,比如我们比较熟悉的ORM,都会在Entity的上面加上一个类似 [Table(TableName=”Order”)] 这样的特性声明,然后再在自己的框架中通过反射的方式去在运行时差找元数据找到这个特性,然后就可以对附加了这个特性的类型进行相关的处理;
这其实没有问题,很正常的设计思路,也是比较通用的设计方法;但是我们的思维被前人固化了,难道特性就只能作为代码的声明吗?问过自己这个问题吗?
我们继续使用上面2】小结中的代码作为本节演示代码,现在我们假设需要在DomainModel中的Entity上面加上两个特性第一个用来断定它是否需要做Cache,第二个用来确定关于Entity操作验证的特性;
看代码:
代码应该很明了,第一EntityCache用来设计实体的缓存,参数是缓存的过期时间;第二个特性EntityValidator用来设置当实体进行相关处理的时候需要的验证类型,这里选择是所有操作;
现在的问题是关于特性的优先级,对于Order类的处理到底是先Cache然后验证,还是先验证然后Cache或者说内部没有进行任何的逻辑处理;如果我们将特性的视为代码的标识而不是真正的逻辑,那么对于优先级的处理会比较棘手,你需要设计如何将不同的特性处理逻辑关联起来;比较合理的设计方法是特性的处理链表;本人之前设计过AOP的简单框架,就遇到过对于特性的优先级的处理经验,也是用的链表的方式将所有的特性按照顺序串联起来然后将对象穿过特性内部逻辑,这也符合DDD的中心思想;
下面我们来看代码:
我们抽象出所有的处理,然后在内部包含下一个处理逻辑的特性实例;然后让各自的Attribute继承自它;
根据特性在类的先后顺序就可以控制他们的优先级;
图2:
上图很直观的表现了链表设计思想,再通过仔细的加工应该会很不错的;
4】扩展方法让你的对象如虎添翼(要学会使用扩展方法的设计思想)
扩展方法我们用的应该不算少的了,在一些新的框架中到处都能看见扩展方法的优势,比如:ASP.NETMVC、EntityFramework等等特别是开源的框架用的很多;
那么我们是不是还停留在原始社会,应该尝试接受新的设计思想,尽管一开始可能不太适应,但是当你适应了之后会让你的设计思想提升一个境界;
下面我们还是使用上面的演示代码来进行本节的代码演示,现在假如有一个这样的需求,为了保证DomainModel的完全干净,我们在应用层需要对领域模型加入一些非业务性的行为,这些行为跟DomainModel本身没有直接关系,换句话说我们这里的Order聚合实体可能需要一个获取Order在Cache中存活了多长时间的方法;那么在以往我们可能提供一个方法然后把Order实例作为参数这样来使用,但是这里我们的需求是该方法是Order对象的方法而不是其他地方的方法;
所以这里使用扩展方法就可以在不改变对象本身业务逻辑的情况下扩展对象行为;最关键的是扩展方法为后面的链式编程提供了基石;从长远来看DomainModel将会被独立到ThreadProcess总,当系统初始化时部分的DomainModel将直接主流在内存中,然后通过系统本地化将扩展方法加入,这样就可以在不改变对象的情况下添加行为,这也为行为驱动设计提供了好的技术实现;
用纯技术性的假设没有说服力,上面说给领域本身加上获取Cache的方法,肯定会有朋友说这完全没有必要,提供一个简单的方法就OK了,恩 我觉得也有道理,那么下面的需求你将不得不说妙;
【需求简述】:对象本身的行为不是固定不变的,尤其我们现在设计对象的时候会将对象在全局情况下的所有行为都定义在对象内部,比如我们正常人,在不同的角色中才具有不同的行为,我们只有在公司才具有“打开服务器”的行为,只有在家里才可以“亲吻”自己的老婆的行为;难道我们在设计User类的时候都将这些定义在对象内部吗?显然不符合逻辑,更不符合面向对象设计思想,当然我们目前基本上都是这么做的;
(有兴趣的朋友可以参考:BDD(行为驱动设计)、DCI(数据、上下文、交互)设计思想;)
现在我们来为Order添加一组行为,但是这组 行为只有在某些场景下才能使用,这里只是为了演示而用,真要在项目中设计还需要考虑很多其他因素;
这里有两个位于不同namespace中的行为,他们对应不同的场景;第一个TaxRate用来计算税率的行为,只有在Order对象已经处于提交状态时用的;那么第二个行为Inventory用来计算库存的,用户在Shoppingcart的时候用来确定是否有足够的库存;当然这里我只是假设;
然后我们就可以在不同的场景下进行命名空间的引用,比如我们现在Shoppingcart阶段将不会使用到TaxRate行为;
例子虽然有点简单,但是应该能说明扩展方法的基本使用方式,对于DCI架构的实现会复杂很多,需要好好设计才行;
5】别怕Static属性(很多人都怕Static在Service模式下的设计,其实要学会使用线程本地存储(ThreadStatic))
很多时候我们在设计对象的时候,尤其是面向Context类型的,很希望能通过某个静态属性直接能拿到Context,所以会定义一个静态属性用来保存对象的某个实例;但是会有很多人都会排斥静态属性,动不动就说性能问题,动不动就收多线程不安全等等借口,难道静态属性就没有存在必要了嘛;
不用静态属性你哪来的ASP.NET中的CurrentContext直接,如果怕因为多线程问题导致数据不完整,建议使用线程本地存储;没有什么好怕的,多用就熟悉了;用也很简单,直接在静态属性上面加上这个特性就OK了,前提是你已经考虑了这个属性是线程内部共享的不是应用程序级别的共享;
6】泛型的协变与逆变(设计架构接口(Interface)时要注意对象的协变、逆变)
越来越多的人喜欢自己捣鼓点东西出来用用,这很不错,时间长了设计能力自然会得到提升的;但是最近发现我们很多泛型在设计上缺乏转换的控制,也就是这里的协变和逆变;我们有一个Item类型,现在我们需要对它进行更加具体化,我们派生出一个Apple类型的Item;
这段代码是编译不通过的,因为List<T> 在定义的时候就不支持逆变、但是如果换成下面这样的代码是完全可以的;
很容易的就可以得到集合的转换,虽然很简单的功能但是在设计上如果运用好的话能大大改变接口的灵活性;你可能会有一个疑问,为什么具体实现List<T>不支持协变而IEnumerable<out T>反而支持协变;这就是面向对象设计的思想,接口本质是抽象的,抽象的不会有具体的实现所以它作为协变不会存在问题,但是逆变就会有问题;
7】使用泛型的类型推断(还在为参数类型烦恼吗)
在设计泛型方法的时候要学会使用类型推断技巧,这样会很方便的在调用的时候减少你显示调用<>的代码,也会显得很优美;大家应该都比较熟悉Func泛型委托,它是C#3中的主角,也是函数式编程的基础,我们在设计某个方法的时候会将逻辑暴露在外部,然后通过Lambda的方式注入进来;现在的LINQ都是这么实现的,比较熟悉的Where方法、Select方法,都需要我们提供一个作为它内部逻辑的函数段;
这里有两种调用Where的代码,哪一种看上去舒服一点有没一点,不用我说了;那我们看一下它的定义:
我们看到TSource类型占位符,很容易理解,这是一个扩展IEnumerable<TSource>类型的方法,系统会自动的匹配TSource;我们在设计的时候也要借鉴这种好的设计思想;
(有兴趣的朋友可以参见本人的:.NET深入解析LINQ框架(一:LINQ优雅的前奏))
8】链式编程(设计符合大脑思维习惯的处理流程)
其实那么多的C#新特性都是为了能让我们编写代码能更方便,总之一句话是为了更符合大脑思维习惯的编程模式;
C#从纯面向对象渐渐的加入了函数式模式,从静态类型逐渐加人动态类型特性;C#现在变成多范式编程语言,其实已经很大程度满足我们的日常需求;以往我们都会为了动态行为编写复杂的Emit代码,用很多CodeDom的技术;现在可以使用Dymanic解决了;
这节我们来看一下关于如何设计线性的链式方法,这不是技术问题,这是对需求的理解能力;可以将链式思想用在很多地方,只要有逻辑有流程的地方都可以进行相关设计,首先你要保证你是一个正常思考问题的人,别设计出来的方法是反的,那么用的人会很不爽的;这里我举一个我最近遇到的问题;
8.1】链式编程(多条件(方法碎片化)调用
我们都熟悉DTO对象,它是从UI传过来的数据集合,简单的业务逻辑Application Layer将它转换成DomainModel中的Entity,如果复杂的业务逻辑是不能直接将DTO进行转换的;但是在转换过程中我们总是少不了对它的属性判断,如果UserName不为空并且Password不为空我才能去验证它的合法性,等等;类似这样的判断;这里我们将运行扩展方法将这些逻辑判断链起来,并且最后输出一个完整的Entity对象;
有一组扩展方法,用来做验证用的;
由于时间关系我这里只是演示一下,完全可以做的很好的,在判断的最后拿到返回的列表引用最后把数据送出来;
(有一个开源验证框架应该还不错,目前工作中在用:FluentValidator)
9】部分类、部分方法的使用(扩大设计范围)
部分类不是新的特性,而部分方法是新特性;我们通过灵活运用部分类可以将发挥很大作用,比如我们完全可以将类的部分实现完全隔离在外部,起到低耦合的作用,甚至可以将声明式设计和元编程运用在C#中,比较经典就是ASP.NET后台代码和前台的模板代码,在运行时然后再通过动态编译合起来,我们不要忘记可以使用部分类、部分方法来达到在运行时链接编译时代码和运行时代码,类似动态调用的效果;由于这部分内容比较简单,是设计思想的东西,所以没有什么要演示的,只是一个总结;
总结:内容虽然简单,但是要想运用的好不简单,这里我只是总结一下,希望对大家有用,谢谢;
示例DEMO地址:http://files.cnblogs.com/wangiqngpei557/ConsoleApplication1.zip
作者:王清培
出处:http://www.cnblogs.com/wangiqngpei557/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
.NET框架设计—常被忽视的C#设计技巧的更多相关文章
- 【转载】 .NET框架设计—常被忽视的C#设计技巧
阅读目录: 1.开篇介绍 2.尽量使用Lambda匿名函数调用代替反射调用(走进声明式设计) 3.被忽视的特性(Attribute)设计方式 4.扩展方法让你的对象如虎添翼(要学会使用扩展方法的设计思 ...
- .NET框架设计—常被忽视的框架设计技巧
阅读目录: 1.开篇介绍 2.元数据缓存池模式(在运行时构造元数据缓存池) 2.1.元数据设计模式(抽象出对数据的描述数据) 2.2.借助Dynamic来改变IOC.AOP动态绑定的问题 2.3.元数 ...
- .NET框架设计(常被忽视的C#设计技巧)
阅读目录: 1.开篇介绍 2.尽量使用Lambda匿名函数调用代替反射调用(走进声明式设计) 3.被忽视的特性(Attribute)设计方式 4.扩展方法让你的对象如虎添翼(要学会使用扩展方法的设计思 ...
- .NET框架设计(常被忽视的框架设计技巧)
阅读目录: 1.开篇介绍 2.元数据缓存池模式(在运行时构造元数据缓存池) 2.1.元数据设计模式(抽象出对数据的描述数据) 2.2.借助Dynamic来改变IOC.AOP动态绑定的问题 2.3.元数 ...
- ASP.NET常被忽视的一些细节
原文:ASP.NET常被忽视的一些细节 前段时间碰到一个问题:为什么在ASP.NET程序中定时器有时候会不工作? 这个问题看起来很奇怪,代码好像也没错,但就是结果与预期不一致. 其实这里是ASP.NE ...
- MySQL数据库设计常犯的错以及对性能的影响
1.过分的反范式化为表建立太多的列 我们在设计数据库的结构时,比较容易犯的第一个错误就是对表进行了过分的反范式化的设计,这就容易造成了表中的列过多,虽然说Mysql允许为一个表建立很多的列,但是由于M ...
- Java 课程设计 "Give it up"小游戏设计(个人) 201521123077
1.团队课程设计博客链接 http://www.cnblogs.com/agts/p/7067948.html 2.个人负责模块或任务说明 个人任务:实现游戏画面的制作,游戏逻辑的编写,以及一些模块与 ...
- UX设计秘诀之注册表单设计,细节决定成败
以下内容由摹客团队翻译整理,仅供学习交流,摹客iDoc是支持智能标注和切图的产品协作设计神器. 说实话,现实生活中,又有多少人会真正喜欢填写表格?显然,并不多.因为填写表单这样的网页或App服务,并非 ...
- MySQL性能调优与架构设计——第12章 可扩展设计的基本原则
第12章 可扩展设计的基本原则 前言: 随着信息量的飞速增加,硬件设备的发展已经慢慢的无法跟上应用系统对处理能力的要求了.此时,我们如何来解决系统对性能的要求?只有一个办法,那就是通过改造系统的架构体 ...
随机推荐
- 【百度地图API】多家地图API文件大小对比
原文:[百度地图API]多家地图API文件大小对比 于2011.6.9日更新百度地图API文件大小.同时更新图片. 任务描述: 明天就是元宵佳节啦~这是一个团团圆圆的节日,于是,再次想把各家API聚在 ...
- c语言发挥帕斯卡三角
我们已经确定了帕斯卡三角的规则,下面是我的代码,非常实用哦! !! #include<stdio.h> void main() { int i,j,n,k; sca ...
- jquery自己主动旋转的登录界面的背景代码登录页背景图
在其他网站上看到比较爽Web登录界面.背景图片可以自己主动旋转. 介绍给大家.有兴趣的可以改改下来作为自己的系统登录界面. 如图: watermark/2/text/aHR0cDovL2Jsb2cuY ...
- ASP.NET 5 Identity
ASP.NET 5 Identity “跌倒了”指的是这一篇博文:爱与恨的抉择:ASP.NET 5+EntityFramework 7 如果想了解 ASP.NET Identity 的“历史”及“ ...
- windows tcp端口映射或端口转发
windows tcp端口映射或端口转发 windows内部有一个叫netsh的玩意,可以把tcp端口进行映射或转发,可惜不支持udp.举个例子:一台windows有一个80端口,对外可以访问.另有一 ...
- unity3d 血液
这里的人物头像和血条是在3d世界生成的,所以有真正的纵深感和遮挡关系,废话不多说,看我是怎么实现的. 第一步.先在UI Root里制作头像和血条. 这个制作步骤基本和我前面一篇文章介绍头像血条的制作步 ...
- Grub启动配置文件
和许多其他linux发行版一样,Fedora使用Grub作为32位和64位X86系统的启动加载器(bootloader).grub的配置文件主要是/boot/grub/grub.conf,而/boot ...
- [译]Java 设计模式之装饰器
(文章翻译自Java Design Pattern: Decorator – Decorate your girlfriend) 1.装饰模式的来历 让我们假设你在寻找一个女朋友.有来自像没美国中国日 ...
- leetcode[55] Merge Intervals
题目:给定一连串的区间,要求输出不重叠的区间. Given a collection of intervals, merge all overlapping intervals. For exampl ...
- 【MS SQL】查看任务执行进度
原文:[MS SQL]查看任务执行进度 备份或还原数据库时,我一般是用MS SQL工具向导来进行,由于工具向导没有显示任务执行过程的状态, 如果数据库比较大执行时间较长的话,对任务啥时候执行完成比较迷 ...