内容简介

模式引入计算机科学领域已经有20余年了，最初人们侧重于面向对象的设计，而现在已经应用于计算机领域的各个方面，它们对计算机产生了深远影响。

若想一本书涵盖所有模式，那么这本书将会非常庞大，以前的设计模式书籍专注于介绍设计模式，虽然读者了解了这些设计模式，但是仍然不知道如何合理地使用它们，往往导致读者为了使用设计模式而设计，而不是从问题出发，使用设计模式优雅地解决这些问题。

本书主要从最基本的设计模式入手，并结合一些J2EE开发过程经常遇见的技术和概念，你将全面理解这10多个设计模式，并在开发过程中，让你真正体会和思考面向对象编程的思想，也只有掌握这些，你才会能成为一位真正的设计专家。

本书不是一本面向对象和Java语言的入门书籍，阅读对象主要是从事Java语言的软件开发人员但不限于Java语言的开发人员。

前言

OOP（Object-Oriented Programming）早已不是一个新概念了，OOP在最近的20多年里发展得异常迅猛，特别是近10年里，OOP相关技术层出不穷，当大家热衷于使用这些新技术时，却不会使用OOP进行软件设计，进而发现新的技术并没有为大家带来任何好处。

很多老的开发人员从过程式开发转向面向对象的开发过程中，由于他们习惯过程式思维的开发，尽管他们使用的是OOP语言，但这并没有给他们带来太多帮助，反而使他们更加厌倦OOP软件开发，认为OOP没有想象中的那么便捷，很多地方没有使用过程式开发来的便捷，于是他们又退化为过程式的开发。

随着越来越多的新开发人员也加入了OOP的潮流，他们追求新的技术，学会使用各种工具和框架，却无暇顾及OOP进行开发设计的核心思想。虽然使用了新技术，代码质量并未提高，反而事与愿违。

当他们沉浸在新技术的使用和业务逻辑的编码实现时，未料到这些看似巧妙的设计导致了代码不易阅读、不易维护、不易扩展、不易测试、不易调试……。大家忙忙碌碌，但是项目进度缓慢，最终往往以失败而告终。归根结底，尽管使用了OOP语言开发其他一些新技术，但对OOP只限于粗浅的了解和相关语言语法使用上的理解，并不会真正使用OOP进行开发设计，以致使用时颠三倒四，未能真正享受到OOP和这些新技术带来的好处，有些新技术非但没有提供帮助，反而成为某些软件失败的罪魁祸首。那么，如何使用OOP进行开发设计呢？

OOP开发新手由于没有这方面的设计经验，在遇到问题时，往往诉求于逻辑的实现，在维护性和扩展性没有考虑或者少有考虑，导致代码乱七八糟，七零八散，随着开发的深入，最终在用户各种各样的需求面前无以应对。而有经验的OOP开发人员会灵活使用各种模式做出优秀的设计，编写的代码健壮性高，易于阅读、维护和扩展，可伸缩性强，开发成本也十分低廉。如果重用他们的开发经验，那么你就不需要在相同问题上重蹈覆辙，也能设计出优秀的软件。

市面上介绍设计模式的书籍非常多，它们一般仅仅给出GoF的23个最基本的设计模式的定义和一些简单的示例，大多数读者充其量只能了解它们，在使用上大打折扣。

本书精心筛选出了一些我们经常在开发设计过程中使用到的模式，使用OOP的眼光分析它们，适时结合一些流行J2EE框架和技术，并从横向和纵向两方面扩展读者的思维，使读者对这些常用的模式有一个全面深刻的认识，也希望能够为正在使用这些框架和技术的读者带来帮助。

本书内容

作为一本技术书籍，严谨是必须的，作者参考了大量资料和文献，并在相关重要地方注明出处，做到有理有据，希望能够从专业角度和用户探讨设计模式与面向对象的设计。

严谨和可读性是没有冲突的，在开发人员之间交流，最好的方式莫过于优质代码了，本书给出了大量代码片段，在一些重要的地方使用黑体、加粗或斜体的字体，并做了详细解释，希望能够抛砖引玉，帮助读者制作出更加出色的代码。另外，本书还添加了很多图片，希望图文并茂，使这本书更加容易阅读。

本书主要分为五篇。

第一篇：模式介绍（第1章～第2章）

第1章讲述了面向对象与模式之间的关系和模式的简史；在第2章介绍了第一个简单的模式，模板方法模式，在这章，我们分析了代码重复所导致的“腐臭气味”，重复的代码是代码“臭味”中最糟糕的，在以后章节将会介绍各种模式来避免代码重复。

第二篇：创建对象（第3章～第6章）

使用OOP语言的语法创建一个对象并不复杂，例如在Java语言中使用new即可。但是随着系统变得越来越复杂，使用new直接创建对象会给系统造成很高的耦合度。创建模式可以封装对象实例化的过程，把使用对象的功能和实例化对象解耦开来，从而降低了耦合度。

在本篇最后，讨论了现在最流行的两个概念，IoC和DI，这二者是目前流行的轻量级容器的基础。

第三篇：构建复杂结构（第7章～第11章）

有时候，创建新的、更强功能的类并不需要重新编写代码，装配已有的类和对象反而更加快捷，也更加灵活。本篇讨论了一些常用的组装对象的模式，你将发现，构建大的、功能更强的对象，不是只有多层继承才能实现，组合往往是最有效的方式，本篇将会看到如何使用继承和组合创建复杂的大结构。

第四篇：行为模式（第12章～第14章）

我们在程序中经常需要封装一些对象的行为或者对象之间的通信，本篇将会讲述三个常用的行为模式：策略模式，状态模式和观察者模式，加上第2章介绍的模板方法模式，本书将一共讲述这四个常用的行为模式。

第五篇：终点还是起点（第15章～第17章）

其实，在很早之前，就有人对面向对象思想的局限性进行了研究，提出了一种新的编程方法AOP，第15章将会介绍AOP的概念及其实现技术。

尽管在前面章节，我们学习了一些常用的模式和一些OO设计的原则，并且了解了AOP方面的一些知识，但这些并不能表示我们已具备解决复杂领域问题的能力，在第16章我们讨论了在实际开发过程中如何使用面向对象进行开发设计以及应该注意的一些问题。

在本篇末尾，第17章，将回顾本书的内容，重新认识OOP设计规范。

最后两章虽然没有直接讲解模式，但却是本书极为重要的部分，开始带领大家思考如何进行软件设计。

附录

本书附录A介绍了一些作者非常喜欢且推荐的书籍，以及一些经常浏览的技术网站和论坛，希望读者也能够获益。

参考文献。

本书读者

本书不是一本面向对象和Java语言的入门书籍，阅读对象主要是从事Java语言的软件开发人员但不限于Java语言的开发人员。

希望读者了解这些Java基本技术：反射、内部类、序列化、线程、动态代理、垃圾回收、类加载器以及Java对象的引用类型等。

本书中会使用到UML的一些图示，主要包括静态类图和时序图等。

希望读者了解或者使用过这些框架和技术：JDBC、Hibernate、Jpa、Spring、Ejb、Pico Container、Guice、XWork、Webwork、Struts和EasyMock等。由于在实际的J2EE开发过程中，我们经常使用到这些框架，笔者将在讲述过程中适时结合这些框架与技术，用以消除因不理解这些技术而造成对模式理解上产生问题。如果读者还想做进一步的了解，可以参看我给大家推荐的相关书籍和网站。

对于其他语言学习者，由于文中有详细的代码解释，只要有OOP语言基础，相信能够看懂示例代码，可以重点关注于对问题的分析和OO眼光来看待这些设计模式。

对于Java初学者，对于提到的相关框架和技术，可以根据作者的介绍说明，重点专注于对问题的分析和OO眼光来看待这些设计模式，由于根据Java语法和技术的特点，扩展了一些模式，初学者可以根据作者介绍，更深入地体会这些语法和技术。

对于使用Java开发多年，却涉及J2EE方面的技术不深的读者来说，同样，重点专注于对问题的分析和OO眼光来看待这些设计模式，由于作者举了一些关于Java新语法和流行的技术，扩展了这些模式，可以作为参考学习，从多个角度扩展视野。

对于在J2EE开发多年的程序员来说，依然，你的重点还是要放在对问题的分析和OO眼光来看待这些设计模式之上，为了给读者广阔的视角，作者引用了大量流行的框架和技术，让读者能够理解这些框架和技术，有些示例可以直接拿过来解决这些常见的问题。

本书代码

要获取本书的示例代码，请登录http：//code.google.com/p/rambling-on-design-patterns/，点击进入Downloads标签页，选择最新的版本下载。也可以安装svn客户端下载最新文件，svn地址为：http：//rambling-on-design-patterns.googlecode.com/svn/trunk/。

为了能够让读者选择学习自己喜欢的章节并分别单独运行这些示例，作者尽量为每一种模式提供了相对独立和完整的代码。

注意：这些示例代码的运行环境是Java 2 Platform Standard Edition SDK V6.0及以上版本，代码使用了一些Java 5及以上的新语法和JDK的最新API，如果读者对它们还不熟悉，请查阅附录的相关推荐书籍。

反馈

尽管笔者尽最大努力去避免正文和代码中出现的任何错误，但是人无完人，书中难免有错误之处。如果读者在阅读过程中发现拼写错误，代码错误，以及内容有混淆之处，希望能够及时反馈，或许它们能够节省其他读者很多宝贵时间，也有利于完善本书，反馈邮箱是：ramblingondesignpatterns@gmail.com。

写后感

为了完成此书，笔者增删了5次，尽管艰辛，但在写书的过程中也发现了不少乐趣，也希望为读者在阅读过程中带来乐趣。

好友赠言

书如其人

Co0der具有典型的高级程序员的一切特质，在日常交流中，有时候让你很头痛，因为他常常会跟你较劲，你很难在一个论点上驳倒他。我细细阅读过他的书后，只有一个感觉，书如其人。这本书是Co0der对Java开发设计的总结，对目前仍旧在IT行业中作为底层“民工”而挣扎着的弟兄们有着极大的指导作用，能让这群人在技术上有一次不小的提高从而大有机会脱离“IT民工”的范围。总而言之，试着去阅读吧，效果一定比你想象的要好。

王林　惠普ITSM技术顾问

一个痴迷于代码的程序员

刘济华在我的印象里是个痴迷于代码的人，大部分空闲时间都用来读书和钻研，博览群书、涉猎甚广。经年的沉淀终于汇总成书，这本书的最大优点是——使用了程序员喜爱的编码方式（请在书中认真体会“喜爱的编码方式”）解释概念，贯穿了很多流行的技术，并进行广泛的类比，讲述严谨。

风云　CENO公司核心技术总监

总结的力量

这本书我读了一遍，感觉很不错。之前也看过一些设计模式书，经常觉得千篇一律，浮于表面，这本书完全是作者根据自己的心得体会写出来的——把复杂的事情变简单，通俗易懂，很有功力。读这本书的过程，能够体会到作者解决问题的思路，与自己的想法进行比较之后，使之前萦绕在心里的很多疑惑得到解决，而且书中的例子都可以独立执行，可以借鉴来在工作中直接使用。那些对设计模式不得要领的朋友可以看一下，是本值得一读的好书。

李楠　普华永道资深软件工程师

博采众家之长并加以提高

作者是我在惠普同一个研发项目组的同事，他是我认识的众多研发团队成员中很突出的一位，对技术的热爱和对完美的执着加上对知识的渴望就是我对他的印象。这本书给我的感觉，是一本非常实用的设计模式书籍，它采用“娓娓道来”的方式，在大量国内外专家思想的精髓基础上，融入了作者独到的理解，再根据中国人的学习习惯加以归类和总结。总的来说，我想推荐这本书给学习设计模式的专业编程人员。

不打不相识

唐宇　惠普资深软件工程师

了解济华是在跟他的一次激烈争论之后，记得是一个关于设计的问题，那次争论之后我才真正体会到他对设计方面的理解。当济华找我帮他的新书作推荐时，我感到非常高兴，同时也非常荣幸！《漫谈设计模式》绝对是一本看了之后让你对设计模式有一个全新理解的书。衷心希望这本书能为大家在设计模式上带来质的飞跃！

钟俊华（Peyton Zhong）易保资深软件工程师

阅书如阅人

记得第一次见刘济华是在新加坡出差，当时一个瘦瘦的、戴着眼镜的小伙儿在我面前，起初我担心他是否能够快速上手。慢慢接触中，发现他虽然年龄小（我常称他为小孩），但能力不小：技术功底非常扎实、逻辑思维相当严谨，对一些技术问题有着独特的理解。通过阅读这本书，发现济华不光技术了得，语言组织功力也相当老练，让我对济华有了更深一步的了解。希望大家能够通过这本书了解他，读懂他，同时希望大家可以通过此书学有所成！

藏红岩IBM大中华区创新中心技术顾问

是程序员，也是作家

我认识Co0der已经两年了，但是我从来没想到他的另外一面——一位作家。我认为他的书籍不仅适合专家级别的IT读者，也适合入门级的读者以及介于这二者之间的IT读者，它有助于你对架构和设计模式有个全面的认识。我想推荐这本书籍给所有读者，希望你们能够阅读、理解以及实践其中所提及的方法，并在你的项目中获得成功。我希望Co0der一路顺风，再接再厉。

Kiran Kumar Pabbathi Technology Consultant III in HP Bangalore

精妙的设计模式

济华是一个谦虚、沉稳、工作中追求完美并对技术有着执着追求的人。本书字里行间表现出严谨、一丝不苟的风格，结合作者自身工作中的经历和感悟，阐述了设计模式的概念和应用场景，希望读者朋友能从中体会到作者对设计模式选择和应用的精妙之处。

秦峰百度软件工程师

编程习惯与编程态度

和Co0der共事过两年，某一天他MSN上给我留言说他出了一本Design Pattern的书，我抱着好奇心下载下来，花了一天时间一口气读完，我才改变了设计模式图书千篇一律的印象。这本书除了阐述一些基本的OOP编程原则，我觉得最重要的是推荐了一种编程习惯和编程态度，配合精简而优雅的实例代码，我想必定会为那些陷入设计模式泥潭的初学者带来一些灵感。

陆健盛大在线基础研发部项目经理

选对正确的学习方向

刘济华做事喜欢把问题了解透彻，没有一点含糊的地方，正是因为这种严谨的做事态度，才有了本书的诞生。这是一本非常实用的书籍，希望这本书能给广大程序爱好者提供一个理解设计模式的正确渠道，使大家在编程中能够熟练运用设计模式思想，拓宽解决思路。

吕游金蝶软件高级顾问

第一篇　模式介绍

模式被引入软件开发和OOP语言的流行是分不开的，在20世纪80年代末至90年代初，面向对象软件设计逐渐成熟，被计算机界广泛理解和接受。然而由于专业开发人员和非专业开发人员作出的设计差异巨大，为了让OOP开发人员能够使用OOP设计进行专业开发，经过多年的不懈努力，最终由GoF四人根据当时的一些成熟经验和解决方案归纳出了23条最基本的设计模式，以供OOP开发人员学习和使用。

本篇首先从面向对象谈起，回顾一下面向对象的一些基础概念，讲述OOP开发设计带来的好处，以及为什么要学习和使用设计模式。接着在第2章将为读者讲述第一个最简单也最容易理解的模式——模板方法（Template Method）模式。

第1章　谈面向对象和模式

本章将简单总结一下面向对象领域的概念，读完本章并不能使你成为面向对象的专家，它将为你学习以下各章做准备。

主要包括内容如下：

面向对象的概念和范式。

模式的概念及与面向对象之间的关系。

学习设计模式常见的问题。

当你阅读完全书后，再回来阅读本章，你将会有更多体会，也一定能回答这些设计模式的常见问题了。

1.1　什么是对象

也许你已经使用面向对象做开发好几年了，面向对象的概念也似乎不难理解，但是很多人并未认识面向对象编程的本质，继续偏向于使用PP/FP（Procedural Programming/Functional Programming）方式编程。在讲述模式之前，我们先回顾一下什么是面向对象。

在OOP世界里，任何事物，不管是无形的还是有形的，都是对象。对象是包含一些行为和属性的一种组合体，它反映的是客观世界的任何事物。比如，马有腿、耳朵和嘴巴等属性，它们会跑，也会嘶叫，这些是它们的行为。每个对象都归属于某一特定的类型，比如说，一匹汗血宝马的类型是马。

面向对象的语言一般都有三个基本特征。

1.封装

封装是面向对象最重要的特征之一，封装就是指隐藏。

对象隐藏了数据（例如Java语言的private属性），避免了其他对象可以直接使用对象属性而造成程序之间的过度依赖，也可以阻止其他对象随意修改对象内部数据而引起对象状态的不一致。

对象隐藏了实现细节。

使用者只能使用公有的方法而不能使用那些受保护的或者私有的方法，你可以随意修改这些非公有的方法而不会影响使用者；

可以隐藏具体类型，使用者不必知道对象真正的类型就可以使用它们（依赖于接口和抽象带来的好处）；

使用者不需要知道与被使用者有关和使用者无关的那些对象，减少了耦合。

由于只能通过公用接口和方法使用它们，这样，客户程序就不能使用那些受保护的方法（例如Java语言里的private方法和protected方法），而你可以随意修改这些方法，并不会影响使用者，从而降低了耦合度。

2.继承

继承可以使不同类的对象具有相同的行为：为了使用其他类的方法，我们没有必要重新编写这些旧方法，只要这个类（子类）继承包含那些方法的类（父类）即可。

从下往上看，继承可以重用父类的功能；从上往下看，继承可以扩展父类的功能。

3.多态

多态可以使我们以相同的方式处理不同类型的对象：我们可以使用同一段代码处理不同类型的对象，只要它们继承/实现了相同的类型。这样，我们没有必要为每一种类型的对象撰写相同的逻辑，极大地提高了代码重用程度。

1.2　面向对象的好处

面向对象有很多优势，我们在这里总结了以下内容。

对象易于理解和抽象：例如马是一个类，一匹马是一个对象，跑是马的行为。正是由于这个特性，我们很容易把客观世界反映到计算机里，极大地方便了编程设计。

对象的粒度更大，模块化程度也更高：与方法（函数）和结构体相比，对象是一组方法和数据的单元，所以粒度更大，这样更方便控制和使用；而模块化程度越高，也越容易抽象。

更加容易重用代码：只要使用继承，就可以拥有父类的方法；只要创建这个对象，就可以使用它们的公有属性和方法；只要使用多态，就可以使用相同的逻辑处理不同类型的对象。

具有可扩充性和开放性：OOP天生就具有扩展性和开放性。

代码易于阅读：在阅读代码过程中，可以不去关注那些具体实现类，只要关注接口的约定即可，这样更容易侧重重点。

易于测试和调试：由于代码易于阅读，所以方便测试，并且由于模块化和抽象化程度高，所以容易发现问题出在哪个模块，也就易于跟踪和调试；最后，测试过程中，有些对象只有在软件交付运行时才能使用（例如一个发送彩信的服务对象），由于对象可依赖于抽象/接口，我们在运行测试时可以使用假对象（Mock对象）替换这些依赖的对象，使之不影响被测试的对象，这样便减少了测试的依赖程度。

代码容易维护：基于以上各种好处，不难想象代码会变得更加容易维护。

1.3　重用

回顾软件开发的历程，我们经历了从最初的二进制编程，然后到汇编语言的编程，最后到高级语言编程的过程，在这个过程中，我们不断提高着语言指令的模块化：汇编语言模块化了机器指令，一条汇编指令可能是多条机器指令的组合；高级语言进一步模块化了汇编语言（例如for循环等）。

使用高级语言编程的过程，其实就是重用这些大结构的模块指令的过程，效率得到大大提升。它们解决了语言到机器的映射问题，却没有针对我们要解决的问题域（Problem domain/Problem space）思考问题，于是对问题域进行建模开始发展。

我们使用结构体和方法来建立模型，这些结构体粒度小，抽象程度不高，可重用程度也不高。随后出现了面向对象的编程，对象是方法和结构体的集合，抽象程度更高，我们可以通过重用对象功能协作解决更复杂的问题。

随着计算机的发展，一个系统的代码也由原来的几百行增加至现在的动辄上百万行，如果从头到尾编写这些代码，那不仅是软件开发人员的噩梦，而是整个软件行业乃至所有使用软件的行业的噩梦。于是，重用从方法、结构体、类的重用，上升到软件的重用。

然而要使用OOP来设计可重用的软件并不容易，尽管面向对象的编程有如此之多的好处，但仍然有很多人倾向于使用PP/FP（Procedural Programming/Functional Programming）进行编程，他们列举很多使用OOP的反面例子，例如最常见的一个是关于使用switch语句的例子，他们认为使用switch语句非常直观和简洁，而如果使用OOP的状态模式（State Pattern）替换，便会产生了大量文件和代码。其实这个驳斥只是溢于表面，没看到状态模式带来的易阅读易扩展等好处。另外，他们也喜欢列举继承所带来的坏处来证明使用OOP进行开发还不如使用PP/FP，其实在OOP开发过程中，我们更愿意使用合成而非继承（Favor Composition over Inheritance），这些不足正是我们在OOP编程过程中应该避免的。

从种种问题来看，敌视OOP的原因归根结底是由于很多人理解了一些面向对象的基本概念和OOP相关语言的语法，却还没领会和掌握面向对象的开发设计方法。在开发设计过程中，往往求助于以前使用的过程式开发设计和简单的面向对象的特征（例如继承性），因而并未享受面向对象编程所带来的模块化的好处、粗粒度的好处、封装的好处、易于抽象的好处、代码重用的好处……。

如何使用面向对象的编程方法进行软件开发便成了问题的关键。其实，有经验的OOP开发人员并不会从头解决问题，他们往往会使用之前的成熟的解决方案来解决类似的问题。如果能够重用前人的经验和思想设计软件，那就好比站在前人的肩膀上解决问题——模式就能够让我们从思想上重用有经验的开发人员的解决方案来解决问题。

重用是指一切知识信息的重用。我们可以看到，对于软件编程，重用非常重要。经过过去几十年的发展，重用包括指令集的重用、方法的重用、代码的重用、服务的重用、软件的重用、设计的重用、思想的重用等。要实现重用并不那么简单，软件发展至今，开发人员一直在重用上摸索着前进，终于迈出了一步。

笔者从开始学习编码至今，一直致力于把“重用”应用于软件开发，本书将会和读者一起探讨重用和如何编写可重用的软件。

1.4　模式简史

现在，模式被广泛地运用到软件设计和其他各个领域中，然而，模式（Pattern）这一词却最先在建筑学里被建筑师Christopher Alexander引入。在20世纪70年代，Christopher Alexander等人创作了一系列书籍来描述建筑学上的模式，其中一本名为“A Pattern Language：Towns, Buildings, Construction”[1] 的书中讲述了建筑领域的253个模式，并为模式的作出了定义，指出这些模式并不会随着时间消逝而褪色。

到1987年，Kent Beck 和Ward Cunningham开始尝试把模式设计引入到编程世界，经过几年坚持不懈的努力和尝试，到了1994年，由Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides四人编写的书籍“Design Patterns：Elements of Reusable ObjectOriented Software”面世，这本书在后来设计模式学习和研究中影响非常广泛，是一本经典的设计模式参考书籍。在此书中，他们总结了多年来软件开发人员的实践经验和研究成果，收编了23个最常用的设计模式。

时至今日，这23个设计模式仍然是最基本，最经典的模式，而这四个人往往被称为“Gang of Four（四人帮，其实是一种开玩笑的戏称）”，简称为“GoF”。

由于20世纪80年代，面向对象技术蓬勃发展，GoF提出的这些模式都来自于面向对象开发设计的经验，即这些模式都是有关面向对象开发设计的。由于面向对象的强大生命力，随着越来越多的人加入面向对象的大军，面向对象的设计模式也得到极大地推广和发展，涌现出了很多出色的新模式。

[1] 参见Christopher Alexander等编著的“A Pattern Language：Towns, Buildings, Construction”一书，Oxford University Press。这本书籍是卷二，卷一是大家非常熟悉的书籍“The Timeless Way of Building”。

1.5　什么是模式

上面简要地介绍了模式的发展历史，但是，什么是模式呢？建筑师Christopher Alexander给出这样的定义。

Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.

每一个模式都描述了一个在我们周围不断发生的问题，以及该问题的解决方案的核心，这样，你就能够无数次地使用该解决方案而不用按照同样的方式重做一遍。

这个定义较长，一般情况下，模式被简单地定义为如下。

A pattern is a solution to a problem in a context.

模式是某一上下文环境中一个问题的解决方案。

是的，模式就是一个解决方案，一个模式解决了一类特定的问题，当我们再次遇到同样的问题时，我们仍然可以使用它解决同样的问题。

这个定义很容易让初学者认识什么是模式，但是很多人对此定义提出了异议，在“Head First Design Patterns”。[1]一书第13章中，作者就列举了一个例子驳斥了该概念。

While an absentminded person might lock his keys in the car often, breaking the car window doesn't qualify as a solution that can be applied over and over（or at least isn't likely to if we balance the goal with another constraint：cost）.

一个健忘的人或许经常会把钥匙锁在汽车里，打破汽车的窗玻璃不是一个能够经常使用的解决方案（至少如果我们权衡另外一个限制条件——花费时，也极不可能使用）。

是的，不是任何一个解决方案都能成为模式。在Christopher Alexander给出的定义里，就明确指出了，如果一个解决方案称得上是模式，那它要能够被使用无数次，经得起时间的考验。GoF为模式给出的定义如下所示。

The design patterns，……，are descriptions of communicating objects and classes that are customized to solve a general design problem in a particular context.

设计模式，描述了在一个特定上下文环境，如何定制这些互相通信的对象和类来解决一个常见设计问题。

可以看出，GoF给出的定义更加侧重于OOP编程设计。其实，我更愿意使用如下定义。

A pattern is a general solution to a problem in a context.

模式是某一上下文环境中一个问题的“常用”解决方案。

模式是一个常用的解决方案（general solution），而非仅仅是一个solution，显然，打碎车窗玻璃并不能被看作是一个可以经常使用的解决方案。这个定义可以帮助读者简单但不失深刻地理解模式含义。

为了方便这23个经典模式的交流和传播，GoF在书中为每个模式定义了4个基本要素。

模式名称（Pattern name）

问题（Problem）

解决方案（Solution）

效果（Consequence）

这4个基本要素全面地描述了在特定的上下文，相关的类和对象如何协作来解决这些常见的问题。而本书并不会从这些要素入手去重复GoF的工作，而是通过一些场景，讨论如何使用这些模式，并尽力纵横扩展我们的视野，希望以此启发读者，为OOP开发设计打下坚实的基础。读者在阅读本书期间，可以参阅GoF—书以作全面了解。

[1]参见Eric T Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra等人编写的“Head First Design Patterns”一书，出版社为O'Reilly Media，出版日期为October 2004。

1.6　学习设计模式的一些常见问题

1.模式有没有标准形式

GoF为了描述每一种设计模式，给它们定义了四个基本要素，旨在使能够得到广泛传播，这并不代表GoF给出的模式就是标准模式。事实上，他们也没有就每一种模式给出标准，而是很多地方给出了它们可能的变化，比如像组合（Composite）模式，有透明的组合模式，也有安全的组合模式，还例如观察者（Observer）模式，有push和pull两种形式。

这些设计模式只是有经验人士为解决常见问题给出的参考方案，避免我们重复的发明轮子，而没有血统之分（哪个标准，哪个不标准），所以这个问题是个伪命题。

2.设计模式和OO什么关系

设计模式最初被引入的时候，正处于面向对象设计高速发展时期，那些模式都是来自成功的面向对象系统的某些部分，所以这23个最基本的模式都是关于面向对象的，但这不表示只有面向对象才有模式。

