欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《Struts2技术内幕》 新书部分篇章连载(二)—— 面向对象浅谈

程序员文章站 2022-05-18 09:33:51
...
第2章 固本清源 —— Web开发浅谈

如今当我们谈到Web开发时,程序员们总是热衷于讨论一些我们耳熟能详的Web开发框架,如Struts2、Spring、Hibernate等。有些程序员将这些框架奉为宝典,并且趋之若鹜地挖掘框架的方方面面、比较各种开发框架的优劣。似乎对于这些框架的熟悉与否,似乎已成为衡量一个程序员是否精通Java,精通J2EE开发的事实标准。甚至在广大程序员找工作的过程中,这些主流的开发框架中的知识细节也常常成为面试中必考的元素,答不上这些问题,无疑会为找工作蒙上一层阴影。

面对这些框架,大家曾经是否真的思考过,我们为什么要学习这些框架?这些框架到底从何而来?框架的本质到底是什么?使用框架,又能够为我们的开发带来什么样的好处呢?在深入分析Struts2及其源码之前,我们首先必须讨论清楚这些比框架更为核心的问题。因为只有了解了为什么,我们才能知道怎么做,才能知道如何才能做得更好

2.1 面向对象浅谈

在谈框架之前,我们不得不首先面对一个比框架更为重要的概念,那就是面向对象的概念。面向对象的概念是一个看起来和听起来简单,却蕴含着丰富内容的概念。众多的国内外学者为了讲清楚这个概念,采用了各种的不同的比喻、也给出了多种多样的代码示例,还为面向对象的概念建立起一套完整的理论体系。

不过至今为止,能够完全将面向对象的来龙去脉讲清楚、讲透彻的毕竟还是少数。随着编程语言从早期的“面向过程式”的C语言发展到后来的C++、Java、甚至近几年来非常热门的Ruby,它们都已经逐渐地将面向对象的基本概念作为编程语言的核心设计法则。因而“面向对象”的概念也逐渐成为了每个程序员都认同,并且在日常编程过程中都遵循的最高纲领。

限于篇幅,我们实在无法涉及面向对象概念的方方面面。不过我们可以将话题聚焦在构成面向对象概念最基本的元素之上,这个基本元素就是:对象。在接下来的章节中,我们将分析对象的构成模型以及对象的关系模型,并以此阐述在面向对象编程过程中的一些基本观点。

2.1.1对象构成模型

2.1.1.1对象的构成分析

对象作为面向对象编程最基本构成元素,它到底由哪些基本要素构成呢?我们可以站在语法的层面,对Java语言中类的定义进行一些构成上的分析,如图2-1所示:

《Struts2技术内幕》 新书部分篇章连载(二)—— 面向对象浅谈
            
    
    博客分类: JavaStruts2  

在图中,我们可以看到构成一个对象的基本要素主要有:

  • 签名(Signature) —— 对象的核心语义概括
  • 属性(Properties)—— 对象的内部特征和状态的描述
  • 方法(Methods) —— 对象的行为特征的描述

在进行面向对象的编程时,首先要做的就是对世间万物进行编程元素的抽象。这个过程说白了,就是通过使用编程语言所规定的语法,例如类(Class)或者接口(Interface)来表达事物的逻辑语义。在上图中我们所谈到的构成一个对象定义的基本要素,实际上不仅反映出我们对世间万物的抽象过程,也是人类使用高级编程语言来实现外部世界表述的基本方式。

从图中我们可以看到,签名(Signature)用以描述事物的核心语义,它的作用实际上是界定我们所描述的事物的范畴。而在对象的内部,作为对象内部构成的重要元素,属性(Properties)方法(Methods)刚好从两个不同的角度对事物的内在特性给予了诠释。其中,属性(Properties)所勾勒的是一个对象的构成特性和内部状态的特性;而方法(Methods)则表达了一个对象的动态行为特性。这就像我们人一样,人由头、躯干、四肢构成,它们可以看作是人这个对象的“属性”。与此同时,人具有“直立行走”的行为特性,我们可以定义一个“方法”来模拟这一行为。

以上的这些分析,我们还停留在语法这个层面,因为无论是属性(Properties)还是方法(Methods),它们都是Java语言的原生语法支持。将事物抽象成为对象,并且赋予这个对象属性和方法,是一个很自然的编程逻辑,这也符合面向对象编程语言的基本思路。不过我们也同时发现对象在实际编程过程中,将表现为三种不同的形态和运作模式:


