《Struts2技术内幕》 新书部分篇章连载(六)—— 框架的本质
程序员文章站
2022-05-17 23:15:13
...
第2章 固本清源 —— Web开发浅谈
2.2 框架的本质
什么是框架?框架从何而来?为什么要使用框架?这是一系列简单而又复杂的问题。简单,是因为它们本身似乎不应该成为问题。框架实实在在存在,并且在开发中发挥着重要的作用,我们的日常工作,遵循着框架所规定的编程模式,在其指导之下,我们能够编写更为强大的程序。说其复杂,是因为框架本身又是如此纷繁复杂,我们在使用框架的同时,往往会迷失其中。
任何事物都有蕴含在其内部的本质。无论框架本身有多复杂,我们所需要探寻的,都是其最为内在的东西。框架为什么会产生?我们来看一个最最简单的例子。
在Java中,如果我们要判定一个输入是否为null或空字符串,我们会使用下面的代码:
这段代码非常普通,一个简单学习过Java语法的程序员都能够读懂并编写。那么这段代码是如何运作的呢?我们所编写的Java程序,首先获得的是来自于Java的基本支持:语法支持与基本功能的API级别的支持(str.length()方法实际上就是JDK所提供的字符串的基本API)。换句话说,我们编写的所有程序,都依赖于一个最最基本的前提条件:JDK所提供的API支持。
当一个需求被重复1000次,那么我们就需要重复1000次针对需求的解决办法,这是一个显而易见的道理。然而当上面的代码片段散落在我们的程序中1000次,我们不免会思考,是不是有什么简单有效的途径可以把事情做得更加漂亮一些呢?我们可以针对代码片段做了一次简单的逻辑抽取重构,如代码清单2-4所示:
在上面的代码段中,我们定义了一个静态方法,将之前写的那段逻辑封装了起来。这一层小小的封装虽然看上去是一个“换汤不换药”的做法,但是从深远意义上来说,我们至少可以从以下两个方面获得好处:
可读性
静态方法的签名从一个角度向我们揭示了一段逻辑的实际意义。比如在这个例子中,isEmpty表示“判定某个输入是否为空”。与之前的代码片段相比,如果我们在一个1000行的程序代码片段中观察这2种不同的代码形式,那么前者往往会被淹没在你的视线中,完全无法引起你的思维停顿,而后者却能够显而易见地在逻辑上给你足够且直观的提示。
可扩展性
如果我们对上述需求稍作改动,程序同时需要对输入为空格的字符串做出同样的判定。我们同样将上述的需求应用1000次,那么前者将导致我们在整个应用中进行搜索并替换修改1000次,而后者只需要针对我们封装的逻辑修改1次即可。
从上面的例子中我们可以看出,虽然仅仅对代码做了一次简单的重构,却在上述的两个方面为我们解决了潜在的问题。这一现象或许直到现在才被你意识到,但它却很早以前就被程序届的很多前辈意识到了。因而,早就有人为此编写了类似的代码。比如说,类似的方法就存在于Apache的commons-lang的JAR包中,如代码清单2-5所示:
当我们将Apache的commons-lang的JAR包加到CLASSPATH中时,我们就能在程序的任何地方“免费地”使用上述方法。也就是说,我们自己无需自行编写代码对JDK进行扩展了,因为Apache的commons-lang已经为我们做了。既然如此,我们唯一所需要做的,只是把别人做的东西加到CLASSPATH中并且使用它而已。
这是一个很熟悉的过程,不是吗?我们在搭建程序运行的基本环境时,指定程序所依赖的JAR文件是其中的一个重要步骤。而这一步骤,实际上包含了Java开发中最最基本而浅显的道理:
我们的程序就像一个金字塔形状。位于最底部的当然是JVM,提供了运行Java程序的基础环境,包括对整个Java程序的编译运行。在这个之上的是JDK,JDK则是构建在JVM之上的基本的对象行为的定义(我们在搭建开发环境时所安装的JDK就是这个)。而再往上,是一个具备层次结构的JAR层,所有被加载到CLASSPATH中的JAR文件都被搭建在JDK层次之上,它们之间可能形成互相依赖,但不管怎么说,它们的作用都是提供JDK以外的功能支持。最后,在金字塔尖的,才是我们日常编写的应用程序,它将依赖于金字塔低端的所有程序。这样一个结构,就如同图2-3所示的那样:
仔细观察一下这个处于中间的JAR层,这个层次的组成结构与其他的层次有着不同。它是由一块块砖头堆砌而成,上层的砖块搭建在下层的砖块之上。如果我们把其中的每一块砖都比作一个JAR文件,它们之间也就形成了明显的具备层次的依赖关系。
这个层次中的任何JAR文件本身可能并不为最终的程序提供具体的功能实现,但是它却为我们编写程序提供了必要的支持。如果查看一个标准的J2EE程序运行时所依赖的CLASSPATH中的JAR包,会发现我们所熟悉的那些“框架”,实际上都蕴含其中。我们在这里给出一个最简单的示例程序在Eclipse中的CLASSPATH截图,如下图2-4所示:
从图中我们看到,JRE System Library是整个应用程序最基本的运行环境。而无论是Struts2还是Spring,他们都以JAR文件的形式被加载到程序运行所依赖的CLASSPATH中去,并为我们的应用程序使用。如果我们用更加通俗的话来表述这一现象:
当我们说一个程序使用了Spring框架,隐藏在背后的潜台词实际上是说,我们把Spring的分发包加入到了CLASSPATH,并且在程序中使用了其功能。框架,其实就是这么回事!就是如此简单的东西!
到现在为止,框架似乎还没有任何在我们的知识范畴以外的东西,它们的本质是如此一致,以至于我们很容易遗忘把一个JAR文件加入到CLASSPATH中的初衷:解决在某个领域的开发中所碰到的困境。正如我们在一开始使用的那个例子一样,框架作为一个JAR包,实际上是许许多多解决各种问题的类和方法的集合。当然,更多时候,它们包含了编写这些JAR包的作者所创造的许多最佳实践。
因此,只有解决问题才是所有框架的共同目标。框架的产生就是为了解决一个又一个在开发中所遇到的困境。不同的框架,只是为了解决不同领域的问题。所以,对于广大程序员来说,千万不要为了学习框架而学习框架,而是要为了解决问题而学习框架,这才是一个程序员的正确学习之道。
2.3 最佳实践
一切程序的编写,都需要遵循特定的规范。这里所说的规范,往往是建立在运行环境之上的一系列概念和实现方法的基本定义,并归纳为一个完整的体系。例如,我们使用Java来进行Web开发,所需要遵循的最基本的规范就是我们所熟悉的Servlet标准、JSP标准,等等。
建立在标准和规范之上的,是各种各样的针对这些标准和规范的实现。这些实现构成了程序运行的基本环境。例如,Tomcat有对Servlet标准的实现方式,而Websphere则有不同的实现方式。然而它们在本质上都实现了Servlet标准所规定的接口,从而让我们的应用程序可以透明地使用这些API,而无需关心真正的Web容器内部的实现机理。
我们所编写的程序,总是建立在一系列的规范和基本运行环境之上。面对纷繁复杂的业务需求,不同的程序员可以按照自己的意愿来编写程序,因此,即使为了表达相同的业务功能,不同的程序代码之间的差异性也是很大的。程序的差异性有时候会给人以创新的灵感,但是更多的时候会造成麻烦。因为差异性越大,维护起来就越麻烦。出于对可维护性和可读性的要求,我们所希望的程序最好能从宏观层面上看上去是一致的,使得每一个程序员都能够读懂并合理运用,这才是我们的目标。这一目标,我们习惯上称之为最佳实践。
所有的这些最佳实践,最终又以一个个JAR包的形式被蕴含在框架之中,对我们的应用程序提供必要的支持,因此我们有必要在这里探寻一些最最基本的最佳实践,从而能够从更加深的层次了解框架存在的意义和框架的设计初衷。在之后的章节中,我们会反复提及这些最佳实践,因为它们不仅能够指导我们进行程序开发,它们本身也被蕴含在了Struts2的内部。
这是一条凌驾于任何最佳实践之上的最佳实践。在我们使用框架编写程序时,程序员最容易犯的毛病就是对某项技术或者某个框架的绝对迷信,并将它生搬硬套于任何程序开发之中。应用程序永远服务于具体的业务场景,对于不同的业务需求,我们的解决方案也会有所区别,自然也会涉及到不同的框架选择。
在实际开发中,我们遇到的许多编程开发的问题都是没有固定答案的哲学取向问题。所以,往往没有“最好”的答案,只有“最合适”的答案。这也将成为日后在面对多种解决方案进行取舍时的一个基本准绳。
可读性、可维护性和扩展性,就像三角架的三个支撑脚,缺一不可。任何对程序的重构,实际上都围绕着这三个基本原则进行,而它们也同时成为衡量程序写得好坏的最基本标准。代码的不断重构、框架的产生实际上都来自于这三个程序内在属性的驱动。
我们之前已经反复提到了程序的可维护性和扩展性。事实上,程序的可读性也是程序所应具备的必不可少的基本属性,失去了可读性的程序,其可维护性和扩展性也就无从谈起了。这三大原则从方法论的角度规定了一切最佳实践都不能违背这三大程序的基本属性。否则,我们迟早会为一些蝇头小利而舍弃程序开发的源头根本。当一个程序失去了可读性、可维护性和扩展性,它也就失去了生命力。
简单是美(Simple is Beauty)是一种指导思想,它其实包含了两个层次的意思。第一层意思是消除重复(Don’t repeat yourself),这是一个显而易见的代码重构标准。第二层意思则是要求我们化繁入简(Heavy to Light),用尽量少的代码语言来表达尽量多的逻辑意义。
简单是美,将最佳实践的要求细化到了方法论的层面。然而,无论我们的程序如何简单,都应该始终记得,简单必须可读,简单必须可扩展。切忌为了一些细节,而忘记了更大的原则。
我们可以看到,这个层面的最佳实践,已经从基本准则和指导思想转向了具体的编程层面。虽然面向对象自身也只是一种编程的指导思想,然而它却是和程序设计和实现息息相关并且对程序编写影响最大的一条编程准则。
面向对象这个概念本身就是一个非常耐人寻味的问题。谈到面向对象的概念、设计和方法论,恐怕一天一夜都讲不完。在本章之初,我们从“对象”这个概念入手,通过对“对象”内部结构的分析,试图向读者展示面向对象编程中的一些重要理论。读者对这些理论不应停留在死记硬背的层面,而是要将他们融入到框架的设计理念中去理解。同时,这些理论也将成为我们判别框架和设计方案优劣的重要标准。
之前我们在分析框架的本质时已经提到,任何Java程序总是依赖于其运行环境(JVM层)和支持应用程序的JAR层。加入到CLASSPATH中JAR越多,就意味着程序对外部环境的依赖度越高,对外部环境的依赖度越高,就意味着程序本身越难以脱离特定的外部环境进行单元测试。因此,减少甚至消除依赖,也成为了许多框架所追求的目标。
Struts2在这一点上做得尤为成功。Struts2不但实现了Web框架与Web容器之间的解耦合,还在此基础之上实现了各个编程元素之间的有效沟通。在之后的章节中,我们会深入探究Struts2在这条最佳实践上所做出的努力。
2.2 框架的本质
什么是框架?框架从何而来?为什么要使用框架?这是一系列简单而又复杂的问题。简单,是因为它们本身似乎不应该成为问题。框架实实在在存在,并且在开发中发挥着重要的作用,我们的日常工作,遵循着框架所规定的编程模式,在其指导之下,我们能够编写更为强大的程序。说其复杂,是因为框架本身又是如此纷繁复杂,我们在使用框架的同时,往往会迷失其中。
任何事物都有蕴含在其内部的本质。无论框架本身有多复杂,我们所需要探寻的,都是其最为内在的东西。框架为什么会产生?我们来看一个最最简单的例子。
在Java中,如果我们要判定一个输入是否为null或空字符串,我们会使用下面的代码:
if(str == null || str.length() == 0) { // 在这里添加你的逻辑 }
这段代码非常普通,一个简单学习过Java语法的程序员都能够读懂并编写。那么这段代码是如何运作的呢?我们所编写的Java程序,首先获得的是来自于Java的基本支持:语法支持与基本功能的API级别的支持(str.length()方法实际上就是JDK所提供的字符串的基本API)。换句话说,我们编写的所有程序,都依赖于一个最最基本的前提条件:JDK所提供的API支持。
当一个需求被重复1000次,那么我们就需要重复1000次针对需求的解决办法,这是一个显而易见的道理。然而当上面的代码片段散落在我们的程序中1000次,我们不免会思考,是不是有什么简单有效的途径可以把事情做得更加漂亮一些呢?我们可以针对代码片段做了一次简单的逻辑抽取重构,如代码清单2-4所示:
// 定义一个类和一个静态工具方法来抽象出将被重复调用的逻辑 public abstract class StringUtils { // 封装了一个静态方法 public static boolean isEmpty(String str) { return str == null || str.length() == 0; } } // 引用静态方法取代之前的代码片段 if(StringUtils.isEmpty(string)) { // 在这里添加你的逻辑 }
在上面的代码段中,我们定义了一个静态方法,将之前写的那段逻辑封装了起来。这一层小小的封装虽然看上去是一个“换汤不换药”的做法,但是从深远意义上来说,我们至少可以从以下两个方面获得好处:
可读性
静态方法的签名从一个角度向我们揭示了一段逻辑的实际意义。比如在这个例子中,isEmpty表示“判定某个输入是否为空”。与之前的代码片段相比,如果我们在一个1000行的程序代码片段中观察这2种不同的代码形式,那么前者往往会被淹没在你的视线中,完全无法引起你的思维停顿,而后者却能够显而易见地在逻辑上给你足够且直观的提示。
可扩展性
如果我们对上述需求稍作改动,程序同时需要对输入为空格的字符串做出同样的判定。我们同样将上述的需求应用1000次,那么前者将导致我们在整个应用中进行搜索并替换修改1000次,而后者只需要针对我们封装的逻辑修改1次即可。
从上面的例子中我们可以看出,虽然仅仅对代码做了一次简单的重构,却在上述的两个方面为我们解决了潜在的问题。这一现象或许直到现在才被你意识到,但它却很早以前就被程序届的很多前辈意识到了。因而,早就有人为此编写了类似的代码。比如说,类似的方法就存在于Apache的commons-lang的JAR包中,如代码清单2-5所示:
package org.apache.commons.lang; public class StringUtils { // 这里省略了许多其他的代码 public static boolean isEmpty(String str) { return str == null || str.length() == 0; } }
当我们将Apache的commons-lang的JAR包加到CLASSPATH中时,我们就能在程序的任何地方“免费地”使用上述方法。也就是说,我们自己无需自行编写代码对JDK进行扩展了,因为Apache的commons-lang已经为我们做了。既然如此,我们唯一所需要做的,只是把别人做的东西加到CLASSPATH中并且使用它而已。
这是一个很熟悉的过程,不是吗?我们在搭建程序运行的基本环境时,指定程序所依赖的JAR文件是其中的一个重要步骤。而这一步骤,实际上包含了Java开发中最最基本而浅显的道理:
downpour 写道
结论 当我们加载一个JAR包到CLASSPATH时,实际上是获得了JAR中所有对JDK的额外支持。
我们的程序就像一个金字塔形状。位于最底部的当然是JVM,提供了运行Java程序的基础环境,包括对整个Java程序的编译运行。在这个之上的是JDK,JDK则是构建在JVM之上的基本的对象行为的定义(我们在搭建开发环境时所安装的JDK就是这个)。而再往上,是一个具备层次结构的JAR层,所有被加载到CLASSPATH中的JAR文件都被搭建在JDK层次之上,它们之间可能形成互相依赖,但不管怎么说,它们的作用都是提供JDK以外的功能支持。最后,在金字塔尖的,才是我们日常编写的应用程序,它将依赖于金字塔低端的所有程序。这样一个结构,就如同图2-3所示的那样:
仔细观察一下这个处于中间的JAR层,这个层次的组成结构与其他的层次有着不同。它是由一块块砖头堆砌而成,上层的砖块搭建在下层的砖块之上。如果我们把其中的每一块砖都比作一个JAR文件,它们之间也就形成了明显的具备层次的依赖关系。
这个层次中的任何JAR文件本身可能并不为最终的程序提供具体的功能实现,但是它却为我们编写程序提供了必要的支持。如果查看一个标准的J2EE程序运行时所依赖的CLASSPATH中的JAR包,会发现我们所熟悉的那些“框架”,实际上都蕴含其中。我们在这里给出一个最简单的示例程序在Eclipse中的CLASSPATH截图,如下图2-4所示:
从图中我们看到,JRE System Library是整个应用程序最基本的运行环境。而无论是Struts2还是Spring,他们都以JAR文件的形式被加载到程序运行所依赖的CLASSPATH中去,并为我们的应用程序使用。如果我们用更加通俗的话来表述这一现象:
downpour 写道
结论 框架只是一个JAR包而已,其本质是对JDK的功能扩展。
当我们说一个程序使用了Spring框架,隐藏在背后的潜台词实际上是说,我们把Spring的分发包加入到了CLASSPATH,并且在程序中使用了其功能。框架,其实就是这么回事!就是如此简单的东西!
到现在为止,框架似乎还没有任何在我们的知识范畴以外的东西,它们的本质是如此一致,以至于我们很容易遗忘把一个JAR文件加入到CLASSPATH中的初衷:解决在某个领域的开发中所碰到的困境。正如我们在一开始使用的那个例子一样,框架作为一个JAR包,实际上是许许多多解决各种问题的类和方法的集合。当然,更多时候,它们包含了编写这些JAR包的作者所创造的许多最佳实践。
downpour 写道
结论 框架是一组程序的集合,包含了一系列的最佳实践,作用是解决某个领域的问题。
因此,只有解决问题才是所有框架的共同目标。框架的产生就是为了解决一个又一个在开发中所遇到的困境。不同的框架,只是为了解决不同领域的问题。所以,对于广大程序员来说,千万不要为了学习框架而学习框架,而是要为了解决问题而学习框架,这才是一个程序员的正确学习之道。
2.3 最佳实践
一切程序的编写,都需要遵循特定的规范。这里所说的规范,往往是建立在运行环境之上的一系列概念和实现方法的基本定义,并归纳为一个完整的体系。例如,我们使用Java来进行Web开发,所需要遵循的最基本的规范就是我们所熟悉的Servlet标准、JSP标准,等等。
建立在标准和规范之上的,是各种各样的针对这些标准和规范的实现。这些实现构成了程序运行的基本环境。例如,Tomcat有对Servlet标准的实现方式,而Websphere则有不同的实现方式。然而它们在本质上都实现了Servlet标准所规定的接口,从而让我们的应用程序可以透明地使用这些API,而无需关心真正的Web容器内部的实现机理。
我们所编写的程序,总是建立在一系列的规范和基本运行环境之上。面对纷繁复杂的业务需求,不同的程序员可以按照自己的意愿来编写程序,因此,即使为了表达相同的业务功能,不同的程序代码之间的差异性也是很大的。程序的差异性有时候会给人以创新的灵感,但是更多的时候会造成麻烦。因为差异性越大,维护起来就越麻烦。出于对可维护性和可读性的要求,我们所希望的程序最好能从宏观层面上看上去是一致的,使得每一个程序员都能够读懂并合理运用,这才是我们的目标。这一目标,我们习惯上称之为最佳实践。
downpour 写道
结论 最佳实践(Best Practice),实际上是无数程序员在经过了无数次的尝试后,总结出来的处理特定问题的特定方法。如果我们把每个程序员的*发挥看作是一条通往成功的路径,最佳实践就是其中的最短路径,它能够极大地解放生产力。
所有的这些最佳实践,最终又以一个个JAR包的形式被蕴含在框架之中,对我们的应用程序提供必要的支持,因此我们有必要在这里探寻一些最最基本的最佳实践,从而能够从更加深的层次了解框架存在的意义和框架的设计初衷。在之后的章节中,我们会反复提及这些最佳实践,因为它们不仅能够指导我们进行程序开发,它们本身也被蕴含在了Struts2的内部。
downpour 写道
最佳实践 永远不要生搬硬套任何最佳实践,真理之锁永远只为最合适的那把钥匙开启。
这是一条凌驾于任何最佳实践之上的最佳实践。在我们使用框架编写程序时,程序员最容易犯的毛病就是对某项技术或者某个框架的绝对迷信,并将它生搬硬套于任何程序开发之中。应用程序永远服务于具体的业务场景,对于不同的业务需求,我们的解决方案也会有所区别,自然也会涉及到不同的框架选择。
在实际开发中,我们遇到的许多编程开发的问题都是没有固定答案的哲学取向问题。所以,往往没有“最好”的答案,只有“最合适”的答案。这也将成为日后在面对多种解决方案进行取舍时的一个基本准绳。
downpour 写道
最佳实践 始终保证程序的可读性、可维护性和扩展性。
可读性、可维护性和扩展性,就像三角架的三个支撑脚,缺一不可。任何对程序的重构,实际上都围绕着这三个基本原则进行,而它们也同时成为衡量程序写得好坏的最基本标准。代码的不断重构、框架的产生实际上都来自于这三个程序内在属性的驱动。
我们之前已经反复提到了程序的可维护性和扩展性。事实上,程序的可读性也是程序所应具备的必不可少的基本属性,失去了可读性的程序,其可维护性和扩展性也就无从谈起了。这三大原则从方法论的角度规定了一切最佳实践都不能违背这三大程序的基本属性。否则,我们迟早会为一些蝇头小利而舍弃程序开发的源头根本。当一个程序失去了可读性、可维护性和扩展性,它也就失去了生命力。
downpour 写道
最佳实践 简单是美(Simple is Beauty)
简单是美(Simple is Beauty)是一种指导思想,它其实包含了两个层次的意思。第一层意思是消除重复(Don’t repeat yourself),这是一个显而易见的代码重构标准。第二层意思则是要求我们化繁入简(Heavy to Light),用尽量少的代码语言来表达尽量多的逻辑意义。
简单是美,将最佳实践的要求细化到了方法论的层面。然而,无论我们的程序如何简单,都应该始终记得,简单必须可读,简单必须可扩展。切忌为了一些细节,而忘记了更大的原则。
downpour 写道
最佳实践 尽可能使用面向对象的观点进行编程。
我们可以看到,这个层面的最佳实践,已经从基本准则和指导思想转向了具体的编程层面。虽然面向对象自身也只是一种编程的指导思想,然而它却是和程序设计和实现息息相关并且对程序编写影响最大的一条编程准则。
面向对象这个概念本身就是一个非常耐人寻味的问题。谈到面向对象的概念、设计和方法论,恐怕一天一夜都讲不完。在本章之初,我们从“对象”这个概念入手,通过对“对象”内部结构的分析,试图向读者展示面向对象编程中的一些重要理论。读者对这些理论不应停留在死记硬背的层面,而是要将他们融入到框架的设计理念中去理解。同时,这些理论也将成为我们判别框架和设计方案优劣的重要标准。
downpour 写道
最佳实践 减少依赖(消除耦合)。
之前我们在分析框架的本质时已经提到,任何Java程序总是依赖于其运行环境(JVM层)和支持应用程序的JAR层。加入到CLASSPATH中JAR越多,就意味着程序对外部环境的依赖度越高,对外部环境的依赖度越高,就意味着程序本身越难以脱离特定的外部环境进行单元测试。因此,减少甚至消除依赖,也成为了许多框架所追求的目标。
Struts2在这一点上做得尤为成功。Struts2不但实现了Web框架与Web容器之间的解耦合,还在此基础之上实现了各个编程元素之间的有效沟通。在之后的章节中,我们会深入探究Struts2在这条最佳实践上所做出的努力。
推荐阅读
-
《Struts2技术内幕》 新书部分篇章连载(二)—— 面向对象浅谈
-
《Struts2技术内幕》 新书部分篇章连载(四)—— 核心分发器
-
《Struts2技术内幕》 新书部分篇章连载(三)—— 多视角透析Struts2
-
《Struts2技术内幕》 新书部分篇章连载(七)—— ThreadLocal模式
-
《Struts2技术内幕》 新书部分篇章连载(六)—— 框架的本质
-
《Struts2技术内幕》 新书部分篇章连载(五)—— 请求响应哲学
-
《Struts2技术内幕》 新书部分篇章连载(八)—— XWork容器概览
-
《Struts2技术内幕》 新书部分篇章连载(十)—— XWork概览
-
《Struts2技术内幕》 新书部分篇章连载(九)—— 强大的OGNL
-
《Struts2技术内幕》 新书部分篇章连载(一)—— 如何学习开源框架