我们知道，模式是某一上下文环境中一个问题的常用解决方案，那么，SOA（Service-Oriented Architecture）可以有模式，Agile也可以有模式，OSGI（Open Services Gateway initiative）也有模式。

3.能不能创造新的设计模式

完全可以，只要你能就一个上下文的问题给出一个通用的解决方案，并能使用GoF的四个基本要素描述它们。

要避免重复的发明轮子，你必须了解你创造的是否已经存在，或者是否是已存在模式的变种。

4.学习了设计模式，就等于学会了设计

学习设计模式只是借鉴OO专家的成功经验，要学会设计，还得向他们一样，学会使用OO的眼光看待问题、解决问题。这样，在解决问题的过程中，模式就会手到擒来，并且做到自然地变化它们以适应你的问题。

5.设计模式是OO设计的根本吗

解决问题才是根本，模式只是关于解决问题的经验总结。

笔者最初学习设计模式，以为优雅的设计就是尽可能的使用设计模式，所以在解决问题时刻意地套用它们，出现了一些拙劣的设计。后来读了Eric Evans的“Domain-Driven Design：Tackling Complexity in the Heart of Software”一书，才意识到为领域建模的重要性，为解决问题，要为你的领域问题建立合理的模型，既然OO能够为软件编程带来巨大的变化，我们就要学会使用OO的眼光分析问题，享受OO给编程带来的莫大好处。

于是紧接着，在后来的一个软件开发中，我开始从问题本质入手，“忘却”模式，为问题提取模型，等设计开发完成之后发现，我已经自然而然地使用了组合（Composite）模式和解释器（Interpreter）等模式。至此，我才意识到使用OO眼光分析问题的重要性，所以，这本书籍并不是单纯介绍模式的书籍，而是和大家一起探讨OOP，分析OOP给编程带来的好处，希望阅读完本书的朋友都能开始使用OO的眼光分析问题。

6.软件的核心是什么

软件的核心是模型，为复杂领域问题提取精炼的模型是根本。我们要学会使用OO这把利器，借助它的眼光来分析问题解决问题，这样才能做出客户满意的软件。

1.7　本章关键词

对象

封装

继承

多态

重用

模式

读书笔记

____________________________

读累了记得休息一会哦~

公众号：古德猫宁李

- 电子书搜索下载

- 书单分享

- 书友学习交流

网站：沉金书屋 https://www.chenjin5.com

- 电子书搜索下载

- 电子书打包资源分享

- 学习资源分享

第2章　第1个模式——模板方法（Template Method）模式

本章将介绍第一个模式，也是最容易理解和使用的模式之一——模板方法模式。本章将从一个简单的场景出发介绍该模式，这个场景在后面的模式中还要继续使用。

本章包括如下内容。

讲述DRY原则。

模板方法模式。

引入回调的模板方法模式。

2.1　从回家过年说起

春节是中国传统节日里最热闹的，对于在外漂泊多时的游子而言，最幸福的事莫过于回家过年。为了回家团圆，我们首先需要购买火车票，然后乘坐火车，最后才能和家人团聚。

这里编写一个简单的程序来模拟这个过程，最初的设计非常简单，编写了一个HappyPeople类，它有一个celebrateSpringFestival（）的方法，把买票、回家和家里庆祝的逻辑代码都写在这个方法里，代码大致如下所示。

后来，我们逐渐发现，有人需要坐火车回家，有人需要坐飞机回家，而有人坐大巴回家就可以了。但是不管你乘坐哪种交通工具回家，都得先买票，然后才能和家人团聚。于是又创建了一个新类PassengerByCoach来实现坐汽车回家过年的逻辑，由于买票和在家庆祝的代码一样，我们把类HappyPeople的代码快速复制了一份，把乘坐交通工具的那部分代码替换成了坐大巴的逻辑，结果如下。

斜体加粗的部分便是我们替换的那部分代码，接着，我们同样使用复制+粘贴（Copy＆Paste）方式编写了类PassengerByAir来实现表示坐飞机回家的那类人的需求。

复制+粘贴看起来非常实用，但是没过几周，我们慢慢发现情况不妙，这几个类的代码开始变得难以维护：

如果买票逻辑有所改变，我们需要分别修改这三个类，但有的时候，马虎的工程师不会在所有类上做相应的修改；

而且，由于这些类的功能发生了变化，相应类的测试代码也要做改变，这样修改这些类的测试代码和修改这些类一样，出现了相同的重复修改的问题；

最后，我们开始变得越来越担心：因为随着交通工具的增多，势必需要开发更多类和测试类，这样维护就变得越来越麻烦。

最终我们停下来开始思考：复制+粘贴真的很好用吗？该不该编写重复的代码呢？让我们首先从DRY（Don't Repeat Yourself）原则谈起吧。

2.1.1　DRY（Don't Repeat Yourself）

写在DRY之前

在这个纷繁的世界上，你能否找到一样东西，它会永久不变？

谚语：

The Only Thing In The World That Doesn't Change Is Change Itself.

世界上唯独不变的是变化本身。

不管是日月星辰，还是软件开发，变化是永恒的。在软件开发过程中，一切都在变化。

需求变化：这是软件开发中最主要的变化，我们经常听到的各种各样的抱怨，例如，之前拟定的需求突然又变化了；客户之前并不知道他们想要什么，现在还不能确定这些就是他们想要的；以前的需求根本是错误的；之前的需求并没有列出所有情形……，这都直接影响着软件开发设计。

技术变化：新技术不停地涌现，旧的技术被逐渐淘汰。

团队结构的变化：团队成员并不稳定，有来的，也有去的；此外团队组织结构也在发生变化，主要体现在管理者的变化。

政治因素的变化：公司出于各种利益的考量，采取一些行为干涉软件开发，这些行为往往是不可预料和没有回旋余地的。虽然这些行为经常被开发人员嗤之以鼻，但是这些变化真真实实地影响了软件的开发。

其他因素变化：自然灾害，交通事故等都可能影响软件开发。

总之，软件开发的最大的特征就是变化，变化总是在发生的，我们不能因为害怕变化而逃避它，否定它。谁能从容应对软件开发中的变化，谁就能最大程度上降低软件开发中的风险，成为这一方面的佼佼者。

开发还是维护

Andy Hunt和Dave Thomas 在“The Pragmatic Programmer”一书中认为，软件开发人员始终处于软件维护过程中。我个人非常认同这个观点，引用此书中的原话如下[1]。

Programmers are constantly in maintenance mode. Our understanding changes day by day.New requirements arrive as we're designing or coding.Perhaps the environment changes.Whatever the reason, maintenance is not a discrete activity, but a routine part of the entire development process.

程序员一直处于维护状态，我们的理解每天都在发生变化，当我们设计和编码时，新的需求总是接踵而至，或许是由于环境的原因吧。不管是什么原因，维护不是一个离散的行为，而是整个日常软件开发的一部分。

没错，不管是在应用发布之前还是之后，我们要么在修改程序错误（我们称为Bug），要么在为增强软件功能（我们称为Enhancement）而修改代码，这些都属于软件维护的范畴。总而言之，软件维护始终贯穿于软件开发的始末。

DRY（Don't Repeat Yourself）

DRY（Don't Repeat Yourself，不要复制自己）：也叫DIE（Duplication Is Evil，即复制是魔鬼），这个原则在Andy Hunt和Dave Thomas所著的“The Pragmatic Programmer”一书中阐述如下。[2]

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

每份知识在一个系统中必须存在唯一的、明确的、权威的表述。

OAOO（Once and Only Once，仅此一次）：OAOO指的是要避免代码重复，代码应该简洁，如果你发现代码“臭味”时，重复的代码是其中最严重的。

从以上定义我们不难发现，DRY原则所涉及的范围其实要比OAOO宽泛得多，DRY涉及的范围不仅包括代码，还包括其他任何信息，例如逻辑、常量、标准、功能、服务等，而OAOO指的是不能编写重复的代码。

我们在本书主要将着力讨论如何使用设计模式避免代码的重复。

[1]参见“The Pragmatic Programmer”一书第二章：A Pragmatic Approach。

[2]参见“The Pragmatic Programmer”一书第二章：A Pragmatic Approach。

2.1.2　变化+重复，如何维护

我们已经知道，在软件开发过程中，时刻发生着变化，特别是需求变化，如果像前述我们编写的重复代码一样，维护这些重复的代码会给我们带来噩梦。

在重复代码中应用相同修改：为了增强功能或者修改错误（Bug），对于某一处代码进行的改动，在其他重复地方也可能需要修改。

开发成本增加：新问题可能和旧问题非常相近，但是由于一些小细节的不同，导致开发出各种不能重用的代码。

不利于测试：如果相同的代码写在不同类里，导致测试代码的重复开发。不利于阅读和维护：随着不断地开发维护，相同功能的实现在不同地方会变得不尽相同，有时甚至出现相同的需求实现逻辑却是不相同的，由于实现的不同，导致这些代码的健壮性也差异很大，很难维护这些代码。

代码重复还会引起性能等诸多问题，比如产生了很多重复的对象。

DRY原则是软件开发里极其重要的一条原则，为了使我们的软件更加健壮，易于阅读和维护，我们应极力避免代码重复的“臭味”。

下面我们将讲述如何使用模板方法（Template Method）模式，避免上述丑陋的重复代码。

2.2　模板方法（Template Method）模式

2.2.1　使用继承

我们知道，为了重复使用代码，可以使用OOP一大特征——继承。既然PassengerByCoach类和HappyPeople类订票和庆祝团圆的逻辑是相同的，我们何不就抽象出一个父类，把这些相同的逻辑写在父类里？这样，我们为父类定义了subscribeTicket（）方法和celebrate（）方法，这些方法实现了不变的部分——订票和庆祝团圆。子类根据需要在travel（）方法里实现各自的回家方式。并且我们在父类里定义了celebrateSpringFestival（）方法供客户对象调用。下面先画出UML静态类图，如图2-1所示。

图2-1

我们重构了HappyPeople类为抽象父类，它包含一个抽象方法travel（）供子类实现自定义的回家方式，celebrateSpringFestival（）方法保证了subscribeTicket（）方法，travel（）方法和celebrate（）方法按照顺序执行。

代码实现

首先实现父类HappyPeople，代码大致如下所示。

现在，编写子类PassengerByAir，它继承于HappyPeople类，travel（）方法实现乘坐飞机回家的逻辑即可，代码如下所示。

这样客户对象使用PassengerByAir对象的celebrateSpringFestival（）方法就可以完成坐飞机回家过年的过程了。同样，PassengerByCoach子类和PassengerByTrain子类分别实现坐汽车回家和坐火车回家的逻辑，代码在这里就不再赘述，详细请参见示例代码。

我们编写如下代码以进行测试。

示例代码

执行结果

使用继承，子类中不需要实现那些重复的订票和庆祝团圆的代码了，避免了代码的重复；子类实现了不同方式的回家方法，把它栓入（hook）到父类中去，实现了完整的回家过年的逻辑。

2.2.2　模板方法模式

回顾上述代码，我们到底做了什么？父类中的方法celebrateSpringFestival（）是我们的一个模板方法（也叫框架方法），它把回家过年分为三步，其中方法travel（）是抽象部分，用于子类实现不同客户化逻辑，我们所使用的正是模板方法模式。GoF给出的模板方法模式的定义如下。

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

定义在一个操作中的一个算法框架，把一些步骤推迟到子类去实现。模板方法模式让子类不需要改变算法结构而重新定义特定的算法步骤。

也就是说模板方法定义了一系列算法步骤，子类可以去实现/覆盖其中某些步骤，但不能改变这些步骤的执行顺序，模板方法有如下功能。

能够解决代码冗余问题。在此例中，PassengerByAir、PassengerByTrain和PassengerByCoach等类没有必要去实现subscribeTicket（）和celebrate（）方法。

把某些算法步骤延迟到子类，子类可以根据不同情况改变/实现这些方法，而子类的新方法不会引起既有父类的功能变化，例如上例中，我们加入PassengerByAir类，它没有影响HappyPeople类。

易于扩展。我们通过创建新类，实现可定制化的方法就可以扩展功能。此例中，如果有人坐船回家，加入PassengerByBoat类实现父类的travel（）抽象方法即可。

父类提供了算法的框架，控制方法执行流程，而子类不能改变算法流程，子类方法的调用由父类模板方法决定。执行步骤的顺序有时候非常重要，我们在容器加载和初始化资源时，为避免子类执行错误的顺序，经常使用该模式限定子类方法的调用次序。比如此例中，你不能先回家再买票，也不能先庆祝再回家。

父类可以把那些重要的、不允许改变的方法屏蔽掉，不让子类去覆写（Override/Overwrite）它们，比如在Java语言中，我们声明这些方法为final或者private即可。

此例中，我们使用protected final关键字修饰celebrate（）和subscribeTicket（）方法，这样，子类不能覆写（Overwrite）它们；如果为了防止子类完全覆写celebrateSpringFestival（）方法，也可以修饰此方法为final的。

2.3　引入回调（Callback）

模板方法模式的应用很广泛，但过分地使用模板方法（Template Method）模式，往往会引起子类的泛滥。比如，我们有如下需求：查询数据库里的记录。

1.首先我们需要得到数据库连接Connection对象；

2.然后创建Statement实例并执行相关的查询语句；

3.最后处理查询出来的结果并在整个执行过程中处理异常。

研究这些步骤，不难发现，整个过程中第一步、第二步以及异常处理的逻辑对于每次查询来说，都是相同的，发生变化的部分主要是在对查询结果的处理上。根据分析，模板方法模式很适合处理这个问题，我们只需要抽象出这个处理查询结果的方法供不同的子类去延迟实现即可。

如果你正是这样做的，你就会慢慢发现，由于各种各样的查询太多，导致我们需要创建很多的子类来处理这些查询结果，引起了子类的泛滥。为了解决此问题，通常我们结合使用回调来处理这种问题。

回调表示一段可执行逻辑的引用（或者指针），我们把该引用（或者指针）传递到另外一段逻辑（或者方法）里供这段逻辑适时调用。

回调在不同语言有不同的实现，例如，在C语言里经常使用函数指针实现回调，在C#语言里使用代理（delegate）实现，而在Java语言里使用内部匿名类实现回调。这里还是以Java语言为例进行说明。

1.类图

为了实现上述数据库的查询，我们设计了SimpleJdbcQueryTemplate为模板方法类，ResultSetHandler为回调接口，UML静态类图如图2-2所示。

图2-2

客户对象使用SimpleJdbcQueryTemplate类的query（String queryString, ResultSetHandler rsHandler）方法时，需要把回调作为第二个参数传递给这个方法，query（……）方法会在做完查询后执行该回调的handle（ResultSet rs）方法处理查询，返回最后的处理结果。

2.代码实现

定义ResultSetHandler接口，如下所示。

注意这里我们使用了Java新语法——泛型（Generics[1]）。

于是，SimpleJdbcQueryTemplate类的代码片段如下所示.

1.SimpleJdbcQueryTemplate的query（……）方法首先会获得一个数据库的连接，即这句：connection=getConnection（）；

2.接着创建了一个PreparedStatement实例，即stmt=connection.prepare Statement（queryString）准备查询；

3.这句ResultSet rs=stmt.executeQuery（queryString）表示stmt对象去数据库执行该查询并返回结果；

4.最后调用回调rsHandler的handle（ResultSet rs）方法来处理处理查询结果并返回，即；return rsHandler.handle（rs）。

为了演示测试代码的执行，我们使用了EasyMock框架，EasyMock是一款非常流行的运行时生成Mock对象的软件，在单元测试中使用非常广泛。我们用它生成java.sql.Connection的Mock实例和java.sql.PreparedStatement实例去模拟真实的查询，这里仅给出测试的那部分代码片段，此例的详细代码请参见示例代码，在这里就不再赘述。

3.测试代码

我们使用了Java的匿名类来回调类处理查询结果，如上加粗斜体的部分所示。这样，即使有一千种不同的查询，也不需要增加一个专门的文件。

这里结合使用了模板方法模式和回调，避免了类的泛滥，此模式在Spring框架里使用十分广泛，有兴趣的读者可以参看其关于ORM（Object-Relational Mapping）框架和Jdbc框架的源码以做进一步深入研究。

[1] 泛型是指能够在运行时动态得到对象类型的一种能力。这样，我们就没有必要每次都写强制类型转换语句了。其实Java是在编译时为生成的二进制代码加入强制类型转换的语句，并非真正在运行时得到对象的类型。

2.4　总结

这一章我们在介绍模板方法模式之前介绍了DRY原则。重复的代码会带来维护的噩梦，DRY是一名优秀的软件开发人员必须恪守的原则之一。此原则看上去很简单，其实实现起来一点也不简单。在以下章节里，我们将继续碰到重复代码的“臭味”，我们会介绍更多的模式来防止代码重复。

在本章，为了解决回家过年的问题，我们使用了模板方法模式。模板方法模式非常简单，相信不少人在学习模式之前早就开始使用了。模板方法模式可以解决某些场景中的代码冗余问题，但也可能引入了类的泛滥问题，随后我们介绍了如何结合使用回调避免类的泛滥。使用回调可以避免类的泛滥，这并不是表示我们将使用带有回调的模板方法模式来替换所有的不带回调的模板方法模式，如果回调实现的接口较多，代码较为复杂时，把这些代码挤在一起会引起阅读问题。

在以下章节读者可以继续看到，如果需求变得更为复杂，我们就得需要更加灵活的设计，使用模板方法模式不能够成为新的复杂需求的解决方案，我们会在策略模式等相关章节继续以该问题为例展开深入讨论。

2.5　本章关键词

DRY

模板方法模式

回调

继承

泛型

第二篇　创建对象

创建一个对象并不难，但当我们不得不为每新添一种新的接口实现或者抽象类的具体子类而到处修改客户代码时，就不得不思考直接使用new创建对象所带来的高耦合问题。

我们知道，如果类之间耦合度高，那么一个类里的一小处变化都可能导致其他类都发生未知的变化，这样的程序本身就充满了“腐臭”的气味，是不易重用的、脆弱的。OOP带给我们的好处之一是封装，如果封装这些实例化的细节，即对客户对象隐藏实例化的过程，那么我们新添一个接口的实现或者抽象类的具体类，也不会影响客户代码了，这样就降低了耦合度。

创建类型的模式把使用对象和实例化对象进行了分离，本篇我们将讲述如何实例化对象，而不会引起客户对象和服务对象之间高耦合的问题，这在提高代码的可移植性、可重用性和方便单元测试等方面都具有重要意义。

第3章：单例（Singleton）模式

我们在一个系统里经常使用到单例对象，这一章将讲述如何保证一个类在系统里只有一个对象被实例化。

第4章：工厂方法（Factory Method）模式

讲述如何实例化对象使得对象和使用者之间的耦合度降低。

第5章：原型（Prototype）模式

这一章将讲述如何通过复制现有的对象快速地创建一个新的复杂对象。

第6章：控制反转（IoC）

我们介绍了时下最流行的两个概念，控制反转（IoC）和注入依赖（DI）。很多人对控制反转和注入依赖在概念上混为一谈，这一章将为读者讲述它们二者之间的关系，并介绍目前关于二者的一些常用技术和框架。

第3章　单例（Singleton）模式

如果要保证系统里一个类最多只能存在一个实例时，我们就需要单例模式。这种情况在应用中经常碰到，例如缓存池、数据库连接池、线程池、一些应用服务实例等。在多线程环境中，为了保证实例的唯一性其实并不简单，本章将和读者一起探讨如何实现单例模式。

本章将介绍单例模式，内容包括：

最简单的单例模式。

如何创建并发访问效率高的单例。

单例模式序列化应该注意的问题。

3.1　最简单的单例

为了限制该类的对象被随意地创建，需要保证该类构造方法是私有的，这样外部类就无法创建该类型的对象了；另外，为了方便给客户对象提供对此单例对象的使用，我们为它提供一个全局访问点，代码如下所示。

代码注解

Singleton类只有一个构造方法，它是被private修饰的，客户对象无法创建该类实例。

我们为此单例实现的全局访问点是public static Singleton getInstance（）方法。

注意：instance变量是私有的，外界无法访问。

读者还可以定义instance变量是public的，这样把属性直接暴露给其他对象，就没必要实现public static Singleton getInstance（）方法，但是可读性没有方法来的直接，而且把该实例变量的名字直接暴露给客户程序，会增加代码的耦合度，如果改变此变量名称，会引起客户类的改变。

还有一点，如果该实例需要比较复杂的初始化过程时，把这个过程应该写在static{……}代码块中。

此实现是线程安全的，当多个线程同时去访问该类的getInstance（）方法时，不会初始化多个不同的对象，这是因为，JVM（Java Virtual Machine）在加载此类时，对于static属性的初始化只能由一个线程执行且仅一次[1]。

由于此单例提供了静态的公有方法，那么客户使用单例模式的代码也就非常简单了，如下所示。

Singleton singleton=Singleton. getInstance（）；

[1] Static属性和Static初始化块（Static Initializers）的初始化过程是串行的，这个由JLS（Java Language Specification）保证，参见James Gosling, Bill Joy, Guy Steele and Gilad Bracha编写的“The Java™ Language Specification Third Edition”一书的12.4节。

3.2　进阶

如果出于性能等方面的考虑，我们希望延迟实例化单例对象（Static属性在加载类时就会被初始化），只有在第一次使用该类的实例时才去实例化，我们应该怎么办呢？

3.2.1　延迟创建

这个其实并不难做到，我们把单例的实例化过程移至getInstance（）方法，而不在加载类时预先创建。当访问此方法时，首先判断该实例是不是已经被实例化过了，如果已被初始化，则直接返回这个对象的引用；否则，创建这个实例并初始化，最后返回这个对象引用。代码片段如下所示。

我们使用这句if（instatnce==null）判断是否实例化完成了。此方法不是线程安全的，接下来我们将会讨论。

3.2.2　线程安全

上节我们创建了可延迟初始化的单例，然而不幸的是，在高并发的环境中，getInstance（）方法返回了多个指向不同的该类实例，究竟是什么原因呢？我们针对此方法，给出两个线程并发访问getInstance（）方法时的一种情况，如下所示。

如果这两个线程按照上述步骤执行，不难发现，在时刻1和2，由于还没有创建单例对象，Thread1和Thread2都会进入创建单例实例的代码块分别创建实例。在时刻3，Thread1创建了一个实例对象，但是Thread2此时已无法知道，于是继续创建一个新的实例对象，导致这两个线程持有的实例并非为同一个。

更为糟糕的是，在没有自动内存回收机制的语言平台上运行这样的单例模式，例如使用C++编写此模式，因为我们认为创建了一个单例实例，忽略了其他线程所产生的对象，不会手动去回收它们，从而引起了内存泄露。

为了解决这个问题，我们给此方法添加synchronized关键字，代码如下。

这样，再多的线程访问都只会实例化一个单例对象。

3.2.3　Double-Check Locking

上述途径虽然实现了多线程的安全访问，但是在多线程高并发访问的情况下，给此方法加上synchronized关键字会使得性能大不如前。

我们仔细分析一下不难发现，使用synchronized关键字对整个getInstance（）方法进行同步是没有必要的：我们只要保证实例化这个对象的那段逻辑被一个线程执行就可以了，而返回引用的那段代码是没有必要同步的。按照这个想法，代码片段大致如下所示。

代码注解

在getInstance（）方法里，首先判断此实例是否已经被创建了，如果还没有创建，首先使用synchronized同步实例化代码块。在同步代码块里，还需要再次检查是否已经创建了此类的实例，这是因为：如果没有第二次检查，这时有两个线程Thread A和Thread B同时进入该方法，它们都检测到instatnce为null，不管哪一个线程先占据同步锁，并创建实例对象，都不会阻止另外一个线程继续进入实例化代码块重新创建实例对象，这样，同样会生成两个实例对象。所以，我们在同步的代码块里，要进行第二次判断，判断该对象是否已被创建。

正是由于使用了两次的检查，我们称之为double-checked locking模式

属性instatnce是被volatile修饰的，因为volatile具有synchronized的可见性特点，也就是说线程能够自动发现volatile变量的最新值。这样，如果instatnce实例化成功，其他线程便能立即发现。

注意：此程序只有在JAVA 5及以上版本才能正常运行，在以前版本不能保证其正常运行。这是由于Java平台的内存模式容许out-of-order writes引起的，假定有两个线程，Thread 1和Thread 2，它们执行以下步骤：

1.Thread 1发现instatnce没有被实例化，它获得锁，并去实例化此对象，JVM容许在没有完全实例化完成时，instance变量就指向此实例，因为这些步骤可以是out-of-order writes的，此时instance==null为false，之前的版本即使用volatile关键字修饰也无效。

2.在初始化完成之前，Thread 2进入此方法，发现instance已经不为null了，Thread 2便认为该实例初始化完成了，使用这个未完全初始化的实例对象，则很可能引起系统的崩溃。

3.2.4　Initialization on demand holder

要使用线程安全的延迟的单例初始化，我们还有一种方法，称为Initialization on demand holder模式，代码如下所示。

当JVM加载LazyLoadedSingleton类时，由于该类没有static属性，所以加载完成后便即可返回。只有第一次调用getInstance（）方法时，JVM才会加载LazyHolder类，由于它包含一个static属性singletonInstatnce，所以会首先初始化这个变量，根据前面的介绍，我们知道此过程并不会出现并发问题（JLS保证），这样即实现了一个既保证线程安全又支持延迟加载的单例模式。

3.2.5　Singleton的序列化

如果单例类实现了Serializable接口，这时要特别注意，因为在默认情况下，每次反序列化（Desierialization）总会创建一个新的实例对象，这样一个系统会出现多个对象供使用。我们应该怎么办呢？

熟悉Java序列化的读者可能知道，我们需要在readResolve（）方法里做文章，此方法在反序列化完成之前被执行，我们在此方法里替换掉反序列化出来的那个新的实例，让其指向内存中的那个单例对象即可，代码实现如下。

方法readResolve（）直接返回singleton单例，这样，我们在内存中始终保持了一个唯一的单例对象。

3.3　总结

通过这一章的学习，我相信大家对于基本的单例模式已经有了一个比较充分的认识。其实本章讨论的是在同一个JVM中，如何保证一个类只有一个单例。

如果在分布式环境中，我们可能需要考虑如何保证在整个应用（可能分布在不同JVM上）只有一个实例，但这也超出本书范畴，在这里将不再做深入研究，有兴趣的读者可以查阅相关资料深入研究。

3.4　本章关键词

单例模式

线程

Double-Check Locking

Initialization on demand holder

序列化

JVM

synchronized

volatile

out-of-order writes

第4章　工厂方法（Factory Method）模式

第3章讲述了如何创建单例对象，本章将讲述如何使用工厂方法模式创建普通对象。

模式是我们在OOP编程中反复看到的模式之一，本章主要内容包括：

介绍工厂方法模式。

如何为每个线程创建独立的拷贝。

静态工厂方法的使用。

4.1　工厂方法模式

工厂方法模式是我们常用的模式之一，我们经常在以下情景使用。

客户类不关心使用哪个具体类，只关心该接口所提供的功能。

创建过程比较复杂，例如需要初始化其他关联的资源类，读取配置文件等。

接口有很多具体实现或者抽象类有很多具体子类时，你可能需要为客户代码写一大串if-else逻辑来决定运行时使用哪个具体实现或者具体子类。

不希望给客户程序暴露过多此类的内部结构，隐藏这些细节可以降低耦合度。

优化性能，比如缓存大对象或者初始化比较耗时的对象。

GoF为工厂方法模式给出的定义如下。

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

为创建对象定义一个接口，让子类决定实例化哪个类。工厂方法让一个类的实例化延迟至子类。