属性-行为模式

这种模式是指一个对象同时拥有属性(Properties)定义和方法(Methods)定义。这是对象最为普遍的一种运行模式,绝大多数的对象都运作在这种模式之上。

属性模式

这种模式是指一个对象只拥有属性(Properties)定义,辅之以相应的setter和getter方法。Java规范为运行在这种模式下的对象取了一个统一的名称:JavaBean。JavaBean从其表现出来的特性看,可以作为一种数据的存储模式和数据的传输载体。

行为模式

这种模式是指构成一个对象的主体是一系列方法(Method)的定义,而并不含有具体的属性(Properties)定义或者说即使含有一些属性(Properties)定义,也是一些无状态的协作对象。运行在这种模式之下的对象,我们往往称之为“无状态对象”,其中最为常见例子是我们熟悉的Servlet对象。

我们发现,对象的运行模式的划分是根据对象的构成特点进行的。这三种对象的运行模式在我们日常编程中都已经见过并且亲自实践过。接下来的章节,我们将针对后两种构成模式做进一步的分析。

2.1.1.2属性对象模式

属性对象模式又称之为JavaBean模式。这种对象的运行模式在我们日常编程中见得非常多。因为作为数据存储和数据传输的载体,运行在JavaBean模式下的对象,在众多的编程层次都会被用到,并且根据作用被冠以各种各样不同的名称:

  • PO(Persistent Object) —— 持久化对象
  • BO(Business Object) —— 业务对象
  • VO(Value Object) —— 值对象
  • DTO(Data Transfer Object) —— 数据传输对象
  • FormBean —— 页面对象

对于这些纷繁复杂的缩写和对象类别,许多初学者会感到非常头疼。它们从形式上看是一系列难记的缩写,不过真正让程序员头疼的,不仅在于它们被用于不同的业务场景和编程层次,还在于它们在某些时候甚至只是同一个对象在不同层次上的不同名称。

不过我们大可不必在对象的名称和叫法上过分纠结。因为对于程序员来说,无论这些对象被冠以什么样花里胡哨的名称,它们只不过是对基本的、运行在JavaBean模式下对象的有效扩展或增强

以PO(Persistent Object)为例,当我们使用Hibernate作为O / R Mapping的工具时,一个典型的PO会被定义成以下的样子,如代码清单2-1所示:

@Entity
@Proxy(lazy = true)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
	
	@Id
	@GeneratedValue
	private Integer id;
	
         @Column
	private String name;

         @Column
	private String password;
	
         @Column
	private String email;
	
        // 这里省略了所有的setter和getter方法
}


假设我们去除那些Annotation,我们会发现这个PO和一个普通的JavaBean并无二异,至少我们无法在形式上将他们区分。因此,我们说Annotation在这里的所用是丰富了一个普通JavaBean的语义,从而使之成为了一个持久化对象(PO)。而当我们使用O / R Mapping的工具Hibernate进行处理时,也是根据这些Annotation才能够对这些PO进行识别并赋予其相应的功能的。因而,JavaBean自身的特性并没有发生改变,我们所引入的只是一些额外的编程元素从而对JavaBean进行了增强。

当一个对象运作在属性对象模式时,其本质是对象的JavaBean特性。我们可以从其表现形式和运行特征中得出这样的一个结论:

引用
结论 JavaBean对象的产生主要为了强调对象的内在特性和状态,同时构造一个数据存储和数据传输的载体。


因而,我们在本节开篇所提到的各种不同的对象名称的定义,它们之间的界定实际上是非常模糊的。同样一个对象,往往可以兼任多种不同的角色,在不同的编程层次表现为不同的对象实体,其最终目的是特定的场合表现出其作用。

在上面的结论中,我们还读到另外一层意思,那就是JavaBean对象的一个重要的性质在于它是一个数据存储和数据传输的载体。有关这一点,我们将在之后的章节中进行分析。

2.1.1.3行为对象模式

根据之前有关对象构成的分析,运行在行为对象模式之下的对象,我们往往称之为无状态对象。在上一节中,我们提到对象的内在特性和状态是由构成对象的属性(Properties)来表达的。所以,所谓的无状态对象实际上就是指对象的方法(Method)所表达的行为特性并不依赖于对象的内部属性(Properties)的状态。而这种无状态的特性,非常适合进行一个请求的响应,并在方法体的内部实现中进行复杂的业务逻辑处理。
我们在这里可以针对对象的行为方法(Methods)的语法做进一步的分析,如图2-2所示:

