赞
踩
所谓定位就是回答几个问题,我出于什么目的要写一个框架,我的这个框架是干什么的,有什么特性适用于什么场景,我的这个框架的用户对象是谁,他们会怎么使用,框架由谁维护将来怎么发展等等。
我们来为本文模拟一个场景,假设我们觉得现有的Spring MVC等框架开发起来效率有点低,打算重复造轮子,对于新框架的定位是一个给Java程序员使用的轻量级的、零配置的、易用的、易扩展的Web MVC框架。
调研
虽然到这里你已经决定去写一个框架了,但是在着手写之前还是至少建议评估一下市面上的类似(成熟)框架。需要做的是通读这些框架的文档以及阅读一些源码,这么做有几个目的:
新 开发一个框架的好处是没有兼容历史版本的包袱,但是责任也同样重大,因为如果对于一开始的定位或设计工作没有做好的话,将来如果要对格局进行改变就会有巨 大的向前兼容的包袱(除非你的框架没有在任何正式项目中使用),兼容意味着框架可能会越来越重,可能会越来越难看,阅读至少一到两个开源实现,做好充分的 调研工作可以使你避免犯大错。
假设我们评估了一些主流框架后已经很明确,我们的MVC框架是一个Java平台的、基于Servlet的轻量级的Web MVC框架,主要的理念是约定优于配置,高内聚大于低耦合,提供主流Web MVC框架的大部分功能,并且易用方面有所创新,新特性体包括:
提供一套通用的控件模版,使得,并且支持多种模版引擎,比如Jsp、Velocity、Freemarker、Mustache等等。
嗯,看上去挺诱人的,这是一个不错的开端,如果你要写的框架自己都不觉得想用的话,那么别人就更不会有兴趣来尝试使用你的框架了。
解决难点
之 所以把解决难点放在开搞之前是因为,如果实现这个框架的某些特性,甚至说实现这个框架的主流程有一些核心问题难以解决,那么就要考虑对框架的特性进行调 整,甚至取消框架的开发计划了。有的时候我们在用A平台的时候发现一个很好用的框架,希望把这个框架移植到B平台,这个想法是好的,但之所以在这以前这么 多年没有人这么干过是因为这个平台的限制压根不可能实现这样的东西。比如我们要实现一个MVC框架,势必需要依赖平台提供的反射特性,如果你的语言平台压 根就没有运行时反射这个功能,那么这就是一个非常难以解决的难点。又比如我们在某个平台实现一个类似于.NET平台Linq2Sql的数据访问框架,但如 果这个目标平台的开发语言并不像C#那样提供了类型推断、匿名类型、Lambda表达式、扩展方法的话那么由于语法的限制你写出来的框架在使用的时候是无 法像.NET平台Linq2Sql那样优雅的,这就违背了实现框架的主要目的,实现新的框架也就变得意义不大了。
对于我们要实现的MVC框 架貌似不存在什么根本性的无法解决的问题,毕竟在Java平台已经有很多可以参考的例子了。如果框架的实现总体上没什么问题的话,就需要逐一评估框架的这 些新特性是否可以解决。建议对于每一个难点特性做一个原型项目来证明可行,以免在框架实现到一半的时候发现有无法解决的问题就比较尴尬了。
分析一下,貌似我们要实现的这8大特性只有第1点要研究一下,看看如何免配置通过让代码方式让我们的Web MVC框架可以和Servlet进行整合,如果无法实现的话,我们可能就需要把第1点特性从零配置改为一分钟快速配置了。
开搞
首先需要给自己框架取一个名字,取名要考虑到易读、易写、易记,也需要尽量避免和市面上其它产品的名字重复,还有就是最好不要起一个侮辱其它同类框架的名字以免引起公愤。
如果将来打算把项目搞大的话,可以提前注册一下项目的相关域名,毕竟现在域名也便宜,避免到时候项目名和域名差距很大,或项目的.com或.org域名对应了一个什么不太和谐的网站这就尴尬了。
然后就是找一个地方来托管自己的代码,如果一开始不希望公开代码的话,最好除了本地源代码仓库还有一个异地的仓库以免磁盘损坏导致抱憾终身,当然如果不怕出丑的话也可以在起步的时候就使用Github等网站来托管自己的代码。
总体设计
对 于总体设计我的建议是一开始不一定需要写什么设计文档画什么类图,因为可能一开始的时候无法形成这么具体的概念,我们可以直接从代码开始做第一步。框架的 使用者一般而言还是开发人员,抛开框架的内在的实现不说,框架的API设计的好坏取决于两个方面。对于普通开发人员而言就是使用层面的API是否易于使 用,拿我们的MVC框架举例来说:
最基本的,搭建一个HelloWorld项目,声明一个Controller和Action,配置一个路由规则让Get方法的请求可以解析到这个Action,可以输出HelloWorld文字,怎么实现?
如果要实现从Cookie以及表单中获取相关数据绑定到Action的参数里面,怎么实现?
如果要配置一个Action在调用前需要判断权限,在调用后需要记录日志,怎么实现?
我们这里说的API,它不一定全都是方法调用的API,广义上来说我们认为框架提供的接入层的使用都可以认为是API,所以上面的一些功能都可以认为是MVC框架的API。
框架除了提供基本的功能,还要提供一定程度的扩展功能,使得一些复杂的项目能够在某些方面对框架进行增强以适应各种需求,比如:
一 般而言如果要实现这样的功能就需要自己实现框架公开的一些类或接口,然后把自己的实现"注册"到框架中,让框架可以在某个时候去使用这些新的实现。这就需 要框架的设计者来考虑应该以怎么样的友好形式公开出去哪些内容,使得以后的扩展实现在自由度以及最少实现上的平衡,同时要兼顾外来的实现不破坏框架已有的 结构。
要想清楚这些不是一件容易的事情,所以在框架的设计阶段完全可以使用从上到下的方式进行设计。也就是不去考虑框架怎么实现,而是以一 个使用者的身份来写一个框架的示例网站,API怎么简单怎么舒服就怎么设计,只从使用者的角度来考虑问题。对于相关用到的类,直接写一个空的类(能用接口 的尽量用接口,你的目的只是通过编译而不是能运行起来),让程序可以通过编译就可以了。你可以从框架的普通使用开始写这样一个示例网站,然后再写各种扩展 应用,在此期间你可能会用到框架内部的20个类,这些类就是框架的接入类,在你的示例网站通过编译的那刹那,其实你已经实现了框架的接入层的设计。
这里值得一说的是API的设计蕴含了非常多的学问以及经验,要在目标平台设计一套合理易用的API首先需要对目标平台足够了解,每一个平台都有一些约定俗成的规范,如果设计的API能符合这些规范那么开发人员会更容易接受这个框架,此外还有一些建议:
下一步工作就是把项目中那些空的类按照功能进行划分。目的很简单,就是让你的框架 的100个类或接口能够按照功能进行拆分和归类,这样别人一打开你的框架就可以马上知道你的框架分为哪几个主要部分,而不是在100个类中晕眩;还有因为 一旦在你的框架有使用者后你再要为API相关的那些类调整包就比困难了,即使你在创建框架的时候觉得我的框架就那么十几个类无需进行过多的分类,但是在将 来框架变大又发现当初设计的不合理,无法进行结构调整就会变得很痛苦。因此这个工作还是相当重要的,对于大多数框架来说,可以有几种切蛋糕的方式:
如果是一个RPC框架,大概是这样的结构:
对于我们的Web MVC框架,举例如下:
这里我们以IXXX来描述一个抽象,可以是接口也可以是抽象类,在具体实现的时候根据需求再来确定。
这 种结构的划分方式完全吻合上面说的切蛋糕方式,可以看到除了横切部分和分层部分,作为一个Web MVC框架,它核心的组件就是routing、model、view、controller、action(当然,对于有些MVC框架它没有route部 分,route部分是交由Web框架实现的)。
如果我们在这个时候还无法确定框架的模块划分的话,问题也不大,我们可以在后续的搭建龙骨的步骤中随着更多的类的建立,继续理清和确定模块的划分。
经过了设计的步骤,我们应该心里对下面的问题有一个初步的规划了:
搭建龙骨
在 经过了初步的设计之后,我们可以考虑为框架搭建一套龙骨,一套抽象的层次关系。也就是用抽象类、接口或空的类实现框架,可以通过编译,让框架撑起来,就像 造房子搭建房子的钢筋混凝土结构(添砖加瓦是后面的事情,我们先要有一个结构)。对于开发应用程序来说,其实没有什么撑起来一说,因为应用程序中很多模块 都是并行的,它可能并没有一个主结构,主流程,而对于框架来说,它往往是一个高度面向对象的,高度抽象的一套程序,搭建龙骨也就是搭建一套抽象层。这么说 可能有点抽象,我们还是来想一下如果要做一个Web MVC框架,需要怎么为上面说的几个核心模块进行抽象(我们也来体会一下框架中一些类的命名,这里我们为了更清晰,为所有接口都命名为IXXX,这点不太 符合Java的命名规范):
接下去就不再详细阐述model、plugin等模块的内容了。
看到这里,我们来总结一下,我们的MVC框架在组织结构上有着高度的统一:
同 时我们框架的相关类的命名也是非常统一的,可以一眼看出这是实现、还是抽象类还是接口;是提供程序,是执行结果还是上下文。当然,在将来的代码实现过程中 很可能会把很多接口变为抽象类提供一些默认的实现,这并不会影响项目的主结构。我们会在模式篇对框架常用的一些高层设计模式做更多的介绍。
到了这里,我们的项目里已经有几十个空的(抽象)类、接口了,其中也定义了各种方法可以把各个模块串起来(各种find()方法和execute()方法),可以说整个项目的龙骨已经建立起来了,这种感觉很好,因为我们心里很有底,我们只需要在接下去的工作中做两个事情:
走通主线流程
所谓走通主线流程,就是让这个框架可以以一个HelloWorld形式跑起来,这就需要把几个核心类的核心方法使用最简单的方式进行实现,还是拿我们的MVC框架来举例子:
在这一步,我们并不一定要去触碰filter和model这部分的内容,我们的主线流程只是解析路由,获得控制器,执行方法,找到视图然后渲染视图。过滤器和视图模型的绑定属于增强型的功能,属于支线流程,不属于主线流程。
虽 然在这里我们说了一些MVC的实现,但本文的目的不在于教你实现一个MVC框架,所以不用深究每一个类的实现细节,这里想说的是,在前面的龙骨搭建完后, 你会发现按照这个龙骨为它加一点肉上去实现主要的流程是顺理成章的事情,毫无痛苦。在整个实现的过程中,你可以不断完善common下的一些 context,把方法的调用参数封装到上下文对象中去,不但看起来清楚且符合开闭原则。到这里,我们应该可以跑起来在设计阶段做的那个示例网站的 HelloWorld功能了。
在这里还想说一点,有些人在实现框架的时候并没有搭建龙骨的一步骤,直接以非OOP的方式实现了主线流程,这种方式有以下几个缺点:
不容易做到SRP单一指责原则,你很容易把各种逻辑都集中写在一起,比如大量的逻辑直接写到了DispatcherServlet中,辅助一些Service或Helper,整个框架就肥瘦不匀,有些类特别庞大有些类特别小。
不容易做到OCP开闭原则,扩展起来不方便需要修改老的代码,我们期望的扩展是实现新的类然后让框架感知,而不是直接修改框架的某些代码来增强功能。
很难实现DIP依赖倒置原则,即使你依赖的确实是IService但其实就没意义,因为它只有一个实现,只是把他当作帮助类来用罢了。
实现各种支线流程
我们想一下,对于这个MVC框架有哪些没有实现的支线流程?其实无需多思考,因为我们在搭建龙骨阶段的设计已经给了我们明确的方向了,我们只需要把除了主线之外的那些龙骨上也填充一些实体即可,比如:
实现了这一步后,你会发现整个框架饱满起来了,每一个包中不再是仅有的那些接口和默认实现,而且会有一种OOP的爽快感,爽快感来源于几个方面:
我们再来总结一下之前说的那些内容,实现一个框架的第一大步就是:
经 过这样的一些步骤后可以发现这个框架是很稳固的,很平衡的,很易于扩展的。其实到这里很多人觉得框架已经完成了,有血有肉,其实个人觉得只能说开发工作实 现了差不多30%,后文会继续说,毕竟直接把这样一个血肉之躯拿出去对外有点吓人,我们需要为它进行很多包装和完善。
单元测试
在这之前我们写的框架只能说是一个在最基本的情况下可以使用的框架,作为一个框架我们无法预测开发人员将来会怎么使用它,所以我们需要做大量的工作来确保框架不但各种功能都是正确的,而且还是健壮的。写应用系统的代码,大多数项目是不会去写单元测试的,原因很多:
对于框架,恰恰相反,没有配套的单元测试的框架(也就是仅仅使用人工的方式进行测试,比如在main中调用一些方法观察日志或输出,或者运行一下示例项目查看各种功能是否正常,是非常可怕的)原因如下:
如果框架的时间需求不是特别紧的话,单元测试的引入可以是走通主线流程的阶段就引入,越早引入框架的成熟度可能就会越高,以后重构返工的机会会越小,框架的可靠性也肯定会大幅提高。之前我有写过一个类库项目,并没有写单元测试,在项目中使用了这个类库一段时间也没有出现任何问题,后来花了一点时间为类库写了单元测试,出乎我意料之外的是,我的类库提供的所有API中有超过一半是无法通过单元测试的(原以为这是一个成熟的类库,其实包含了数十个BUG),甚至其中有一个API是在我的项目中使用的。你可能会问,为什么在使用这个API的时候没有发生问题而在单元测试的时候发生问题了呢?原因之前提到过,我是框架的设计者,我在使用类库提供的API的时候是知道使用的最佳实践的,因此我在使用的时候为类库进行了一个特别的设置,这个问题如果不是通过单元测试暴露的话,那么其它人在使用这个类库的时候基本都会遇到一个潜在的BUG。
示范项目
写一个示例项目不仅仅是为了给别人参考,而且还能够帮助自己去完善框架,对于示例项目,最好兼顾下面几点:
完善日志和异常
一个好的框架不但需要设计精良,日志和异常的处理是否到位也是非常重要的标准,这里有一些反例:
其实个人觉得,一个框架的主逻辑代码并不一定是最难的,最难的是对一些细节的处理,让框架保持一套规范的统一的日志和异常的使用反而对框架开发者来说是一个难点,下面是针对记录日志的一些建议:
1、首先要对框架使用的日志级别有一个规范,比如定义:
2、按照上面的级别规范,在需要记录日志的地方记录日志,除了DEBUG级别的日志其它日志不能记录过多,如果框架总是在运行的时候输出几十个WARNNING也容易让使用者忽略真正的问题。
3、日志记录的消息需要是明确的,最好包含一些上下文信息,比如"无法在xxx下找到配置文件xxx.config,框架将采用默认的配置",而不是"加载配置失败!"
下面是一些针对使用异常的建议:
完善配置
配置的部分可以留到框架写的差不多了再去写,因为这个时候已经可以想清楚哪些配置是:
一般来说配置有几种方式:
很多框架提供了多种配置方式,比如Spring MVC同时支持上面三种方式的配置,个人觉得对配置,我们还是应该区别对待,而不是无脑把所有的配置项都同时以上面三种方式提供配置,我们要考虑高内聚和低耦合原则,对于Web框架来说,高内聚需要考虑的比低耦合更多,我的建议是对不同的配置项提供不同的配置方式:
提供状态服务
所谓状态服务就是反映框架内部运作状态的服务,很多开源服务或系统(Nginx、Mongodb等)都提供了类似的模块和功能,作为框架的话我觉得也有必要提供一些内部信息(主要是配置、数据统计以及内部资源状态)出来,这样使用你框架的人可以在开发的时候或线上运作的时候了解框架的运作状态,我们举两个例子,对于一个我们之前提到的Web MVC框架来说,可以提供这些信息:
对于一个Socket框架来说,有一些不同,Socket框架是有状态的,其状态服务提供的信息除了当前生效的配置信息之外,更多的是反映当前框架内部一些资源的状态以及统计数据:
状态服务可以以下面几种形式来提供:
检查线程安全
框架对多线程环境支持的是否好,是框架质量的一个重要的评估标准,往往可以看到甚至有一些成熟的框架也会有多线程问题。这里涉及几个方面:
1,你无法预料框架的使用者会怎么样去实例化和保存你的API的入口类,如果你的入口类被用成为了一个单例,在并发调用的情况下会不会有单线程问题?
这是一个老话题,之前已经说过很多次,你在设计框架的时候心里如果把一个类定位成了单例的类但却没有提供单例模式,你是无法要求使用者来帮你实现单例的。这其中涉及的不仅仅是多线程问题,可能还有性能问题。比如见过某分布式缓存的客户端的CacheClient在文档中要求使用者针对一个缓存集群保持一个CacheClient的单例(因为其中有了连接池),但是用的人还是每一次都实例化了一个CacheClient出来,几小时后就会产生几万个半死的Socket导致网络奔溃。又见过某类库的入口工厂的代码注释中写了要求使用的人把XXXFactory作为单例来使用(因为其中缓存了大量数据),但是用的人就没有注意到这个注释,每一次都实例化了一个XXXFactory,造成GC的崩溃。所以我觉得作为框架的设计者开发人员,最好还是把框架的最佳实践直接做到API中,使得使用者不可能出错(之前说过一句话,再重复一次,好的框架不会让使用的人犯错)。你可能会说对于CacheClient的例子,不可能做成单例的,因为我的程序可能需要用到多个缓存的集群,换个思路,我们完全可以在封装一层,通过一个CacheClientCreator之类的类来管理多个单例的CacheClient。即使在某些极端的情况下,你不能只提供一条路给使用者去走,也需要在框架内做一些检测机制,及时提醒使用者 "我们发现您这样使用了框架,这可能会产生问题,你本意是否打算那样做呢?"
2,如果你的入口类本来就是单例的,那么你是类中是否持有共享资源,你的API在并发的情况下被调用是否可以确保这些资源的线程安全?在解决多线程问题的时候往往有几个难点:
百密难有一疏,你很难想到这段代码会有人这样去并发调用。比如某init()方法,某config()方法,你总是假设使用者会调用并且仅调用一次,但事实不一定这样,有的时候调用者自己也不清楚我的容器会调用我这段代码多少次。
好吧,解决多线程问题各种烦躁,那就对各种涉及到共享资源的方法全部加锁。对方法进行粗犷(粒度)的锁可能会导致性能急剧下降甚至是死锁问题。
自以为使用了优雅的无锁代码或并发容器但却达不到目的。我们往往在大量使用了并发集合心中暗自窃喜解决了多线程问题的同时又达到了极佳的性能,但你以为这样是解决了线程安全问题但其实根本就没有,我们不能假设A和B都方法是线程安全的,但对A和B方法调用的整个代码段是线程安全的。
对于多线程问题,我没有好的解决办法,不过下面的几条我觉得可以尝试:
需要非常仔细的过一遍代码,把涉及到共享资源的地方,以及相关的方法和类列出来,不要去假设什么,只要API暴露出去了则假设它可能被并发调用。共享资源不一定是静态资源,哪怕资源是非静态的,在并发环境下对相同对象的资源进行操作也可能产生问题。
一般而言对于公开的API,作为框架的设计者我们需要确保所有的静态方法(或但单例类的实例方法)是线程安全的,对于实例方法我们可以不这么做(因为性能原因),但是需要在注释中明确提示使用者方法的非线程安全,如果需要并发调用请自行处理线程安全问题。
可以看看是否有可能让这些资源(字段)变为方法内的局部变量,有的时候我们并不是真正的需要类持有一个字段,只是因为多个方法要使用相同的东西,随手一写罢了。
对于使用频率低的一些方法相关的一些资源没有必要使用并发容器,直接采用粗狂的方式进行资源加锁甚至是方法级别加锁,先确保没有线程安全,如果以后做压测出现性能问题再来解决。
对于使用频率高的一些方法相关的一些资源可以使用并发容器,但需要仔细思考一下代码是否会存在线程安全问题,必要的话为代码设计一些多线程环境的单元测试去验证。
性能测试和优化
之前也提到过,你不会预测到你的项目会在怎么样的访问量下使用,我们不希望框架和同类的框架相比有明显的性能差距(如果你做的是一个ORM框架或RPC框架,这个工作就是必不可少的),所以在框架基本完成后我们需要做Benchmark:
封装和扩展
个人觉得一个框架如果只是能用那是第一个层次,能很方便的进行扩展或二次开发那是另外一个层次,如果我们龙骨阶段的工作做的足够好,框架是一个立体饱满的框架,那么这部分的工作量就会小很多,否则我们需要对框架进行不少的重构以便可以达到这个层次。
重构还是重构
光是重构这个事情其实就可以说一本书了,其实我有一点代码的洁癖,这里列一些我自己写代码的时候注重的地方:
除了上面说的一些问题,我觉得对于重构,最重要的一句话就是:不要让同一段代码出现两遍,主要围绕这个原则进行重构往往就会解决很多设计问题,要实现这个目标可能需要:
其实也不一定是在重构的时候再去处理上面所有的问题,如果在写代码的时候都带着这些意识来写的话那么重构的负担就会小一点(不过写代码思想的负担比较大,需要同时考虑封装问题、优雅问题、日志异常问题、多线程问题等等,所以写一套能用的代码和写一套好的代码其实不是一回事情)。
项目文档
如果要别人来使用你的框架,除了示例项目来说提供和维护一份项目文档是很有必要的,我建议文档分为这几个部分:
开源
开源的好处是有很多人可以看到你的代码帮助你改进,你的框架也可能会在更多的复杂环境下使用,框架的发展会较快框架的代码质量也会有很大的提升。
要把框架进行开源,除了上面的各种工作之外可能还有一些额外的工作需要做:
看到这里你可能相信我一开始的话了吧,框架可以使用到完善可以商用差距还是很大的,而且还要确保在迭代的过程中框架不能偏离开始的初衷不能有很大的性能问题出现,任重道远。
原文链接:http://www.cnblogs.com/lovecindywang/p/4444915.html
原文链接:http://www.cnblogs.com/lovecindywang/p/4447739.html
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。