工厂方法模式是对实例化过程进行封装而形成的，客户对象无须关心实例化这些类的细节，把它们交给工厂类，在GoF的定义中，为工厂类定义了一个工厂接口。该模式比较简单，我们从UML静态类图着手。

4.1.1　类图

我们有一个Product接口，其实现类为ConcreteProduct，工厂负责创建Product对象，这样客户对象就不用关心使用哪个实例以及如何初始化该实例，以后Product实现类如果发生了改变，也不会引起客户代码的改动，只要维护相应的工厂类即可。此模型中，Factory接口是工厂接口，其实现类是ConcreteFactory，如图4-1所示。

图4-1

代码实现

根据上述的UML图示，不难写出实现代码，如下所示。

代码注解

Factory接口定义了createProduct（）方法来返回Product类型的实例对象，ConcreteFactory类实现了该方法，每次调用都会实例化一个新的ConcreteProduct对象返回。

工厂方法模式使用起来也非常简单，代码如下所示。

如果具体实现类较多，我们可以定义一个参数化的工厂方法，根据不同的输入返回不同的实现子类，代码片段如下。

客户对象可以根据需要传入不同的参数获得不同类型的对象。

4.1.2　创建数据库连接对象

有时候实例化某些对象可能会非常消耗资源，比如创建数据库连接。我们需要考虑缓存这些实例对象，以方便下次能够继续重用它们，我们可以在工厂对象里创建一个缓存池缓存这些实例对象。在J2EE Web应用中，我们经常使用java.lang.ThreadLocal缓存数据连接对象，我们这里首先讲解一下这个类。

ThreadLocal主要用来管理同一个线程里持有一个实例的副本，即当前的线程绑定一个实例对象，这个对象对于其他线程是不可见的，当线程执行结束后被回收时，JVM便会自动回收这些绑定的对象。

这里要注意的是，ThreadLocal对象使用了弱引用（Weak Reference），这可以一定程度防止内存泄露。也许有人使用Java很多年了，还不了解弱引用，我们就JVM支持的引用类型做个简单介绍。

引用类型

从Java2开始，引用被分为4个级别，我们一般使用到强引用，只要没有引用指向该对象时，这个对象便会被垃圾回收器回收。还有其他3种特别的引用，分别是软引用（Soft Reference）、弱引用（Weak Reference）和幻影引用（Phantom Reference），它们的级别依次减弱，并都弱于强引用。

软引用：如果一个对象没有强引用只有软引用时，当JVM发现内存不够时，垃圾回收器便会回收这些对象。

弱引用：如果一个对象只具有弱引用，在每次回收垃圾时，该对象都会被回收。

幻影引用：如果一个对象仅持有幻影引用，那么它就和没有引用指向它一样，在任何时候该对象都可能被垃圾回收器回收。

幻影引用和软引用与弱引用的区别在于：幻影引用必须和引用队列（ReferenceQueue）一起使用。幻影引用可以用来跟踪对象被回收的活动，因为当垃圾回收器准备回收一个对象时，如果发现它还有幻影引用，就会在回收之前，把这个幻影引用加入到与之关联的引用队列（ReferenceQueue）中去。

这样，程序可以通过判断引用队列中是否已经加入了幻影引用，来了解被引用的对象是否将要被垃圾回收器回收，如果发现某个幻影引用已经被加入到引用队列，那么就可以在所引用的对象被回收之前采取必要的行动。

软引用和弱引用非常适合做缓存，关于它们更详细介绍，请参见java.lang.ref包。

ThreadLocal

java. lang.ThreadLocal是如何实现一个线程绑定一个对象副本的呢？

其实每个线程都包含一个ThreadLocal.ThreadLocalMap类型的变量threadLocals（延迟创建的，节省空间和时间），这个映射（Map）目的就是为每个线程存储对象的副本，由于一个线程可能使用到多个关联到不同的ThreadLocal对象的副本值，所以我们这里使用ThreadLocal.ThreadLocalMap来做映射，ThreadLocal对象便是它的键（Key）。于是，每次调用ThreadLocal的get（）方法，其实就是先获取当前线程（Thread.currentThread（）），然后从当前线程的threadLocals映射里，查找当前ThreadLocal关联的副本。

可能大部分人的想法和我当初的想法一样，都是以为在ThreadLocal里使用一个Map，这个Map的键为Thread，值为绑定的副本，如果这样做是有如下问题的。

当线程回收时，该线程绑定的变量不能被自动地回收：因为变量存储在ThreadLocal里，必须显式地去回收。但如果此变量存储在线程里，那么线程回收时，这个变量没有被其他引用指向的话，它便随着线程一起被销毁。

另外，不这样做还有一个好处：如果Map在ThreadLocal里，那你必须得考虑多线程同步访问这个Map，但是这样做确实没有什么必要，因为一个线程访问自己空间内的副本，不应该影响其他线程，所以如果把映射放在线程里，就再不需要做同步的处理（一个CPU总是处理一个线程），这样就极大地加快了ThreadLocal的访问速度。

我们知道，ThreadLocal.ThreadLocalMap映射使用的键是被WeakReference包装的ThreadLocal对象，如果ThreadLocal对象没有其他强引用和软引用指向时，线程也便不会继续持有ThreadLocal对象，根据JVM规范，它会被垃圾回收器在下次回收时被销毁，这一定程度避免了内存泄露。

但不表示不会出现内存泄露，关于ThreadLocal引起的内存泄露，主要集中在一些对象不能回收，导致其关联的ClassLoader不能被回收的问题上，如果ClassLoader不能被回收，意味着所有被此ClassLoader加载的二进制代码不能被回收，如果使用动态生成字节码的工具或者Java动态代理方法，这个问题将尤为严重。网上有很多文章都在讨论这个问题。

从Java 1.5开始，加入了remove（）方法，这样我们可以显式地调用此方法，释放内存，避免内存泄露，所以使用ThreadLocal要特别注意内存泄露的问题。

了解这么多之后，我们来看看在J2EE Web应用中如何使用。由于每接收到一个HTTP请求时，就会启用一个线程来处理这个请求，使用ThreadLocal类很容易保证在处理同一个请求的整个过程中，尽可能使用同一个数据库连接对象。代码片段如下。

代码注解

定义一个ThreadLocal对象来做数据连接的缓冲池，即这句，private final ThreadLocal＜Connection＞connections=new ThreadLocal＜Connection＞（）。如果当前线程没有绑定的Connection对象（第一次获取数据连接或者已被回收），则connections.get（）返回null，这时我们就创建一个新的java.sql.Connection对象，并把它绑定到当前线程，即这句，connection.set（conn），于是，以后当前请求总会得到刚创建的Connection对象。虽然我们这里为每个HTTP请求缓存Connection对象，但在实际应用中，我们并不是直接去创建一个数据连接对象，我们往往从另外的数据连接池获得缓存的Connection对象。试想如果在一个小型应用中，对10000次HTTP请求创建10000个数据连接，往往会引起不必要的数据库瓶颈问题。数据连接池提供多个线程可以重用的数据库连接对象，它们不会随着线程消亡而被关闭或回收，数据库连接池并非本书重点，在这里就不再深入探讨。

注意：Servlet规范里并未保证每个HTTP请求都会启用一个新的线程对象，比如Tomcat会为每个请求生成一个新的线程，但WebSphere以及OC4J的某些版本，如果是相同的客户端发送过来的请求，应用服务器有可能重用了前一请求的线程来继续处理当前请求，所以要在适当时候（比如请求处理结束的时候或者请求开始时）使用ThreadLocal的remove方法移除connection变量以防止出错。

4.2　静态工厂方法

工厂模式非常实用，但是为每一个类创建一个工厂方法方法类会引起工厂类的泛滥，此时，我们可以使用静态工厂方法来避免——在每个类里实现一个静态的工厂方法，就不需要额外的工厂类了。静态工厂方法在“Effective Java”[1]一书中有详细的介绍，我们也经常使用它们。例如，在Java 5版本里，为创建基本类型Integer、Long、Boolean对象都提供了静态工厂方法，以Integer类为例，它的静态工厂方法如下所示。

代码注解

java. lang.Integer.IntegerCache.high 默认值是127，可以通过设置VM的启动参数值来设定（大于127才有意义）。如果在[-128，IntegerCache.high]之间，则返回IntegerCache类缓存的Integer对象，否则创建一个新的Integer对象。

这里需要注意的是，由于Integer是不可变（immutable）对象，所以这些缓存对象不会引起计算上的错误。

静态工厂方法的优缺点

优点

可以为静态工厂选择合适的命名，提高程序的可读性。一段优秀的代码具有可读性，并不一定需要冗长的注释，有时候根据类名、方法名、变量名的良好命名更容易使读者读懂程序。

静态工厂和工厂模式一样，可以封装复杂的初始化过程，实现实例的缓存。

还可以根据不同的输入返回不同实现类/具体类对象。

缺点

一般为了强迫使用工厂方法，不直接使用构造方法来构造实例，我们会强迫类只含有私有构造方法，这样，会导致此类不能被子类化。

如果添加了一个新的该类的子类（该类有非私有的构造方法），此静态工厂方法可能需要重写，以加入该子类的实例化过程，导致扩展性不强。

静态方法没有面向对象的特征，比如继承、动态多态等，不可被覆写（Overwritten）。

[1] 参见该书的Item 1：Consider providing static factory methods instead of constructors。

4.3　总结

工厂模式让我们把对象实例化的过程进行了封装，客户对象再也不必为实例化而考虑更多，正是分离了实例化的逻辑，使得对象间的耦合性大大降低。

4.4　本章关键词

工厂方法模式

静态工厂方法

软引用（Soft Reference）

弱引用（Weak Reference）

幻影引用（Phantom Reference）

数据库

J2EE

Sevlet

HTTP

第5章　原型（Prototype）模式

很多Java程序员容易把原型模式和Object.clone（）方法混为一谈，本章将全面讲述原型模式，包括如下内容：

什么是原型模式，原型模式的优点和适用场景。

原型模式的深拷贝和浅拷贝，及它们的实现。

5.1　原型模式

谈到原型模式，学过Java的人可能会想到java.lang.Cloneable这个接口，以为Java的原型模式描述的就是java.lang.Cloneable接口的使用，这就大错特错了。其实，原型模式在我们日常生活中经常可以看到，比如你刚给你的客厅做了装修，你朋友正好也希望给他的客厅做装修，那么，他可能会把你家的装修方案拿过来改改就成，你的装修方案就是原型。

由于很多OOP语言都支持对象的克隆（拷贝）以方便复制对象，但这些方式并不那么完美，后述我们将会讨论。

当创建这些对象（一般情况是一些大对象）非常耗时，或者创建过程非常复杂时，非常有用，GoF给出的原型模式定义如下。

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

使用原型实例指定将要创建的对象类型，通过复制这个实例创建新的对象。

原型模式的静态类图非常简单，如图5-1所示。

图5-1

Client使用Prototype的clone（）方法得到这个对象的副本，其实复制原型对象不一定是指从内存中进行复制，我们的原型数据也可能保存在数据库里。

一般情况下，OOP语言都提供了内存中对象的复制能力，Java语言提供了对象的浅拷贝（Shallow copy），也就是说复制一个对象时，如果它的一个属性是引用，则复制这个引用，使之指向内存中同一个对象；但如果为此属性创建了一个新对象，让其引用指向它，即是深拷贝（Deep copy）。

5.2　寄个快递

下面是一个邮寄快递的场景。

“给我寄个快递。”顾客说。

“寄往什么地方？寄给……？”你问。

“和上次差不多一样，只是邮寄给另外一个地址，这里是邮寄地址……”顾客一边说一边把写有邮寄地址的纸条给你。

“好！”你愉快地答应。

因为你保存了用户以前的邮寄信息，只要复制这些数据，然后通过简单的修改就可以快速地创建新的快递数据了。

5.3　实现

我们在复制新的数据时，需要特别注意的是，我们不能把所有数据都复制过来，例如，当对象包含主键时，不能使用原型数据的主键，必须创建一个新的主键。

我们这里提供一个静态工厂方法来获得原型数据，然后复制这些数据，最后做相应的初始化。为了操作安全，我们不直接使用原型数据，而是使用clone（）方法从内存克隆这条原型数据再做后续操作。我们使用Java提供java.lang.Cloneable接口克隆数据，它实现对象的浅拷贝，关于java.lang.Cloneable接口的使用请看后续介绍。

5.3.1　UML静态类图

我们首先画出静态类图，如图5-2所示。

图5-2

Client使用PackageInfo提供的静态工厂方法clonePackage（String userName）创建一个新对象：首先根据userName加载一条用户以前的数据作为原型数据（可以是数据库，可以是其他任何保存数据的地方），然后在内存中克隆这条数据，最后初始化该数据并返回。

5.3.2　代码实现

代码注解

Java的java.lang.Object方法里就提供了克隆方法clone（），原则上似乎所有类都拥有此功能，但其实不然，关于它的使用有如下限制。

1.要实现克隆，必须实现java.lang.Cloneable接口，否则在运行时调用 clone（）方法，会抛出CloneNotSupportedException异常。

2.返回的是Object类型的对象，所以使用时可能需要强制类型转换。

3.该方法是protected的，如果想让外部对象使用它，必须在子类重写该方法，设定其访问范围是public的，参见PackageInfo的clone（）方法。

4.Object的clone（）方法的复制是采用逐字节的方式从内存复制数据，复制了属性的引用，而属性所指向的对象本身没有被复制，因此所复制的引用指向了相同的对象。由此可见，用这种方式复制对象是浅拷贝，不是深拷贝。静态工厂方法public static PackageInfo clonePackage（String userName）方法根据原型创建一份副本：首先拿出用户以前的一条数据，即这句PackageInfo prototype=loadPackageInfo（userName），然后调用它的clone（）方法完成内存复制，即prototype.clone（），最后初始化这条新数据，比如使id为空等。

现在来看看测试代码，如下所示。

我们通过这句：PackageInfo currentInfo=PackageInfo.clonePackage（"John"），复制了一份快递信息出来，通过设置currentInfo.setReceiverName（"Ryan"）和currentInfo.setReceiverAddress（"People Square, Shanghai"），便完成了第二个包裹的信息录入，测试结果如下。

在实际应用中，使用原型模式创建对象图（Object Graph）非常便捷。

对象图不是一个单个对象，而是一组聚合的对象，该组对象有一个根对象。

5.4　深拷贝（Deep Copy）

通过上述学习，我们知道Java提供了浅拷贝的方法，那么，如何实现一个深拷贝呢？一般情况下，我们有两种方式来实现。

1.复制对象时，递归地调用属性对象的克隆方法。读者可以根据具体的类，撰写出实现特定类型的深拷贝方法。

一般来说，我们很难实现一个一般性的方法来完成任何类型对象的深拷贝。有人根据反射得到属性的类型，然后依照它的类型构造对象，但前提是，这些属性的类型必须含有一个公有的默认构造方法，否则作为一个一般性的方法，很难确定传递给非默认构造方法的参数值；此外，如果属性类型是接口或者抽象类型，必须提供查找到相关的具体类方法，作为一个一般性的方法，这个也很难办到。

2.如果类实现了java.io.Serializable接口，把原型对象序列化，然后反序列化后得到的对象，其实就是一个新的深拷贝对象。

我们给出第二种方法的实现，代码片段大致如下所示。

代码注解

DeepCopyBean实现了java.io.Serializable接口，它含有一个原始类型（Primitive Type）的属性primitiveField和对象属性objectField。

此类的deepCopy（）方法首先序列化自己到流中，然后从流中反序列化，得到的对象便是一个新的深拷贝。

为了验证是不是实现了深拷贝，我们编写了如下测试代码。

代码注解

originalBean. setObjectField（new String（"123456"））和originalBean.set ObjectField（"123456"）两句代码是不一样的，前者创建了两个String对象，其中一个是在JVM的字符串池（String pool）里，另外一个在堆中，并且属性引用指向的对象在堆里。后者属性引用指向了JVM字符串池中的“123456”对象。

我们知道，如果是浅拷贝，即引用指向同一内存地址，则newBean.getObject Field（）==originalBean.getObjectField（）为true，如果是深拷贝，则创建了不同对象，引用指向的地址肯定不一样，即此值应为false。但是这两种方式，使用这句newBean.getObjectField（）.equals（originalBean.getObjectField（））进行比较，其结果必须为true，测试结果如下所示。

和我们预想的结果一样，原始类型使用==进行比较，结果相等，而引用类型使用==比较，结果显示未指向相同的地址，但是使用equals（）方法比较的结果为true，即证明我们实现了深拷贝。

使用这种方式进行深拷贝，一方面，它只能复制实现Serializable接口类型的对象，其属性也是可序列化的；另一方面，序列化和反序列化比较耗时。

选用此方式实现深拷贝时需要做这两方面的权衡。

5.5　总结

我们以前使用java.lang.Cloneable的一个很大原因是使用new创建对象的速度相对来说比较慢，如今，随着JVM性能的提升，new的速度已经很接近Object的clone（）方法的速度了，然而这并没有使原型模式的使用失去多少光泽——使用原型模式有以下优点。

创建大的聚合对象图时，没有必要为每个层次的子对象创建相应层次的工厂类。

方便实例化，只要复制对象，然后初始化对象，就可以得到你想要的对象，并不需要过多的编程。

5.6　本章关键词

原型模式

浅拷贝

深拷贝

序列化

对象图

读书笔记

____________________________

第6章　控制反转（IoC）

我们经常提及反转控制（IoC），但其实很少有人能够正确理解这个概念，本章将讲述如下内容。

大量使用工厂模式引起的问题。

什么是反转控制，反转控制在创建对象方面的应用和实现分类。

反转控制和依赖注射（DI）的关系。

根据最新流行的技术，总结和完善依赖注射（DI）的分类。

6.1　从创建对象谈起

我们之前讲解了使用工厂方法模式创建对象，比如现在有这样的场景：Client对象需要使用Service1的execute（）方法完成特定功能，而Service1的实现Service1Impl类又依赖于Service2的实现类Service2Impl, UML静态类图如图6-1所示。

图6-1

为了减少依赖，我们为Service1和Service2对象的实例化分别提供工厂方法类的实现，代码大致如下。

于是，接口Service1和Service2工厂类的实现代码如下所示。

这样，Client类的实现如下。

我们的测试代码非常简单，如下所示。

new Client（）. doSomething（）；

执行结果如下。

Service1 is doing something.

Service2 is doing something.

6.2　使用工厂方法模式的问题

大规模地使用工厂方法模式，会引起诸多问题。

工厂类泛滥：在实际应用中，经常出现几百个像这样的服务类，如果使用工厂方法模式的话，就可能出现几百个这样的工厂类。我们不能使用静态工厂模式的理由也很简单，因为你不会知道将来会产生多少子类扩展系统。

依赖关系复杂：往往这几百个Service对象之间存在复杂的依赖关系，工厂类的装配逻辑也随之变得十分复杂。我们为装配这些服务对象花费了很多精力，而不能很好地专注于业务功能的开发。

不易进行单元测试：我们需要创建一些Mock对象来进行单元测试，以本例来说明，我们要测试Client的doSomething（）方法，需要使用一个Service1的Mock对象，一般情况下，我们在测试时修改getService1Instance（）方法，测试完成后，再改回来，这样做非常麻烦，极易出错。

为了解决这些问题，我们先从控制反转（IoC, Inversion of Control）说起。

6.3　Inversion of Control（控制反转）

大家可能已经知道好莱坞原则（Hollywood Principle）：

Don't call us, we'll call you.

不要找我们，我们会找你。

好莱坞原则在软件开发领域中极受追捧：我们经常把控制逻辑写在其他地方（例如Framework）而非客户化的代码里，这样就可以更专注于客户化的逻辑，也就是说，外部逻辑负责调用客户化的逻辑。在软件开发领域，我们又给它取了一个名字叫控制反转（IoC）[1]。

控制反转概念的涉及面十分广泛，有人认为它是一个模式，但是我更倾向于认为它是一个原则（Principle）。

很多模式都实现了控制反转（例如模板方法模式），例如，我们第2章讲解的模板方法模式就是控制反转的一个很好应用——父类的模板方法控制子类的方法调用。还有，使用回调的方法都是控制反转的很好应用。

再如，在Java标准库里，我们常用到查找和排序的这两个方法：binarySearch（……）和sort（……）方法，它们在执行过程中会调用对象的compareTo（）方法（如果这些对象实现了java.lang.Comparable接口的话），或者调用我们所传递的回调接口java.util.Comparator对象的compare（）方法来比较大小，最终完成查找/排序操作，这些都是控制反转的例子。

此外，我们经常提到的框架（Framework），它最典型的特点其实就是控制反转：框架抽象了通用流程，我们可以通过框架提供的规范（比如子类化抽象类或者实现相关的接口，实现相关的交互协议和接口等）就可以把客户化的代码植入流程中，完成各种定制的需求。

框架和工具库（Library）的区别是：如果框架没有实现控制反转，那么框架就会退化为工具库。也就是说，使用工具库，你必须撰写调用它们的逻辑，每次调用它们都会完成相应的工作，并把控制权返回给调用者；而框架不需要客户程序撰写控制调用的逻辑，由框架专门负责。

以时下非常流行的MVC框架Struts为例，我们只需要实现相关的Action、Form类以及相关View，并配置好Action-Form-View的映射关系，这样每次客户端提交请求，该框架都会选择相应的Action去处理它，并根据返回的执行结果选择相应的视图。这些控制逻辑由Struts框架为我们完成了，不需要实现如何调用相应Action执行的逻辑，也不需要实现如何选择View显示的逻辑。

6.3.1　IoC和DI（Dependency Injection，依赖注入）

后来，随着轻量级容器/框架不断地涌现，控制反转的概念越来越被开发人员提及，很多轻量级容器/框架纷纷标榜使用到控制反转。其实通过上述介绍我们知道，任何容器/框架都实现了控制反转，它们所说的控制反转指的是对服务/对象/组件的实例化和查找实现了控制反转，这只是控制反转的一种而已。关于这方面的控制反转主要有两种形式。

Service Locator（服务定位器）

Dependency Injection（依赖注入，简写为DI）

下面我们就这两种形式展开讨论。

[1] 这个短语首先在Johnson and Foote撰写的论文“Designing Reusable Classes”使用到。

6.3.2　Service Locator（服务定位器）

服务定位器封装了服务/对象/组件查找，为它们提供一个全局的入口。例如在EJB中，为了查找所需要的组件/服务，我们使用javax.naming.InitialContext对象去查找。

为了直观说明，我们这里为上述例子实现了一个非常简单的服务定位器。首先我们构建一个ServiceLocator类，代码如下所示。

从上述实现可以看到，我们创建了一个HashMap＜String, Object＞对象来持有这些服务对象，Service1和Service2的对象其实是按照单例创建的。这样我们的Client类的实现如下所示。

使用之前，我们需要使用ServiceLocator.configure（）初始化，以下是我们的测试代码。

服务定位器封装了查找逻辑，隐藏了组件/服务/对象之间的依赖关系，客户对象只要依赖于它就能获取想要的组件/服务/对象。

工厂方法模式和服务定位器的区别是：服务定位器为整个应用组件/服务/对象的获取提供了单一的入口，而一个工厂只提供特定类型的实例化，如果一个工厂能提供所有组件/服务/对象的装配和实例化，那它就被进化为服务定位器。

使用服务定位器时，容器/框架侵入了代码，降低了代码移植性，单元测试也相对比较麻烦。以上述场景为例，为了测试Client的方法，我们需要替换掉Service1服务对象，代码如下所示。

我们在正式测试之前，用MockService1服务对象替换掉原来的Service1服务对象，即ServiceLocator.registerService（"service1"，mockService1）。在测试结束之后，我们要把原来的服务对象注册回去，否则很可能影响其他测试。于是，在最后，为了避免测试过程中发生异常中断，我们把重新注册回原来service1服务对象的逻辑放在finally代码块里。由于每个测试都要做类似的清理操作，给单元测试带来了极大的不便。

6.3.3　Dependency Injection

外部程序把服务对象通过某种方式注入到客户对象供其使用的方法称之为依赖注入。

根据注入方式的不同，在这里把依赖注入分为6类：

Setter注入

Constructor注入

Annotation注入

Interface注入

Parameter注入

其他形式的注入

目前实现依赖注入的框架有很多，下面我们就结合现在非常流行的一些轻量级容器/框架来作介绍。

1.Setter注入

Spring框架是时下被广泛使用的J2EE开源框架，它不只是一款轻量级的依赖注射容器，还包括持久化框架、事务管理框架、Web应用框架（MVC框架）、AOP等框架。它实现了Setter注入等多种注入方式，这里以Spring为例来介绍Setter方式的注入。Setter注入是指外部程序通过调用setting方法为客户对象注入所依赖的对象。

为了给Client注入所依赖的Service1服务对象，我们为该属性提供了一个setting方法，代码如下。

以同样方式为Service1Impl类的依赖对象service2提供一个setting方法，由于非常简单，代码就不再赘述。

接下来需要在Spring配置文件里配置它们之间的依赖关系，如下所示。

配置文件里，节点＜bean id="client"class="pattern.part2.chapter6.setter.Client"＞……＜/bean＞定义了一个名字为"client"的单例服务对象，其实现类是pattern.part2.chapter6.setter.Client，子节点＜property name="service1"ref="service1"/＞表示对象client的属性service1含有一个setting方法，并且依赖指向了名字为"service1"的对象。同样在"service1"服务对象的配置里，属性service2的依赖指向"service2"对象。

这样，我们就完成了它们之间的依赖关系，为了测试注入效果，我们编写的代码大致如下。

ApplicationContext ctx=new ClassPathXmlApplicationContext（……）加载配置文件，并根据XML配置文件定义好的依赖关系初始化所有实例对象，这样只要我们从Spring容器得到的对象就是通过setting方法装配好依赖的对象，为了演示client对象的doSomething（）方法调用，我们首先得到"client"对象，即Client client=（Client）ctx.getBean（"client"）。

看看执行效果如何，结果如下所示。

Service1 is doing something.

Service2 is doing something.

由于使用了setting方法注入依赖，在单元测试时，我们也非常方便地使用其注入Mock对象，于是Client单元测试代码如下。

在单元测试时，我们手动调用setting方法注入依赖对象，Spring框架是使用反射机制调用setting方法注入依赖的对象，它们之间没有什么本质区别。

因为我们的Mock对象并未替换容器里的对象，所以在单元测试结束之后不必要撰写清理逻辑。

2.Constructor注入

Constructor注入，顾名思义，就是通过带参数的构造方法注入依赖对象。Pico Container是一款非常流行的轻量级DI容器，我们以其为例进行说明。为了给Client对象注入Service1的对象，我们为其创建一个带有参数的构造方法，代码如下所示。

同样我们也为Service1Impl类实现一个带参数的构造函数，如下所示。

Pico Container可以根据构造函数参数的类型，在注册的服务类里查找相应的实现类，这样我们就可以省去一些简单的依赖配置了。

我这里写了一个配置方法来注册服务类，如下所示。

由于Pico Container没有提供像Spring框架通过配置文件定义这些组件类和依赖关系的功能，需要我们手工实现，在实际使用中，大家可以根据各自喜好选择编程配置，也可以写个程序从配置文件读取这些定义。

我们在使用这个容器之前，调用这个配置方法，测试代码如下。

由于Pico Container支持泛型，这里也不需要做强制类型转换就能得到Client对象，得到Client对象的这句代码是：

Client client=pico. getComponent（Client.class）

测试运行结果如下。

Service1 is doing something.

Service2 is doing something.

同样，在单元测试时，我们根据带参数的构造函数，手工注入Service1的Mock对象即可，在测试完成之后，我们也不需要做额外的清理工作，代码如下所示。

3.Annotation注入