《Struts2技术内幕》 新书部分篇章连载(二)—— 面向对象浅谈
            
    
    博客分类: JavaStruts2  

从图中,我们看到对象方法(Method)的语法定义,同样体现出一定的规律性:

  • 方法签名(Signature) —— 行为动作的逻辑语义概括
  • 参数(Parameters) —— 行为动作的逻辑请求输入
  • 返回值(Return) —— 行为动作的处理响应结果输出

我们可以看到,方法的定义实际上是一种触发式的逻辑定义。当需要完成某种行为动作(业务逻辑)时,我们会将请求作为参数输入到方法的参数列表中,而返回值则自然而然地成为了业务逻辑的处理结果。由此,我们可以体会出一个结论:

引用
结论 对象中的方法定义是进行请求响应的天然载体。


这个结论我们将在之后对各种Web框架对Http请求的响应设计中再次提及。因为在Java标准中,对Http请求的响应是通过Servlet标准来实现的。而我们知道,Servlet对象就是一个非常典型的运行在行为对象模式之上的无状态对象。

上述的结论也将对读者理解“请求-响应”的过程在编程语言中的逻辑表达有很大的帮助。因而读者应仔细体会这些构成要素在“请求-响应”过程中所起到的作用,从而对Web框架的设计有更深的感悟。

2.1.2对象关系模型

对象的构成模型是站在对象内部结构的角度,对面向对象编程中的基本元素进行的分析。在本节中,我们分析的角度将由“内”转向“外”,考虑对象与对象之间的关系。
谈到对象之间的关系,我们很容易想到两个不同的层次:

  • 从属关系 —— 一个对象在逻辑语义上隶属于另外一个对象
  • 协作关系 —— 对象之间通过协作来共同表达一个逻辑语义

这两种关系在面向对象编程语言中,分别拥有不同的表现形式和逻辑意义。然而我们却可以发现,它们两者构成了绝大多数的对象关系模型。接下来,我们就来分别分析这两种对象关系模型。

2.1.2.1对象的从属关系

对象的“从属关系”,主要指一个对象在逻辑语义上隶属于另外一个对象。这个定义实际上依然非常抽象。要理解这一定义,我们就必须从“隶属”这个词本身入手。逻辑语义上的“隶属”,主要有两种不同的含义:

归属

归属的逻辑含义很直观。比如说,一个人总是归属于一个国家;一本书总是有作者。因而,当我们把人和国家、书和作者都映射成面向对象编程语言中所定义的一个个对象时,它们之间自然而然就形成了归属关系。这种归属关系是由于外部世界的逻辑关系映射到编程元素之上而带来的。

继承

继承的逻辑含义就有点晦涩。比如说,马、白马和千里马之间的关系。首先,白马和千里马都是马的一种,然而白马和千里马却各自拥有自己独有的特性:白马是白色的、千里马一日可行千里。此时,我们可以说白马和千里马都属于马,它们都继承了马的基本特征,却又各自扩展了自身独有的特质。

明确了“隶属”的两层含义,我们就需要把它们和面向对象编程语言联系在一起。归属和继承,它们在面向对象的编程语言中又以怎样的形式表现出来呢?

我们先来看看“归属”这层含义。“归属”关系的编程形式,我们可以得出这样的结论:

引用
结论 “归属”关系在面向对象编程语言中,主要以对象之间互相引用的形式存在。


我们以书和作者之间的对象定义作为例子来说明,其相关源码,如代码清单2-2所示:

public class Book {
	
	private String name;
	
	private List<Author> authors;

}


我们在这里所表达的是书和作者之间的“归属”关系。从代码中,可以很明显看到,一本书可能有多个作者,所以我们在书(Book)的属性定义中,加入了一个List的容器结构,List中的对象类型是作者(Author)。这样一来,书和作者之间就形成了一个引用关系

使用对象之间的引用来表达“归属”关系,是一种非常廉价的做法。因为这种关系的表达来源于对象定义的基本模式,不会对对象自身产生破坏性影响。

细心的读者还会发现,我们这里所说的“归属”关系,实际上还蕴含了一层“数量”的对应关系。在上面的例子中,我们发现书和作者的数量关系是“一对多”。除了“一对多”以外,对象之间的归属关系在数量这个维度上还可以是“一对一”和“多对多”。有趣的是,这三种归属关系正好也和我们关系型数据库模型中所定义的基本关系一一对应。这种关系模型也就为我们在Java世界中进行O / R Mapping打下了理论基础。

