如何写一个框架_第1页
如何写一个框架_第2页
如何写一个框架_第3页
如何写一个框架_第4页
如何写一个框架_第5页
已阅读5页,还剩12页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

你知道如何写一个框架吗?详细步骤放你知道如何写一个框架吗?详细步骤放 送送 定位定位 所谓定位就是回答几个问题,我出于什么目的要写一个框架,我的这个框架是干什么的, 有什么特性适用于什么场景,我的这个框架的用户对象是谁,他们会怎么使用,框架由谁 维护将来怎么发展等等。 如果你打算写框架,那么肯定心里已经有一个初步的定位,比如它是一个缓存框架、Web MVC 框架、IOC 框架、ORM/数据访问框架、RPC 框架或是一个用于 Web 开发的全栈式框 架。 是 否要重复造轮子?除非是练手项目,一般我们是有了解决不了问题的时候才会考虑不使 用既有的成熟的框架而重复造轮子的,这个时候需要列出新框架主要希望解决 什么问题。 有关是否应该重复造轮子的话题讨论了很多,我的建议是在把问题列清后进行简单的研究 看看是否可以通过扩展现有的框架来解决这个问题。一般而言大 部分成熟的框架都有一定 的扩展和内部组件的替换能力,可以解决大部分技术问题,但在如下情况下我们可能不得 不自己去写一个框架,比如即使通过扩展也无法满 足技术需求、安全原因、需要更高的生 产力、需要让框架和公司内部的流程更好地进行适配、开源的普适框架无法满足性能需求、 二次开发的成本高于重新开发的成 本等等。 主打轻量级?轻量级是很多人打算自己写一个新框架的原因,但我们要明白,大部分项目 在一开始的时候其实都是轻量级的,随着框架 的用户越来越多,它必定需要满足各种奇怪 的需求,在经过了无数次迭代之后,框架的主线流程就会多很多扩展点、检测点,这样框 架势必变得越来越重(从框架的 入口到框架的工作结束的方法调用层次越来越多,势必框 架也就越来越慢),如果你打算把框架定位于一个轻量级的框架的话,那么在今后的迭代 过程中需要进行一 些权衡,在心中有坚定的轻量级的理念的同时不断做性能测试来确保框 架的轻量,否则随着时间的发展框架可能会越来越重进而偏离了开始的定位。 特性?如果你打算写一个框架,并且只有轻量级这一个理由的话,你或许应该再为自己的 框架想一些新特性,就像做一个产品一样,如果找不出两个以上的亮点,那么这个产品不 太可能成功,比如你的新框架可以是一个零配置的框架,可以是一个前端开发也能用的后 端框架。 其它?一般来说框架是给程序员使用的,我们要考虑框架使用的频度是怎么样的,这可能 决定的框架的性能需求和稳定性需求。还有,需要考虑框架将来怎么发展,是希望走开源 路线还是商业路线。当然,这些问题也可以留到框架有一个大致的结构后再去考虑。 我们来为本文模拟一个场景,假设我们觉得现有的 Spring MVC 等框架开发起来效率有点低, 打算重复造轮子,对于新框架的定位是一个给 Java 程序员使用的轻量级的、零配置的、易 用的、易扩展的 Web MVC 框架。 调研调研 虽然到这里你已经决定去写一个框架了,但是在着手写之前还是至少建议评估一下市面上 的类似(成熟)框架。需要做的是通读这些框架的文档以及阅读一些源码,这么做有几个 目的: 通过分析现有框架的功能,可以制定出一个新框架要实现的功能列表。 通过分析现有框架的问题,总结出新框架需要避免的东西和改善的地方。 通过阅读现有框架的源码,帮助自己理清框架的主线流程为总体设计做铺垫(后面总体设 计部分会更多谈到)。 如果能充分理解现有的框架,那么你就是站在巨人的肩膀上写框架,否则很可能就是在井 底造轮子。 新 开发一个框架的好处是没有兼容历史版本的包袱,但是责任也同样重大,因为如果对于 一开始的定位或设计工作没有做好的话,将来如果要对格局进行改变就会有巨 大的向前兼 容的包袱(除非你的框架没有在任何正式项目中使用),兼容意味着框架可能会越来越重, 可能会越来越难看,阅读至少一到两个开源实现,做好充分的 调研工作可以使你避免犯大 错。 假设我们评估了一些主流框架后已经很明确,我们的 MVC 框架是一个 Java 平台的、基于 Servlet 的轻量级的 Web MVC 框架,主要的理念是约定优于配置,高内聚大于低耦合,提 供主流 Web MVC 框架的大部分功能,并且易用方面有所创新,新特性体包括: 起手零配置,总体上约定由于配置,即使需要扩展配置也支持通过代码和配置文件两种方 式进行配置。 除了 Servlet 之外不依赖其它类库,支持通过插件方式和诸如 Spring 等框架进行整合。 更优化的项目结构,不需要按照传统的 Java Web 项目结构那样来分离代码和 WEB-INF,视 图可以和代码在一起,阅读代码更便利。 拦截器和框架本身更紧密,提供 Action、Controller 和 Global 三个级别的“拦截器“(或者说 过滤器)。 丰富的 Action 的返回值,返回的可以是视图、可以是重定向、可以是文件、可以是字符串、 可以是 Json 数据,可以是 Javascript 代码等等。 支持针对测试环境自动生成测试的视图模型数据,以便前端和后端可以同时开发项目。 支持在开发的时候自动生成路由信息、模型绑定、异常处理等配置的信息页面和调试页面, 方便开发和调试。 提供一套通用的控件模版,使得,并且支持多种模版引擎,比如 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。 框架除了提供基本的功能,还要提供一定程度的扩展功能,使得一些复杂的项目能够在某 些方面对框架进行增强以适应各种需求,比如: 我的 Action 是否可以返回图片验证码? 我的 Action 的参数绑定是否可以从 Memcached 中获取数据? 如果出现异常,能否在开发的时候显示具体的错误信息,在正式环境显示友好的错误页面 并且记录错误信息到数据库? 一 般而言如果要实现这样的功能就需要自己实现框架公开的一些类或接口,然后把自己的 实现“注册“到框架中,让框架可以在某个时候去使用这些新的实现。这就需 要框架的设计 者来考虑应该以怎么样的友好形式公开出去哪些内容,使得以后的扩展实现在自由度以及 最少实现上的平衡,同时要兼顾外来的实现不破坏框架已有的 结构。 要想清楚这些不是一件容易的事情,所以在框架的设计阶段完全可以使用从上到下的方式 进行设计。也就是不去考虑框架怎么实现,而是以一 个使用者的身份来写一个框架的示例 网站,API 怎么简单怎么舒服就怎么设计,只从使用者的角度来考虑问题。对于相关用到 的类,直接写一个空的类(能用接口 的尽量用接口,你的目的只是通过编译而不是能运行 起来),让程序可以通过编译就可以了。你可以从框架的普通使用开始写这样一个示例网 站,然后再写各种扩展 应用,在此期间你可能会用到框架内部的 20 个类,这些类就是框 架的接入类,在你的示例网站通过编译的那刹那,其实你已经实现了框架的接入层的设计。 这里值得一说的是 API 的设计蕴含了非常多的学问以及经验,要在目标平台设计一套合理 易用的 API 首先需要对目标平台足够了解,每一个平台都有一些约定俗成的规范,如果设 计的 API 能符合这些规范那么开发人员会更容易接受这个框架,此外还有一些建议: 之 所以我们把 API 的设计先行,而不是让框架的设计先行是因为这样我们更容易设计出好 用的 API,作为框架的实现者,我们往往会进行一些妥协,我们可能会为 了在框架内部 DRY 而设计出一套丑陋的 API 让框架的使用者去做一些重复的工作;我们也可能会因为想 让框架变得更松耦合强迫框架的使用者去使用到框架的一 些内部 API 去初始化框架的组件。 如果框架不是易用的,那么框架的内部设计的再合理又有什么意义? 尽量少暴露一些框架内部的类名吧,对 于框架的使用者来说,你的框架对他一点都不熟悉, 如果要上手你的框架需要学习一到两个类尚可接受,如果要使用到十几个类会头晕脑胀的, 即使你的框架有非常 多的功能以及配置,可以考虑提供一个入口类,比如创建一个 ConfigCenter 类作为入口,让使用者可以仅仅探索这个类便可对框架进行所有的配置。 一 个好的框架是可以让使用者少犯错误的,框架的设计者务必要考虑到,框架的使用者没 有这个业务来按照框架的最佳实践来做,所以在设计 API 的时候,如果你希 望 API 的使用 者一定要按照某个方式来做的话,可以考虑设置一个简便的重载来加载默认的最合理的使 用方式而不是要求使用者来为你的方法初始一些什么依赖, 同时也可以在 API 内部做一些 检测,如果发现开发人员可能会犯错进行一些提示或抛出异常。好的框架无需过多的文档, 它可以在开发人员用的时候告知它哪里错 了,最佳实践是什么,即便他们真的错了也能以 默认的更合理的方式来弥补这个错误。 建议所有的 API 都有一套统一的规范,比如入口都叫 XXXCenter 或 XXXManager,而不是叫 XXXCenter、YYYManager 和 ZZZService。API 往往需要进行迭代和改良的,在首个版本中把 好名字用掉也不一定是一个好办法,最好还是给自己的框架各种 API 的名字留一点余 地, 这样以后万一需要升级换代不至于太牵强。 下一步工作就是把项目中那些空的类按照功能进行划分。目的很简单,就是让你的框架 的 100 个类或接口能够按照功能进行拆分和归类,这样别人一打开你的框架就可以马上知 道你的框架分为哪几个主要部分,而不是在 100 个类中晕眩;还有因为 一旦在你的框架有 使用者后你再要为 API 相关的那些类调整包就比困难了,即使你在创建框架的时候觉得我 的框架就那么十几个类无需进行过多的分类,但是在将 来框架变大又发现当初设计的不合 理,无法进行结构调整就会变得很痛苦。因此这个工作还是相当重要的,对于大多数框架 来说,可以有几种切蛋糕的方式: 分 层。我觉得框架和应用程序一样,也需要进行分层。传统的应用程序我们分为表现层、 逻辑层和数据访问层,类似的对于很多框架也可以进行横向的层次划分。要分 层的原因是 我们的框架要处理的问题是基于多层抽象的,就像如果没有 OSI 七层模型,要让一个 HTTP 应用去直接处理网络信号是不合理的也是不利于重用的。 举一个例子,如果我们要写一个 基于 Socket 的 RPC 的框架,我们需要处理方法的代理以及序列化,以及序列化数据的传输, 这完全是两个层面的问题,前者 偏向于应用层,后者偏向于网络层,我们完全有理由把我 们的框架分为两个层面的项目(至少是两个包),rpc.core 和 rpc.socket,前者不关心 网络 实现来处理所有 RPC 的功能,后者不关心 RPC 来处理所有的 Socket 功能,在将来即使我们 要淘汰我们的 RPC 的协议了,我们也可以重用 rpc.socket 项目,因为它和 RPC 的实现没有 任何关系,它关注的只是 socket 层面的东西。 横切。刚才说的分层是横向的分 割,横切是纵向的分割(横切是跨多个模块的意思,不是 横向来切的意思)。其实横切关注点就是诸如日志、配置、缓存、AOP、IOC 等通用的功能, 对于这部 分功能,我们不应该把他们和真正的业务逻辑混淆在一起。对于应用类项目是这 样,对于框架类项目也是这样,如果某一部分的代码量非常大,完全有理由为它分出 一个 单独的包。对于 RPC 项目,我们可能就会把客户端和服务端通讯的消息放在 common 包内, 把配置的处理单独放在 config 包内。 功能。也就是要实现一个框架主要解决的问题点,比如对于上面提到的 RPC 框架的 core 部 分,可以想到的是我们主要解决是客户端如何找到服务端,如何把进 行方法调用以及把方 法的调用信息传给目标服务端,服务端如何接受到这样的信息根据配置在本地实例化对象 调用方法后把结果返回客户端三大问题,那么我们可能 会把项目分为 routing、client、server 等几个包。 如果是一个如果是一个 RPC 框架,大概是这样的结构:框架,大概是这样的结构: 对于我们的对于我们的 Web MVC 框架,举例如下:框架,举例如下: 我们可以有一个 mvc.core 项目,细分如下的包: common:公共的一组件,下面的各模块都会用到 config:配置模块,解决框架的配置问题 startup:启动模块,解决框架和 Servlet 如何进行整合的问题 plugin:插件模块,插件机制的实现,提供 IPlugin 的抽象实现 routing:路由模块,解决请求路径的解析问题,提供了 IRoute 的抽象实现和基本实现 controller:控制器模块,解决的是如何产生控制器 model:视图模型模块,解决的是如何绑定方法的参数 action:action 模块,解决的是如何调用方法以及方法返回的结果,提供了 IActionResult 的 抽象实现和基本实现 view:视图模块,解决的是各种视图引擎和框架的适配 filter:过滤器模块,解决是执行 Action,返回 IActionResult 前后的 AOP 功能,提供了 IFilter 的抽象实现以及基本实现 我们可以再创建一个 mvc.extension 项目,细分如下的包: filters:一些 IFilter 的实现 results:一些 IActionResult 的实现 routes:一些 IRoute 的实现 plugins:一些 IPlugin 的实现 这里我们以 IXXX 来描述一个抽象,可以是接口也可以是抽象类,在具体实现的时候根据需 求再来确定。 这 种结构的划分方式完全吻合上面说的切蛋糕方式,可以看到除了横切部分和分层部分, 作为一个 Web MVC 框架,它核心的组件就是 routing、model、view、controller、action(当然,对于有些 MVC 框架它没有 route 部 分, route 部分是交由 Web 框架实现的)。 如果我们在这个时候还无法确定框架的模块划分的话,问题也不大,我们可以在后续的搭 建龙骨的步骤中随着更多的类的建立,继续理清和确定模块的划分。 经过了设计的步骤,我们应该心里对下面的问题有一个初步的规划了: 我们的框架以什么形式来提供如何优雅的 API? 我们的框架包含哪些模块,模块大概的作用是什么? 搭建龙骨搭建龙骨 在 经过了初步的设计之后,我们可以考虑为框架搭建一套龙骨,一套抽象的层次关系。也 就是用抽象类、接口或空的类实现框架,可以通过编译,让框架撑起来,就像 造房子搭建 房子的钢筋混凝土结构(添砖加瓦是后面的事情,我们先要有一个结构)。对于开发应用 程序来说,其实没有什么撑起来一说,因为应用程序中很多模块 都是并行的,它可能并没 有一个主结构,主流程,而对于框架来说,它往往是一个高度面向对象的,高度抽象的一 套程序,搭建龙骨也就是搭建一套抽象层。这么说 可能有点抽象,我们还是来想一下如果 要做一个 Web MVC 框架,需要怎么为上面说的几个核心模块进行抽象(我们也来体会一 下框架中一些类的命名,这里我们为了更清晰,为所有接口都命名为 IXXX,这点不太 符 合 Java 的命名规范): routing MVC 的入口是路由 每一个路由都是 IRoute 代表了不同的路由实现,它也提供一个 getRouteResult()方法来返回 RouteResult 对象 我们实现一个框架自带的 DefaultRoute,使得路由支持配置,支持默认值,支持正则表达 式,支持约束等等 我们需要有一个 Routes 类来管理所有的路由 IRoute,提供一个 findRoute()方法来返回 RouteResult 对象,自然我们这边调用的就是 IRoute 的 getRouteResult()方法,返回能匹配到 的结果 RouteResult 对象就是匹配的路由信息,包含了路由解析后的所有数据 controller 路由下来是控制器 我们有 IControllerFactory 来创建 Controller,提供 createController()方法来返回 IController IController 代表控制器,提供一个 execute()方法来执行控制器 我们实现一个框架自带的 DefaultControllerFactory 来以约定由于配置的方式根据约定规则 以及路由数据 RouteResult 来找到 IController 并创建它 我 们为 IController 提供一个抽象实现,AbstractController,要求所有 MVC 框架的使用者创 建的控制器需要继承 AbstractController,在这个抽象实现中我们可以编写一些便捷的 API 以便开发人员使用,比如 view()方法、file()方法、 redirect()方法、json()方法、js()方法等等 action 找到了控制器后就是来找要执行的方法了 我们有 IActionResult 来代表 Action 返回的结果,提供一个 execute()方法来执行这个结果 我们的框架需要实现一些自带的 IActionResult,比如 ContentResult、ViewResult、FileResult、JsonResult、RedirectResult 来对应 AbstractController 的一些便捷方法 再来定义一个 IActionInvoker 来执行 Action,提供一个 invokeAction()方法 我们需要实现一个 DefaultActionInvoker 以默认的方式进行方法的调用,也就是找到方法的 一些 IFilter 按照一定的顺序执行他们,最后使用反射进行方法的调用得到上面说的 IActionResult 并执行它的 execute()方法 filter 我们的框架很重要的一点就是便捷的过滤器 刚才提到了 IFilter,代表的是一个过滤器,我们提供 IActionFilter 对方法的执行前后进行过 滤,提供 IResultFilter 对 IActionResult 执行前后进行过滤 我们的 IActionInvoker 怎么找到需要执行的 IFilter 呢,我们需要定义一个 IFilterProvider 来 提供过滤器,它提供一个 getFilters()方法来提供所有的 IFilter 的实例 我 们的框架可以实现一些自带的 IFilterProvider,比如 AnnotationFilterProvider 通过扫描 Action 或 Controller 上的注解来获取需要执行的过滤器信息;比如我们还可以实现 GlobalFilterProvider,开发人员可以直接通过配置或代 码方式告知框架应用于全局的 IFilter 既然我们实现了多个 IFilterProvider,我们自然需要有一个类来管理这些 IFilterProvider,我 们实现一个 FilterProviders 类并提供 getFilters()方法(这和我们的 Routes 类来管理 IRoute 是类似的,命名统一) view 各种 IActionResult 中最特殊最复杂的就是 ViewResult,我们需要有一个单独的包来处 理 ViewResult 的逻辑 我们需要有 IViewEngine 来代表一个模版引擎,提供一个 getViewEngineResult()方法返回 ViewEngineResult ViewEngineResult 包含视图引擎寻找视图的结果信息,里面包含 IView 和寻找的一些路径等 IView 自然代表的是一个视图,提供 render()方法(或者为了统一也可以叫做 execute)来 渲染视图 我 们的框架可以实现常见的一些模版引擎,比如 FreemarkerViewEngine、VelocityViewEngine 等,VelocityViewEngine 返回的 ViewEngineResult 自然包含的是一个实现 IView 的 VelocityView,不会返回 其它引擎的 IView 同样的,我们是不是需要一个 ViewEngines 来管理所有的 IViewEngine 呢,同样也是实现 findViewEngine()方法 common 这里可以放一些项目中各个模块都要用到的一些东西 比 如各种 context,context 代表的是执行某个任务需要的环境信息,这里我们可以定义 HttpContext、 ControllerContext、ActionContext 和 ViewContext,后者继承前者,随着 MVC 处理流程的进行,View 执行时的 上下文相比 Action 执行时的上下文信息肯定是多了视图 的信息,其它同理,之所以把这个信息放在 common 里面而不是放在各个模块自己的包内 是因为这 样更清晰,可以一目了然各种对象的执行上下文有一个立体的概念 比如各种 helper 或 utility 接下去就不再详细阐述接下去就不再详细阐述 model、plugin 等模块的内容了。等模块的内容了。 看到这里,我们来总结一下,我们的看到这里,我们来总结一下,我们的 MVC 框架在组织结构上有着高度的统一:框架在组织结构上有着高度的统一: 如果 xxx 本身并无选择策略,但 xxx 的创建过程也不是一个 new 这么简单的,可以由 xxxFactory 类来提供一个 xxx 如果我们需要用到很多个 yyy,那么我们会有各种 yyyProvider(通过 getyyy()方法)来提供 这些 yyy,并且我们需要有一个 yyyProviders 来管理这些 yyyProvider 如果 zzz 的选择是有策略性的,会按照需要选择 zzz1 或 zzzN,那么我们可能会有一个 zzzs 来管理这些 zzz 并且(通过 findzzz()方法)来提供合适的 zzz 同 时我们框架的相关类的命名也是非常统一的,可以一眼看出这是实现、还是抽象类还是 接口;是提供程序,是执行结果还是上下文。当然,在将来的代码实现过程中 很可能会把 很多接口变为抽象类提供一些默认的实现,这并不会影响项目的主结构。我们会在模式篇 对框架常用的一些高层设计模式做更多的介绍。 到了这里,我们的项目里已经有几十个空的(抽象)类、接口了,其中也定义了各种方法 可以把各个模块串起来(各种 find()方法和 execute()方法),可以说整个项目的龙骨已经建 立起来了,这种感觉很好,因为我们心里很有底,我们只需要在接下去的工作中做两个事 情: 实现各种 DefaultXXX 来走通主流程 实现各种 IyyyProvider 和 Izzz 接口来完善支线流程 走通主线流程走通主线流程 所谓走通主线流程,就是让这个框架可以以一个 HelloWorld 形式跑起来,这就需要把几个 核心类的核心方法使用最简单的方式进行实现,还是拿我们的 MVC 框架来举例子: 从 startup 开始,可能需要实现 ServletContextListener 来动态注册我们框架的入口 Servlet, 暂且起名为 DispatcherServlet 吧,在这个类中我们需要走一下主线流程 调用 Routes.findRoute()获得 IRoute 调用 IRoute.getRouteResult()来获得 RouteResult 使用拿到的 RouteResult 作为参数调用 DefaultControllerFactory.createController()获得 IController(其实也是 AbstractController) 调用 IController.execute() 在 config 中创建一个 IConfig 作为一种配置方式,我们实现一个 DefaultConfig,把各种默认 实现注册到框架中去,也就是 DefaultRoute、DefaultControllerFactory、DefaultActionInvoker,然后把各种 IViewEngine 加 入 ViewEngines 然后需要完成相关默认类的实现: 实现 Routes.findRoute() 实现 DefaultRoute.getRouteResult() 实现 DefaultControllerFactory.createController() 实现 AbstractController.execute() 实现 DefaultActionInvoker.invokeAction() 实现 ViewResult.execute() 实现 ViewEngines.findViewEngine() 实现 VelocityViewEngine.getViewEngineResult() 实现 VelocityView.render() 在这一步,我们并不一定要去触碰 filter 和 model 这部分的内容,我们的主线流程只是解析 路由,获得控制器,执行方法,找到视图然后渲染视图。过滤器和视图模型的绑定属于增 强型的功能,属于支线流程,不属于主线流程。 虽 然在这里我们说了一些 MVC 的实现,但本文的目的不在于教你实现一个 MVC 框架,所 以不用深究每一个类的实现细节,这里想说的是,在前面的龙骨搭建完后, 你会发现按照 这个龙骨为它加一点肉上去实现主要的流程是顺理成章的事情,毫无痛苦。在整个实现的 过程中,你可以不断完善 common 下的一些 context,把方法的调用参数封装到上下文对 象中去,不但看起来清楚且符合开闭原则。到这里,我们应该可以跑起来在设计阶段做的 那个示例网站的 HelloWorld 功能了。 在这里还想说一点,有些人在实现框架的时候并没有搭建龙骨的一步骤,直接以非 OOP 的 方式实现了主线流程,这种方式有以下几个缺点: 不容易做到 SRP 单一指责原则,你很容易把各种逻辑都集中写在一起,比如大量的逻辑直 接写到了 DispatcherServlet 中,辅助一些 Service 或 Helper,整个框架就肥瘦不匀,有些类 特别庞大有些类特别小。 不容易做到 OCP 开闭原则,扩展起来不方便需要修改老的代码,我们期望的扩展是实现新 的类然后让框架感知,而不是直接修改框架的某些代码来增强功能。 很难实现 DIP 依赖倒置原则,即使你依赖的确实是 IService 但其实就没意义,因为它只有一 个实现,只是把他当作帮助类来用罢了。 实现各种支线流程实现各种支线流程 我们想一下,对于这个 MVC 框架有哪些没有实现的支线流程?其实无需多思考,因为我们 在搭建龙骨阶段的设计已经给了我们明确的方向了,我们只需要把除了主线之外的那些龙 骨上也填充一些实体即可,比如: 实现更多的 IRoute,并注册到 Routes 实现更多的 IViewEngine,并注册到 ViewEngines 实现必要的 IFilterProvider 以及 FilterProviders,把 IFilterProvider 注册到 FilterProviders 增强 DefaultActionInvoker.invokeAction()方法,在合适的时候调用这些 IFilter 实现更多的 IActionResult,并且为 AbstractController 实现更多的便捷方法来返回这些 IActionResult 实现更多 model 模块的内容和 plugin 模块的内容 实现了这一步后,你会发现整个框架饱满起来了,每一个包中不再是仅有的那些接口和默 认实现,而且会有一种 OOP 的爽快感,爽快感来源于几个方面: 面对接口编程抽象和多态的放心安心的爽快感 为抽象类实现具体类享受到父类大量实现的满足的爽快感 实现了大量的接口和抽象类后充实的爽快感 我们再来总结一下之前说的那些内容,实现一个框架的第一大步就是: 设计一套合理的接口 为框架进行模块划分 为框架搭建由抽象结构构成的骨架 在这个骨架的基础上实现一个 HelloWorld 程序 为这个骨架的其它部分填充更多实现 经 过这样的一些步骤后可以发现这个框架是很稳固的,很平衡的,很易于扩展的。其实到 这里很多人觉得框架已经完成了,有血有肉,其实个人觉得只能说开发工作实 现了差不多 30%,后文会继续说,毕竟直接把这样一个血肉之躯拿出去对外有点吓人,我们需要为它 进行很多包装和完善。 单元测试单元测试 在这之前我们写的框架只能说是一个在最基本的情况下可以使用的框架,作为一个框架我 们无法预测开发人员将来会怎么使用它,所以我们需要做大量的工作来确保框架不但各种 功能都是正确的,而且还是健壮的。写应用系统的代码,大多数项目是不会去写单元测试 的,原因很多: 项目赶时间,连做一些输入验证都没时间搞,哪里有时间写测试代码。 项目对各项功能的质量要求不高,只要能在标准的操作流程下功能可用即可。 项目基本不会去改或是临时项目,一旦测试通过之后就始终是这样子了,没有迭代。 对于框架,恰恰相反,没有配套的单元测试的框架(也就是仅仅使用人工的方式进行测试, 比如在 main 中调用一些方法观察日志或输出,或者运行一下示例项目查看各种功能是否正 常,是非常可怕的)原因如下: 自动化程度高,回归需要的时间短,甚至可以整合到构建过程中进行,这是人工测试无法 实现的。 框架一定是有非常多的迭代和重构的, 每一次修改虽然只改了 A 功能,但是可能会影响 到 B 和 C 功能,人工测试的话你可能只会验证 A 是否正常,容易忽略 B 和 C,使用单元测 试的话只要所有功能都有覆盖,那么几乎不可能遗漏因为修改导致的潜在问题,而且还能 反馈出来因为修改导致的兼容性问题。 之前说过,一旦框架开放出去,框架的使用者可能会以各种方式在各种环境来使用你的框 架,环境不同会造成很多怪异的边界输入或非法输入,需要使用单元测试对代码进行严格 的边界测试,以确保框架可以在严酷的环境下生存。 单元测试还能帮助我们改善设计,在写单元测试的时候如果发现目标代码非常难以进行模 拟难以构建有效的单元测试,那么说明目标代码可能有强依赖或职责过于复杂,一个被单 元测试高度覆盖的框架往往是设计精良的,符合高内聚低耦合的框架。 如果框架的时间需求不是特别紧的话,单元测试的引入可以是走通主线流程的阶段就引入, 越早引入框架的成熟度可能就会越高,以后重构返工的机会会越小,框架的可靠性也肯定 会大幅提高。之前我有写过一个类库项目,并没有写单元测试,在项目中使用了这个类库 一段时间也没有出现任何问题,后来花了一点时间为类库写了单元测试,出乎我意料之外 的是,我的类库提供的所有 API 中有超过一半是无法通过单元测试的(原以为这是一个成 熟的类库,其实包含了数十个 BUG),甚至其中有一个 API 是在我的项目中使用的。你可 能会问,为什么在使用这个 API 的时候没有发生问题而在单元测试的时候发生问题了呢? 原因之前提到过,我是框架的设计者,我在使用类库提供的 API 的时候是知道使用的最佳 实践的,因此我在使用的时候为类库进行了一个特别的设置,这个问题如果不是通过单元 测试暴露的话,那么其它人在使用这个类库的时候基本都会遇到一个潜在的 BUG。 示范项目示范项目 写一个示例项目不仅仅是为了给别人参考,而且还能够帮助自己去完善框架,对于示例项 目,最好兼顾下面几点: 是一个具有一定意义的网站或系统,而不是纯粹为了演示特性而演示。这是因为,很多时 候只有那些真正的业务逻辑才会暴露出问题,演示特性的时候我们总是有一些定势思维会 规避很多问题。或者可以提供两个项目,一个纯粹演示特性,一个是示例项目。 覆盖尽可能多的特性或使用难点,在项目的代码中提供一些注释,很多开发人员不喜欢阅 读文档,反而喜欢看一下示例项目直接上手(模仿示例项目,或直接拿示例项目中的代码 来修改)。 项目中的代码,特别是涉及到框架使用的代码一定要规范,原因上面也说了,作为框架的 设计者你不会希望大家复制的代码粘帖的代码一团糟吧。 如果你的项目针对的不仅仅是 Web 项目,那么示例项目最好提供 Web 和桌面两个版本, 一来你自己容易发现因为环境不同带来的使用差异,二来可以给予不同类型项目不同的最 佳实践。 完善日志和异常完善日志和异常 一个好的框架不但需要设计精良,日志和异常的处理是否到位也是非常重要的标准,这里 有一些反例: 日志的各种级别的使用没有统一的标准,甚至是永远只使用某个级别的日志。 几乎没有任何的日志,框架的运行完全是一个黑盒。 记录的日志多且没有实际含义,只是调试的时候用来观察变量的内容。 异常类型只使用 Exception,不使用更具体化的类型,没有自定义类型。 异常的消息文本只写“错误“字样,不写清楚具体的问题所在。 永远只是抛出异常,让异常上升到最外层,交给框架的使用者去处理。 用异常来控制代码流程,或本应该在方法未达到预期效果的时候使用异常却使用返回值。 其实个人觉得,一个框架的主逻辑代码并不一定是最难的,最难的是对一些细节的处理, 让框架保持一套规范的统一的日志和异常的使用反而对框架开发者来说是一个难点,下面 是针对记录日志的一些建议: 1、首先要对框架使用的日志级别有一个规范,比如定义: DEBUG:用于观察程序的运行流程,仅在调试的时候开启 INFO:用于告知程序运行状态或阶段的变化,可以在测试环境开启 WARNING:用于告知程序可以自己恢复的错误或异常,或不影响主线流程执行的错误或问 题,可以在正式环境开启 ERROR:用于告知程序无法恢复,主线流程中断,需要开发或运维人员知晓干预的错误或 异常,需要在正式环境开启 2、按照上面的级别规范,在需要记录日志的地方记录日志,除了 DEBUG 级别的日志其它 日志不能记录过多,如果框架总是在运行的时候输出几十个 WARNNING 也容易让使用者忽 略真正的问题。 3、日志记录的消息需要是明确的,最好包含一些上下文信息,比如“无法在 xxx 下找到配 置文件 xxx.config,框架将采用默认的配置“,而不是“加载配置失败!“ 下面是一些针对使用异常的建议:下面是一些针对使用异常的建议: 框架由于配置错误或使用错误或运行错误,不能完成 API 名字所表示的功能,考虑抛出转 化后的异常,让调用者知道发什么了什么情况,同时框架可以建立自己的错误处理机制 对于可以预料的错误,并且错误类型可以枚举,考虑以返回值的形式告知调用者可以根据 不同的结果来处理后续的逻辑 对于框架内部功能实现上遇到的调用者无能力解决的错误,如果错误可以重试或不影响返 回,可以记录警告或错误日志 可以为每一个模块都陪伴自定义的异常类型,包含相关的上下文信息(比如 ViewException 可以包含 ViewContext),这样出现异常可以很方便知晓是哪个模块出现问题并且可以得到 出现异常时的环境信息 如果异常跨了实现层次(比如从框架到应用),那么最好进行一下包装转换(比如把文件 读取失败的提示改为加载配置文件失败的提示),否则上层人员是不知道怎么处理这些内 部问题的,内部问题需要由框架自己来处理 异常的日志中可以记录和当前操作密切相关的参数信息,比如搜索的路径,视图名等等, 有关方法的信息不用过多记录,异常一般都带有调用栈信息 如果可能的话,出现异常的时候可以分析一下为什么会出现这样的问题,在异常信息中给 一些解决问题的建议或帮助链接方便使用者排查问题 异常处理从坏到好的层次是,出现了严重问题的时候: 使用者什么都不知道,程序的完整性和逻辑得到破坏 使用者既不知道出现了什么问题也不知道怎么去解决 使用者能明确知道出现了什么问题,但无法去解决 使用者不但知道发生了什么,还能通过异常消息的引导快速解决问题 完善配置完善配置 配置的部分可以留到框架写的差不多了再去写,因为这个时候已经可以想清楚哪些配置是: 需要公开出去给使用者配置的,并且配置会根据环境不同而不同 需要公开出去给使用者来配置的,配置和部署环境无关 仅仅需要在框架内供框架开发人员来配置的 无需是一个配置,只要在代码中集中存储这个设定即可 一般来说配置有几种方式:一般来说配置有几种方式: 通过配置文件来配置,比如 XML 文件、JSON 文件或 property 文件 通过注解或特性(Annotation/Attribute)方式(对类、方法、参数)进行配置 通过代码方式进行配置(比如单独的配置类,或实现配置类或调用框架的配置 API) 很多框架提供了多种配置方式,比如 Spring MVC 同时支持上面三种方式的配置,个人觉得 对配置,我们还是应该区别对待,而不是无脑把所有的配置项都同时以上面三种方式提供 配置,我们要考虑高内聚和低耦合原则,对于 Web 框架来说,高内聚需要考虑的比低耦合 更多,我的建议是对不同的配置项提供不同的配置方式: 如果配置项目是需要让使用者来配置的,特别是和环境相关的,那么最好使用配置方式来 配置,比如开放的端口、内存、线程数配置,不过要注意: 所有配置项目需要有默认值,如果找不到配置使用默认值,如果配置不合理使用默认值 (你不会希望使用你框架的人把框架内部的线程池的 min 设置为 999999,或定时器的间隔 设置为 0 毫秒吧?) 框架启动的时候检测所有配置,如果不合理给予提示,大多人只会在启动的时候看一下日 志,使用的时候根本就不管 不知道大家对于配置文件的格式倾向于 XML 呢还是 JSON 呢还是键值对呢? 对于所有仅在开发时进行的配置,都尽量不要去使用配置文件,并且让配置尽量和它所配 置的对象靠在一起: 如果是对框架整体性进行的设置扩展类型的配置,那就可以提供代码方式进行配置,比如 我们要实现的 MVC 框架的各种 IRoute、IViewEngine 等,最好可以提供 IConfig 接口让开发 人员可以去实现接口,这样他们可以知道有哪些东西可以配置,代码就是文档 如果是那种对模型、Action 进行的配置,比如模型的验证规则、Filter 等一律采用注解的方 式进行配置 有的人说使用配置文件进行配置非常灵活,使用代码方式和注解方式来配置不灵活而且可 能有侵入性。我觉得还是要权衡对待,我的建议是不要把太多框架内在的东西放在配置文 件中,增加使用者的难度(而且很多时候,大多数人只是复制配置为了完成配置而配置, 并不是为了真正的灵活性而去使用配置文件来配置你的框架,看看网上这么所 SSH 配置文 件的抄来抄去就知道了)。 最后,我建议很多太内部的东西对于轻量级的应用型框架可以不去提供任何配置选项,只 需要在某个常量文件中定义即可,让真正有需求进行二次开发的开发人员去修改,对于一 个框架如果一下子暴露上百个“高级“配置项给使用者,他们会晕眩的。 提供状态服务提供状态服务 所谓状态服务就是反映框架内部运作状态的服务,很多开源服务或系统(Nginx、Mongodb 等)都提供了类似的模块和功能,作为框架的话我觉得也有必要提供一些内部信息(主要 是配置、数据统计以及内部资源状态)出来,这样使用你框架的人可以在开发的时候或线 上运作的时候了解框架的运作状态,我们举两个例子,对于一个我们之前提到的 Web MVC 框架来说,可以提供这些信息: 路由配置 视图引擎配置 过滤器配置 对于一个 Socket 框架来说,有一些不同,Socket 框架是有状态的,其状态服务提供的信息 除了当前生效的配置信息之外,更多的是反映当前框架内部一些资源的状态以及统计数据: 各种配置(池配置、队列配置、集群配置) Socket 相关的统计数据(总打开、总关闭、每秒收发数据、总收发数据、当前打开等等) 各种池的当前状态 各种队列的当

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

最新文档

评论

0/150

提交评论