在Java5以上版本做过开发的人都应该了解注解（Annotation）[1]。我们知道，Annotation可以注解各种类型，例如属性、类型、方法、本地变量、构造方法等。只要这些Annotation是被@Retention（RUNTIME）注解的，那么，此类型的Annotation注解信息就可以在运行时通过反射读取。自从Java引入Annotation, Java世界就变得更加热闹了。

如果我们把实例化信息和对象之间的依赖关系信息使用Annotation注解，那么，只要在运行时得到它们，就知道如何初始化服务对象了。Guice是Google旗下的一款非常出色的轻量级DI容器，由于使用了Annotation实现注入依赖，加之其优良的性能，得到广大编程爱好者的一致好评，这里我们就以其为例来作说明。

Guice使用@com.google.inject.ImplementedBy指定此类型的具体实现，例如接口Service1实现类是Service1Impl，那么注解的代码就如下所示。

同样，我们可以注解接口Service2，这里不再赘述。

Guice提供Annotation@com.google.inject.Inject表示在实例化该类型的对象时，需要为被注解目标注入依赖，此Annotation可以注解方法，构造函数和属性3种类型。

我们使用它分别注解Service1Impl类的setting方法和Client类的构造函数，代码如下所示。

由于依赖关系比较简单，不需要额外的配置。现在只要从Injector查找我们需要的对象，它就会根据此类型的定义读取注解，实例化我们想要的对象。测试使用Annotation注入的代码如下。

注意：同样由于Guice支持泛型（Generics），我们在使用Injector的getInstance（……）方法时不需要进行强制类型转化。

由于使用@com.google.inject.Inject注解了Client构造函数，我们照样可以使用该构造函数注入我们的Mock对象进行单元测试，代码如下所示。

另外，还需要提醒的是，如果对私有属性进行注解，虽然实现了更加精准的注入，但是会给单元测试造成一定的麻烦，因为私有属性在其他类里是不可见的，所以不能通过直接赋值的方式注入Mock对象（框架使用反射机制，在给私有属性赋值之前，调用field.setAccessible（true），除非你也是用反射初始化你的类），建议大家尽量避免使用。

4.Interface注入

客户程序通过实现容器/框架所规范的某些特殊接口，在为客户对象返回这些依赖的对象之前，容器回调这些接口的方法，注入所依赖的服务对象。

例如很久以前Apache的Avalon框架就使用这种方式注入。现在，Struts2也使用了这种方式注入一些依赖的对象，比如，如果你的Action实现了接口ServletRequestAware、SessionAware等，只要Action类实现这些接口，Struts框架就会回调这些接口的相应方法，注入HttpServletRequest、HttpSession对象。这里给出一个简单的示例，演示如何实现接口注入。

首先我们定义一个ServiceAware接口，如下所示。

Service接口及其实现类的代码如下。

只要服务类实现这个接口，容器就会注入Service对象，我们定义一个Client类实现该接口，代码如下。

我们下一步来创建这个简单的接口注入容器，代码如下所示。

我们使用根据Class类型查找服务对象的方式，第一次查找时采用反射机制，使用默认的构造函数生成对象。代码的核心部分是这句：

if（service instanceof ServiceAware）{……}.

通过接口装配依赖的对象：如果对象是ServiceAware的实例，则调用此接口的方法注入ServiceAware对象，即这句：（（ServiceAware）service）.injectService（new ServiceImpl（）），为每个ServiceAware实例注入ServiceImpl对象。

现在，我们看看如何使用，代码如下。

执行效果如下：

Service is doing something……

我们根据接口方法，也可以轻松地手工注入Service的Mock对象完成对Client类的单元测试，同样未影响容器里的对象，在测试结束时也不必做额外的清理操作，代码如下所示。

这种方式不够灵活，容器必须预先定义一些接口实现注入，适合实现少数特定类型的对象注入。

5.Parameter注入

这种方式下，外部程序可以通过函数参数，给客户程序注入所依赖的服务对象，非常简单。Client类的代码如下所示。

方法doSomething（Service service）采用外部传递Service对象方式，至于Service如何是实例化的，它一无所知。

如何使用呢？其实非常简单，代码如下。

外部程序实例化一个Service实例，传递给Client的doSomething（Service service）方法使用。

单元测试代码非常简单，和使用一样，在测试调用方法时传入的是Mock对象，测试结束时当然也不需要做额外的清理操作，代码如下所示。

注意：这种形式的注入比较特殊，Client类的doSomething（Service service）方法不光完成了依赖的装配，而且执行了Service回调的方法execute（），完成了其他逻辑。

如果是一个提供Parameter注入的框架程序，在没有特殊需求的情况下，在实例化的过程中，应该使用Parameter注入完成服务对象的装配逻辑，这里如此举例的目的只是为了简要说明这种方式。

其实，可以看出，Setter注入是一种特殊的参数注入，它规定只能使用setting方法注入依赖，同样，Constructor注入也是一种特殊的参数注入，只能对构造函数实现参数注入。

一些流行DI框架在使用参数注入实例化对象时，往往结合Annotation注入。Interface注入等方式一起使用，但是明显的一条，这些实现方法不应包含除依赖装配之外的其他逻辑。

6.其他形式的注入

很多框架还有其他特点的注入，这些注入形式往往和框架有关。例如，Pico Container和Guice使用Providers的注入方式，Spring框架的lookup方法注入方式，这些都是这些容器/框架所提供的一些高级用法，主要用来解决一些特殊问题，读者以后有机会可以慢慢实践，这里不再赘述。

[1] Java 5及以上版本支持的特性，Annotation能够修饰方法、类、参数、变量、构造方法、包和类型（类、接口、枚举等）的声明。可以在编译、加载、运行过程中读取这些有用信息，根据这些信息执行相应的操作。具体使用请参见附录A推荐的Java相关书籍。

6.4　总结

IoC使得客户专注于客户化的逻辑设计，把程序流程的控制交给外部代码，实现高内聚低耦合目标。

同样，我们把实例化的过程交给框架/容器来处理，这样就更专注于业务逻辑的开发，这也是轻量级容器迅速发展的一个重要原因。IoC实现实例化的方式主要有两种：一种是使用服务定位器，另外一种是使用依赖注入。依赖注入的Setter注入和Constructor注入是最常见的注入方式，代码往往不会受到容器/框架的入侵，可以在多种轻量级容器上移植，而其他方式或多或少都受到容器/框架代码的入侵。

关于IoC/DI这方面流行的轻量级框架有Spring框架、Guice、Picocontainer等，前面也使用它们编写了一些简单的实例代码进行说明，感兴趣的读者可以访问相关网站作更深入研究。

Martin Fowler有一篇精彩的文章讲述IoC和DI之间的关系，链接地址为：http：//martinfowler.com/articles/injection.html

6.5　本章关键词

控制反转（IoC）

工厂方法模式

单元测试

好莱坞原则（Hollywood Principle）

框架

Mock对象

Service Locator（服务定位器）

Dependency Injection（依赖注入，简写为DI）

Setter

Constuctor

Annotation

Interface

第三篇　构建复杂结构

我们知道，一个类不能完成一个系统的全部功能，为了完成某些复杂的功能，我们往往需要多个对象组成更大更复杂的结构来协作完成。使用面向对象来合成更大单元的方式，主要有合成、继承、还有像Ruby语言的Mixin 11[1]等，我们这里主要讨论合成和继承两种。

通常情况下，为了使用现有类的功能，避免代码重复，我们可以继承该类，从而具有父类的性质；或者使用合成，执行它的功能。到底那种方式更好呢？或者是否需要结合二者呢？本部分内容会给你提供建议。

第7章　装饰器（Decorator）模式

提到装饰器模式，使用过Java IO编程的人应该非常熟悉，Java IO包就是使用装饰器模式设计的。譬如，为了快速地从InputStream流读取数据，我们使用BufferedInputStream装饰该流，被它装饰过的流便增加了缓冲数据的功能。装饰器模式可以让我们在运行时动态地增强对象的功能，而不影响客户程序的使用。

本章将介绍一个记录操作日志的案例，它包括如下内容：

描述一个记录修改日志的真实案例。

开放—封闭原则（Open-Close Principle）。

灵活地使用装饰器模式解决这个案例。

装饰器模式的优缺点。

7.1　记录历史修改

几年前，我曾接到过一个任务，需要开发一个数据访问层（Data Access Layer），提供对持久化数据的访问。我们当时使用的是关系数据库（Relational Database），所以选用了Hibernate这款非常著名的ORM [2]（Object-Relational Mapping）工具来实现持久化。

刚开始，我编写了一个非常简单的接口GenericRepository用于完成CRUD操作[3]，代码如下所示。

幸亏有Hibernate工具，让我们的实现变得异常简单了：使用Hibernate Session对象的save（To）方法可以根据ORM映射关系把对象持久化到数据库，它的update（To）方法可以更新持久化的数据，其他方法就不再一一叙述，Hibernate官方文档非常全面，感兴趣的读者可以登录官方网站下载相关文档进行学习。

我们的代码大致如下所示。

我们的方法getCurrentSession（）用于得到Hibernate的Session对象，然后调用Session对象的相应方法就可以创建和更新数据了。

在4.1.2节，我们讲述了如何在Web应用中使用ThreadLocal类给当前线程绑定一个Connection对象，我们这里的getCurrentSession（）方法就是使用ThreadLocal给当前线程绑定一个Session对象，关于此类的使用，请参见ThreadLocal类的介绍。

随着开发的深入，几个星期之后，用户希望对某些业务操作进行回滚，为了实现回滚，我们要对修改前的数据记录日志，这样才能完成回滚。比如，他们的这些操作会影响用户账户状态，我们就为用户账户信息记录日志，把数据存储在user这张表中，把日志记录在另外一张日志表user_log中。

在刚开始的讨论中，很多人想到了直接修改GenericRepositoryHibernate这个类，这样，update（T o）方法的代码便会被修改为如下所示。

这种方式对现有类进行了修改，但对之前健壮的代码进行修改，很可能导致这些代码不再健壮。为了保证以前的代码能够正常执行，我们必须对该类重新做单元测试，更麻烦的是，由于此方法非常基础，为了保证不影响到其他功能，我们还要为所有受影响的其他类和方法以及它们的单元测试代码进行修改，这一工作量相当大。能否不修改这些健壮的代码又能扩展它的功能呢？我们首先看看一条设计原则——OCP原则（OpenClose Principle）。

[1] Mixin是一种复用代码的方式，可以把不同模块的代码通过include关键字引入。

[2]对象关系映射（ObjectRelational Mapping）：由于关系数据库和面向对象的数据之间存在不匹配现象，为了像使用面向对象数据库那样使用关系数据库，我们使用这方面技术为面向对象编程虚拟出对象数据库以供数据访问。

[3]是指四个最基础的持久化操作：Create、Read、Update 和Delete操作，缩写为CRUD。

7.2　Open-Closed Principle（开放—封闭原则，OCP）

Software Entities（Classes, Modules, Functions, etc.）Should Be Open For Extension, But Closed For Modification.

软件实体（类、模块、功能等）应当对扩展开放，但是对修改关闭。

就是说，使用OOP进行开发，我们只能修改已有的类的错误（Bugs），若是扩展它们的功能，我们不能修改它们，而是要通过添加新类（实体）来解决，这即开放—封闭原则。为了不破坏OCP原则，我们下面尝试了两种方案。

方案一

对于上述问题，为了避免增加修改已有GenericRepositoryHibernate类的代码，有人提议创建一个新类实现GenericRepository接口，代码片段大致如下。

问题：代码重复。

不难看出，上述代码斜体加粗的部分与GenericRepositoryHibernate类的update（Object o）方法的代码重复，虽然未违反OCP原则，却违反了DRY原则。

方案二

我们想到，为了重用代码，可以使用继承，可以让LogRepositoryHibernate继承于GenericRepositoryHibernate类，不错，我们看看如何实现，代码大致如下所示。

问题：扩展性不高，代码重复仍然未能避免。

上述解决方法一定程度地避免了代码重复，如果我们使用JPA来实现持久化，该怎么办呢？我们就必须得增加新持久化实现，设计可能如图7-1所示。

图7-1

这样明显会造成类的膨胀，我们每使用一种新持久化方法，就需要一个新的LogRepository实现类，有没有更好的方式呢？答案是有的，请看下节。

7.3　装饰器（Decorator）模式

我们知道，要解决代码重复问题，我们还可以使用合成。另外，为了保证作出的改变对客户对象透明，新添的类型应该保持同样接口。如果按照这个想法来设计，这会是一种什么样的结构呢？

7.3.1　类图

让我们先画出UML静态类如图7-2所示。

图7-2

在静态类图中，LogRepository对象聚合了一个GenericRepository的对象，并且LogRepository实现了GenericRepository接口，它备份完之前的数据状态，然后把请求转发给GenericRepository的对象去做更新操作。

7.3.2　实现

为了记录日志，我这里给出一个简单实现：在更新之前把数据的全部信息备份在另外一张日志表user_log中，那些没有修改的信息，也保存在日志表中。

我们给出创建user表和user_log表结构的DDL语句，如下所示。

这样，我们在更新操作之前在user表里查找到这条记录，然后把这条旧记录备份到日志表user_log，最后把新记录持久化到日志表即可。

User类的代码大致如下所示。

为了记录日志，我们为日志类增加了一个接口Loggable，如下所示。

那么，UserLog类如下所示。

上述使用Hibernate提供的Annotation配置ORM关系，由于这不是本章重点，大家只要知道有了上述Annotation注解，就可以把User对象及其日志对象持久化到数据库即可，感兴趣的读者可以登录Hibernate官方网站作更深一层次的了解。

现在，我们来编写LogRepository类代码，在update（Object o）方法中，首先执行saveLog（Object o）方法把这条记录前一个状态持久化到日志表中，之后我们调用被修饰对象genericRepository的update（Object o）方法更新改变，代码大致如下所示。

7.3.3　一点变化

备份之前记录的状态需要重新从数据库加载，性能会受到一定影响。为使其达到同样的效果，我们在每次做创建和更新记录操作时，把这条内存中的记录同时持久化到user表和user_log表里，虽然日志表里的那条最新的记录永远是多余的，占用了存储空间，但是能保证日志表里跟踪了记录的所有变化，提高了性能（以空间换性能）。

按照这个想法，我们实际没有按照上述给出的代码编写，而是撰写了如下代码。

这样，既扩展了GenericRepositoryHibernate和GenericRepositoryJpa对象的行为，又没有对它们的类进行任何修改，这就不会影响既有的健壮代码，符合OCP原则。

7.3.4　如何使用

现在，要使用记录日志的持久化功能，我们只要运行时装饰GenericRepository对象即可，代码如下。

对被修饰过的GenericRepository对象使用起来和以前一样，调用save（Object o）和update（Object o）方法就能完成相应的持久化操作。

7.3.5　测试

这里给出save操作的测试代码，其他部分请参见示例代码。

这里简单介绍一下测试步骤。

1.首先建立测试环境，即这句decoratorTest.setup（this）：如果使用Jpa，那么会创建GenericRepositoryJpa对象并使用LogRepository修饰，然后调用Jpa的API设置一个事务的保存点（savepoint）；使用Hibernate的过程一样。

2.创建一条user数据并提交，即genericrepository.save（user），接着我们提交事务并设置下一个保存点，即decoratorTest.commitAndRenewTx（）。

3.在完成测试验证之后，提交数据并撤销资源，即这句：decoratorTest.tear Down（）。

4.已经熟悉策略（Strategy）模式的读者阅读完代码之后不难发现，我们正是采用了策略模式撰写了此测试，DecoratorTestDrive类承担Context角色，DecoratorTest.HBTEST和DecoratorTest.JPATEST是其中的两个具体策略（Concrete Strategy）：在Hibernate的上下文中，使用Hibernate的API设置保存点，提交数据和释放连接资源等；在Jpa的上下文中，使用Jpa的API设置保存点，提交数据和释放连接资源等。

对策略模式有兴趣的读者可以参见本书第12章。

7.4　装饰器模式的优缺点

装饰器模式的优点

比静态继承更灵活：上述例子中，被LogRepository修饰过的任何GenericRepository对象都可以记录日志，相比之下，使用静态继承则需要创建两个子类分别实现Jpa和Hibernate的日志记录功能。

接口一致，不影响客户对象的使用：上例中，不管是save操作还是update操作，使用起来和没有添加功能之前的对象是一样的。

可以产生叠加效果：可以重复修饰同一个对象，也可以使用不同的装饰器修饰同一个对象，产生叠加的效果。

可以作为一种AOP的简单实现方式（参见AOP的章节），因为我们可以在这类对象的所有方法之前或者之后添加新的功能并控制该方法的调用，但是局限性也比较明显，我们只能动态修改实现该接口的类对象，不能修改其他类型的对象。

它的缺点

产生了很多看上去很类似的小对象，不宜学习。

对象功能与如何动态地修饰这些对象有关，从它们的类型不易判断，不宜排错和调试。

一个被修饰过的对象和之前的对象是不同对象，如果有必要，要特别注意覆写equals（Object object）和hashCode（）方法，最好不要依赖于对象的标识符。

7.5　总结

本章我们介绍了OCP原则。我们都想做可扩展的应用，因为很容易定制客户的需求，但在开发过程中，很多人只沉浸于逻辑的实现，以为实现这些逻辑的开发就万事大吉，结果为了扩展新功能破坏了以前的健壮代码，导致以前的健壮代码不能正常运行。

在持续的开发中，由于扩展性差，添加新功能变得越来越困难，到最后，大家累得筋疲力尽，而软件不是失败就是延期。如果一开始大家就考虑可扩展性，就不会出现这样的局面。

在软件开发中，逻辑的实现往往不是最主要的，最主要的可能是我们应该把这些逻辑怎么封装，封装在哪里。

7.6　本章关键词

装饰器模式

数据访问层（Data Access Layer）

关系数据库（Relational Database）

ORM（Object-Relational Mapping）

开放—封闭原则（OCP）

DDL

Annotation

策略模式

JPA

读书笔记

____________________________

第8章　代理（Proxy）模式

有时候，需要对实际对象的访问进行控制，我们把这层访问控制封装成一个新的代理对象来代替实际对象，交由客户对象直接访问，由此引入代理模式。

本章将讨论代理模式，主要包含如下内容：

通过介绍分布式对象的原理，讲解静态代理模式。

使用J2SE动态代理。

和装饰器模式的比较。

8.1　代理（Proxy）模式

代理（Proxy）模式经常在以下情景中使用。

代理一些开销很大的对象，这样便能迅速返回，进行其他操作，只有在真正需要时才去实例化。

安全控制、同步控制，缓存处理结果，缓存初始化开销很大的对象，统计对象的使用以及异常处理等方面。

分布式对象访问控制，使得客户对象像使用本地对象一样使用分布式对象。

8.1.1　类图

为了保证客户对象透明地使用代理对象，代理类和实际类具有一致的接口，UML静态类图如图8-1所示。

图8-1

8.1.2　访问分布式对象

熟悉EJB的读者可能非常了解分布式对象通信机制，EJB的Stub正是使用代理模式的好例子。

为了不增加学习该模式的难度，我们并不打算使用EJB来介绍代理模式，而是仿照其原理来创建自己的分布式对象进行说明。

我们首先看看分布式对象的实现原理，如图8-2所示。

图8-2

Client对象使用Stub代理对象进行远程访问，对Client对象来说，由于接口一致，操作远程对象和本地对象并没有什么不同。Stub对象转发这些操作的请求给远程对象，Skeleton接收请求返回Service对象执行结果，这即是一次远程调用执行的全过程。

我们这里有一个实现了Service接口的ServiceImpl分布式对象。Service接口包含一个hello（）方法。

实现类ServiceImpl的hello（）方法非常简单，仅返回“Server says hello！”字符串。代码如下所示。

我们引入了Service_Stub代理对象，实现Service接口。它将hello（）方法的调用请求发远程Skeleton对象，Skeleton将服务对象的方法执行结果写回给Service_Stub代理，这样Service_Stub对象为客户对象隐藏了远程访问。Stub的代码片段大致如下所示。

我们在该例中使用Java NIO实现远程通信，如果有人对NIO不够熟悉，可以参看下面的详细解说，有兴趣想深入学习的读者可以参考附录A的推荐书籍。

Service_Stub通讯过程中，我们用到两种SelectionKey：一种关联一个读操作，即key.isReadable（）==true，我们把读操作的处理写在if（key.isReadable（））{……}代码块里；另一种关联一个写操作，即key.isWritable（）==true，我们写在else{……}代码块里。

Stub通讯主要分为3个步骤。

1.建立连接：和Skeleton服务建立通信连接，并注册一个写操作，即socketChannel.register（sel, SelectionKey.OP_WRITE），这样，接下来Stub就可以向服务端发送请求了。

2.发送请求，即写操作：Stub向Skeleton发送“hello”字符串，然后注册一个读操作，这样Stub开始准备从通信通道socketChannel等待执行结果。

3.接收返回，即读操作：读取远程调用返回的结果，关闭此次的通信通道socketChannel，最后返回这次执行的结果。

Service_Skeleton类继承于Thread类，让我们来看看它如何实现的，代码片段大致如下所示。

Service_Skeleton循环等待接收客户端发送来的请求，直至处理完成一次Stub的服务请求，它的通讯主要分4步。

1.建立Server端：监听指定的服务端口，然后注册连接操作，即这句serverSocketChannel.register（sel, SelectionKey.OP_ACCEPT），等待Stub端的访问。

2.和客户端建立连接：监听到Stub发送的连接请求，建立连接，为此次通信通道注册读操作。

3.接收请求，即读操作：接收Stub发送过来的请求数据，如果接收到的字符串是“hello”，则注册一个写操作并准备向客户端发送执行结果。

4.发送结果，即写操作：执行服务对象的hello（）方法，把执行结果写回客户端，关闭此通信通道，最后设置finished=true以关闭服务端。

到此为止，我们已经完成了分布式调用的所有机制。为了等待Stub对象的调用，我们必须先启动Service_Skeleton服务，启动代码如下。

现在，让我们看看客户端如何使用，代码如下。

我们可以看到，客户端实例化了一个Service_Stub对象，调用它的hello（）方法，但是它并不知道这个Stub对象只是远程对象的一个网络代理，即客户端对远程对象的访问具有透明性，运行结果如下。

Server says hello！

实际应用中的EJB分布式调用比这个例子复杂，这里只是以其为例介绍代理模式。

8.2　J2SE动态代理

上节介绍了代理模式，我们创建了静态代理[1]Service_Stub类，其实Java还可以支持在运行时创建代理类，即所谓的动态代理。

8.2.1　类和接口

Java自从1.3版本开始，就引入了动态代理。在介绍如何使用之前，我们首先讲述一些相关的类和接口。

1.Proxy类

Proxy类提供了用于创建动态代理类和实例的静态方法，并且，它是你所创建的所有动态代理类的父类。我们看看这个类的两个主要方法。

返回代理类的java.lang.Class对象，需要提供类加载器和指定代理的接口数组作为参数，如下所示。

public static Class＜？＞getProxyClass（ClassLoader loader, Class＜？＞……interfaces）

返回代理对象，需要指定类加载器，指定需要代理的接口数组和InvocationHandler对象作为参数，如下所示。

public static Object newProxyInstance（ClassLoader loader, Class＜？＞[]interfaces, InvocationHandler h）

其实这个方法逻辑相当于下面这句。

2.InvocationHandler接口

InvocationHandler对象集中控制所有代理的方法，这个接口只有一个方法需要实现。

public Object invoke（Object proxy, Method method, Object[]args）throws Throwable

其中：

第一个参数proxy是代理类的实例；

第二个参数method是调用代理实例的Method方法实例；

第三个参数args是调用代理实例的接口方法时所传递的参数数组，如果调用的方法无参数，则传递值为null，如果是基本类型（Primitive）的参数，会被转化为相应基本包装器类的实例（例如int变量，会转化为一个Integer对象）。

生成的动态代理会首先把请求统一转发给InvocationHandler对象的invoke（）方法，如果想要直接调用目标对象target的相应方法，则代码如下所示。

[1]相对于动态代理而言，即在编译期就就产生了class文件，这样就决定了运行时的功能。

8.2.2　调用原理

动态代理类实现了一组运行时确定的接口，客户对象在调用动态代理对象的方法时，它把请求转发给InvocationHandler对象的invoke（Object proxy, Method method, Object[]args）方法，所有方法都被拦截在InvocationHandler对象，由invoke（……）方法统一处理。

假如调用的接口方法是a（），则序列图如图8-3所示。

图8-3

8.2.3　实现同步

我们这里有一个简单的接口Increasable，该接口含有一个increase（int delta）方法，IncreaseImpl实现了这个接口，按照传递参数delta，以其大小从0开始增长，增长max次之后，判断结果是否还是delta的整数倍，即这句count%delta！=0，如果不是，则抛出IllegalStateException（"Count state is illegal！"）的异常。Increasable接口和实现IncreaseImpl类的代码如下所示。

顺序执行同一IncreaseImpl对象的方法，是不会有异常抛出的，但是让多个线程，以不同的delta参数并发增长，则很可能会抛出异常。我们为此撰写的测试代码如下所示。

1.我们使用Executors.newFixedThreadPool（2）生成一个可以包含两个线程的线程池；

2.然后提交两个FutureTask对象给线程池，即这句：pool.submit（new Runn able（）{……}）；

3.接着等待这两个FutureTask对象执行完成；

4.FutureTask对象调用Increase对象的increase（int delta）方法。

由于两个线程使用同一个对象，如果increase（int delta）方法不是线程安全的，那么很容易抛出IllegalStateException异常。这样，我们在此测试方法里就会捕捉到ExecutionException异常，该异常的cause应该是increase（int delta）方法刚刚抛出来IllegalStateException异常；反之，如果increasable是线程安全的，则不会抛出ExecutionException异常。

非线程安全的测试方法如下。

IncreaseImpl对象是非线程安全的，那么最终必然会抛出异常消息为"Count state is illegal！"的异常。由于能否抛出异常与执行有很大关系，我们可以使用循环多运行几次，直至抛出异常。

现在，我们使用动态代理为IncreaseImpl对象创建一个运行时安全的代理对象，只要在调用increase（int delta）方法时为其加上这个对象作为同步锁，代码如下所示。

当调用目标对象的increase（int delta）方法时，我们添加了synchronized（delegate）{……}同步代码块。

为了验证我们的想法，我们编写了方法testSynchronizedIncrease（），把线程安全的代理对象传递给increase（final Increasable increasable）方法，循环max次。测试代码大致如下所示。

启动线程安全的和非线程安全的测试，代码如下。

其实不管max值为多少，线程安全的和非线程安全的测试执行结果都一样，如下所示。

8.2.4　总结

Java动态代理只能代理接口。感兴趣的读者可以借助如下窍门反编译动态生成代理类的二进制代码，借以了解动态生成的代理类。

小窍门：如果读者使用Sun公司JDK做开发，则可以使用sun.misc.ProxyGenerator.generateProxyClass（String s, Class aclass[]）方法生成代理类的二进制class代码，然后反编译这个class文件一窥究竟。

其实创建动态代理方式不只有JDK提供的这一种方式，很多动态字节码工具都可以生成动态代理。例如，使用CGLib就可以，它不仅可以动态代理接口，还可以动态代理具体的类。由于在运行时创建代理类非常灵活，动态代理在事务管理，AOP（Aspect-Oriented Programming，切面编程）实现等方面发挥了出色的作用。

8.3　和装饰器（Decorator）模式的比较

代理模式和装饰器模式非常相似，很多人甚至把这两者看做同一种模式，的确，这二者区别并不太明显，他们都提供了到目的对象方法的间接过渡，但它们的设计目的不同。