看完了“归属”关系,我们再来看看“继承”关系。有关“继承”关系,我们可以用下述结论来进行说明:

引用
结论 “继承”关系在面向对象编程语言中,主要以原生语法的形式获得支持。


什么是“以原生语法的形式获得支持”呢?我们来看看之前我们所说的那个白马的例子,其相关源码如代码清单2-3所示:

public class Horse {
	
	public void run() {
		
	}

}

public class WhiteHorse extends Horse {

}

public class ThousandMileHouse extends Horse {

}


白马和马之间,我们使用了Java中的关键字extends来表达前者继承自后者。这种方式与我们之前所看到的对象之间的引用模式完全不同,它使用了编程语言中的原生语法支持。

这种对象关系的表达方式非常简单而有效,不过当我们引入一个语法,就不得不遵循这个语法所规定的编程规范所带来的编程限制。这一点也是“继承”这种方式经常被诟病的原因之一。

在Java中,除了extends关键字以外,还有implements关键字来表达一个实现类与接口类之间的关系。实际上这是一种特殊的“继承”关系,它无论从语义上还是表现形式上,与extends都基本相同。

2.1.2.2对象的协作关系

对象的从属关系从现实世界逻辑语义的角度描述了对象与对象之间的关系。从之前的分析中,我们可以发现无论是“归属”关系还是“继承”关系,它们都围绕着对象构成要素中的属性(Properties)做的文章。那么读者不禁要问,围绕着对象的行为动作特征,对象之间是否能够建立起关系模型呢?

从哲学的观点来看,万事万物都存在着普遍而必然的联系。从对象的行为特性上分析,一个对象的行为特征总是能够与另外一个对象的行为特征形成依赖关系。而这种依赖关系,在极大程度上影响着对象的逻辑行为模式。例如,一个人“行走”这样一个动作,则需要手脚的共同配合才能完成,具体来说就是“摆手”和“抬脚”。而当我们把手和脚分别看作一个对象时,“摆”和“抬”就成为了手和脚的行为动作了。

这样一说,似乎对象之间的协作关系就显得非常容易理解:

引用
结论 当对象的行为动作需要其他对象的行为动作进行配合时,对象之间就形成了协作关系。


可以想象,一个对象在绝大多数情况下都不是孤立存在的,它总是需要通过与其他对象的协作来完成其自身的业务逻辑。这是软件大师Martin Fowler曾经提过的一个重要观点。然而这却为我们的编程带来了一些潜在的问题:如何来管理对象和协作对象之间的关系呢?有关这一问题,我们将在第五章中详细进行讲解。

对象的协作关系在对象运行在行为模式时显得尤为突出。因为当我们使用一个具体的方法(Method)来进行动作响应时,我们总是会借助一些辅助对象的操作来帮助我们共同完成动作的具体逻辑。也就是说,我们会将一个动作从业务上进行逻辑划分,将不同的业务分派到不同的对象之上去执行。这就成为了我们所熟知的分层开发模式的理论依据。

2.1.3面向对象编程的基本观点

在我们了解了对象的构成模型和对象的关系模型之后,读者不免要问,这些内容和我们的日常编程有关系吗?答案是有关系!而且不仅是有关系,还是有相当大的关系!在本节中,我们就以基结论辅之以分析的方法,为读者展示面向对象编程中的一些基本观点。

引用
结论 每一种对象的构成模型,都有其特定的应用范围。


根据之前我们有关对象的构成模型的分析,我们可以发现三种对象的构成模型在日常的编程过程中都曾经碰到过。因此,我们应该首先明确的观点是每一种对象的构成模型都有其存在的合理性,并没有任何一种模型是错误的模型这一说

既然如此,我们所要做的就是认清这些对象构成模式的特性,并且能够在最恰当的业务场景中选择最合适的模型加以应用。那么,从面向对象思想的角度,如果我们将这些对象运作模式做一个纵向的比较,它们互相之间有没有优劣之分呢?

引用
结论 将对象运作在“属性-行为”模式上,最符合面向对象编程思想的本意。