代理模式并不实现和目标对象类似的业务功能，而是一些与目标对象功能联系不太紧密的职责，也就是说，二者处理的是不同的问题域，比如权限控制、连接服务端、处理异常、缓存目标对象等这些与目标对象的功能不是同一类的问题域；而装饰器对象处理的问题与目标对象处理的问题都是同一问题域，目的是增加目标对象那方面的能力，例如Java IO的BufferedInputStream和修饰对象InputStream都是为实现流读取设计的，它使得InputStream可以缓冲读取数据。

装饰器动态地为目标对象添加功能或者减少功能，还可以递归地装饰目标对象，产生叠加的效果，而代理模式不做此用途。

8.4　总结

代理模式在安全性控制、线程同步控制、缓存执行结果、代理创建一些开销很大的对象（例如，Hibernate使用延迟加载创建一些代理对象）等方面应用广泛，由于没有破坏现有的类而扩展了它们的功能，是OCP原则的又一成功应用。

8.5　本章关键词

代理模式

静态代理

J2SE多态代理

分布式对象

Stub

Skeleton

装饰器模式

第9章　适配器（Adapter）模式

如果你去美国出差，你带的笔记本电脑插头是中国标准的，就没办法直接连接他们的电源插孔。一般，你不会为笔记本电脑再去买一个新的可以连接美国插座的电源，往往我们会买一个插座转换器，用这个转换器连接美国插座，而你的插头连接这个转换器就可以了。

现实世界中，我们有数不尽的适配器，例如交直流转换器、PS2-USB的鼠标/键盘接口转换线，还有在汽车上用于给手机、iPod等充电用的电源转换器。在编程世界，我们也经常碰到类似的情况：我们需要把一个接口转换成另外接口，以此使得客户对象能够继续使用这类对象而不需任何改变，我们把这个转换类就叫做适配器。

本章我们将讨论适配器在面向对象编程中的应用，主要包含如下内容。

适配器模式。

类适配器和双向适配器。

和代理模式的比较。

9.1　打桩

我们这里以打桩为场景来介绍适配器模式。我们需要建造一幢高楼，需要打方形桩，这里抽象出一个IPeg接口，SquarePeg是它的实现类，UML静态类图如图9-1所示。

图9-1

我们来编写它们的模拟代码，如下所示。

现在我们还需要打圆形桩，实现圆形桩不是一件容易的事情，所幸的是，我找到了一个RoundPeg类，它实现了IRoundPeg接口的insertIntoRoundHole（）的方法。UML静态类图如图9-2所示。

图9-2

模拟代码如下所示。

尽管RoundPeg类实现了insertIntoRoundHole（）的逻辑，但是客户对象不能像使用SquarePeg对象一样直接使用RoundPeg对象，因为它们的接口不一致，RoundPeg的接口不是IPeg。

现在轮到适配器模式上场了，我们为此撰写一个适配器类，使它实现IPeg接口，但是它除了把insertIntoHole（）请求转发给IRoundPeg的insertIntoRoundHole（）方法处理之外，自己不做任何额外的逻辑，不难画出UML静态类图，如图9-3所示。

图9-3

IPeg接口是我们的Target（目标）接口，IRoundPeg是Adaptee接口（被适配的接口），而RoundPegAdapter就是Adapter类（适配器类）。

适配器的代码非常简单，如下所示。

9.2　其他适配器模式

9.2.1　类适配器

上节我们实现的适配器称为对象适配器（Object Adapter），RoundPegAdapter适配器依赖于IRoundPeg对象，可以说，我们是使用了合成的方式重用了IRoundPeg的功能，除了使用合成外，我们还可以使用继承的方式实现适配器，UML静态类图如图9-4所示。

图9-4

RoundPegAdapter2类继承于RoundPeg类并实现IPeg接口，而insertIntoHole（）方法把请求转发给父类的insertIntoRoundHole（）方法，即：

我们把使用继承方式实现的适配器称之为类适配器（Class Adapter），这种情况在一些支持多继承的语言中（比如C++，Python等），应用的比较多，在不支持多继承的语言里应用的并不是很广泛。

9.2.2　双向适配器

有时候，我们希望能够实现一个相互转换的适配器，即双向适配器（Twoway Adapter），双向适配器要同时实现这两个接口，以满足不同的客户处理方式，于是UML静态类图如图9-5所示。

图9-5

不难写出如下代码。

9.3　测试

终于到我们测试的时刻了，测试代码如下所示。

测试结果如下所示。

9.4　和代理（Proxy）模式的比较

适配器模式和代理模式都是把请求转发给目标对象来处理，但是实现的接口不一样：代理模式的代理类和被代理类（目标类）都实现了相同的接口，而适配器实现新的满足客户类的接口（目标接口），实现兼容客户类的目的。

9.5　总结

适配器模式是用途很广的模式之一，它把一个不兼容的接口/类转化为我们需要的接口/类，我们用的最多的是对象适配器和双向适配器。

9.6　本章关键词

适配器模式

对象适配器

类适配器

双向适配器

代理模式

第10章　外观（Facade）模式

外观模式在现实世界中比比皆是，比如你想吃蛋糕，你没有必要去买面粉、鸡蛋、奶油等，然后和面加工烘烤，你只要拿着钱去蛋糕店订做一个就好了；另外，以开机为例，你没必要在打开电源之后，输入一些指令，使CPU开始运行，然后检测内存和硬盘、加载硬盘数据、检测鼠标键盘设备……，只要你按一下开关，操作系统就会为你做完这一切。蛋糕店和操作系统都为我们提供了外观，使我们没必要了解蛋糕是怎样制作的，电脑是按照哪些步骤启动的，我们直接使用它们就可以了。

在软件设计里，如果软件系统和一些子系统有非常繁复的交互，你可能希望减少和它们之间的交互以达到简化的目的，这就使用到了外观模式。外观模式是我们最常使用的设计模式之一，非常直观和简单，读者可能已经在使用了，只不过可能还没有意识到它就是外观模式。

本章我们将讨论外观模式，主要包含如下内容。

介绍外观模式。

引入最少知识原则（Least Knowledge Principle）。

实现一个老板请客的场景。

外观模式在EJB里的使用。

10.1　外观（Facade）模式

一个系统在使用外观模式之前，客户类和子系统的类之间的交互关系如图10-1所示。

图10-1

引入外观类之后，它们之间的交互关系就变为图10-2所示。

图10-2

10.2　Least Knowledge Principle（最少知识原则）

外观模式用到了一条非常有用的设计原则：最少知识原则（Least Knowledge Principle），也称为迪米特原则（Law of Demeter）。

Only talk to your immediate friends, don't talk to strangers.

只和你的朋友说话，不要和陌生人说话。

如果你的应用有很多类，并且它们有复杂的依赖关系，那么，你的应用将很难维护和扩展，因为对其中一处的改动，会影响很多使用者。迪米特原则让我们减少和其他类的依赖关系，这样，一处改动只影响很小的范围，降低了风险。

在外观模式里，我们添加了Facade类，高层次的类和Facade类直接打交道，不和低层次的类直接交互，这样，低层次类的变化不会影响高层次的类，从而降低了客户类和子系统类之间的耦合度。

10.3　懒惰的老板请客

这里给出一个老板请客吃饭的场景，老板发现大家最近干活很努力，准备犒劳一下大家，老板想了一下整个过程，大致需要做如下事情。

预定桌子：subscribe（）

点　　菜：waitForAnOrder（）

做　　菜：cookDish（）

老板讲话：address（）

上　　菜：serveDishes（）

结　　账：check（）

“原来请人吃饭也要这么麻烦”，老板嘀咕着，抬起头正好看见了助理，他就索性把请客的事情全部交给助理去办了。

这一个是虚拟的场景，或许你已经是老板，让助理处理这些事情也早已习以为常了，那么，这里提到的助理正是充当了Façade的角色。有了助理，老板请客吃饭就简单了许多。

助理准备晚餐：prepareDinner（）

老板发表讲话：address（）

助理结束晚餐：endDinner（）

助理的模拟代码片段大致如下所示。

在prepareDinner（）方法中，助理预定位置，点菜并吩咐厨师做饭；在endDinner（）方法中，助理负责让服务员上菜，最后结账。这样，模拟老板的代码如下所示。

老板除了自己发言之外，其他事情都交给助理去处理了，我们的测试程序如下所示。

测试的执行结果如下所示。

10.4　EJB里的外观模式

使用过EJB做开发的人应该知道会话外观（Session Facade）模式，比如，Client对象需要调用远程对象Session Bean1、Session Bean2和Entity Bean来完成特定功能，使用会话外观模式之前的序列图如图10-3所示。

图10-3

由于Session Bean1、Session Bean2和Entity Bean都是远程对象，Client对象要完成一个操作需要三次远程调用。这三次调用都要消耗网络带宽，如果引入我们刚才讲的外观模式，在远程服务端创建一个会话对象Session Facade Bean，就可以减少与远程对象的通信次数，节省了带宽。引入之后，序列图如图10-4所示。

图10-4

Session Facade Bean负责与Session Bean1、Session Bean2和Entity Bean交互，Client对象不直接和它们交互，这样，Client对象完成这个操作后，和Session Facade Bean只进行一次远程通讯就可以了，大大减少了网络通讯的代价。

除了提高网络通信效率外，和基础的外观模式一样，会话外观模式减少了客户对象和服务对象之间的依赖程度，把低层服务对象的接口和它们之间的复杂依赖和交互通过Session Facade bean隐藏起来，降低了客户端和服务端的耦合性。

由于会话外观模式和普通的外观模式没有太大区别，是其在EJB里的一个应用而已，这里就不再撰写代码演示如何使用了，有兴趣的读者可以自己实现。

10.5　总结

外观封装了子系统之间复杂的交互和依赖关系，为客户对象提供了单一简单的界面，降低了系统的复杂性。在讲解外观模式同时，介绍了最少知识原则，我们知道，类之间耦合度越低，越易扩展和实现重用。

10.6　本章关键词

外观模式

最少知识原则（Least Knowledge Principle）

EJB

会话外观（Session Facade）

Session Bean

Entity Bean

第11章　组合（Composite）模式

我们在开发过程中经常使用到树形结构，它分为叶子节点和和分支节点两种，客户对象在使用这两种对象时经常要对它们加以区别，这样增加了代码的复杂度，也非常容易出错。组合模式为这两种类型提供了统一的接口，可以让我们像操作叶子节点那样方便地操作分支节点。

本章我们将讨论组合模式，主要包含如下内容。

介绍组合模式。

介绍安全的组合模式和透明的组合模式。

使用组合模式应该注意的问题。

11.1　组合模式概述

关于组合模式，GoF给出的定义如下所示。

Compose objects into tree structures to represent partwhole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

将对象组合成树形结构来表现部分-整体的层次关系。组合使得客户一致地使用单个对象和组合对象。

要使客户程序对分支节点和叶子节点进行一样的操作，那么它们外形必须一样——得有相同的接口隐藏具体实现。下面我们先从UML静态类图入手。

11.1.1　类图

静态类图如11-1所示。

图11-1

叶子类Leaf和分支类Composite都实现了Component接口，这样就保证了访问的一致性，此外，还要注意以下两点。

Component接口的operation（）方法的实现逻辑一般是：如果这个节点是叶子结构，则执行自己的operation（）逻辑；如果是分支结构，除了执行自己的那部分逻辑外，还要递归地调用所有子节点的operation（）方法。

Composite除了operation（）操作外，还要提供管理子节点的操作，例如add、remove、getChild等操作。

11.1.2　使用组合（Composite）模式

银行一般有很多分行，这些分行分布在不同的地方，分行下面可能还有其他分支机构。为了显示银行的组织结构，我们使用组合模式，首先为抽象父类BranchComponent定义一个display（）方法。它还包括getName（）和getDiscription（）两个方法分别用以得到银行的名称和描述。

UML静态类图如图11-2所示。

图11-2

BranchComponent类即为我们的Component类（这里未使用接口），供分支类BranchLeaf和叶子类BranchComposite继承。

BranchComponent类的代码如下所示。

分支类BranchComposite的代码片段大致如下所示。

BranchComposite类的display（）方法除了显示自己的信息外，还要递归地显示所有子节点的信息，我们使用for-each循环[1]显示子节点信息。

它还包含管理子节点的方法：add（BranchComponent child）、remove（BranchComponent child）和getChild（int i）。

叶子类BranchLeaf的代码片段大致如下所示。

[1]Java 5及以上版本的新语法，参见附录A的推荐资源。

11.1.3　测试

这样，显示分支分行结构和显示叶子分行的操作一样，我们编写如下方法用以显示银行结构。

现在假定某银行的组织结构如图11-3所示。

图11-3

编写测试代码表示图11-3所示的结构，代码如下。

接下来显示中国总行、上海分行和浦东子行的结构，看看执行效果如何，如下所示。

11.2　透明的组合模式

在上述结构中，我们对于子节点的管理操作（add、remove和getChild等）放在了分支类（BranchComposite）里，叶子类（BranchLeaf）看不到这些操作的定义。我们还有一种方式，把管理子节点的操作放在Component，然后让叶子类的这些操作在运行时失效（可以不做任何处理，也可以抛出异常等），这样对客户对象来说，这种方式更加透明。

我们把前者称为安全的组合模式，因为叶子节点没有add、remove和getChild这样的操作，如果尝试调用这些方法，在编译时就会报错；而后者在运行过程中才可能被发现，但是这种方式具有良好的透明性，我们称之为透明的组合模式。

透明的组合模式对叶子节点调用add等操作的处理有两种方式。

运行时报错，例如抛出一个异常等。

忽略这些无意义的操作，不做任何逻辑处理，对于没有自动内存回收机制的语言来说，使用这种方式要特别注意垃圾回收。

我们现在就为上述场景重构透明的组合模式，首先我们需要把以前子类（BranchComposite）的add、remove和getChild操作上移至父类（BranchComponent）。为了让叶子节点的这些操作失效，我们有两种选择。

父类Component中声明这些方法为abstract，然后分别在Composite子类和leaf子类中实现这些操作，但是Leaf类在这些操作中只能实现失效的逻辑。

父类中就实现这些失效的操作，那么只在Composite子类中覆写这些操作。

我们这里选用第2种方式，使用BranchComponent类的add、remove和getChild操作会抛出异常，这样前述的BranchComposite类和BranchLeaf类的代码则不需要发生任何改变。UML静态类图如图11-4所示。

图11-4

BranchComponent类的代码大致如下。

测试代码和执行非常简单，这里就不再赘述，请参见之前的示例代码。

11.3　安全的组合模式VS透明的组合模式

安全的组合模式编译时就可以发现为叶子节点执行add等无意义的管理子节点的操作，但造成了Leaf和Composite接口不一致，损失了透明性。透明的组合模式只有在运行时才可能（如果不忽略这些无意义调用的话）暴露错误地使用叶子节点的操作，牺牲了安全性，但具有良好的透明性。

这二者各有优缺点，读者在实际编程中需要自己权衡来决定应该使用哪种方式。

11.4　还需要注意什么

1.执行顺序

如果子节点的执行是有序的，特别要注意对子节点管理和访问的实现；另外还要注意父节点操作和子节点操作的执行顺序，例如上述实现中，我们总是先显示父行信息，然后依次显示子行信息。

2.存储结构

存储子节点的结构可以是哈希（Hash）表、数组、链表、树等任何集合结构，读者在实际操作中，可以自行选择效率最高的结构。

3.缓存

Composite可以按需提供高速缓存等方式，以提升对子节点的遍历等相关操作的性能。但是注意，如果子节点发生了变化，父节点关于子节点的缓存也随之失效，在执行这些操作时，要提供一种方式告诉父节点缓存已经失效，一般我们可以为子节点定义指向一个父节点引用，这样便于使用父节点缓存失效的相关操作。

11.5　总结

我们经常遇到“整体—部分”的结构，这时，我们有透明的组合模式和安全的组合模式可供选择，这二者各有利弊，读者可酌情采用。

11.6　本章关键词

组合模式

树形结构

叶子节点

分支节点

安全的组合模式

透明的组合模式

哈希表（Hash表）

读书笔记

____________________________

第四篇　行为模式

本篇将介绍一些常用的行为模式，它们描述如何控制执行的流转以及对象之间如何交互。在行为模式中，我们把行为中变化的部分进行了封装。我们前面提到的模板方法模式就是行为模式的一种，它把算法进行了分解，子类封装了那些可变的步骤。

在本篇，我们将继续为大家讲解三个常用的行为模式，带领大家一起领悟OOP给我们带来的好处，特别是封装带来的好处。

第12章　策略（Strategy）模式

本章我们将讨论策略模式和如何封装变化，主要包含如下内容。

通过回家过年的场景介绍如何封装变化。

介绍策略模式。

引入优先使用合成而非继承（Favor Composition over Inheritance）。

12.1　既要坐飞机又要坐大巴

还记得我们第2章给出的回家过年的例子吗？我们使用模板方法模式解决了代码重复的问题，我们现在又有新的需求了，如果Jennifer先要坐飞机，然后坐大巴才能回到家，考虑使用模板方法模式解决这个问题，我们必须增加了一个新类PassengerByAirAndCoach，表示既要坐飞机又要坐大巴回家的这类人，那么，代码片段如下所示。

让我们看看加粗的部分，不难发现，该死的代码重复又摆在我们面前了，在类PassengerByAir和类PassengerByCoach你会重复发现这些加粗的代码。这或许只是麻烦的开始，试想如果我们还需要实现坐火车再坐大巴回家的这类人，坐大巴再坐火车回家的这类人，坐大巴再坐大巴的人……，别再想下去了，重复的噩梦已经开始了。

继承虽然解决了第2章提到的简单需求，避免了代码重复，但是对于现在的需求而言，它仍然带来了“代码重复”的臭味。

12.2　封装变化

我们先给出一条OOP关于封装变化的设计原则。

Identify the aspects of your application that vary and separate them from what stays the same.

找出你的应用程序变化的那些部分，并把它们和那些不变的部分分开。

封装这些变化，这样，你可以改变或者扩展那些可变的部分，而不去改变那些不变的部分，改变和扩展可变的部分就不会影响那些不变的部分。

由于回家的方式会发生变化，我们之前按照回家人的分类把它进行了封装，但是这样的分类太多了，其实我们发现更合理的方法是把回家的方式进行封装，而非按照人的分类封装。于是我们抽象出Travellable接口，把坐飞机、坐大巴等回家的方式封装在各自的实现类里，于是，UML静态类图如图12-1所示。

图12-1

ByAir类、ByCoach类和ByTrain类实现了Travellable接口。PassengerByAirAnd Coach对象含有两个Travellable对象：ByAir对象和ByCoach对象。

Travellable接口及其实现类的代码如下。

接下来，我们来看看PassengerByAirAndCoach类如何实现，代码如下。

PassengerByAirAndCoach类包含ByAir类的对象first和ByCoach类的对象second。于是，我们的测试代码如下。

最后，执行结果如下。

12.3　策略模式

上述封装了具体的回家方式的办法，实际上是使用了策略模式，GoF给出的定义是如下。

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

定义一组算法，对每一种进行封装，使它们可以相互交换。此模式可以使算法独立于使用它的客户程序而变化。

在本章的例子中Travellable接口是策略接口，我们的三个实现类，ByAir类、ByCoach类和ByTrain类都实现了各自不同的具体回家方式；PassengerByAirAndCoach类和PassengerByAir等类都包含了不同的回家算法，它们在此模式里被称为Context，维护指向具体策略类的对象引用。

细心的读者可能会回想起第7章所讲的那段测试代码。我们在测试代码中使用了策略模式，枚举类型（enum）DecoratorTest提供了抽象的算法接口，枚举值HBTEST和JPATEST是具体算法的实现，关于枚举类型的介绍，请参见13.5 使用enum类型一节。DecoratorTestDrive是相应的Context类，在运行时可以选择Hibernate环境和Jpa环境分别进行测试。

12.4　还需要继承吗

尽管之前我们抽象出了Travellable接口，但依然在使用继承，为了创建先坐大巴，然后再转大巴回家的这类人，我们还得创建一个新类，虽然避免了代码重复，但又引起了类的泛滥，既然策略模式可以在运行时交换算法，如果运行时决定他们的回家方式，那现在还需要HappyPeople的子类吗？

既然这样，我们没必要使用静态方式创建这些不同回家方式的人，在运行时同样可以决定他们的回家行为。

1.由于HappyPeople和Travellable之间是一对多的关系，我们为Happy People 添加了一个链表来持有Travellable对象，在运行时动态地把回家方式添加到这个链表就可以。

2.方法travel（）依次执行每个Travellable对象的travel（）方法。

3.最后，为了管理Travellable对象，我们还增加了addTransportation（Travellable transportation）、getTransportation（int index）等方法。

4.于是，UML静态类图如图12-2所示。

图12-2

有了上述结构，不管乘坐什么交通工具回家，乘坐多少次交通工具，我们都可以实现，而且没有了重复的代码。

我们看看HappyPeople的代码如何实现的。

12.5　优先使用合成而非继承

其实我们这里又使用到了一条OOP的原则。

Favor Composition over Inheritance.

优先使用对象组合，而不是继承。

OOP的一个重要特点是继承，继承提供了一条避免代码重复的途径，尽管如此，我们还是优先考虑合成，HappyPeople对象合成了Travellable对象，这比继承来的更加灵活。

虽然在重用方面，继承比合成来的更为简单，但其有如下缺点。

1.子类和父类之间是高耦合的

这体现在三个方面。

子类可以看到父类的一些非公有的细节（protected属性和方法），破坏了封装性。

子类的行为和父类的私有性质之间的协作也是直接的，父类可以直接控制子类的行为，父类私有行为的改变有时会给子类的运行带来不可预知的错误。

父类公有性质的改变，直接影响了子类性质的改变。

如果使用合成，只能依赖于它的公有性质，它们之间不会轻易改变这些公有的约定。

2.子类的行为在运行时无法改变

这些行为都是编译时决定的，是静态的。而如果使用合成，只要依赖于接口或者抽象，运行时就可以动态替换这些对象，享受到了依赖于接口/抽象的好处。

在实际开发中，我们不一定孤立地使用它们，有时候往往结合它们，比如我们之前讲解的装饰器模式、组合模式等都是结合使用合成与继承很好的示例。

12.6　总结

封装变化是行为模式的一个重要主题：把独立变化的部分抽象出来，使得不变的部分不受变化的那部分影响，这样非常便于测试和调式。在策略模式，变化的那方面是策略，不变的是Context。

从上例可以看到，并不是所有的模式拿过来使用都能解决当前的问题。我们之前使用的模板方法模式，并不能解决后来面临的复杂问题，于是选择了策略模式，这并不意味着模板方法模式比策略模式好，只是在此场景使用策略模式会更好地帮助我们解决问题。

其实模式本身没有错，错就错在我们没在正确的场合合理地使用它们。

12.7　本章关键词

策略模式

继承

封装

优先使用继承

读书笔记

____________________________

第13章　状态（State）模式

状态模式是非常有意思的一个模式，使用它可以避免难看的if-else/switch-case语句，本章主要包含如下内容。

通过一个电子颜料板介绍如何封装变化，从而引入了状态模式。

使用Java 5新语法的枚举类型（Enum）实现状态模式。

和策略模式的比较。

13.1　电子颜料板

有这样一块电子颜料板，它上面有个手柄，可以拉（pull）也可以推（push），每次操作，都会改变颜料板的颜色，它们的变化关系如图13-1所示。

图13-1

13.2　switch-case实现

这又是一个关于变化的例子，在讲解如何封装变化之前，首先给出使用switch-case语句解决这类问题的代码。关于push操作的代码片段，大致如下。

这段代码看似并没有什么不妥。我们知道，需求总是不停变化的，如果又有新的状态需要加入时，你就得在push（）方法和pull（）方法里添加新的case分支语句，这就破坏了OCP原则。另外，如果逻辑比较复杂，一个case分支语句的代码甚至会超过几百行，代码会变得很难阅读和维护。

13.3　如何封装变化

从图13-1我们知道，在不同颜色状态下push操作和pull操作是不一样的。譬如，当前状态是红色，push操作会使状态变成绿色；如果状态是绿色，则会变成蓝色。不同的状态关联不同的行为，这就是该场景的变化方式。为此，我们抽象出一个状态的接口，它定义了与状态相关的方法，于是，具体的状态就可以封装与之相关的行为了。

现在就让我们来构造它们：State是状态接口，含有push（PaintBoard paintBoard）和pull（PaintBoard paintBoard）两个方法；PaintBoard类包含push（）和pull（）两个方法，它把这两个操作委托给当前状态完成。

UML静态类图如图13-2所示。

图13-2

来看看PaintBoard类的代码，大致如下。

每个具体状态的push操作和pull操作都会回调PaintBoard的方法setState（State state）来设置PaintBoard的新状态。State接口和RedState实现类的代码如下所示。

其他具体状态类与RedState类相似，具体代码我们就不再赘述，请参见示例代码。

下一步我们创建测试代码，如下所示。

最后看看测试效果，如下所示。

当前状态代理了PaintBoard的push和pull操作，这样，PaintBoard类的代码里再也找不到“丑陋”的条件分支语句，代码也就更加容易阅读。如果需要添加更多的状态，只要添加新的State接口实现即可，符合OCP原则。

13.4　状态模式

我们为变化的部分抽象了State接口，把不同的行为封装在相应的状态类中，PaintBoard在执行与状态相关的行为时，把请求转发给当前状态类去执行，这正是状态模式。GoF为状态模式给出的定义是。

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

允许一个对象在其内部状态发生变化时改变自己的行为，该对象看起来好像修改了它的的类型。

此例中，PaintBoard类是担任了Context角色，客户对象直接操作此类对象，State是状态接口，BlueState、GreenState和RedState这些都是具体状态类。

13.5　使用Enum类型

我们把不同状态下的行为封装在不同的State实现类里，每一种状态就是一个独立的类，其实我们还可以使用枚举类型（Enum），每一种状态是一个枚举值。枚举类型是从Java 5版本开始引入的一种新类型，它包含了几种特定的值供程序选择。Java语言的枚举类型和C语言的枚举类型有点不同，C语言的枚举值其实是一个整型常量，而Java语言的枚举值是一个对象，所以我们可以在其中封装方法和属性。

枚举类型仍然实现了State接口，每种枚举值封装了push和pull操作。枚举类型Color的实现代码大致如下。

在这里，每种枚举值都必须实现State接口的这些方法，否则会抛出Unsupported OperationException异常。

我们看看RED枚举值是如何实现的，代码如下所示。

RED枚举值覆写了push（PaintBoard paintBoard）方法和pull（PaintBoard paintBoard）方法，和之前具体类的实现没太大区别，其他枚举类型的实现与RED类似，请参见示例代码。

其实我们甚至还可以省略State接口，在enum类型Color里定义这两个方法，push（PaintBoard paintBoard）方法和pull（PaintBoard paintBoard）方法。使用接口的好处是，如果你需要增加新的状态时，就没必要为增加新的枚举值而修改此枚举类型，你可以创建新的具体实现类，不影响既有的代码。

如果你用的是Jdk 1.4及以下版本，读者可以使用《Effective Java》一书中提出的Typesafe Enum模式[1]来实现。

其实Java的枚举类型沿袭了“Effective Java”一书中提出的Typesafe Enum模式，编译器在编译enum类型时，会生成一个真正的类代替枚举类型。其实Java没有真正的枚举类型，这个新生成的类继承于java.lang.Enum类，每个枚举值的类型都是这个新类的子类。为了方便理解，这里给出此例反编译出来的代码以供参考，如下所示。

我们看到，RED是这个新生成的Color类的一个内部类对象，RED、GREEN和BLUE都是Color类的成员常量，有兴趣的读者可以自己动手反编译生成的相关class二进制文件来做更进一步的了解。

我们这里给出了应用Java枚举类型实现状态模式的方法，在一些操作不是特别复杂的应用中，可以避免类的膨胀，但如果需要封装一些复杂的操作时，不同状态的方法挤在一起，代码也不容易阅读。

为了能够扩展新的状态，笔者建议大家仍然为枚举类型创建State接口，否则，只能修改该枚举类型以添加新的状态，不符合OCP原则。

[1]参见《Effective Java》一书的第五章：Chapter 5.Substitutes for C Constructs, Item 21：Replace enum constructs with classes。

13.6　与策略（Strategy）模式的比较

都把行为封装到具体的类中。策略模式把不同的算法封装到不同的子类；状态模式把不同状态下的行为封装到与之对应的状态子类中。

它们都是使用了合成，代理了上下文的相关操作。

一个状态对象封装了与状态相关的行为，并控制状态之间的转换。策略对象封装了算法，算法之间不存在切换。

状态模式隐藏了状态接口和子类。策略模式中，客户对象可以在运行时选择不同的算法，策略接口和具体实现对它是可见的；但是状态模式中，客户对象只能操作Context对象，状态接口和子类对它是不可见的。

13.7　总结

每个状态封装了当前状态的行为和状态之间的转换，我们在后面使用Java新语法——枚举类型——实现了状态模式。其实只要学会了如何封装变化，你就能找到合适的解决问题的模式。

13.8　本章关键词

状态模式

封装

枚举类型

策略模式

OCP原则

第14章　观察者（Observer）模式

本章包括如下内容。

通过一个炒股的场景引入了观察者模式。

Java标准库的观察者模式，以及它的特点和局限性。

14.1　股票价格变了多少

小D喜欢炒股，经过严格的筛选，终于看中了一只股票。为了及时地了解这只股票价格的信息，他每天都要上网查询好几次。刚开始，由于新鲜感的驱使，小D并没感到太麻烦，但是几个星期后，他逐渐觉得这样做十分不便：有时候，几天过去了，所关注的股票几乎没有波动；有时候，这只股票的价格会在一天之内上下大幅波动好几次，小D为此错失了好多机会。如果你是小D，你该怎么办？

我不知道你是否有更好的方法，小D是这么想的，如果这只股票的价格变化波动到一定程度时能通知自己，那他就不必不停地上网查询了。于是，他决定设计一个工具能够自动地把股票信息发送给自己。

14.2　观察者模式

小D的这种想法涉及了两个角色：一个是观察者，即小D，另一个是被观察的对象，即股票。当股票价格发生一定改变时，股票去通知观察者小D，这样小D就会根据这些变化作出相应的反应。其实，不光像小D这样的散户希望收到通知，还有一些大型机构投资者也希望收到通知，这些机构也是观察者。

14.2.1　如何实现

为了发送通知，主题应该知道观察者是谁，所以应该提供注册和删除观察者的接口方法的功能；当发生变化时或者变化到一定程度时，就通知这些观察者。我们首先给出UML实现类图，如图14-1所示。

图14-1

Stock类可以通过addBuyers（StockBuyerbuyer）和removeBuyers（StockBuyer buyer）来注册和移除观察者：股票投资者或投资机构；方法setPrice（float newPrice）设置股票的价格，当股票价格的波动超过一定范围（maxGainAndLoss）时，便会触发notifyBuyers（）方法通知所有股票投资者。

观察者接口StockBuyer有一个update（float price）方法，其实现类定制股票价格发生变化时各自具体的行为，接口代码如下。

接下来，我们来看看Stock类的代码实现，大致如下所示。

当股票价格变化超过一定范围时，notifyBuyers（）方法调用每一个观察者的update（float price）方法。

InstitutionalInvestor（机构投资者）实现了StockBuyer接口，当股价高于maxPrice时，机构投资者会卖出100000股，当股价低于minPrice时，会买进20000股。模拟代码大致如下。

同样，PrivateInvestor（散户）也实现StockBuyer接口，当股价高于maxPrice时，散户将会买进500股，当股价低于minPrice时，会卖出1000股。模拟代码大致如下。

我们来编写测试代码，如下所示。

机构投资者Company E的maxPrice是20f、minPrice是18.5f；散户小D的maxPrice是20f、minPrice是18.9f。

最后，我们看看执行结果如何，如下所示。

14.2.2　观察者模式

在这个场景中，股票和投资者之间的关系是1：N的关系，当价格发生一定变化时，股票以广播的方式通知投资者，这正是观察者模式。GoF给出的定义如下。

Define a onetomany dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

定义了对象之间的“一对多”的依赖关系，这样，当一个对象的状态发生变化时，所有依赖于这个对象的相关对象都被通知并自动更新。

我们把被观察的对象也叫做主题（Subject），这里的主题即是Stock类，StockBuyer便是观察者接口。当有多个主题时，你可以抽象出一个Subject接口，这样，就可以独立地改变主题和观察者而不会相互影响。

我们上述实现的模式是push方式的观察者模式，即股票把观察者需要的所有信息都推给了它们，这里是指把股票价格推给了目标。

我们还有一种方式，我们称之为pull方式，即股票在通知观察者时并不传递股票信息，而由观察者显式地查询股票信息。

使用前者时，如果观察者需要获得更多的信息，可能导致观察者接口不能重用；使用后者时，观察者可能不容易知道哪些信息发生了改变。

14.2.3　Java标准库的观察者模式

观察者模式的应用极其广泛，Java标准库中有很多地方用到了观察者模式，特别是在Java GUI编程中。Java Uitl包中还提供了类java.util.Observable（主题）和java.util.Observer（观察者）接口以方便Java开发人员在编程过程中快速地使用观察者模式，我们先来介绍这两个类型。

1.java.util.Observer接口

Observer接口的代码如下所示。

和我们的StockBuyer接口一样，都含有一个方法，用于主题发生变化时去告知该观察者。执行这个方法update（Observable o, Object arg）时，能得到主题Observable对象，它的第二个参数用于传递主题和观察者之间的变化数据。例如，上例中的股票价格，如果不需要传递数据，则可传入null值。这种方式把push方式和pull方式结合了起来，读者在使用过程中可以灵活实现。

2.java.util.Observable类

方法addObserver（Observer o）/deleteObserver（Observer o）用于注册/删除观察者。

方法setChanged（）和clearChanged（）设置状态变化情况：在调用notifyObservers（）/notifyObservers（Object arg）方法之前，一定都要调用setChanged（）方法，设置状态发生了改变，否则不会触发通知；clearChanged（）方法清除状态变化，这两个方法都是protected的，代码如下所示。

方法notifyObservers（）和notifyObservers（Object arg）用于通知观察者，相当于我们前述Stock类的notifyBuyer（）方法，这两个方法的区别是：notify Observers（）方法相当于notifyObservers（null）。让我们看看notifyObser vers（Object arg）方法是如何实现的，代码如下。

此方法首先判断Subject状态是否发生了改变，如果没有改变则返回，否则准备通知观察者，并调用clearChanged（）方法清除这次状态变化。

然后依次调用每个观察者的updateupdate（Observable o, Object arg）的方法。

可以看出，如果调用notifyObservers（Object arg）方法，则是使用了pull和push结合的方式；而调用notifyObservers（）方法，则是选择了pull的方式。

最后，还需注意的是，它采用了倒序的方式通知所有的观察者，即这句for（int i=arrLocal.length-1；i＞=0；i--）。

3.如何使用

有了上述基础之后，我们来使用它们重写代码，如下所示。

注意：在调用notifyObservers（Object arg）方法之前，必须调用setChanged（）方法，否则据上分析，它会忽略此次改变。

接着，我们让InstitutionalInvestor类实现java.util.Observer接口，代码如下所示。

构造函数里，我们使用stock.addObserver（this）替换stock.addBuyers（this），来为股票添加此观察者，update（float price）的方法签名也换成update（Observable o, Object arg）。PrivateInvestor类与此类似，在这里不再赘述，具体请参见示例代码。

最后，让我们验证一下执行效果，测试代码和以前一样，执行结果如下所示。

注意下划线的部分，由于采用倒序的方法通知观察者，所以执行结果中，小D先于机构投资者Company E得知股票变化。

4.还需注意

java. util.Observable是具体类，而不是接口，对于Java这样的单继承语言，不能让其同时继承Observable类和其他父类。这里为大家提供一种思路来解决此问题：你可以使该新类所依赖的java.util.Observable子类（必须是子类，随后你将看到），当对象的状态发生变化时调用java.util.Observable子类的通知方法，来达到通知的效果。

必须注意的是，你一定要在调用通知之前修改状态变化的标志，即调用setChanged（）方法，但是此方法在java.util.Observable类中是protected的，为了能够使用，必须在该java.util.Observable子类中覆写此方法，使其变为public的，另外clearChanged（）方法在必要时也需要覆写。

正如前面所提到的，观察者的通知顺序和我们之前的实现不一样，一般地，观察者的通知顺序是没有要求的，如果你的通知必须要按照特定的顺序执行，可以专门维护一个ChangeManager类来处理这种问题，而且，在ChangeManager里还可以实现一些复杂的通知策略。

14.3　总结

如果你还记得好莱坞原则（不要找我们，我们会找你）的话，你一定发现了，观察者模式又是一个它的应用。观察者模式降低了主题和观察者的耦合度，使主题依赖于观察者的接口，而不去关心哪个观察者会关注该主题，在图形化编程和分布式事件处理系统中，此模式被广泛使用，也是在JDK中使用最多的模式之一。

14.4　本章关键词

观察者模式

GUI

好莱坞原则

第五篇　终点还是起点

世界上，任何事物都不是完美的，OOP也一样，它不能解决所有的编程问题，OOP在纵向分解问题方面表现十分出色，但对于水平问题，有点无能为力，而AOP恰好能够弥补这方面的不足，我们在第15章将介绍时下最流行的AOP技术。

了解和使用模式对于开发者来说，还远远不够，软件的核心是模型，开发大型的复杂业务的应用，需要精炼的模型，如果模型不够精炼，也不能享受到设计模式带来的好处，在第16章我们将介绍如何提炼模型解决复杂的领域问题。

在本篇末尾，第17章，我们将回顾本书的内容。

第15章　面向切面的编程（AOP）

OOP目前已经是软件开发的主流了，我们可以非常方便地把领域问题进行分解，封装成对象。然而和过程式编程（Procedural Programming, PP）或者函数式编程语言（Functional Programming, FP）等传统编程技术一样，还有一类问题它们都无法优雅地解决，而产生这些问题的原因正是由于它们的共同优点（把问题分解为小的单元——对象、方法、结构体等）引起的。进而引入最流行的概念之一：面向切面的编程（AOP）。本章我们首先给出一个例子引入这类问题的讨论。

本章主要包括如下内容。

为什么使用AOP。

AOP的一些重要概念。

AOP的一些流行框架。

AOP联盟。

AOP技术的风险以及AOP与OOP之间的关系。

15.1　记录时间

我们依然采用大家熟悉的回家过年的例子来描述该问题，以第2章的示例代码为基础，假如我们现在需要记录travel（）方法所耗费的时间，可以使用模板方法模式来解决，重构代码大致如下所示。

代码注解

我们使用Stopwatch类记录travel（）的运行时间。Stopwatch（秒表）类正如其名，它从调用reset（）方法时开始计时，当调用info（）方法时会打印开始计时到当前操作这段执行时间的长度。

为了记录travel（）方法的执行时间，我们把这个方法分解为三个部分，分离出的抽象方法onTravel（）方法代替之前travel（）方法，执行之前该方法的全部逻辑；在执行onTravel（）方法之前设定计时时刻；在执行该方法之后打印出这段时间的长度。

那么以PassengerByAirAop类为例，onTravel（）方法代码大致如下。

为了方便计算时间，让线程休息200毫秒，即：TimeUnit.MILLISECONDS.sleep（200l），其他子类与之类似，我们在这里将不再赘述。

我们撰写的测试代码如下所示。

某一次的测试执行结果如下所示。

模板方法模式解决了对onTravel（）方法运行时间的统计问题，如果我们想更进一步对subscribeTicket（）和celebrate（）所耗的时间进行统计，那该怎么办？我们只能为每个方法添加了如下斜体加粗部分的代码，如下所示。

冗余代码的“臭味”再度摆在我们面前了，到目前为止，还没有一个已经介绍的模式能够解决这类重复的代码问题。使用模板方法模式对少数几个方法进行这样的处理，似乎问题不会太严重，但如果对几十个，甚至上千个方法进行同样处理，重复代码的“臭味”将会弥漫在整个程序中。

有没有更加优雅的方案把这些零散的代码模块化在一处呢？答案是有的，这即是我们下节要介绍的AOP技术。

15.2　AOP（Aspect-Oriented Programming）

我们在软件设计时，会把系统分割为小的单独的单元来处理，我们把这种设计方法称之为SoC（即Separation of Concerns）——一个关注点（Concern）就是一个问题。编程过程中，我们把这些关注点模块化在编程语言所提供的单元里，例如，在传统的编程语言中（这里指过程式编程/函数式语言），会把系统分割成数据结构和方法，在OOP语言中，我们通常把它们分解为类。

SoC一方面分解了问题：我们把系统表示为一堆软件和实体之间的交互问题，把复杂的问题按照功能分解成了小粒度的简单问题，主要是纵向分解了问题，然后各个击破。但另一方面，正是由于分解产生了很多小单元，自然而然地出现了一些横跨这些单元的相同逻辑，即在水平方向上出现了相同的逻辑。

在上述例子中，记录travel（）的执行时间和记录celebrate（）的执行时间，都是相同的问题。使用传统的软件编程和OOP，我们都不容易解决这种重复问题，例如使用模板方法模式仍然导致在这两个方法分别添加相同逻辑的代码。为了把这种水平方向上的逻辑进行模块化，随之便产生了AOP编程。

AOP可以在水平方向上补充OOP的这一缺点，它可以把这些离散的、跨模块的、混入其他关注点的逻辑放在一个独立的模块里，即从横向模块化了问题。比如，对于上述计时问题，我们可以把记录这些方法执行时间的逻辑模块化在同一个单元里，这个新的模块化单元就称之为切面（Aspect）。

编程过程中有很多“水平”问题，例如事务管理、异常处理、日志记录、安全管理、性能优化、透明的持久化等，这些问题也是J2EE关注的重要问题，由于AOP在这方面发挥出色，AOP也成了时下很多J2EE流行框架的重要部分。

下面我们就从一些基础概念入手了解AOP。

15.2.1　一些重要概念

下面我们先介绍一些AOP的重要概念和术语。

1.切面（Aspect）

横切关注点（Concerns）模块化后称为切面。在传统的软件编程方法里，我们把关注点模块化为函数、过程、类等，而在AOP编程里，我们把关注点模块化为切面。

2.连接点（Joinpoint）

程序执行过程中可被识别的某个点叫做连接点。连接点可以是某个方法的调用，也可以是某个赋值操作，是AOP里最基础的概念。例如上例中travel（）方法调用onTravel（）方法的位置就是一个连接点。

3.增强（Advice，有时也翻译为通知）

连接点处执行的逻辑叫做增强。分为前增强、后增强、环绕增强、异常增强和最终增强。

前增强（Before advice）：在连接点之前执行的增强。

后增强（After returning advice）：在连接点正常执行完成后执行的增强。

异常增强（After throwing advice）：捕捉连接点抛出特定异常后执行的增强。

最终增强（After/finally advice）：调用连接点之后始终执行的增强，不管此连接点是异常退出还是正常退出。

环绕增强（Around Advice）：包围一个连接点的增强。它控制连接点的执行，是最强大的一种增强，它在连接点之前和之后都完成一些操作，负责调用连接点，甚至可以不执行连接点，直接抛出异常。

4.切入点（Pointcut）

某些特定连接点（Joinpoint）的逻辑叫做切入点。根据切入点我们可以判断哪些连接点（Joinpoint）需要增强，哪些不需要。

5.织入（Weaving）

把这些水平的切面连接到执行过程以组合成完整流程的行为称之为织入。织入可以发生在编辑期、编译期、类加载期和运行期。

15.2.2　OOP实现横切

我们知道OOP在解决横切问题时，有很大局限性，但并不表示不能使用。可以使用它解决一些简单问题或者特定问题，在某时候是非常有价值的，因为引入新的软件和技术支持（AOP）总是有风险的，没有必要因为一些简单问题而引入太大的风险。下面列举了一些常用的、使用面向对象编程解决这类问题的方法。

1.模板方法（Template Method）模式

上例中我们使用了模板方法模式计算onTravel（）方法的执行时间，我们发现，它实现了环绕增强。对于极少数方法进行横切是可行的，因为它只能完成对所有子类的onTravel（）方法执行时间的计算，如果要针对其他方法也进行计算，需要加入冗余的代码。

2.装饰器（Decorator）模式

其实和模板方法模式一样，解决地也不够彻底，例如我们在讲解装饰器模式时所举的记录（log）的例子，虽然对update操作记录了日志，然而，如果对于其他方法，如delete（）方法也要记录日志，我们仍然要加入重复的代码，而且它只能对具有相同接口的对象织入增强。

3.代理（Proxy）模式

和装饰器模式在这方面没有区别。

4.其他模式

如Observer模式等，都只能完成对部分连接点的增强，水平跨度不大，我们在这里就不再赘述。

5.拦截器框架

这个功能比较强大，但是作为一个框架，实现起来并不太容易，具体请参见下节介绍。

OOP可以解决一些AOP问题，但是使用它们终究不够优雅和全面，下面我们来介绍一些常见AOP实现技术。

15.2.3　AOP实现技术

目前Java的AOP实现技术主要有以下几种，了解这些技术对读者使用和深入了解这些框架是非常有帮助的，笔者在这里将为大家一一介绍。

1.J2SE动态代理

我们在代理模式一章为大家介绍了J2SE动态代理技术，它可以动态代理所有指定的接口方法，这为AOP的实现提供了一个契机：动态代理把所有代理接口的方法转发给InvocationHandler的invoke（……）方法统一处理，我们在运行时就可以很轻松地根据切入点为这些方法织入增强，由于接口一致，客户对象在使用时并不会察觉到不同之处。

我们看看如何使用动态代理解决这个计时问题。由于动态代理只能代理接口，所以我们还需要提供一个IHappyPeople接口，如下所示。

HappyPeopleHandler类实现了InvocationHandler接口，在invoke（Object proxy, Method method, Object[]args）方法里，我们实现此环绕增强的逻辑，代码大致如下所示。

其中isMethodQualified（Method method）方法判断该代理方法是否需要增强，它封装了切入点的逻辑。

在调用代理方法之前，如果满足切入点条件，则织入开始计时的逻辑，即：“stopwatch.reset（）：”。

同样，执行完代理方法之后，如果满足条件则织入这句用来打印的时间：

stopwatch. info（"Method"+method.getName（）+"（）used"）

这样，一个简单的AOP环绕增强的例子完成了。

对于切入点逻辑，可以使用更为通用的方式实现，譬如可以根据模式字符串来判断是否需要增强，这样，上例中切入点的逻辑可以写为类似于这样的模式匹配字符串：“**.subscribeTicket（）||**.travel（）||**.celebrate（）”。

接下来我们写出测试代码，如下所示。

以下是某一次的执行结果。

这样，我们把记录时间的逻辑封装在了HappyPeopleHandler类里，再也看不到重复的代码。

注意：细心的读者可能发现了，我们在测试代码中未直接调用HappyPeople的celebrateSpringFestival（）方法，而是依次调用了它的subscribeTicket（）、travel（）和celebrate（）这三个方法。如果直接调用celebrateSpringFestival（）方法，你就会发现并未执行计时逻辑，这是什么原因呢？

原来，动态代理把方法请求交给了HappyPeopleHandler的invoke（Object proxy, Method method, Object[]args）方法，它会调用目标对象的celebrateSpringFestival（）方法，即HappyPeople对象的celebrateSpringFestival（）方法，目标对象调用自己的这三个未织入增强的方法，而不是调用代理对象的那三个可以织入增强的方法，所以没有打印出执行时间。

如何解决这个问题呢？其实方法很多，我们可以把庆祝团圆封装成一个独立的Celebration类，它的方法为：celebrateSpringFestival（IHappyPeople ppl），代码大致如下所示。

这样，客户对象传入织入增强的代理对象，执行Celebration对象的celebrate SpringFestival（IHappyPeople ppl）方法即可。

其实聪明的读者可能想到了在HappyPeople使用如下方法。

这种方法虽然可以解决代理对象未彻底代理所有方法引起的问题，但是从模型上来考虑，它是欠妥的。HappyPeople类的职责会变得不明确——为什么不是自己而是别人来庆祝团圆？并且使用static方法会丧失面向对象所带来的继承和多态等特性，不易扩展。

一个优秀的软件开发人员，不仅要熟悉各种模式，还需要为问题抽象出精炼的模型，关于如何提炼模型，请参见下章。

因为Java语言自动支持动态代理技术，所以不需要依赖于任何第三方软件和库，移植性和兼容性得到了保证。但是，一方面动态代理只能局限于接口的代理，不能对类进行代理；另一方面，通过上述示例，我们看到由于Java动态代理的实现机制问题——把方法转发给了目标对象，导致逻辑织入地不够彻底。

动态代理作为AOP的一种实现技术，被广泛应用到了诸多框架之中，例如Nanning、Spring、Pico Container等。而这些成熟的AOP框架往往结合一些其他AOP技术（如动态字节码、拦截器框架等），以实现更强大的AOP框架。

2.动态字节码

由于Java语言的开放性，涌现出了很多动态字节码工具，像Apache的BCEL。Jboss的Javassist。ObjectWeb的ASM和开源工具CGLib等，它们可以在运行时分析、生成和改变Java Class文件，在运行时也可以非常方便地织入增强，是AOP实现的另一条途径。这方面比较流行的工具是CGLib，它曾被Hibernate早期版本成功地应用于延迟加载等方面，我们这里就以其为例作介绍。

CGLib的net.sf.cglib.proxy.MethodInterceptor和java.lang.reflect.Invocation Handler一样，都能统一控制被代理的方法，使用二者的区别不大，代码如下所示。

可以看到，除了intercept（Object o, Method method, Object[]objects, MethodProxy methodProxy）的方法签名不同之外，代码并未和Java动态代理的InvocationHandler实现类有很大区别。

CGLib不仅可以代理接口，还可以代理类，所以这里我们不再创建接口。以下代码显示如何使用CGLib生成代理类的对象。

我们的测试代码如下所示。

这里给出了测试的某一次执行结果，如下所示。

细心的读者可能早就发现，我们这次直接调用HappyPeople类的celebrate SpringFestival（）方法而完成了执行时间的计算，这是由于CGLib生成代理类的方式和J2SE动态代理生成代理类的方式不同引起的。

J2SE动态代理生成代理类继承于java.lang.reflect.Proxy类，所有方法指向了InvocationHandler实现对象的invoke（……）方法，而invoke（……）方法把请求再次转发给目标对象处理；

CGLib生成的代理类继承于被代理类/目标类，这样，上述执行代理对象的celebrateSpringFestival（）方法时，而不会把请求直接转发给目标对象，而是执行自己被织入增强的方法，也正是由于此缘故——使用继承的方式创建代理类，CGLib不能代理任何final的方法和类。

通过介绍CGLib，旨在希望大家对Java字节码工具在AOP方面的实现上有个大体的认识，由于这种工具很多，限于篇幅，不再赘述。关于CGLib的详细使用，有兴趣的读者可以登录：http：//cglib.sourceforge.net/

3.拦截器（Interceptor）框架

拦截器（Interceptor）一般在Command框架里都有相应实现，它能围绕被执行的业务对象织入增强。我们可以把切面都封装在拦截器的具体实现类里。当然拦截器框架是使用OOP语言设计的一个框架，其中往往结合使用了Java动态代理、字节码工具和Java反射（Reflection）等技术。

拦截器框架的一般结构如图15-1所示。

图15-1

客户对象直接使用的是代理对象BusinessObjProxy，它持有控制执行逻辑的Invocation对象。

Invocation持有Interceptor链和真正被代理的BusinessObj对象，和前述invoke（……）方法的功能一样，负责织入增强，即依次调用拦截器进行拦截，最终执行BusinessObj对象的方法。

Interceptor实现类便是我们的切面。

BusinessObj是目标对象，即要被织入增强的对象。

Invocation负责BusinessObjProxy的执行，在执行过程中它负责Interceptor链的每个拦截器方法被依次执行，以及被代理对象BusinessObj的执行。为了便于描述，我们给出拦截器的序列图，如图15-2所示。

图15-2

Client调用BusinessObjProxy对象的invoke（）方法，BusinessObjProxy把请求转给Invocation对象，Invocation执行完拦截器链，接着执行BusinessObj的方法，最后执行拦截器链未执行完的堆栈逻辑，并返回执行结果。

我们在这里实现一个非常简单的拦截器框架，来说明拦截器框架的工作原理。首先实现定义一个Invocation接口，代码如下。

接下来给出一个简单的Invocation实现，经上述介绍，我们知道，拦截器框架织入逻辑会在此类中实现，如下所示。

我们的织入逻辑在invoke（）方法里：首先判断拦截器链是否未执行完毕，即：if（interceptors.hasNext（）），如果没有，则取出下一个拦截器继续执行；如果拦截器链执行完毕，这才去执行真正被代理对象的方法，即：result=invokeBusinessObjectMethod（）。

invokeBusinessObjectMethod（）方法会调用目标方法，我们这里的实现非常简单、使用反射得到配置的方法并执行。

ProxyFactory工厂类用于创建BusinessObjectProxy对象，代码如下。

Interceptor接口的代码如下所示。

我们这里实现记录时间的拦截器StopWatchInterceptor，代码如下。

我们在Object result=invocation.invoke（）之前加入这句stopwatch.reset（）开始计时，Object result=invocation.invoke（）这句非常重要，保证了Invocation能够继续执行拦截器链上其他还未执行的拦截器以及目标对象的被拦截方法。

最后，由于方法的堆栈调用，Invocation仍然要返回该拦截器，执行连接点invocation.invoke（）之后的逻辑，即打印出所消耗的时间，即：stopwatch.info（"……"）。

我们这里做了一个非常简单的配置类Config，用于描述对象的哪个方法需要拦截，当然你可以撰写更为复杂的配置支持你的框架，代码如下所示。

在使用之前，和其他框架一样，我们需要建立上下文环境，我这里写了一个简单的NaiveInterceptorFrameWork类初始化环境上下文，如下所示。

我们为PassengerByAir对象的三个方法[subscribeTicket（）、travel（）和celebrate（）]配置了StopWatchInterceptor拦截器，测试代码如下所示。

执行结果如下所示。

优点：

我们不需要在编译和加载类时做任何额外的操作，方便编译和发布。从上述实现我们知道，可以对任何对象进行拦截。

从上述例子知道，我们可以对同一类型的不同对象织入不同的逻辑，只要为它们配置不同的拦截器即可。

拦截器的代码不会侵入目标的类和方法。

缺点：

框架的不同，拦截器的能力强弱差别比较大。

框架的一次执行只能拦截一个目标方法，如果要统计三个方法的执行时间，我们要配置三个拦截器链。

拦截器在很多框架中均有实现，最常见的有Jboss的拦截器、xwork框架[1]的拦截器和Spring AOP的拦截器等，这些框架往往结合其他AOP技术，例如J2SE动态代理和动态字节码。

4.源代码生成

在EJB早期实现里，我们经常使用这种方式生成织入增强的Java源代码，这种方式随着Java动态代理技术和字节码工具的出现已经开始逐渐退出潮流。

5.在编译时织入二进制代码

我们可以使用特定的编译器在编译时织入增强，这种方式的织入，运行性能总比运行时织入要优越一些[2]。它的运行速度可以媲美Java源代码生成，这二者都是静态织入，目标对象被织入的功能在编译时就决定了。