这一结论承接了上一个结论,可以说是对象建模方式的一种合理的理解和扩展,也回答了我们刚才的问题。当我们进行对象建模的时候,总是首先需要根据业务情况选择一个对象建模设计的角度,而这个角度往往取决于对象在整个程序中所起的作用。例如,当我们需要进行数据传输或者数据映射时,我们应该基于对象的“属性模式”来进行对象建模;当我们需要进行动作响应时,我们应该基于对象的“行为模式”来进行对象建模。
然而,运行在“属性模式”中的对象并不是说完全就不能具备行为动作。基于某一种模式进行建模,只是我们考虑对象设计的角度。如果我们站在一个“对象构成的完整性”这样一个高度来看待每一个对象,它们总是由属性(Properties)和方法(Methods)共同构成。因此,在任何一种对象的构成模式上走极端都是不符合面向对象编程思想的本意的。
软件大师Martin Fowler就曾经撰文指出,在对象建模时不应极端地将对象设计成单一的“属性模式”。读者可以参考:http://www.martinfowler.com/bliki/AnemicDomainModel.html获得全文描述。

有关这一点,也引起了许多国内软件开发人员的深入讨论,并且引申出许多极具特色的名词,诸如:“贫血模型”、“失血模型”、“充血模型”、“胀血模型”等等。这些讨论非常有价值,对于对象建模有兴趣的读者可以使用搜索引擎就相关的讨论进行搜索。
存在着那么多有关“领域模型”的理解方式,为什么Martin Fowler这样的软件大师还是推荐尽可能使对象运行在“属性-行为”模式之上呢?除了它自身在构成形式上比较完整,能够比其它两种运行方式更容易表达对象的逻辑语义之外,还有什么别的特殊考虑吗?笔者通过思考和分析,给出可能的两个理由:

当对象运作在“属性-行为”模式上时,我们能够最大程度地应用各种设计模式

对于设计模式有深入研究的读者,应该可以体会到这个基本观点。设计模式的存在基础是对象,因而设计模式自身的分类也围绕着对象展开。我们可以发现,绝大多数的设计模式需要通过类、接口、属性、方法这些语法元素的共同配合才能完成。因而,单一的属性模式和行为模式的对象,在设计模式的应用上很难施展拳脚。

当对象运作在“属性-行为”模式上时,我们能够最大程度地发挥对象之间的协作能力

如果我们仔细分析对象的关系模型,我们会发现无论是对象的从属关系还是对象的协作关系,它们在绝大多数情况下是通过对象之间的属性引用来完成的。这种属性引用的方式,只是在形式上解决了对象和对象之间进行关联的问题。而真正谈到对象之间的配合,则不可避免地需要通过行为动作的逻辑调用来完成,这也是对象协作的本质内容

对象建模是一个很深刻的哲学问题,它将直接影响我们的编程模式。所以对于建模这个问题,大家应该综合各家之言,并形成自己的观点。笔者在这里的观点是:对象建模方式首先是一个哲学问题,取决于设计者本身对于业务和技术的综合考虑。任何一种建模方式都不是绝对正确或者绝对错误的方式。我们所期待看到的对象建模的结果是提高程序的“可读性”、“可维护性”和“可扩展性”。一切建模方式都应该首先服务于这一基本的程序开发的最佳实践

引用
结论 建立对象之间的关系模型是面向对象编程中的核心内容。


对象建模是一个很复杂的逻辑抽象过程。事实上,对象建模最难的地方并不在于设计某一个单体对象的属性(Properties)构成或者方法(Methods)构成。因为之前我们也提到,对象总不能以单体的形式孤立存在。对象与对象之间总是以某种方式相互关联,相互配合。这种关联要么就形成对象之间的从属关系,要么就是通过对象的行为方法进行互相协作。

由此可见,我们在进行对象建模的时候,必须优先考虑的就是对象与对象之间的关系模型,从而决定我们进行对象关联的具体形式,选择合适的编程语言语法进行关联关系的表达。

将对象之间的协作和关联关系作为设计对象的最重要的考虑因素,可以时刻提醒我们不要将过多的逻辑放在一个对象之中。因为当我们考虑到对象之间的协作和关联关系,我们就可以充分挖掘每一个对象的职责和语义,从而避免一个对象过于复杂而变得不可维护。
  • 《Struts2技术内幕》 新书部分篇章连载(二)—— 面向对象浅谈
            
    
    博客分类: JavaStruts2  
  • 大小: 27.9 KB
  • 《Struts2技术内幕》 新书部分篇章连载(二)—— 面向对象浅谈
            
    
    博客分类: JavaStruts2  
  • 大小: 9.8 KB