AspectJ和AspectWerkz都支持这种策略，Cobertura是一款统计测试代码覆盖率的软件，它就是在编译时把统计覆盖率的逻辑织入已编译好的Java二进制代码里，然后运行这些二进制文件，完成了覆盖率的统计。

6.定制类加载器（Class Loader）

我们还有一种织入方式，那就是在类加载器加载类时织入增强，由于Java类加载器的可扩展性，我们可以定制自己的类加载器，让其在加载类时织入增强。AspectWerkz（称之为在线织入，Online Weaving）和AspectJ都支持这种方式。

但是这种方式可能在某些J2EE服务器上并不能被使用，因为这些服务器一般有一套自制的完整类加载结构体系，这种方式可能导致不能定制我们自己的类加载器，否则会引起冲突。

[1]xwork框架是一款非常出色的command框架软件，著名的Struts2和Webwork框架就是使用其实现的，它的拦截器实现了对command/action的动态拦截。

[2]其实运行时织入并没有想象中那么慢，现在由于JVM性能的提升，对动态代理等做了很大优化，那些字节码工具在运行时表现也越来越出色。

15.3　AOP框架介绍

本节介绍一些AOP流行框架，有兴趣的读者可以登录它们的网站作更深入的了解。

1.AspectJ

AspectJ是目前最流行也最完善的AOP实现，它给自己的定义是。

AspectJ is a seamless aspectoriented extension to the Javatm programming language, Java platform compatible, easy to learn and use.

AspectJ是一个Javatm编程语言在面向方面上的无缝扩展，并且与Java平台兼容，容易学习和使用。

AspectJ支持编译时、类加载时和运行时织入。其中，编译时织入不仅支持源代码的生成（其称之为Compile Weaving），也支持对已编译好的二进制文件（例如jar包、class文件）织入增强（其称之为Post-compile Weaving，也叫二进制织入，Binary Weaving）。正如定义所说，它是Java的扩展，这也体现在语法上面，增加了初学者的难度。

2.Nanning

Nanning是一款非常小巧的AOP框架，名字源于中国广西南宁。它使用了Java反射技术实现了拦截器框架，可以实现基于方法的拦截。该项目最后发布日期是2003年8月。

3.AspectWerkz

AspectWerkz是一款非常优秀的AOP框架，它使用了字节码工具，可以在编译时、类加载时和运行时修改二进制代码。该项目最后发布日期是2005年3月。

4.Spring AOP框架

Spring现在不仅仅包含Spring框架，在Webflow、SOA、Andriod、云计算等各个方面均有出色的贡献。Spring框架本身也包含一个AOP框架，但并未和Spring框架强行绑定，读者可以在使用时自由选择。

Spring AOP框架最开始使用了Java动态代理和动态字节码工具，其本身只支持方法级别上的拦截和增强，后来由于实现了和AspectJ的集成，所以可以完成属性（field）的访问拦截等AspectJ才有的功能。并且由于Spring AOP和其IoC框架实现了无缝集成，使其成为使用Spring框架做项目的首选AOP方案。

5.JBoss AOP

JBoss AOP的框架目前已经支持属性（field）、方法、构造方法上的拦截，但它是一个EJB的容器，所以其AOP框架和EJB服务器实现了绑定，读者选择时需要在移植性方面作考量。

15.4　AOP联盟（AOP Alliance）

AOP联盟为Java和AOP爱好者建立了一个AOP的规范，它不提供具体实现，而是在AOP联盟成员之间达成标准，旨在使你的代码在这些成员的软件之间可以互相移植。由于AOP联盟对AOP的发展一直持谨慎态度，目前只能支持对构造方法和成员方法的拦截。

15.5　使用AOP编程的风险

虽然AOP弥补了OOP水平方向进行模块化的不足，避免代码的重复，但是我们对AOP编程一直持谨慎的态度，这是因为。

一个类如果被多个切面横切，可能会产生叠加效果，有些叠加效果正是你所需要的，但有些意外的叠加效果可能会带来严重的、无法预知的错误。特别是在使用JPM（Joint Point Model）时，由于很容易加入切入点的规则，所以很容易造成意想不到的结果。

为了避免因叠加效果而产生的无法预料的问题，AspectWerkz 推荐使用正交的连接模型（Orthogonal weaving model），即切面可以以任一顺序组合使用。然而，这往往并不现实，有时候我们希望多个切面之间存在先后顺序，比如，我们总是希望验证用户安全性的增强在其他操作之前执行，而且很多框架都支持顺序执行，例如拦截器框架会按照配置顺序执行拦截器。

由于增强逻辑可以横切多个模块，这样，很难让各个模块的开发人员都知道我们为某些连接点织入了增强，由此可能造成相关开发人员不知道究竟执行了哪些逻辑代码。

在使用AOP编程的过程中，应该规范团队开发，对相关代码进行有效地约束和管理，譬如，所有切入点的定义遵从一定的命名规范，或者在指定的目录下，这样方便开发人员查阅代码。并且在开发过程中，可以完善相关文档，对软件中哪些地方使用了AOP技术一目了然。

不容易测试和调试。如果你选择生成织入增强的源代码，无论使用编译器织入，还是在类加载期织入，这些代码都不易调试，排错困难。

15.6　OOP还是AOP

我们使用SoC方法把系统分解成小单元时，就不可避免地遇到需要把一些洒落在零散小单元里的相同逻辑模块化为同一单元的问题。不管是过程式编程、函数式编程还是OOP编程，只要你把问题由大到小进行分割，都会出现相同的问题。

由于近年来OOP编程的迅猛发展，我们往往把这个诟病归结于OOP，当出现AOP编程技术时，自然而然产生了AOP编程是否能像OOP在某些开发领域取代过程式编程/函数式编程一样取代OOP编程的疑问。

其实纵向切割系统是非常有用的，大多数问题在纵向上分解之后非常容易解决，如果横向分解系统，那就会产生大量的切面，大量切面产生的叠加效果往往是难以估计的、灾难性的。

可见，单纯的使用水平切割系统和垂直切割系统都会引入问题，只有把二者结合起来才能帮助我们更加完善地模块化问题，现在大多数软件开发人员都认为AOP是OOP的重要补充。

15.7　总结

AOP经过10多年的发展，在企业级软件开发方面发挥了举足轻重的作用，我们在享受AOP编程带来方便的同时，也要注意AOP编程带来的负面因素，特别是切面过多所产生的不可控的叠加效果。

OOP和AOP在模块化和代码重用方面功不可没，重复并没有因此而消失。不管怎样，它们二者模块化的最小单元都是类，粒度相对来说还是比较小的，有时候我们需要更为宏观的重用，例如重用服务、系统等这些更大粒度的单元。

比如，公司有一个日常收发邮件的系统，现在正在开发一个新的财务系统需要为一些紧急事件发邮件通知，如果为此财务系统再开发一个新的邮件系统，这将会是巨大的浪费：对于邮件系统来说，发送一封通知和发送一封普通邮件并无很大区别。

为了重用这些软件服务，出现了面向服务的架构（SOA, Service-Oriented Architecture），面向服务的架构提供更大粒度[1]的单元封装，便于服务之间的重用，比较流行的实现是Web Service。

SOA让我们在重用方面迈出了更远的一步，但并不表示重复的结束，其实DRY涉及的内容非常广泛，并非一两种技术就能完全实现，它涉及的领域同样十分广泛，不仅在软件开发领域，其他领域都会出现重复的知识，并且这些知识的粒度同样有大有小。

[1]面向服务的架构是粗粒度的架构，尽管有人会提出粒度可大可小，但是实际都是一些粗粒度地服务/组件接口，只是在这些粗粒度的服务/组件里，粒度还能根据大小进行分类。举一个极端的例子，如果你为isNull（Object o）这样小的方法也建立一个Service的话，那将是一个笑话。

15.8　本章关键词

AOP

OOP

关注点（Concerns）

Separation of Concerns

切面（Aspect）

连接点（Joinpoint）

增强（Advice）

前增强（Before advice）

后增强（After returning advice）

异常增强（After throwing advice）

最终增强（After/finally advice）

环绕增强（Around Advice）

切入点（Pointcut）

织入（Weaving）

切面（Aspect）

拦截器（Interceptor）

J2SE多态代理

类加载器

AOP联盟（AOP Alliance）

第16章　面向对象开发

懂得设计模式并不等于已经学会了使用面向对象进行开发，虽然熟悉这些模式有助于使用这些成功的经验来解决问题，但是由于问题本身的复杂性和隐藏性等特点，如何使用面向对象的方法建立合理的模型就成为了关键。

本章，笔者将和大家一起讨论如何建立OO模型。

16.1　写在面向对象设计之前

我是一个Agile和Domain-Driven Design（简称DDD）的爱好者。在以下各节的讲述中，所提到的一些开发设计方法很大部分和它们有关，如果你是它们的反对者，可以忽略本章。这里首先简单介绍一下两个概念。

1.Agile（敏捷开发）

Agile兴起于20世纪90年代，像大家非常熟悉的Scrum（1995），Extreme Programming（极限开发，简称XP，1996）等轻量级软件开发方法现在都被认为是Agile的实现方法。在2001年2月，17个软件开发大师在美国犹他州（Utah）的一处滑雪胜地度假，期间讨论了各种轻量级软件开发的方式，最终一起发表了题为“Manifesto for Agile Software Development”的软件开发宣言。在此宣言中，提出了12条软件开发原则，由此便形成了我们以后知道的敏捷开发和敏捷联盟。

2.Domain—Driven Design（领域驱动设计）

Domain-Driven Design（DDD），是一门面向对象设计的哲学[1]，在解决复杂领域问题方面，卓有成效。Eric Evans在“Domain-Driven Design：Tackling Complexity in the Heart of Software”一书，详细讲述了如何进行DDD开发，此书极具影响力，也是笔者最喜欢的软件设计图书之一。对于极限开发（XP），缺乏经验的新手往往会带来过于简单的设计，而一些经验丰富的老手在开发设计中，往往沉浸于技术和设计，带来了过度的开发（Over-engineering），DDD开发介于过度开发和过于简单的开发之间，能为复杂的领域迅速迭代提炼出出色的模型。

我们有很多人已经使用模式好长时间了，而是否享受到面向对象设计的好处，并不见得。软件的核心是模型，如果在不能表达丰富功能需求的模型上应用模式，将不会有太大成效，往往因为误使了模式而带来开发的泥沼。例如，如果使用模板方法模式解决第12章提出的问题时，则会弄巧成拙。

本章将给读者分享一些我在面向对象开发设计过程中的经验，虽然这不是本书的重点，但对读者进行OOP开发设计有重要意义，其中有些部分虽然超越了OOP的范畴，但都属于软件开发设计的范畴。

[1]参见DomainDriven Design：Tackling Complexity in the Heart of Software, Eric Evans，2003，在序言中，作者原话为：“……it has never been formulated clearly, a philosophy has emerged as an undercurrent in the object community, a philosophy I call domaindriven design”。

16.2　汲取知识

前不久，我们承接了一个项目，其中一个模块需要做两个系统的集成。一开始，做该集成开发的工作人员有两个，过了一个月，其中一位离开了公司。又过了一个月，集成仍然迟迟不能发布，于是经理派我去着手解决这个问题。由于我只了解其中一个系统，对另一个系统，除名字之外我一无所知，我并没有立即打开繁琐的技术文档，而是向之前的开发人员了解需求。开发人员告诉我，没有正式的需求文档，只有一封潦草的邮件，其中关于集成需求的描述只有一句话：系统A和系统B的集成。

由于领域专家不在，我就坐下来和该开发人员进行了半个小时左右的沟通，慢慢地我发现该开发人员对需求并不太理解，我提到的很多细节问题他不能如愿作答。此时，他对需求的理解就是：把系统A的一条记录发送到系统B，然后生成一条新记录，由于数据格式不匹配，二者之间需要定义一个映射。他在这上面花了很多时间做尝试，最后都被领域专家否定，他也不知道领域专家需要什么。

所幸的是，一天后，领域专家赶了回来，我直接找他了解需求，在长达2个小时的讨论中，我发现之前的交流存在一些问题：领域专家未能完全表达他的想法，也不知道开发人员能否按照自己的想法做出功能，而软件开发人员也不知道真正的需求是什么，双方并未有效沟通。

根据这次探讨，我了解到：由于系统A和系统B之前就有集成，后来系统B中的一个模块发生了变化，主要是为这模块的模型新添了一些属性，导致使用之前的映射不能创建一条完整的记录，为了在不修改系统A的前提下，要使之前的集成正常运行，我们要给那些属性配置一些默认值。

这次讨论之后，我看了下系统A和B集成的技术文档，然后画出一个简单的设计草图，如图16-1所示，你也许发现，它简单到了极点。

图16-1

需求比较简单，但在动手之前，我拿着设计草案又和领域专家进行了一次讨论，发现我们可以根据记录的某些属性，给出更为具体的默认值，比如，如果记录的一个属性优先级的值为“高”，那么新添加的这个属性影响我们最好设置为“重要”。而之前没有深入讨论这个的原因是，他觉得这样可能难于实现，所以自己也未朝这个方面多想。经过这次讨论之后，我们确定了设计方案图，如图16-2所示。

图16-2

Condition表示如果Module记录信息满足该条件，那么就是用此模板提供的默认值给记录赋值。真实的设计草图当然比这个复杂，这里为了讨论的方便，我们就到此为止，不再给出细节。

经过这两次的讨论，我和领域专家敲定了我们的需求，他也理解了我们能为用户做些什么，我们也知道他们想要什么。后来的事情就相对简单了许多，经过我两天多的苦力编程，终于顺利完成了开发。

这样，2个月未做完的需求我一共花了不到4天就做完了（包括讨论需求），这并不是我的技术能力比之前的开发人员好很多，原因在于。

开发人员在编写软件时，对需求和领域知识总是未引起足够重视，只专注于学习二者如何集成的一些技术问题，越是简单的需求越是不重视，导致做了很多无用的工作；

领域专家并非对领域的每一方面有着极其深入的了解，只有和他们进行深入讨论，他才会明白你能做什么，不能做什么，这样才能开发出比他们当初想象更为强大的功能。

提示：能够高效建模的人具有超强的消化领域知识的能力，也能帮助用户分析用户到底想要什么，消化知识需要的不是独立的单方面的行为，而需要开发人员去主导，开发团队和需求专家一起分析信息，消化知识，才能建立有效的模型。

16.3　横看成岭侧成峰

我们很多人都有表，它们形形色色，各种各样，有的是用于装饰的，有的是可以在水里显示时间的，也有的是专门记录体育比赛时间的……，我自己也有一只表，让我来说说它的功能吧。

我的这只表有闹钟的功能，可以提醒我上班不要迟到；能播放MP3，我可以用它听音乐；使用它还能查阅各种文件，还能照相留念；这些还不够，我的这只表还提供上网浏览网页和收发电子邮件的功能，让我随时随地都能工作（我不是工作狂，千万别凭此判断）；这些还不够绝，更绝的是，我能用它收发短信，接听电话。

全世界有数以亿计这样的手表，我相信你也有块这样的手表，只是在日常生活中，我们大部分人把它们叫做手机。自从我有了手机后，我再也没有用过手表了，每当有人向我询问几点时，我都会拿它看时间。它到底是手表还是手机呢？

苏东坡当年在游览庐山时，被美景所迷，“往来山南北十余日”后，终于做出“横看成岭侧成峰，远近高低各不同”的诗句。当然他在那里的寓意和我这里的用意并不相同，我在这里要强调的是，站在不同角度分析事物时，事物所呈现的形态不尽相同。从某种角度来认识手机，它就是“手表”。

这么说的目的并不是不去认识必要的手机特征来完善一个合理模型，而是强调让那些不相干的信息别去干扰我们对模型的认识。那么在抽象一个时钟系统的模型时，手机的通话功能对我们来说是无用的，不需要去提取一个通话模型，所以，而手机和时钟之间的关系可能如图16-3所示。

图16-3

在软件设计过程中，即使对于同一事物建模，也不一定能够使用相同的模型，因为你站的角度不同。譬如，在一个简单的工资系统里，经理的角色和其他员工的角色没太大的区别，每个月都需要给他们账户发钱，工资系统不需要了解经理下属有多少职工，而在职工管理系统里，我们需要知道经理下属有多少职工，所以二者模型并不相同。甚至在同一个应用里，相同事物的模型也不一定完全相同。Eric Evans在“Domain-Driven Design：Tackling Complexity in the Heart of Software”[1]对此进行了更为深刻的探讨。

[1]参见“DomainDriven Design：Tackling Complexity in the Heart of Software”，Part IV：Strategic Design的Chapter Fourteen.Maintaining Model Integrity一章，指出一个复杂的应用里，实现相同的领域模型非常困难，往往存在多个领域模型。

16.4　提炼模型

软件设计的其中一个目标是实现模块内部的高内聚，要实现这个目标，对象应该职责分明，如果对象承担了应该由其他对象承担的职责时，它是低内聚的，换句话说，高内聚需要限制对象的职责，尽可能为它们分解单一的、不重复的职责。

在第12章，我们讲述了策略模式，从模型来看，其实它还不算是精炼的。

让我们再仔细看看这个模型，我们为HappyPeople类添加了addTransportation（Travellable transportation）、getTransportation（int index）和removeTransportation（Travellable transportation）等方法，添加这些方法的意图无外乎就是管理Travellable对象。随着业务的深入，或许需要更多的方法来管理它们，这样HappyPeople多出了很多与Travellable管理相关的职责。其实更进一步，我们应该分解这个类，把管理Travellable对象的职责封装在一个新类里，这样便形成了如图16-4所示的设计图。

图16-4

这样，类之间的结构便是高内聚的，对于Travellable对象的处理都在Travel对象里，HappyPeople类便能更加关注自己的职责了。

我们看看Travel类的实现代码，大致如下所示。

HappyPeople类的代码如下所示。

于是，我们撰写测试的代码大致如下所示。

最后，执行结果如下所示。

提炼模型不是在开发中的某一个阶段应该做的事情，而是一个不停迭代的过程，随着我们对领域知识的不断消化，我们也就能提炼出更为精准的模型。提炼模型比使用技术更为重要，一个好的软件架构能够使我们选用不同的实现技术（所谓的Reversibility[1]），而如果模型粗糙、拙劣，不管使用多么先进多么流行的技术，都是无法应对复杂的领域问题。软件的核心不是技术，也不是模式，而是模型。

[1]参见“The Pragmatic Programmer”一书的第二章：A Pragmatic Approach。

16.5　应用设计模式

使用设计模式和提炼模型有时候是分不开的，有些设计模式可以直接拿过来做模型，例如状态模式、策略模式和组合模式等。在一个完整的软件里，不仅需要使用多个模式，可能需要把多个模式结合起来，比如，使用代理模式时，可以结合工厂模式，让工厂模式实例化代理对象；又如，在使用组合模式时，为了使用没有实现Component接口的既有类，可以使用适配器模式达到此目的。

在挖掘领域问题时，应用这些设计模式的经验，可以把那些隐含的、深层次的概念找出来，提炼出更为有用的模型。

16.6　不能脱离实现技术

提炼模型时，如果只专注于业务模型，而忽略了实现技术，这样有可能导致因无法实现而最终搁浅。

三年前，我曾经开发了一款应用软件，当时为模型设计了回滚处理，由于使用了组合模式，当时并没有考虑到嵌套事务的回滚问题，当时的问题是：在一个树形对象图中，如果子对象的事务回滚了，我们需要对其父对象的所有子对象的事务回滚，并且父对象的事务也要回滚。比如，对象a有三个子对象b、c和d，当b和c提交了操作，而d对象的事务需要回滚时，那么已经提交事务的父对象a和兄弟对象b和c都需要回滚。由于当时只能使用简单的JDBC事务，这种需求是不能被支持的，虽然JTA（Java Transaction API）[1]2事务可以实现（执行子对象提交之前，在父对象a设置回滚点，如果b, c提交成功、d失败，可以回滚到父对象a设置的回滚点），但是由于其中种种原因，我们并不能使用JTA。

当时的开发工作已经完成了3/4，幸运的是，当时碰巧读到一篇文章，提到了Ebay电子商务网站的应用并没有使用数据库事务管理，由此大受启发。我开始思考不是使用传统的数据库回滚，而是实现了业务操作的回滚，即如果d操作需要回滚，我让a、b和c对象执行业务上的回滚操作。试想，如果不是受这篇文章的启发，可能此项目将会失败。

我这里要强调的是，在任何时间，都要联系到实现技术。有时候即使技术能够实现，对你的团队来说，可能由于大家都未曾考虑过这些问题，当最终摆在面前时，大家便会手足无措。实现技术是多种多样的，即便你用到的实现技术不是最好的，但它至少能解决你的问题；倘若刚开始你没有这样做，即便有最简单的技术实现，也可能因为你当时的无知而失败。

联系到技术有利于把握时间，成本等其他因素，降低其中的风险。上述例子之所以可以考虑实现业务上的回滚，是因为我们的应用从一开始就支持那些业务的回滚操作，如果不支持，那么重新开发将是极其复杂的事情，因为会引起核心的大修改，很可能导致该软件半途而废。

[1]是Java EE分布式事务处理的APIs，是关于跨越多个XA资源的分布式事务的Java EE标准。

16.7　重构

什么是重构呢？Martin Fowler在“Refactoring：Improving the Design of Existing Code”一书对重构定义[1]如下。

Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.

重构是采用这样一种方式改变软件系统的一个过程：它不改变代码的外部行为，但是提高了内部结构的质量。

重构是一个持续过程，提炼模型也属于重构，重构不仅能够提高代码质量，还能偿还技术债务[2]，为以后新功能的扩展提供了可能性。

关于如何进行重构，大家可以好好研读Martin Fowler的这本书籍，它详细讲述了重构代码的过程。

但是，重构绝对不等于推翻了重做，有相当一部分初学者甚至一些所谓的“资深”开发人员可能由于追求完美的“偏执”所致，他们都喜欢推翻了重做。

重新开发虽然可以规避之前的一些拙劣的设计，但是在重用之前的健壮代码上会大打折扣；

而且，由于之前的拙劣设计，代码也不易阅读，为移植和重写这些拙劣代码带来了问题，往往因为不理解其中的“特殊含义”而重写的逻辑不能很好运行；

另外，和一般的重构一样，新的代码总会引入新的未知问题。

总之，重构并不等于推翻了重做，它也不是重构的最好办法，做推翻重做决定时一定要慎重。

一个良好的项目，开发步骤往往是由慢变快的，起步比较慢，在往后的迭代开发过程中，会变得越来越快；而一个急功近利型的项目往往一开始就快速启动，在随后的开发中，过分地强调了快速发布，导致开发了很多不可扩展代码，随着新的功能不断加入，开发设计变得越来越拙劣，项目也变得越来越慢。如果你的项目目前处境是这样，那么最好停下脚步，考虑是否要花点时间对它进行重构。

我们既要避免轻率地“推翻了重做”，也要避免因为安于现状而导致项目陷入泥沼。

[1]参见该书的前言部分对重构的定义。

[2]Ward Cunningham于1992年在自己的一篇实验报告里引入这个比喻。Vikas Hazrati在如下链接里对技术债务进行了归纳和讨论：http：//www.infoq.com/news/2009/10/dissectingtechnicaldebt

16.8　过度的开发（Over-engineering）

几年前，曾参与了一个产品重构的开发，该产品有4个模块，为了避免风险，我们从一个最简单也最基本的模块入手，原计划是在半年内完成重构，然而经过了一年的开发仍未结束，设计人员总是觉得某些方面可以做得更好，特别是技术方面，他们经常发现某些技术上的一些小的以前未使用的功能，他们拼命地把这些功能加入软件，并且随着时间的发展，新的技术、新的软件和新版本的发布带来了更好的功能，这些最终让他们看花了眼，导致不断推翻之前使用的技术，而未提炼模型，最终软件迟迟不能发布，功能却没有得到明显的增强。

一年之后，我便离开了那家公司。又过了两年，我碰巧遇到一位以前参与开发的工程师，他告诉我那个简单的模块还没有完成重构，其他模块也没有开始重构。

这是一个真实的故事，我至今还记得他们为了使用Hibernate的一个小小功能而大肆修改，导致其他使用该产品的项目停滞等待。虽然他们在不停地完善软件，但只注重了使用技术，而没有花时间去提炼完善的模型，殊不知最重要的是模型。其间往往导致不仅功能没有增强，反而因为模型的粗糙而不得不经常推翻以前所使用的新技术。

虽然导致三年仍未完成一个模块重构的原因是多方面的，但是过度的开发是其失败的一个重要原因。从某种程度上来说，一个软件在任何时候都需要重构，但是完美的软件是不存在的，你必须首先承认这一点，然后才能开发出成功的软件。过度开发不仅浪费了时间和金钱，还造成了代码过度复杂臃肿，产生很多无用的接口和配置，为了增加一个小小的功能必须得大动干戈，给维护造成很大的麻烦，而且可配置的功能太多会造成使用上的不方便。

在软件开发中，我们经常使用到一条原则叫Pareto原则[1]（也称为8020原则），这条原则在各个领域都经常被使用到。

20%的工作需要花费80%的精力。

为了避免过度开发，我们在这里也可以应用该原则。

你的开发可以让80%的常用功能非常容易实现，而剩下20%的功能可以实现但需要花费较多的精力。

这样，你就可以避免为那些不常用的功能和需求而花费大量的精力，导致过度的设计。使用XP（极限开发）可以由浅入深地开发，一定程度上避免过度的开发，但是经验并不丰富的开发人员在开发过程中往往造成了过于简单和粗糙的设计，而不易扩展和完成必要的功能。既想避免过度开发，又能设计出扩展性良好的软件，绝非一朝一夕就能够做到的。

Eric Evans的“Domain-Driven Design：Tackling Complexity in the Heart of Software”，书中详细地讲述了如何使用DDD解决复杂领域问题，使用DDD开发能够在开发中平衡这二者，有兴趣的读者可以阅读此书以作更深入的了解。

[1]Pareto法则最初在1906年被Vilfredo Pareto发现，当时是为了说明经济现象：意大利80%的土地掌握在20%的人手里。这条原则被广泛地使用到各个领域，例如在商业上：80%的销售来自20%的客户。

16.9　总结

客观世界要反映在计算机里，需要我们为其归纳出合理的模型，为问题建模是软件开发最核心的问题之一。如果建立的模型本身有问题，哪怕你是设计模式的专家，也不能够开发出有用的软件。

在本章，我们讨论了关于OOP设计应该注意的一些问题，以上提到的所有过程都是不停迭代的过程，它们之间并没有绝对的先后顺序。软件设计的关键是建立能够表达丰富功能需求的领域模型。

真的OOP开发设计高手要有优秀的抽象概括能力，不断地汲取领域知识，选择合适的设计模式，甚至创造你所需要的模式，为问题领域迭代提炼出完善的模型，结合相关的实现技术，才能开发出成功的软件。

16.10　本章关键词

OOP

Agile

Domain-Driven Design（简称DDD）

Scrum

Extreme Programming

过度的开发（Over-engineering）

Reversibility

重构

技术债务

JTA

Pareto原则

第17章　结语

本章将对本书内容做个回顾，总结我们学到了什么，主要内容如下。

面向对象开发的范式。

本书讲解的一些面向对象的设计原则。

使用OO眼光分析问题的本质。

17.1　感悟

笔者认为，尽管面向对象的开发早已不再新鲜了，但是很多人仍然倾向于使用面向过程的开发。这样，虽然使用了OOP语言开发设计，却没有享受到面向对象所带来的简单性、高质量、可维护性、易扩展性、高性能、可重用性、可伸缩性等方面的好处。笔者撰写此书的目的就是试图通过一些常用的设计模式和设计原则来介绍如何通过面向对象视角来分析和设计问题，分享它给我们带来的设计上的好处。

笔者发现，如果站在为了使用设计模式而学习设计模式的角度考虑问题，不会给你的设计带来如愿以偿的效果，我们要学会使用OOP的眼光分析这些模式，才能明白这些模式的本质，才能明白有经验的OOP开发者如何使用OOP的眼光开发软件。

本书重点结合面向对象开发范式向读者介绍这些模式，中间穿插一些软件设计原则，这些原则不一定只是关于OOP的，但是对软件设计一定是至关重要的。在本书第15章，笔者讲述了OOP开发设计的缺陷，这也是AOP能发展的最主要原因。

下面我们对这些模式做一下简单的回顾。

17.2　面向对象的开发范式

对象是包含了方法和数据的结构体，这给开发者带来了新的视角去分析问题。

面向对象的最大好处就是封装，让我们再回顾一下这些设计模式都封装了哪些方面，如表17-1所示。

尽管对象的粒度有大有小，但是一般地，对象的粒度超过了单个的方法或者结构体，这种大的模块化的单元给我们带来了很多实现上的好处。

为封装提供了前提保证，使我们可以实现数据和方法的隐藏。

易于分解问题：为了实现一个系统，我们总是把系统分割成大模块，然后再分割成小的单元实现，如果没有对象，我们需要把大的模块直接分解成方法，这样分割的跨度比较大。而对象相对粗的粒度正好弥补了这个缺陷，使我们在设计系统时，把视角转向了对象，考虑对象应该实现什么功能，以及它们之间是怎么协作的，这样问题更加容易分解。易于重用：由于对象的粒度较大，只要重用一个对象，就可以重用对象里的所有可见的方法。

易于组合更强功能：只要组合这些大粒度的对象，让它们按照需要进行协作，就能完成更强大的功能。

多态可以让我们关心在什么时间去做，而不去关心如何去做，因为如何去做可以在后续开发中扩展。多态强调以相同的方式处理不同类型的对象，而这些对象有什么样的行为却在运行时决定。

OOP强调面向接口和抽象的编程，隐藏了具体的实现，减少了耦合。

17.3　一些原则

我们这里简要总结一下前面讨论过的一些软件设计原则，如表17-2所示。

17.4　写在模式之后

很久之前，和朋友去一家快餐店吃饭，我点了一个汉堡、一包薯条和几包番茄酱，我留下了一包番茄酱，把其他所有番茄酱都挤到汉堡中了，我的朋友瞪大眼睛看着我，然后一本正经地对我说，番茄酱是用来吃薯条的，不是吃汉堡的。我顿时哑然失语。

模式不是你把它们强加于你的设计，你的设计就会变得优雅而成功了；最重要的事情是了解你的领域问题本身，你如果能够清楚认识你的问题本质而非臆想它们时，你就能在设计时自然而然地选择它们、使用它们。当你需要番茄酱作为调味剂的任何场合，只要你需要它，你就可以添加它，不会再有“番茄酱是用来吃薯条”的束缚。

我们要避免刻意地为使用模式而拼装设计，这不会带来优雅的设计，而是要站在你的问题上，使用OO眼光，分析你所遇到问题的本质，只要到那一步，不管问题有多复杂，我们总能够找到需要的模式（不管是别人的还是自己创造的）把我们领入设计的道路。也许那时，你就真正地懂得了OO设计，至于设计模式，也就没有那么重要了。

我相信，读者通过这本书的介绍，得到的不仅仅是了解模式，而是学会使用OO的眼光分析问题，解决问题。

17.5　本章关键词

创建型模式

模板方法模式

装饰器模式

代理模式

适配器模式

外观模式

组合模式

策略模式

状态模式

观察者模式

DRY原则

好莱坞原则/控制反转

OCP原则

最少知识原则

封装变化

优先使用合成

Pareto原则

附录A　推荐阅读资源

本书是一本关于设计模式理论性和实践性的书籍，希望本书能给读者对面向对象的开发带来启发和更有价值的思考。在本书的写作过程中，笔者参考了大量书籍，其中很多书籍值得一读，希望能给有这方面需要的读者一些建议。

最后，作者给出一些网站和论坛，希望读者能够实时关注有关开发的新话题。

1　Java语言相关学习图书

这里给出的英文书籍都是英文原版，由于有些书籍的中文翻译版本较多，在这里就不一一列举，读者有需要可以购买相应的中文译本。

1.Bruce Eckel.Thinking in Java，3rd Edition.Prentice-Hall, December 2002

这本书籍已经是第四版了，新版介绍了Java 5的一些新的语法。它是公认的Java语言学习的权威书籍，不仅讲述了Java语言的语法，还涵盖了许多面向对象的思想，如果你想学习使用Java语言进行面向对象的编程与设计，此书非常值得一读。

2.Ron Hitchens.Java NIO.O'Reilly，2002

本书讲述了Java NIO的编程技术（特别是网络IO的编程）。

3.James Gosling, Bill Joy, Guy Steele and Gilad Bracha.The Java™Language Specification Third Edition.ADDISON-WESLEY，2005

这是一本介绍Java语言规范的书籍，大多数语法介绍书籍不会全面介绍JVM如何加载和初始化类、线程等一些相关技术和概念，这些都可以在此书中找到，并且它是Java语言的官方书籍，权威性不言而喻。

4.Joshua Bloch.Effective Java, Second Edition.Addison-Wesley，2008

如果你已经使用Java开发好几年了，但是你还想知道那些专业的Java开发人员如何编写高效代码的，这本书绝对值得一读。

5.David Flanagan and Brett McLaughlin.Java 1.5 Tiger：A Developer's otebook.O'Reilly，2004

如果你不熟悉Java 5的新语法，可以参考本书和“Think in Java”第四版相关章节。

2　J2EE技术相关图书

1.Rod Johnson.Expert One-on-One J2EE Design and Development.Wiley Publishing, Inc，2003

这本书介绍了一些J2EE的常用技术，深入探讨了J2EE编程中经常出现的问题和风险，帮助读者创建高效的J2EE应用。

2.Rod Johnson and Juergen Hoeller.Expert One-on-One J2EE Development without EJB.Wiley Publishing, Inc，2003

这本书可以说是上面一本书的续篇，它颠覆了一些传统的J2EE观点，审视了EJB所带来巨大复杂性。现在阅读本书可能没有当时那么震撼，因为读者对不使用EJB来创建J2EE应用已经习以为常了，很多Java架构采用SSH（Struts+Spring+Hibernate）等技术创建应用。但它详细讲述了Spring核心框架的实现技术，对正在使用Spring框架或者对其实现技术感兴趣的读者，此书值得一读。

3.Deepak Alur, John Crupi and Dan Malks.Core J2EE Patterns：Best Practices and Design Strategies, Second Edition.Prentice Hall PTR，2003

这本书籍主要讲述了一些非常重要的J2EE模式，J2EE架构师和开发人员值得一读。

3　面向对象设计相关图书

1.Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides.Design Patterns：Elements of Reusable Object-Oriented Software.Addison-Wesley，1995

这是影响最大的设计模式的经典书籍，读者在使用相关模式时，都可以拿来翻一翻。

2.Andrew Hunt and David Thomas.The Pragmatic Programmer：From Journeyman to Master.Addison-Wesley，1999

这本书讲述了如何高效开发出高质量软件的方法，讲述过程中穿插了很多寓言故事，深入浅出，是一本有经验的软件开发人员继续“修炼”的哲学书籍。

3.Alan Shalloway and James R.Trott.Design Patterns Explained：A New Perspective on Object-Oriented Design，2nd Edition.Addison-Wesley，2004

这本书从面向对象的视角分析设计模式，是一本学习模式的好书籍。

4.Eric T Freeman, Elisabeth Robson, Bert Bates and Kathy Sierra.Head First Design Patterns.O'Reilly Media, October 2004

这本书非常适合初学者学习设计模式，由于使用了Head First的写作风格，通俗易懂。

5.Martin Fowler.Refactoring：Improving the Design of Existing Code.Addison-Wesley，1999

这本书讲述了如何重构你的代码，重构代码是一个复杂的过程，很容易引起各种各样的问题，这本书讲述了重构的整个过程，书写风格也十分流畅，非常易于阅读。

6.Martin Fowler.Analysis Patterns：Reusable Object Models.Addison-Wesley，1997

做金融开发的人员都有必要阅读这本书，在医药领域、金融领域、测量领域、贸易等领域使用书中所提到的分析模式建模有莫大的帮助，当然这些分析模式不局限于这些领域。在此书中，Martin Fowler把自己丰富的对象建模经验与读者分享，如果你想为复杂领域建模，但是没有足够把握，强烈推荐你学习此书。

7.Eric Evans.Domain-Driven Design：Tackling Complexity in the Heart of Software.Addison-Wesley, August，2003

解决复杂领域问题的关键是有精炼的模型，这本书讲解了如何使用领域驱动设计迅速提炼有用的模型，本人强烈推荐此书。

8.陈臣，王斌，研磨设计模式，北京：清华大学出版社.2010

这是推荐的为数不多的中文原创设计模式类图书，终于看到有国内高手写出高档次的技术资料了。此书实际上算得上GoF设计模式的精密诠释版，字里行间充斥着行家里手的思维与想法，配合GoF阅读，效果将更好。

9.程杰，大话设计模式，北京：清华大学出版社.2008

推荐这本书时犹豫再三，其一，这本书是基于.NET实现的，其二，内容似乎不够深入。但是这里还是推荐一下，因为本书作为一本菜鸟入门书非常合适，阅读也很轻松，说不定因为这本书的低门槛会将你带入缤纷的编程世界。

4　给Agile（敏捷）开发人员推荐的书籍

1.Kent Beck and Cynthia Andres.Extreme Programming Explained：Embrace Change.Addison-Wesley，2004

这是敏捷开发人员必读的一本书，它为大家消除了很多开发上的错误观念。

2.Henrik Kniberg.Scrum and XP from the Trenches（Enterprise Software Development）.Lulu.com，2007

这本书是一本非常浅显易读的Scrum书籍，作者把一年来实施Scrum过程和经验进行分享，没有高深的理论，只有故事和实践。这本书的电子版本在InfoQ网站上有下载：http：//www.infoq.com/minibooks/scrum-xp-from-the-trenches

5　网站和论坛

读者可以从以下网站了解一些J2EE前言技术和话题。

http://www. theserverside.com/

这个大家最熟悉不过了，是讨论J2EE技术的最大社区，不用多做介绍了。

http://www. infoq.com/

企业级软件开发的一个很活跃的社区之一，这里讨论一些最流行开发技术和方法，包括SOA、Agile、Rubby等。此网站有很多软件开发名人视频采访。它的中文站点为：http：//www.infoq.com/cn/，其上不仅翻译英文站点上的文章，还有一些国内软件开发牛人的一些采访视频和文章，还制作有免费的电子期刊，值得软件开发爱好者浏览和收藏。

http://stackoverflow. com/

国外比较著名的技术社区，特别是问答频道非常出名，一个问题一般在几分钟之内就有人给你回复，而且通常是非常专业的回答。

http://picocontainer. org/

Pico Container官方网站。

http://code. google.com/p/google-guice/

Guice官方网站。

http://www. springsource.org/

Spring的门户网站。

http://www. hibernate.org/

Hibernate的官方网站。

http://www. opensymphony.com/xwork/

xwork框架的官方网站。

http://www. opensymphony.com/webwork/

webwork框架的官方网站。

http://struts. apache.org/

struts框架的官方网站。

参考文献

[1]Gregor Kiczales, John Lamping, Anurag Mendhekar et al. Aspect-Oriented Programming.In Proceedings of the European Conference on Object-Oriented Programming（ECOOP），LNCS 1241，Springer-Verlag，1997

[2]Aspect-Oriented Software Development Community＆Conference. Web site：http：//www.aosd.net

[3]Ramnivas Laddad. AspectJ in Action：Practical Aspect-Oriented Programming.Manning，2003

[4]Hafedh Mili, Amel Elkharraz and Hamid Mcheick. Understanding separation of concerns.In Proceedings of the 3rd Workshop on Early Aspects，3rd International Conference on Aspect-OrientedSoftware Development.Lancaster，2004

[5]Eduardo Kessler Piveta and Luiz Carlos Zancanella. Observer Pattern usingAspect-Oriented Programming.In Proceeds of the 3rd Latin American Conferenceon Pattern Languages of Programming.Porto de Galinhas, PE, Brazil, August 2003

[6]Bill Burke and Adrian Brock. Aspect-Oriented Programming and JBoss.Web site：http：//onjava.com/pub/a/onjava/2003/05/28/aop_jboss.html？page=1.May 2003

[7]Bruce Eckel. Thinking in Java，3rd Edition.Prentice-Hall, December 2002

[8]Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. Design Patterns：Elements of Reusable Object-Oriented Software.Addison-Wesley，1995

[9]Alan Shalloway and James R. Trott.Design Patterns Explained：A New Perspective on Object-Oriented Design，2nd Edition.Addison-Wesley，2004

[10]Eric Evans. Domain-Driven Design：Tackling Complexity in the Heart of Software.Addison-Wesley, August，2003

[11]Joshua Bloch. Effective Java：Programming Language Guide.Addison-Wesley，2001

[12]Andrew Hunt and David Thomas. The Pragmatic Programmer：From Journeyman to Master.Addison-Wesley，1999

[13]Patrick Lightbody and Jason Carreira. WebWork in Action.Manning，2006

[14]Rod Johnson. Expert One-on-One J2EE Design and Development.Wiley Publishing, Inc，2003

[15]Rod Johnson and Juergen Hoeller. Expert One-on-One J2EE Development without EJB.Wiley Publishing, Inc，2003

[16]Martin Fowler. Analysis Patterns：Reusable Object Models.Addison-Wesley，1997

[17]Eric T Freeman, Elisabeth Robson, Bert Bates and Kathy Sierra. Head First Design Patterns.O'Reilly Media, October 2004

[18]Ralph E. Johnson and Brian Foote.Designing Reusable Classes.Department of Computer Science, June/July 1988，Volume 1，Number 2，pages 22-35

[19]Christopher Alexander, Sara Ishikawa and Murray Silverstein. A Pattern Language：Towns, Buildings, Construction.Oxford University Press，1977

[20]Alexander Christopher. The Timeless Way of Building.Oxford University Press，1979

[21]Martin Fowler. Refactoring：Improving the Design of Existing Code.Addison-Wesley，1999

[22]Richard Monson-Haefel. Enterprise JavaBeans, Second Edition.O'Reilly，2001

[23]Ron Hitchens. Java NIO.O'Reilly，2002

[24]James Gosling, Bill Joy, Guy Steele and Gilad Bracha. The Java™ Language Specification Third Edition.Addison-Wesley，2005

[25]Joshua Bloch. Effective Java, Second Edition.Addison-Wesley，2008

[26]Deepak Alur, John Crupi and Dan Malks. Core J2EE Patterns：Best Practices and Design Strategies, Second Edition.Prentice Hall PTR，2003

[27]Kent Beck and Cynthia Andres. Extreme Programming Explained：Embrace Change.Addison-Wesley，2004

[28]Martin Fowler. TechnicalDebt.Web site：http：//martinfowler.com/bliki/Technical Debt.html.2004

[29]Martin Fowler. InversionOfControl.Web site：http：//martinfowler.com/bliki/Inversion OfControl.html.2005

[30]Martin Fowler. Inversion of Control Containers and the Dependency Injection pattern.Web site：http：//martinfowler.com/articles/injection.html.2004

[31]Vikas Hazrati. Dissecting Technical Debt.Web site：http：//www.infoq.com/news/2009/10/dissecting-technical-debt.Oct，2009

[32]Dirk Riehle. Framework Design：A Role Modeling Approach.Web site：http：//dirkriehle.com/computer-science/research/dissertation/index.html.2000

[33]Spring Framework. Web Site：http：//www.springsource.org/

[34]Guice. Web Site：http：//code.google.com/p/google-guice/

[35]Pico Container. Web Site：http：//picocontainer.org/

[36]Hiberante. Web Site：http：//www.hibernate.org/

[37]CGLib. Web Site：http：//cglib.sourceforge.net/

[38]XWork. Web Site：http：//www.opensymphony.com/xwork/

[39]Avalon. Web Site：http：//avalon.apache.org/

[40]Struts. Web Site：http：//struts.apache.org/

[41]Webwork. Web Site：http：//www.opensymphony.com/webwork/

[42]EasyMock. Web Site：http：//easymock.org/

[43]CGLib. Web Site：http：//cglib.sourceforge.net/

Table of Contents

- 封面

- 内容简介

- 前言

- 好友赠言

- 第一篇　模式介绍

- 第1章　谈面向对象和模式

- 1.1　什么是对象

- 1.2　面向对象的好处

- 1.3　重用

- 1.4　模式简史

- 1.5　什么是模式

- 1.6　学习设计模式的一些常见问题

- 1.7　本章关键词

- 第2章　第1个模式——模板方法（Template Method）模式

- 2.1　从回家过年说起

- 2.1.1　DRY（Don't Repeat Yourself）

- 2.1.2　变化+重复，如何维护

- 2.2　模板方法（Template Method）模式

- 2.2.1　使用继承

- 2.2.2　模板方法模式

- 2.3　引入回调（Callback）

- 2.4　总结

- 2.5　本章关键词

- 第二篇　创建对象

- 第3章　单例（Singleton）模式

- 3.1　最简单的单例

- 3.2　进阶

- 3.2.1　延迟创建

- 3.2.2　线程安全

- 3.2.3　Double-Check Locking

- 3.2.4　Initialization on demand holder

- 3.2.5　Singleton的序列化

- 3.3　总结

- 3.4　本章关键词

- 第4章　工厂方法（Factory Method）模式

- 4.1　工厂方法模式

- 4.1.1　类图

- 4.1.2　创建数据库连接对象

- 4.2　静态工厂方法

- 4.3　总结

- 4.4　本章关键词

- 第5章　原型（Prototype）模式

- 5.1　原型模式

- 5.2　寄个快递

- 5.3　实现

- 5.3.1　UML静态类图

- 5.3.2　代码实现

- 5.4　深拷贝（Deep Copy）

- 5.5　总结

- 5.6　本章关键词

- 第6章　控制反转（IoC）

- 6.1　从创建对象谈起

- 6.2　使用工厂方法模式的问题

- 6.3　Inversion of Control（控制反转）

- 6.3.1　IoC和DI（Dependency Injection，依赖注入）

- 6.3.2　Service Locator（服务定位器）

- 6.3.3　Dependency Injection

- 6.4　总结

- 6.5　本章关键词

- 第三篇　构建复杂结构

- 第7章　装饰器（Decorator）模式

- 7.1　记录历史修改

- 7.2　Open-Closed Principle（开放—封闭原则，OCP）

- 7.3　装饰器（Decorator）模式

- 7.3.1　类图

- 7.3.2　实现

- 7.3.3　一点变化

- 7.3.4　如何使用

- 7.3.5　测试

- 7.4　装饰器模式的优缺点

- 7.5　总结

- 7.6　本章关键词

- 第8章　代理（Proxy）模式

- 8.1　代理（Proxy）模式

- 8.1.1　类图

- 8.1.2　访问分布式对象

- 8.2　J2SE动态代理

- 8.2.1　类和接口

- 8.2.2　调用原理

- 8.2.3　实现同步

- 8.2.4　总结

- 8.3　和装饰器（Decorator）模式的比较

- 8.4　总结

- 8.5　本章关键词

- 第9章　适配器（Adapter）模式

- 9.1　打桩

- 9.2　其他适配器模式

- 9.2.1　类适配器

- 9.2.2　双向适配器

- 9.3　测试

- 9.4　和代理（Proxy）模式的比较

- 9.5　总结

- 9.6　本章关键词

- 第10章　外观（Facade）模式

- 10.1　外观（Facade）模式

- 10.2　Least Knowledge Principle（最少知识原则）

- 10.3　懒惰的老板请客

- 10.4　EJB里的外观模式

- 10.5　总结

- 10.6　本章关键词

- 第11章　组合（Composite）模式

- 11.1　组合模式概述

- 11.1.1　类图

- 11.1.2　使用组合（Composite）模式

- 11.1.3　测试

- 11.2　透明的组合模式

- 11.3　安全的组合模式VS透明的组合模式

- 11.4　还需要注意什么

- 11.5　总结

- 11.6　本章关键词

- 第四篇　行为模式

- 第12章　策略（Strategy）模式

- 12.1　既要坐飞机又要坐大巴

- 12.2　封装变化

- 12.3　策略模式

- 12.4　还需要继承吗

- 12.5　优先使用合成而非继承

- 12.6　总结

- 12.7　本章关键词

- 第13章　状态（State）模式

- 13.1　电子颜料板

- 13.2　switch-case实现

- 13.3　如何封装变化

- 13.4　状态模式

- 13.5　使用Enum类型

- 13.6　与策略（Strategy）模式的比较

- 13.7　总结

- 13.8　本章关键词

- 第14章　观察者（Observer）模式

- 14.1　股票价格变了多少

- 14.2　观察者模式

- 14.2.1　如何实现

- 14.2.2　观察者模式

- 14.2.3　Java标准库的观察者模式

- 14.3　总结

- 14.4　本章关键词

- 第五篇　终点还是起点

- 第15章　面向切面的编程（AOP）

- 15.1　记录时间

- 15.2　AOP（Aspect-Oriented Programming）

- 15.2.1　一些重要概念

- 15.2.2　OOP实现横切

- 15.2.3　AOP实现技术

- 15.3　AOP框架介绍

- 15.4　AOP联盟（AOP Alliance）

- 15.5　使用AOP编程的风险

- 15.6　OOP还是AOP

- 15.7　总结

- 15.8　本章关键词

- 第16章　面向对象开发

- 16.1　写在面向对象设计之前

- 16.2　汲取知识

- 16.3　横看成岭侧成峰

- 16.4　提炼模型

- 16.5　应用设计模式

- 16.6　不能脱离实现技术

- 16.7　重构

- 16.8　过度的开发（Over-engineering）

- 16.9　总结

- 16.10　本章关键词

- 第17章　结语

- 17.1　感悟

- 17.2　面向对象的开发范式

- 17.3　一些原则

- 17.4　写在模式之后

- 17.5　本章关键词

- 附录A　推荐阅读资源

- 1　Java语言相关学习图书

- 2　J2EE技术相关图书

- 3　面向对象设计相关图书

- 4　给Agile（敏捷）开发人员推荐的书籍

- 5　网站和论坛

- 参考文献

- 封底

Table of Contents

封面

内容简介

前言

好友赠言

第一篇　模式介绍

第1章　谈面向对象和模式

1.1　什么是对象

1.2　面向对象的好处

1.3　重用

1.4　模式简史

1.5　什么是模式

1.6　学习设计模式的一些常见问题

1.7　本章关键词

第2章　第1个模式——模板方法（Template Method）模式

2.1　从回家过年说起

2.1.1　DRY（Don't Repeat Yourself）

2.1.2　变化+重复，如何维护

2.2　模板方法（Template Method）模式

2.2.1　使用继承

2.2.2　模板方法模式

2.3　引入回调（Callback）

2.4　总结

2.5　本章关键词

第二篇　创建对象

第3章　单例（Singleton）模式

3.1　最简单的单例

3.2　进阶

3.2.1　延迟创建

3.2.2　线程安全

3.2.3　Double-Check Locking

3.2.4　Initialization on demand holder

3.2.5　Singleton的序列化

3.3　总结

3.4　本章关键词

第4章　工厂方法（Factory Method）模式

4.1　工厂方法模式

4.1.1　类图

4.1.2　创建数据库连接对象

4.2　静态工厂方法

4.3　总结

4.4　本章关键词

第5章　原型（Prototype）模式

5.1　原型模式

5.2　寄个快递

5.3　实现

5.3.1　UML静态类图

5.3.2　代码实现

5.4　深拷贝（Deep Copy）

5.5　总结

5.6　本章关键词

第6章　控制反转（IoC）

6.1　从创建对象谈起

6.2　使用工厂方法模式的问题

6.3　Inversion of Control（控制反转）

6.3.1　IoC和DI（Dependency Injection，依赖注入）

6.3.2　Service Locator（服务定位器）

6.3.3　Dependency Injection

6.4　总结

6.5　本章关键词

第三篇　构建复杂结构

第7章　装饰器（Decorator）模式

7.1　记录历史修改

7.2　Open-Closed Principle（开放—封闭原则，OCP）

7.3　装饰器（Decorator）模式

7.3.1　类图

7.3.2　实现

7.3.3　一点变化

7.3.4　如何使用

7.3.5　测试

7.4　装饰器模式的优缺点

7.5　总结

7.6　本章关键词

第8章　代理（Proxy）模式

8.1　代理（Proxy）模式

8.1.1　类图

8.1.2　访问分布式对象

8.2　J2SE动态代理

8.2.1　类和接口

8.2.2　调用原理

8.2.3　实现同步

8.2.4　总结

8.3　和装饰器（Decorator）模式的比较

8.4　总结

8.5　本章关键词

第9章　适配器（Adapter）模式

9.1　打桩

9.2　其他适配器模式

9.2.1　类适配器

9.2.2　双向适配器

9.3　测试

9.4　和代理（Proxy）模式的比较

9.5　总结

9.6　本章关键词

第10章　外观（Facade）模式

10.1　外观（Facade）模式

10.2　Least Knowledge Principle（最少知识原则）

10.3　懒惰的老板请客

10.4　EJB里的外观模式

10.5　总结

10.6　本章关键词

第11章　组合（Composite）模式

11.1　组合模式概述

11.1.1　类图

11.1.2　使用组合（Composite）模式

11.1.3　测试

11.2　透明的组合模式

11.3　安全的组合模式VS透明的组合模式

11.4　还需要注意什么

11.5　总结

11.6　本章关键词

第四篇　行为模式

第12章　策略（Strategy）模式

12.1　既要坐飞机又要坐大巴

12.2　封装变化

12.3　策略模式

12.4　还需要继承吗

12.5　优先使用合成而非继承

12.6　总结

12.7　本章关键词

第13章　状态（State）模式

13.1　电子颜料板

13.2　switch-case实现

13.3　如何封装变化

13.4　状态模式

13.5　使用Enum类型

13.6　与策略（Strategy）模式的比较

13.7　总结

13.8　本章关键词

第14章　观察者（Observer）模式

14.1　股票价格变了多少

14.2　观察者模式

14.2.1　如何实现

14.2.2　观察者模式

14.2.3　Java标准库的观察者模式

14.3　总结

14.4　本章关键词

第五篇　终点还是起点

第15章　面向切面的编程（AOP）

15.1　记录时间

15.2　AOP（Aspect-Oriented Programming）

15.2.1　一些重要概念

15.2.2　OOP实现横切

15.2.3　AOP实现技术

15.3　AOP框架介绍

15.4　AOP联盟（AOP Alliance）

15.5　使用AOP编程的风险

15.6　OOP还是AOP

15.7　总结

15.8　本章关键词

第16章　面向对象开发

16.1　写在面向对象设计之前

16.2　汲取知识

16.3　横看成岭侧成峰

16.4　提炼模型

16.5　应用设计模式

16.6　不能脱离实现技术

16.7　重构

16.8　过度的开发（Over-engineering）

16.9　总结

16.10　本章关键词

第17章　结语

17.1　感悟

17.2　面向对象的开发范式

17.3　一些原则

17.4　写在模式之后

17.5　本章关键词

附录A　推荐阅读资源

1　Java语言相关学习图书

2　J2EE技术相关图书

3　面向对象设计相关图书

4　给Agile（敏捷）开发人员推荐的书籍

5　网站和论坛

参考文献

封底