经典的GOF设计模式笔记

更新时间:2024-01-02 18:42:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

东方标准 http://soft.oneedu.cn

设计模式(Patterns in Java)

R子 总结笔记 2006.10.17

创建模式

Factory(工厂模式) : 工厂模式就象使用new一样频繁。

Prototype(原型模式) :用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

Builder(建造模式) :汽车由车轮 方向盘 发动机很多部件组成,同时,将这些部件组装成汽车也是一件复杂的工作,Builder模式就是将这两种情况分开进行。

Singleton(单例模式) :保证一个类只有一个实例,并提供一个访问它的全局访问点。

结构模式

F?ade(门面模式) :可扩展的使用JDBC针对不同的数据库编程,Facade提供了一种灵活的实现。

Proxy(代理模式) : 以Jive为例,剖析代理模式在用户级别授权机制上的应用。 Adapter(适配器模式) : 使用类再生的两个方式:组合(new)和继承(extends),这个已经在\中提到过。

Composite(合成模式) :就是将类用树形结构组合成一个单位.你向别人介绍你是某单位,你是单位中的一个元素,别人和你做买卖,相当于和单位做买卖。文章中还对Jive再进行了剖析。

Decorator(装饰模式) :Decorator是个油漆工,给你的东东的外表刷上美丽的颜色。

Bridge(桥接模式) :将\牛郎织女\分开(本应在一起,分开他们,形成两个接口),在他们之间搭建一个桥(动态的结合) 。

Flyweight(享元模式) : 提供Java运行性能,降低小而大量重复的类的开销。

行为模式

Template(模板方法模式) : 实际上向你介绍了为什么要使用Java 抽象类,该模式原理简单,使用很普遍。

Memento(备忘录模式) : 很简单一个模式,就是在内存中保留原来数据的拷贝。 Observer(观察者模式) : 介绍如何使用Java API提供的现成Observer。

Chain of Responsibility(责任链模式) :各司其职的类串成一串,好象击鼓传花,当然如果自己能完成,就不要推委给下一个。

Command(命令模式) :什么是将行为封装,Command是最好的说明。 State(状态模式) : 状态是编程中经常碰到的实例,将状态对象化,设立状态变换器,便可在状态中轻松切换。

Strategy(策略模式) :不同算法各自封装,用户端可随意挑选需要的算法。

Mediator(调停者模式) :很象十字路口的红绿灯,每个车辆只需和红绿灯交互就可以。 Interpreter(解释器模式) :主要用来对语言的分析,应用机会不多。

Visitor(访问者模式) : 访问者在进行访问时,完成一系列实质性操作,而且还可以扩展。 Iterator(迭代模式) : 这个模式已经被整合入Java的Collection在大多数场合下无需自己制造一个Iterator,只要将对象装入Collection中,直接使用Iterator进行对象遍历。

1

东方标准 http://soft.oneedu.cn

1. 创建 / 工厂模式(Factory)

工厂模式定义:

提供创建对象的接口,有工厂方法(Factory Method) 抽象工厂(Abstract Factory)两种。 工厂模式是我们最常用的模式了,著名的Jive论坛 ,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见。

为什么工厂模式是如此常用?因为工厂模式就相当于创建实例对象的new,我们经常要根据类Class生成实例对象,如A a=new A() 工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑实用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。

我们以类Sample为例, 如果我们要创建Sample的实例对象: Sample sample=new Sample();

可是,实际情况是,通常我们都要在创建sample实例时做点初始化的工作,比如赋值、查询数据库等。

首先,我们想到的是,可以使用Sample的构造函数,这样生成实例就写成: Sample sample=new Sample(参数);

但是,如果创建sample实例时所做的初始化工作不是象赋值这样简单的事,可能是很长一段代码,如果也写入构造函数中,那你的代码很难看了(就需要Refactor重整)。

为什么说代码很难看,初学者可能没有这种感觉,我们分析如下,初始化工作如果是很长一段代码,说明要做的工作很多,将很多工作装入一个方法中,相当于将很多鸡蛋放在一个篮子里,是很危险的,这也是有背于Java面向对象的原则,面向对象的封装(Encapsulation)和分派(Delegation)告诉我们,尽量将长的代码分派\切割\成每段,将每段再\封装\起来(减少段和段之间偶合联系性),这样,就会将风险分散,以后如果需要修改,只要更改每段,不会再发生牵一动百的事情。

在本例中,首先,我们需要将创建实例的工作与使用实例的工作分开, 也就是说,让创建实例所需要的大量初始化工作从Sample的构造函数中分离出去。

这时我们就需要Factory工厂模式来生成对象了,不能再用上面简单new Sample(参数)。还有,如果Sample有个继承如MySample, 按照面向接口编程,我们需要将Sample抽象成一个接口.现在Sample是接口,有两个子类MySample 和HisSample .我们要实例化他们时,如下:

Sample mysample=new MySample(); Sample hissample=new HisSample();

随着项目的深入,Sample可能还会\生出很多儿子出来\那么我们要对这些儿子一个个实例化,更糟糕的是,可能还要对以前的代码进行修改:加入后来生出儿子的实例.这在传统程序中是无法避免的。

但如果你一开始就有意识使用了工厂模式,这些麻烦就没有了。 工厂方法:你会建立一个专门生产Sample实例的工厂: public class Factory{

public static Sample creator(int which){

//getClass 产生Sample 一般可使用动态类装载装入类。

if (which==1)

return new SampleA(); else if (which==2)

return new SampleB();

2

东方标准 http://soft.oneedu.cn

}

}

那么在你的程序中,如果要实例化Sample时.就使用 Sample sampleA=Factory.creator(1);

这样,在整个就不涉及到Sample的具体子类,达到封装效果,也就减少错误修改的机会,这个原理可以用很通俗的话来比喻:就是具体事情做得越多,越容易范错误.这每个做过具体工作的人都深有体会,相反,官做得越高,说出的话越抽象越笼统,范错误可能性就越少.好象我们从编程序中也能悟出人生道理?呵呵。

使用工厂方法 要注意几个角色,首先你要定义产品接口,如上面的Sample,产品接口下有Sample接口的实现类,如SampleA,其次要有一个factory类,用来生成产品Sample,如下图,最右边是生产的对象Sample:

进一步稍微复杂一点,就是在工厂类上进行拓展,工厂类也有继承它的实现类concreteFactory了。

抽象工厂

抽象工与工厂方法区别在于需要创建对象的复杂程度上。如果我们创建对象的方法变得复杂了,如上面工厂方法中是创建一个对象Sample,如果我们还有新的产品接口Sample2。

这里假设:Sample有两个concrete类SampleA和SamleB,而Sample2也有两个concrete类Sample2A和SampleB2

那么,我们就将上例中Factory变成抽象类,将共同部分封装在抽象类中,不同部分使用子类实现,下面就是将上例中的Factory拓展成抽象工厂: public abstract class Factory{

public abstract Sample creator();

public abstract Sample2 creator(String name); }

public class SimpleFactory extends Factory{ public Sample creator(){ .........

return new SampleA }

public Sample2 creator(String name){ .........

return new Sample2A } }

3

东方标准 http://soft.oneedu.cn

public class BombFactory extends Factory{ public Sample creator(){ ......

return new SampleB }

public Sample2 creator(String name){ ......

return new Sample2B } }

从上面看到两个工厂各自生产出一套Sample和Sample2,也许你会疑问,为什么我不可以使用两个工厂方法来分别生产Sample和Sample2?

抽象工厂还有另外一个关键要点,是因为 SimpleFactory内,生产Sample和生产Sample2的方法之间有一定联系,所以才要将这两个方法捆绑在一个类中,这个工厂类有其本身特征,也许制造过程是统一的,比如:制造工艺比较简单,所以名称叫SimpleFactory。

在实际应用中,工厂方法用得比较多一些,而且是和动态类装入器组合在一起应用, 举例:

我们以Jive的ForumFactory为例,这个例子在前面的Singleton模式中我们讨论过,现在再讨论其工厂模式:

public abstract class ForumFactory {

private static Object initLock = new Object();

private static String className = \ private static ForumFactory factory = null;

public static ForumFactory getInstance(Authorization authorization) { //If no valid authorization passed in, return null. if (authorization == null) { return null; }

//以下使用了Singleton 单态模式 if (factory == null) {

synchronized(initLock) { if (factory == null) { ...... try {

//动态转载类

Class c = Class.forName(className); factory = (ForumFactory)c.newInstance(); }

catch (Exception e) { return null; } } } }

4

东方标准 http://soft.oneedu.cn

//Now, 返回 proxy.用来限制授权对forum的访问 return new ForumFactoryProxy(authorization, factory,

factory.getPermissions(authorization)); }

//真正创建forum的方法由继承forumfactory的子类去完成. public abstract Forum createForum(String name, String description) throws UnauthorizedException, ForumAlreadyExistsException; .... }

因为现在的Jive是通过数据库系统存放论坛帖子等内容数据,如果希望更改为通过文件系统实现,这个工厂方法ForumFactory就提供了提供动态接口:

private static String className = \你可以使用自己开发的创建forum的方法代替

com.jivesoftware.forum.database.DbForumFactory就可以.

在上面的一段代码中一共用了三种模式,除了工厂模式外,还有Singleton单态模式,以及proxy模式,proxy模式主要用来授权用户对forum的访问,因为访问forum有两种人:一个是注册用户,一个是游客guest,那么那么相应的权限就不一样,而且这个权限是贯穿整个系统的,因此建立一个proxy,类似网关的概念,可以很好的达到这个效果.

看看Java宠物店中的CatalogDAOFactory: public class CatalogDAOFactory { /**

* 本方法制定一个特别的子类来实现DAO模式。 * 具体子类定义是在J2EE的部署描述器中。 */

public static CatalogDAO getDAO() throws CatalogDAOSysException { CatalogDAO catDao = null; try {

InitialContext ic = new InitialContext(); //动态装入CATALOG_DAO_CLASS

//可以定义自己的CATALOG_DAO_CLASS,从而在无需变更太多代码 //的前提下,完成系统的巨大变更。

String className =(String) ic.lookup(JNDINames.CATALOG_DAO_CLASS); catDao = (CatalogDAO) Class.forName(className).newInstance(); } catch (NamingException ne) {

throw new CatalogDAOSysException(\

CatalogDAOFactory.getDAO: NamingException while getting DAO type : \\n\ } catch (Exception se) {

throw new CatalogDAOSysException(\

CatalogDAOFactory.getDAO: Exception while getting DAO type : \\n\ }

return catDao;

5

东方标准 http://soft.oneedu.cn

} }

CatalogDAOFactory是典型的工厂方法,catDao是通过动态类装入器className获得CatalogDAOFactory具体实现子类,这个实现子类在Java宠物店是用来操作catalog数据库,用户可以根据数据库的类型不同,定制自己的具体实现子类,将自己的子类名给与CATALOG_DAO_CLASS变量就可以。

由此可见,工厂方法确实为系统结构提供了非常灵活强大的动态扩展机制,只要我们更换一下具体的工厂方法,系统其他地方无需一点变换,就有可能将系统功能进行改头换面的变化。

《===============戏说工厂模式=================》

?客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无

须修改就可以接纳新产品。

?缺点是当产品修改时,工厂类也要做相应的修改。

?追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味

有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory 。

?核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个

抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。

?请MM去麦当劳吃汉堡,不同的MM有不同的口味,要每个都记住是一件烦人的事情,

我一般采用Factory Method模式,带着MM到服务员那儿,说“要一个汉堡”,具体要什么样的汉堡呢,让MM直接跟服务员说就行了。

6

东方标准 http://soft.oneedu.cn

2. 创建 / 原型模式(Prototype)

定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.

Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

如何使用?

因为Java中的提供clone()方法来实现对象的克隆,所以Prototype模式实现一下子变得很简单,以勺子为例:

public abstract class AbstractSpoon implements Cloneable {

String spoonName;

public void setSpoonName(String spoonName) {this.spoonName = spoonName;} public String getSpoonName() {return this.spoonName;}

public Object clone() {

Object object = null; try {

object = super.clone();

} catch (CloneNotSupportedException exception) {

System.err.println(\ }

return object; } }

有个具体实现(ConcretePrototype):

public class SoupSpoon extends AbstractSpoon {

public SoupSpoon() {

setSpoonName(\ } }

public class SaladSpoon extends AbstractSpoon {

public SaladSpoon() {

setSpoonName(\ } }

7

东方标准 http://soft.oneedu.cn

调用Prototype模式很简单:

AbstractSpoon spoon = new SoupSpoon(); AbstractSpoon spoon2 = spoon.clone();

当然也可以结合工厂模式来创建AbstractSpoon实例。

在Java中Prototype模式变成clone()方法的使用,由于Java的纯洁的面向对象特性,使得在Java中使用设计模式变得很自然,两者已经几乎是浑然一体了。这反映在很多模式上,如Interator遍历模式。

《===============戏说原型模式=================》

? 通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的

方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。 ? 缺点是每一个类都必须配备一个克隆方法。

? 跟MM用QQ聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,需要时

只要copy出来放到QQ里面就行了,这就是我的情话prototype了。(100块钱一份,你要不要)

8

东方标准 http://soft.oneedu.cn

3. 创建 / 建造模式(Builder)

Builder模式定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

Builder模式是一步一步创建一个复杂的对象,它允许用户可以只通过指定复杂对象的类型和内容就可以构建它们.用户不知道内部的具体构建细节.Builder模式是非常类似抽象工厂模式,细微的区别大概只有在反复使用中才能体会到。

为何使用? 是为了将构建复杂对象的过程和它的部件解耦.注意: 是解耦过程和部件。 因为一个复杂的对象,不但有很多大量组成部分,如汽车,有很多部件:车轮 方向盘 发动机还有各种小零件等等,部件很多,但远不止这些,如何将这些部件装配成一辆汽车,这个装配过程也很复杂(需要很好的组装技术),Builder模式就是为了将部件和组装过程分开。

如何使用? 首先假设一个复杂对象是由多个部件组成的,Builder模式是把复杂对象的创建和部件的创建分别开来,分别用Builder类和Director类来表示。

首先,需要一个接口,它定义如何创建复杂对象的各个部件: public interface Builder {

//创建部件A 比如创建汽车车轮 void buildPartA();

//创建部件B 比如创建汽车方向盘 void buildPartB();

//创建部件C 比如创建汽车发动机 void buildPartC();

//返回最后组装成品结果 (返回最后装配好的汽车)

//成品的组装过程不在这里进行,而是转移到下面的Director类中进行. //从而实现了解耦过程和部件 Product getResult(); }

用Director构建最后的复杂对象,而在上面Builder接口中封装的是如何创建一个个部件(复杂对象是由这些部件组成的),也就是说Director的内容是如何将部件最后组装成成品: public class Director {

private Builder builder;

public Director( Builder builder ) { this.builder = builder; }

// 将部件partA partB partC最后组成复杂对象

//这里是将车轮 方向盘和发动机组装成汽车的过程 public void construct() { builder.buildPartA(); builder.buildPartB(); builder.buildPartC(); } }

Builder的具体实现ConcreteBuilder:

通过具体完成接口Builder来构建或装配产品的部件; 定义并明确它所要创建的是什么具体东西;

9

东方标准 http://soft.oneedu.cn

提供一个可以重新获取产品的接口:

public class ConcreteBuilder implements Builder { Part partA, partB, partC; public void buildPartA() {

//这里是具体如何构建partA的代码 };

public void buildPartB() {

//这里是具体如何构建partB的代码 };

public void buildPartC() {

//这里是具体如何构建partB的代码 };

public Product getResult() { //返回最后组装成品结果 }; }

复杂对象:产品Product:

public interface Product { } 复杂对象的部件:

public interface Part { } 我们看看如何调用Builder模式:

ConcreteBuilder builder = new ConcreteBuilder(); Director director = new Director( builder ); director.construct();

Product product = builder.getResult(); Builder模式的应用

在Java实际使用中,我们经常用到\池\的概念,当资源提供者无法提供足够的资源,并且这些资源需要被很多用户反复共享时,就需要使用池。\池\实际是一段内存,当池中有一些复杂的资源的\断肢\比如数据库的连接池,也许有时一个连接会中断),如果循环再利用这些\断肢\将提高内存使用效率,提高池的性能.修改Builder模式中Director类使之能诊断\断肢\断在哪个部件上,再修复这个部件。

《===============戏说建造模式=================》

? 将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同

的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。

? 建造模式可以强制实行一种分步骤进行的建造过程。

? MM最爱听的就是“我爱你”这句话了,见到不同地方的MM,要能够用她们的方言跟

她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到MM我只要按对应的键,它就能够用相应的语言说出“我爱你”这句话了,国外的MM也可以轻松搞掂,这就是我的“我爱你”builder。(这一定比美军在伊拉克用的翻译机好卖) 。

10

东方标准 http://soft.oneedu.cn

4. 创建 / 单例模式(Singleton)

单态定义:

Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。 在很多操作中,比如建立目录 数据库连接都需要这样的单线程操作。还有, singleton能够被状态化; 这样,多个单态类在一起就可以作为一个状态仓库一样向外提供服务,比如,你要论坛中的帖子计数器,每次浏览一次需要计数,单态类能否保持住这个计数,并且能synchronize的安全自动加1,如果你要把这个数字永久保存到数据库,你可以在不修改单态接口的情况下方便的做到。

另外方面,Singleton也能够被无状态化。

提供工具性质的功能,Singleton模式就为我们提供了这样实现的可能。使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection)。我们常常看到工厂模式中类装入器(class loader)中也用Singleton模式实现的,因为被装入的类实际也属于资源。

一般Singleton模式通常有几种形式: public class Singleton { private Singleton(){}

//在自己内部定义自己一个实例,是不是很奇怪? //注意这是private 只供内部调用

private static Singleton instance = new Singleton();

//这里提供了一个供外部访问本class的静态方法,可以直接访问 public static Singleton getInstance() { return instance; } }

第二种形式:

public class Singleton {

private static Singleton instance = null;

public static synchronized Singleton getInstance() {

//这个方法比上面有所改进,不用每次都进行生成对象,只是第一次 //使用时生成实例,提高了效率! if (instance==null)

instance=new Singleton(); return instance; } }

使用Singleton.getInstance()可以访问单态类。上面第二中形式是lazy initialization,也就是说第一次调用时初始Singleton,以后就不用再生成了。注意到lazy initialization形式中的synchronized,这个synchronized很重要,如果没有synchronized,那么使用getInstance()是有可能得到多个Singleton实例。关于lazy initialization的Singleton有很多涉及double-checked locking (DCL)的讨论,有兴趣者进一步研究。

一般认为第一种形式要更加安全些。

11

东方标准 http://soft.oneedu.cn

使用Singleton注意事项:

有时在某些情况下,使用Singleton并不能达到Singleton的目的,如有多个Singleton对象同时被不同的类装入器装载;在EJB这样的分布式系统中使用也要注意这种情况,因为EJB是跨服务器,跨JVM的。

我们以SUN公司的宠物店源码(Pet Store 1.3.1)的ServiceLocator为例稍微分析一下: 在Pet Store中ServiceLocator有两种,一个是EJB目录下;一个是WEB目录下,我们检查这两个ServiceLocator会发现内容差不多,都是提供EJB的查询定位服务,可是为什么要分开呢?仔细研究对这两种ServiceLocator才发现区别:在WEB中的ServiceLocator的采取Singleton模式,ServiceLocator属于资源定位,理所当然应该使用Singleton模式。但是在EJB中,Singleton模式已经失去作用,所以ServiceLocator才分成两种,一种面向WEB服务的,一种是面向EJB服务的。

Singleton模式看起来简单,使用方法也很方便,但是真正用好,是非常不容易,需要对Java的类 线程 内存等概念有相当的了解。

总之:如果你的应用基于容器,那么Singleton模式少用或者不用,可以使用相关替代技术。

《===============戏说单例模式=================》

? 单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例

单例模式。

? 单例模式只应在有真正的“单一实例”的需求时才可使用。

? 俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只

要说道“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事)

12

东方标准 http://soft.oneedu.cn

5. 结构 / 门面模式(Facade)

Facade模式的定义: 为子系统中的一组接口提供一个一致的界面。 Facade一个典型应用就是数据库JDBC的应用,如下例对数据库的操作:

public class DBCompare { Connection conn = null;

PreparedStatement prep = null; ResultSet rset = null; try {

Class.forName( \

conn = DriverManager.getConnection( \

String sql = \ prep = conn.prepareStatement( sql ); prep.setString( 1, \ rset = prep.executeQuery(); if( rset.next() ) {

System.out.println( rset.getString( \ }

} catch( SException e ) { e.printStackTrace(); } finally {

rset.close(); prep.close(); conn.close(); } }

上例是Jsp中最通常的对数据库操作办法。

在应用中,经常需要对数据库操作,每次都写上述一段代码肯定比较麻烦,需要将其中不变的部分提炼出来,做成一个接口,这就引入了facade外观对象.如果以后我们更换Class.forName中的也非常方便,比如从Mysql数据库换到Oracle数据库,只要更换facade接口中的driver就可以。

我们做成了一个Facade接口,使用该接口,上例中的程序就可以更改如下: public class DBCompare {

String sql = \ try {

Mysql msql=new mysql(sql);

prep.setString( 1, \ rset = prep.executeQuery(); if( rset.next() ) {

System.out.println( rset.getString( \ }

} catch( SException e ) {

13

东方标准 http://soft.oneedu.cn

e.printStackTrace(); } finally {

mysql.close(); mysql=null; } }

可见非常简单,所有程序对数据库访问都是使用改接口,降低系统的复杂性,增加了灵活性. 如果我们要使用连接池,也只要针对facade接口修改就可以。

由上图可以看出, facade实际上是个理顺系统间关系,降低系统间耦合度的一个常用的办法,也许你已经不知不觉在使用,尽管不知道它就是fa?ade。

《===============戏说门面模式=================》

? 外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高

层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。 ? 我有一个专业的Nikon相机,我就喜欢自己手动调光圈、快门,这样照出来的照片

才专业,但MM可不懂这些,教了半天也不会。幸好相机有Facade设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样MM也可以用这个相机给我拍张照片了。

14

东方标准 http://soft.oneedu.cn

6.结构 / 代理模式(Proxy)

定义: 为其他对象提供一种代理以控制对这个对象的访问。

理解并使用设计模式,能够培养我们良好的面向对象编程习惯,同时在实际应用中,可以如鱼得水,享受游刃有余的乐趣。代理模式是比较有用途的一种模式,而且变种较多,应用场合覆盖从小结构到整个系统的大结构,Proxy是代理的意思,我们也许有代理服务器等概念,代理概念可以解释为:在出发点到目的地之间有一道中间层,意为代理。

为什么要使用Proxy?

1.授权机制--不同级别的用户对同一对象拥有不同的访问权利,如Jive论坛系统中,就使用Proxy进行授权机制控制,访问论坛有两种人:注册用户和游客(未注册用户),Jive中就通过类似ForumProxy这样的代理来控制这两种用户对论坛的访问权限。 2.某个客户端不能直接操作到某个对象,但又必须和那个对象有所互动。 举例两个具体情况:

(1)如果那个对象是一个是很大的图片,需要花费很长时间才能显示出来,那么当这个图片包含在文档中时,使用编辑器或浏览器打开这个文档,打开文档必须很迅速,不能等待大图片处理完成,这时需要做个图片Proxy来代替真正的图片。

(2)如果那个对象在Internet的某个远端服务器上,直接操作这个对象因为网络速度原因可能比较慢,那我们可以先用Proxy来代替那个对象。

总之原则是,对于开销很大的对象,只有在使用它时才创建,这个原则可以为我们节省很多宝贵的Java内存。所以,有些人认为Java耗费资源内存,我以为这和程序编制思路也有一定的关系。

如何使用Proxy?

以Jive论坛系统为例,访问论坛系统的用户有多种类型:注册普通用户、论坛管理者、系统管理者、游客,注册普通用户才能发言;论坛管理者可以管理他被授权的论坛;系统管理者可以管理所有事务等,这些权限划分和管理是使用Proxy完成的。

Forum是Jive的核心接口,在Forum中陈列了有关论坛操作的主要行为,如论坛名称、论坛描述的获取和修改,帖子发表删除编辑等。

在ForumPermissions中定义了各种级别权限的用户:

public class ForumPermissions implements Cacheable { /**

* Permission to read object. */

public static final int READ = 0; /**

* Permission to administer the entire sytem. */

public static final int SYSTEM_ADMIN = 1; /**

* Permission to administer a particular forum. */

public static final int FORUM_ADMIN = 2;

15

东方标准 http://soft.oneedu.cn

/**

* Permission to administer a particular user. */

public static final int USER_ADMIN = 3; /**

* Permission to administer a particular group. */

public static final int GROUP_ADMIN = 4; /**

* Permission to moderate threads. */

public static final int MODERATE_THREADS = 5; /**

* Permission to create a new thread. */

public static final int CREATE_THREAD = 6; /**

* Permission to create a new message. */

public static final int CREATE_MESSAGE = 7; /**

* Permission to moderate messages. */

public static final int MODERATE_MESSAGES = 8; ......

public boolean isSystemOrForumAdmin() {

return (values[FORUM_ADMIN] || values[SYSTEM_ADMIN]); } ...... }

因此,Forum中各种操作权限是和ForumPermissions定义的用户级别有关系的,作为接口Forum的实现:ForumProxy正是将这种对应关系联系起来。比如,修改Forum的名称,只有论坛管理者或系统管理者可以修改,代码如下:

public class ForumProxy implements Forum { private ForumPermissions permissions; private Forum forum;

this.authorization = authorization;

public ForumProxy(Forum forum, Authorization authorization, ForumPermissions permissions){ this.forum = forum;

this.authorization = authorization; this.permissions = permissions;

16

东方标准 http://soft.oneedu.cn

} ......

public void setName(String name) throws UnauthorizedException,

ForumAlreadyExistsException{ //只有是系统或论坛管理者才可以修改名称 if (permissions。isSystemOrForumAdmin()) { forum。setName(name); } else {

throw new UnauthorizedException(); } } ...... }

而DbForum才是接口Forum的真正实现,以修改论坛名称为例: public class DbForum implements Forum, Cacheable { ...

public void setName(String name) throws ForumAlreadyExistsException { ...

this.name = name;

//这里真正将新名称保存到数据库中 saveToDb(); ... } ... }

凡是涉及到对论坛名称修改这一事件,其他程序都首先得和ForumProxy打交道,由ForumProxy决定是否有权限做某一样事情,ForumProxy是个名副其实的\网关\安全代理系统\。

在平时应用中,无可避免总要涉及到系统的授权或安全体系,不管你有无意识的使用Proxy,实际你已经在使用Proxy了。

我们继续结合Jive谈入深一点,下面要涉及到工厂模式了,如果你不了解工厂模式,请看我的另外一篇文章:设计模式之Factory。

我们已经知道,使用Forum需要通过ForumProxy,Jive中创建一个Forum是使用Factory模式,有一个总的抽象类ForumFactory,在这个抽象类中,调用ForumFactory是通过getInstance()方法实现,这里使用了Singleton(也是设计模式之一,由于介绍文章很多,我就不写了,看这里),getInstance()返回的是ForumFactoryProxy。为什么不返回ForumFactory,而返回ForumFactory的实现ForumFactoryProxy?

原因是明显的,需要通过代理确定是否有权限创建forum。在ForumFactoryProxy中我们看到代码如下:

public class ForumFactoryProxy extends ForumFactory { protected ForumFactory factory;

17

东方标准 http://soft.oneedu.cn

protected Authorization authorization; protected ForumPermissions permissions;

public ForumFactoryProxy(Authorization authorization, ForumFactory factory, ForumPermissions permissions) {

this.factory = factory;

this.authorization = authorization; this.permissions = permissions; }

public Forum createForum(String name, String description)

throws UnauthorizedException, ForumAlreadyExistsException {

//只有系统管理者才可以创建forum

if (permissions.get(ForumPermissions。SYSTEM_ADMIN)) { Forum newForum = factory。createForum(name, description); return new ForumProxy(newForum, authorization, permissions); } else {

throw new UnauthorizedException(); } }

方法createForum返回的也是ForumProxy, Proxy就象一道墙,其他程序只能和Proxy交互操作。注意到这里有两个Proxy:ForumProxy和ForumFactoryProxy。 代表两个不同的职责:使用Forum和创建Forum;

至于为什么将使用对象和创建对象分开,这也是为什么使用Factory模式的原因所在:是为了\封装\分派\换句话说,尽可能功能单一化,方便维护修改。

Jive论坛系统中其他如帖子的创建和使用,都是按照Forum这个思路而来的。

以上我们讨论了如何使用Proxy进行授权机制的访问,Proxy还可以对用户隐藏另外一种称为copy-on-write的优化方式。拷贝一个庞大而复杂的对象是一个开销很大的操作,如果拷贝过程中,没有对原来的对象有所修改,那么这样的拷贝开销就没有必要。用代理延迟这一拷贝过程。

比如:我们有一个很大的Collection,具体如hashtable,有很多客户端会并发同时访问它。其中一个特别的客户端要进行连续的数据获取,此时要求其他客户端不能再向hashtable中增加或删除东东。

最直接的解决方案是:使用collection的lock,让这特别的客户端获得这个lock,进行连续的数据获取,然后再释放lock。 public void foFetches(Hashtable ht){ synchronized(ht){

//具体的连续数据获取动作... } }

但是这一办法可能锁住Collection会很长时间,这段时间,其他客户端就不能访问该Collection了。

18

东方标准 http://soft.oneedu.cn

第二个解决方案是clone这个Collection,然后让连续的数据获取针对clone出来的那个Collection操作。这个方案前提是,这个Collection是可clone的,而且必须有提供深度clone的方法。Hashtable就提供了对自己的clone方法,但不是Key和value对象的clone,关于Clone含义可以参考专门文章。

public void foFetches(Hashtable ht){

Hashttable newht=(Hashtable)ht.clone(); }

问题又来了,由于是针对clone出来的对象操作,如果原来的母体被其他客户端操作修改了, 那么对clone出来的对象操作就没有意义了。

最后解决方案:我们可以等其他客户端修改完成后再进行clone,也就是说,这个特别的客户端先通过调用一个叫clone的方法来进行一系列数据获取操作。但实际上没有真正的进行对象拷贝,直至有其他客户端修改了这个对象Collection。

使用Proxy实现这个方案。这就是copy-on-write操作。

Proxy应用范围很广,现在流行的分布计算方式RMI和Corba等都是Proxy模式的应用。 更多Proxy应用,见http://www.research.umbc.edu/~tarr/cs491/lectures/Proxy.pdf

Sun公司的 Explore the Dynamic Proxy API Dynamic Proxy Classes

《===============戏说代理模式=================》

? 代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代

理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。

? 跟MM在网上聊天,一开头总是“hi,你好”,“你从哪儿来呀?”“你多大了?”“身高多

少呀?”这些话,真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,怎么样,酷吧。

19

东方标准 http://soft.oneedu.cn

7.结构 / 适配器(Adapter)

适配器模式定义 : 将两个不兼容的类纠合在一起使用,属于结构型模式,需要有Adaptee(被适配者)和Adaptor(适配器)两个身份。

为何使用 ? 我们经常碰到要将两个没有关系的类组合在一起使用,第一解决方案是:修改各自类的接口,但是如果我们没有源代码,或者,我们不愿意为了一个应用而修改各自的接口。怎么办? 使用Adapter,在这两种接口之间创建一个混合接口(混血儿)。

如何使用 ? 实现Adapter方式,其实\的\类再生\一节中已经提到,有两种方式:组合(composition)和继承(inheritance),如果你有些Java使用的经验,已经发现,这种模式经常使用。

假设我们要打桩,有两种类:方形桩 圆形桩。 public class SquarePeg{

public void insert(String str){

System.out.println(\ } }

public class RoundPeg{

public void insertIntohole(String msg){

System.out.println(\ } }

现在有一个应用,需要既打方形桩,又打圆形桩。那么我们需要将这两个没有关系的类综合应用。假设RoundPeg我们没有源代码,或源代码我们不想修改,那么我们使用Adapter来实现这个应用:

public class PegAdapter extends SquarePeg{ private RoundPeg roundPeg;

public PegAdapter(RoundPeg peg)(this.roundPeg=peg;) public void insert(String str){ roundPeg.insertIntoHole(str);} }

在上面代码中,RoundPeg属于Adaptee,是被适配者。PegAdapter是Adapter,将Adaptee(被适配者RoundPeg)和Target(目标SquarePeg)进行适配。实际上这是将组合方法(composition)和继承(inheritance)方法综合运用。

PegAdapter首先继承SquarePeg,然后使用new的组合生成对象方式,生成RoundPeg的对象roundPeg,再重载父类insert()方法。从这里,你也了解使用new生成对象和使用extends继承生成对象的不同,前者无需对原来的类修改,甚至无需要知道其内部结构和源代码。

进一步使用:

上面的PegAdapter是继承了SquarePeg,如果我们需要两边继承,即继承SquarePeg 又继承RoundPeg,因为Java中不允许多继承,但是我们可以实现(implements)两个接口(interface) public interface IRoundPeg{

public void insertIntoHole(String msg); }

public interface ISquarePeg{

public void insert(String str); }

20

东方标准 http://soft.oneedu.cn

9.结构 / 修饰模式(Decorator)

Decorator定义 : 动态给一个对象添加一些额外的职责,就象在墙上刷油漆。使用Decorator模式相比用生成子类方式达到功能的扩充显得更为灵活。Decorator还常被翻译成装饰,我觉得翻译成\油漆工\更形象点,油漆工(decorator)是用来刷油漆的,那么被刷油漆的对象我们称decoratee。这两种实体在Decorator模式中是必须的。

为什么使用Decorator ? 我们通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性,同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了,是静态的。

使用Decorator的理由是 : 这些功能需要由用户动态决定加入的方式和时机。Decorator提供了\即插即用\的方法,在运行期间决定何时增加何种功能。

如何使用 ? 举Adapter中的打桩示例,在Adapter中有两种类:方形桩 圆形桩,Adapter模式展示如何综合使用这两个类,在Decorator模式中,我们是要在打桩时增加一些额外功能,比如,挖坑 在桩上钉木板等,不关心如何使用两个不相关的类。 我们先建立一个接口:

public interface Work{

public void insert(); }

接口Work有一个具体实现:插入方形桩或圆形桩,这两个区别对Decorator是无所谓。我们以插入方形桩为例:

public class SquarePeg implements Work{ public void insert(){

System.out.println(\方形桩插入\ } }

现在有一个应用:需要在桩打入前,挖坑,在打入后,在桩上钉木板,这些额外的功能是动态,可能随意增加调整修改,比如,可能又需要在打桩之后钉架子(只是比喻)。

那么我们使用Decorator模式,这里方形桩SquarePeg是decoratee(被刷油漆者),我们需要在decoratee上刷些\油漆\这些油漆就是那些额外的功能。 public class Decorator implements Work{ private Work work;

//额外增加的功能被打包在这个List中 private ArrayList others = new ArrayList();

//在构造器中使用组合new方式,引入Work对象; public Decorator(Work work) { this.work=work; others.add(\挖坑\ others.add(\钉木板\ }

public void insert(){ newMethod(); }

//在新方法中,我们在insert之前增加其他方法,这里次序先后是用户灵活指定的 public void newMethod()

26

东方标准 http://soft.oneedu.cn

{

otherMethod(); work.insert(); }

public void otherMethod() {

ListIterator listIterator = others.listIterator(); while (listIterator.hasNext()) {

System.out.println(((String)(listIterator.next())) + \正在进行\ } } }

在上例中,我们把挖坑和钉木板都排在了打桩insert前面,这里只是举例说明额外功能次序可以任意安排。 好了,Decorator模式出来了,我们看如何调用:

Work squarePeg = new SquarePeg();

Work decorator = new Decorator(squarePeg); decorator.insert(); Decorator模式至此完成。

如果你细心,会发现,上面调用类似我们读取文件(I/O)时的‘修饰器’调用:

FileReader fr = new FileReader(filename); BufferedReader br = new BufferedReader(fr);

实际上Java 的I/O API就是使用Decorator实现的,I/O变种很多,如果都采取继承方法,将会产生很多子类,显然相当繁琐。 Jive中的Decorator实现

在论坛系统中,有些特别的字是不能出现在论坛中如\打倒XXX\我们需要过滤这些\反动\的字体。不让他们出现或者高亮度显示。

在IBM Java专栏中专门谈Jive的文章中,有谈及Jive中ForumMessageFilter。java使用了Decorator模式,其实,该程序并没有真正使用Decorator,而是提示说:针对特别论坛可以设计额外增加的过滤功能,那么就可以重组ForumMessageFilter作为Decorator模式了。

所以,我们在分辨是否真正是Decorator模式,以及会真正使用Decorator模式,一定要把握好Decorator模式的定义,以及其中参与的角色(Decoratee 和Decorator)。

《===============戏说修饰模式=================》

? 装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提

供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。 ? Mary过完轮到Sarly过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,

拿出我去年在华山顶上照的照片,在背面写上“最好的的礼物,就是爱你的Fita”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设计的Mike设计了一个漂亮的盒子装起来……,我们都是Decorator,最终都在修饰我这个人呀,怎么样,看懂了吗?

27

东方标准 http://soft.oneedu.cn

10.结构 / 桥梁模式(Bridge)

Bridge模式定义 : 将抽象和行为划分开来,各自独立,但能动态的结合。

任何事物对象都有抽象和行为之分,例如人,人是一种抽象,人分男人和女人等;人有行为,行为也有各种具体表现,所以,“人”与“人的行为”两个概念也反映了抽象和行为之分。

在面向对象设计的基本概念中,对象这个概念实际是由属性和行为两个部分组成的,属性我们可以认为是一种静止的,是一种抽象,一般情况下,行为是包含在一个对象中,但是,在有的情况下,我们需要将这些行为也进行归类,形成一个总的行为接口,这就是桥模式的用处。

为什么使用 ? 不希望抽象部分和行为有一种固定的绑定关系,而是应该可以动态联系的。

如果一个抽象类或接口有多个具体实现(子类、concrete subclass),这些子类之间关系可能有以下两种情况:

1. 这多个子类之间概念是并列的,如前面举例,打桩,有两个concrete class:方形桩和圆形

桩;这两个形状上的桩是并列的,没有概念上的重复。

2. 这多个子类之中有内容概念上重叠。那么需要我们把抽象共同部分和行为共同部分

各自独立开来,原来是准备放在一个接口里,现在需要设计两个接口:抽象接口和行为接口,分别放置抽象和行为。

例如,一杯咖啡为例,子类实现类为四个:中杯加奶、大杯加奶、 中杯不加奶、大杯不加奶。

但是,我们注意到:上面四个子类中有概念重叠,可从另外一个角度进行考虑,这四个类实际是两个角色的组合:抽象 和行为,其中抽象为:中杯和大杯;行为为:加奶 不加奶(如加橙汁 加苹果汁)。

实现四个子类在抽象和行为之间发生了固定的绑定关系,如果以后动态增加加葡萄汁的行为,就必须再增加两个类:中杯加葡萄汁和大杯加葡萄汁。显然混乱,扩展性极差。 那我们从分离抽象和行为的角度,使用Bridge模式来实现。

如何实现 ? 以上面提到的 咖啡 为例

我们原来打算只设计一个接口(抽象类),使用Bridge模式后,我们需要将抽象和行为分开,加奶和不加奶属于行为,我们将它们抽象成一个专门的行为接口。 先看看抽象部分的接口代码: public abstract class Coffee {

CoffeeImp coffeeImp;

public void setCoffeeImp() {

this.CoffeeImp = CoffeeImpSingleton.getTheCoffeImp(); }

public CoffeeImp getCoffeeImp() {return this.CoffeeImp;} public abstract void pourCoffee(); }

其中CoffeeImp 是加不加奶的行为接口,看其代码如下: public abstract class CoffeeImp {

28

东方标准 http://soft.oneedu.cn

public abstract void pourCoffeeImp(); }

现在我们有了两个抽象类,下面我们分别对其进行继承,实现concrete class: //中杯

public class MediumCoffee extends Coffee {

public MediumCoffee() {setCoffeeImp();} public void pourCoffee() {

CoffeeImp coffeeImp = this.getCoffeeImp();

//我们以重复次数来说明是冲中杯还是大杯 ,重复2次是中杯 for (int i = 0; i < 2; i++) {

coffeeImp.pourCoffeeImp(); } } }

//大杯

public class SuperSizeCoffee extends Coffee {

public SuperSizeCoffee() {setCoffeeImp();} public void pourCoffee() {

CoffeeImp coffeeImp = this.getCoffeeImp();

//我们以重复次数来说明是冲中杯还是大杯 ,重复5次是大杯 for (int i = 0; i < 5; i++) {

coffeeImp.pourCoffeeImp(); } } }

上面分别是中杯和大杯的具体实现.下面再对行为CoffeeImp进行继承: //加奶

public class MilkCoffeeImp extends CoffeeImp {

MilkCoffeeImp() {}

public void pourCoffeeImp() {

System.out.println(\加了美味的牛奶\ } }

//不加奶

public class FragrantCoffeeImp extends CoffeeImp {

29

东方标准 http://soft.oneedu.cn

FragrantCoffeeImp() {}

public void pourCoffeeImp() {

System.out.println(\什么也没加,清香\ } }

Bridge模式的基本框架我们已经搭好了,别忘记定义中还有一句:动态结合,我们现在可以喝到至少四种咖啡: 1.中杯加奶 2.中杯不加奶 3.大杯加奶 4.大杯不加奶

看看是如何动态结合的,在使用之前,我们做个准备工作,设计一个单态类(Singleton)用来hold当前的CoffeeImp:

public class CoffeeImpSingleton {

private static CoffeeImp coffeeImp;

public CoffeeImpSingleton(CoffeeImp coffeeImpIn) {this.coffeeImp = coffeeImpIn;}

public static CoffeeImp getTheCoffeeImp() {

return coffeeImp; } }

看看中杯加奶 和大杯加奶 是怎么出来的: //拿出牛奶

CoffeeImpSingleton coffeeImpSingleton = new CoffeeImpSingleton(new MilkCoffeeImp()); //中杯加奶

MediumCoffee mediumCoffee = new MediumCoffee(); mediumCoffee.pourCoffee(); //大杯加奶

SuperSizeCoffee superSizeCoffee = new SuperSizeCoffee(); superSizeCoffee.pourCoffee();

注意: Bridge模式的执行类如CoffeeImp和Coffee是一对一的关系, 正确创建CoffeeImp是该模式的关键。

Bridge模式在EJB中的应用

EJB中有一个Data Access Object (DAO)模式,这是将商业逻辑和具体数据资源分开的,因为不同的数据库有不同的数据库操作。将操作不同数据库的行为独立抽象成一个行为接口DAO。如下:

1.Business Object (类似Coffee)

实现一些抽象的商业操作:如寻找一个用户下所有的订单, 涉及数据库操作都使用DAOImplementor。 2.Data Access Object (类似CoffeeImp)

30

东方标准 http://soft.oneedu.cn

12.行为 / 模板模式(Template)

Template模板模式定义 : 定义一个操作中算法的骨架,将一些步骤的执行延迟到其子类中。

使用Java的抽象类时,就经常会使用到Template模式,因此Template模式使用很普遍,而且很容易理解和使用。

public abstract class Benchmark { /**

* 下面操作是我们希望在子类中完成 */

public abstract void benchmark(); /**

* 重复执行benchmark次数 */

public final long repeat (int count) { if (count <= 0) return 0; else {

long startTime = System.currentTimeMillis();

for (int i = 0; i < count; i++) benchmark();

long stopTime = System.currentTimeMillis(); return stopTime - startTime; } } }

在上例中,我们希望重复执行benchmark()操作,但是对benchmark()的具体内容没有说明,而是延迟到其子类中描述:

public class MethodBenchmark extends Benchmark { /**

* 真正定义benchmark内容 */

public void benchmark() {

for (int i = 0; i < Integer.MAX_VALUE; i++){ System.out.printtln(\ } } }

至此,Template模式已经完成,是不是很简单?

我们称repeat方法为模板方法,它其中的benchmark()实现被延迟到子类MethodBenchmark中实现了,看看如何使用:

Benchmark operation = new MethodBenchmark();

long duration = operation.repeat(Integer.parseInt(args[0].trim()));

36

东方标准 http://soft.oneedu.cn

System.out.println(\

也许你以前还疑惑抽象类有什么用,现在你应该彻底明白了吧? 至于这样做的好处,很显然啊,扩展性强,以后Benchmark内容变化,我只要再做一个继承子类就可以,不必修改其他应用代码。

《===============戏说模板模式=================》

? 模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一

些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。 ? 追MM圣经规定了追MM在不同时期有不同的步骤(Template method),但每个步骤针对不同的情

况,都有不一样的做法,这就要看你随机应变啦(具体实现)。

37

东方标准 http://soft.oneedu.cn

13.行为 / 备忘录模式(Memento)

定义:memento是一个保存另外一个对象内部状态拷贝的对象.这样以后就可以将该对象恢复到原先保存的状态。 备忘录模式相对也比较好理解,我们看下列代码: public class Originator { private int number; private File file = null;

public Originator(){} // 创建一个Memento public Memento getMemento(){ return new Memento(this); } // 恢复到原始值

public void setMemento(Memento m){ number = m.number; file = m.file; } }

我们再看看Memento类:

private class Memento implements java.io.Serializable{ private int number; private File file = null;

public Memento( Originator o){ number = o.number; file = o.file;

} }

可见 Memento中保存了Originator中的number和file的值,通过调用Originator中number和file值改变的话,通过调用setMemento()方法可以恢复。Memento模式的缺点是耗费大,如果内部状态很多,再保存一份,无意要浪费大量内存。 Memento模式在Jsp+Javabean中的应用在Jsp应用中: 我们通常有很多表单要求用户输入,比如用户注册,需要输入姓名和Email等, 如果一些表项用户没有填写或者填写错误,我们希望在用户按\提交Submit\后,通过Jsp程序检查,发现确实有未填写项目,则在该项目下红字显示警告或错误,同时,还要显示用户刚才已经输入的表项。 如右图中 First Name是用户已经输入,Last Name没有输入,我们则提示红字警告: 这种技术的实现,就是利用了Javabean的scope=\或scope=\特性,也就是Memento模式。 具体示例和代码见 JavaWorld的英文原文 , Javabean表单输入特性参见我的另外一篇文章 << Jsp中的session使用>>。 38

东方标准 http://soft.oneedu.cn

附:Jsp中的session使用 Jsp的session是使用bean的一个生存期限,一般为page,session意思是在这个用户没有离开网站之前一直有效,如果无法判断用户何时离开,一般依据系统设定,tomcat中设定为30分钟. 我们使用seesion功能,可以达到多个jsp程序从操作同一个java bean, 那么这个java bean可以作为我们传统意义上的\全局变量池\在java中我们可以使用static静态化一个变量和方法,使用singleton唯一化对象.) 在项目实践中,我们Jsp程序中很多参数需要从数据库中读取,有的参数实际读取一次就可以,如果设计成每个用户每产生一个页面都要读取数据库,很显然,数据库的负载很大,同时也浪费时间,虽然可能有数据库连接池优化,但是尽量少使用数据库是我们编程的原则. 比如,我们的test.jsp 和test1.jsp都需要得到一个参数userdir,这个userdir是从数据库中得知,使用session将大大优化性能,程序如下: 设计一个javabean 存储userdir. public class UserEnv { private String userdir = \private String userurl = \public UserEnv(){ //构建方法初始化userdir,可以从数据库中读取,这里简单给值ppp userdir=\System.out.println(\} public String getUserdir() throws Exception{ return userdir; } } test1.jsp程序: <%@ page contentType=\ Untitled this is test1.jsp:<%=myenv.getUserdir()%> test2.jsp程序: <%@ page contentType=\ Untitled this is test2.jsp:<%=myenv.getUserdir()%> 无论用户先调用test1.jsp还是test2.jsp, java bean UserEnv总是先初始化一次, 由于这个bean存在周期是seesion,因此该用户第二次以后只要在seesion有效期内再调用,myenv.getUserdir()将直接从bean内存中读取变量,不必再初始化.这样提高速度,又减少数据库访问量. 这样,我们就有了一个jsp程序之间共享变量或方法 的实现办法.

《===============戏说模板模式=================》

? 备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用

意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。

? 同时跟几个MM聊天时,一定要记清楚刚才跟MM说了些什么话,不然MM发现

了会不高兴的哦,幸亏我有个备忘录,刚才与哪个MM说了什么话我都拷贝一份放到备忘录里面保存,这样可以随时察看以前的记录啦。

39

东方标准 http://soft.oneedu.cn

14.行为 / 观察者模式(Observer)

Java深入到一定程度,就不可避免的碰到设计模式(design pattern)这一概念,了解设计模式,将使自己对java中的接口或抽象类应用有更深的理解。设计模式在java的中型系统中应用广泛,遵循一定的编程模式,才能使自己的代码便于理解,易于交流,Observer(观察者)模式是比较常用的一个模式,尤其在界面设计中应用广泛,而本站所关注的是Java在电子商务系统中应用,因此想从电子商务实例中分析Observer的应用。

虽然网上商店形式多样,每个站点有自己的特色,但也有其一般的共性,单就\商品的变化,以便及时通知订户\这一点,是很多网上商店共有的模式,这一模式类似Observer pattern观察者模式。

具体的说,如果网上商店中商品在名称、价格等方面有变化,如果系统能自动通知会员,将是网上商店区别传统商店的一大特色。这就需要在商品product中加入Observer这样角色,以便product细节发生变化时,Observer能自动观察到这种变化,并能进行及时的update或notify动作。

Java的API还为为我们提供现成的Observer接口Java。util.Observer.我们只要直接使用它就可以。

我们必须extends Java.util.Observer才能真正使用它: 1.提供Add/Delete observer的方法;

2.提供通知(notisfy) 所有observer的方法;

//产品类 可供Jsp直接使用UseBean调用 该类主要执行产品数据库插入、更新 public class product extends Observable{ private String name; private float price;

public String getName(){ return name;} public void setName(String name){ this.name=name; //设置变化点 setChanged();

notifyObservers(name); }

public float getPrice(){ return price;} public void setPrice(float price){ this.price=price; //设置变化点 setChanged();

notifyObservers(new Float(price));

40

东方标准 http://soft.oneedu.cn

}

//以下可以是数据库更新 插入命令. public void saveToDb(){ ...... }

我们注意到,在product类中 的setXXX方法中,我们设置了 notify(通知)方法, 当Jsp表单调用setXXX(如何调用见我的另外一篇文章),实际上就触发了notisfyObservers方法,这将通知相应观察者应该采取行动了。

下面看看这些观察者的代码,他们究竟采取了什么行动:

//观察者NameObserver主要用来对产品名称(name)进行观察的 public class NameObserver implements Observer{ private String name=null;

public void update(Observable obj,Object arg){ if (arg instanceof String){ name=(String)arg;

//产品名称改变值在name中

System.out.println(\ } } }

//观察者PriceObserver主要用来对产品价格(price)进行观察的 public class PriceObserver implements Observer{ private float price=0;

public void update(Observable obj,Object arg){ if (arg instanceof Float){

price=((Float)arg).floatValue();

System.out.println(\ } } }

Jsp中我们可以来正式执行这段观察者程序:

if (request.getParameter(\{

product.saveToDb();

41

东方标准 http://soft.oneedu.cn

out.println(\产品数据变动 保存! 并已经自动通知客户\ }else{

//加入观察者

product.addObserver(nameobs); product.addObserver(priceobs); %>

//request.getRequestURI()是产生本jsp的程序名,就是自己调用自己 <% } %>

执行改Jsp程序,会出现一个表单录入界面, 需要输入产品名称 产品价格, 点按Submit后,还是执行该jsp的 if (request.getParameter(\之间的代码。

由于这里使用了数据javabeans的自动赋值概念,实际程序自动执行了setName setPrice语句。你会在服务器控制台中发现下面信息:

NameObserver :name changet to ?????(Jsp表单中输入的产品名称) PriceObserver :price changet to ???(Jsp表单中输入的产品价格); 这说明观察者已经在行动了.!!

同时你会在执行jsp的浏览器端得到信息 : 产品数据变动 保存! 并已经自动通知客户。上文由于使用jsp概念,隐含很多自动动作,现将调用观察者的Java代码写如下: public class Test {

public static void main(String args[]){

Product product=new Product();

NameObserver nameobs=new NameObserver(); PriceObserver priceobs=new PriceObserver(); //加入观察者

product.addObserver(nameobs); product.addObserver(priceobs); product.setName(\橘子红了\product.setPrice(9.22f); } }

《===============戏说观察模式=================》

? 观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题

对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。

? 想知道咱们公司最新MM情报吗?加入公司的MM情报邮件组就行了,tom负责搜

集情报,他发现的新情报不用一个一个通知我们,直接发布给邮件组,我们作为订阅者(观察者)就可以及时收到情报啦

42

东方标准 http://soft.oneedu.cn

15.行为 / 责任链模式(Chain of Responsibility)

定义 :(CoR) 是用一系列类(classes)试图处理一个请求request,这些类之间是一个松散的耦合,唯一共同点是在他们之间传递request. 也就是说,来了一个请求,A类先处理,如果没有处理,就传递到B类处理,如果没有处理,就传递到C类处理,就这样象一个链条(chain)一样传递下去。

如何使用 ? 虽然这一段是如何使用CoR,但是也是演示什么是CoR。有一个 Handler接口:

public interface Handler{

public void handleRequest(); }

这是一个处理request的事例, 如果有多种request,比如 请求帮助/请求打印或请求格式化: 最先想到的解决方案是:在接口中增加多个请求: public interface Handler{

public void handleHelp(); public void handlePrint(); public void handleFormat(); }

具体是一段实现接口Handler代码:

public class ConcreteHandler implements Handler{ private Handler successor;

public ConcreteHandler(Handler successor){ this.successor=successor; }

public void handleHelp(){

//具体处理请求Help的代码 ... }

public void handlePrint(){

//如果是print 转去处理Print successor.handlePrint(); }

public void handleFormat(){

//如果是Format 转去处理format successor.handleFormat(); } }

一共有三个这样的具体实现类,上面是处理help,还有处理Print 处理Format这大概是我们最常用的编程思路。

虽然思路简单明了,但是有一个扩展问题,如果我们需要再增加一个请求request种类,需要修改接口及其每一个实现。

第二方案:将每种request都变成一个接口,因此我们有以下代码 : public interface HelpHandler{

43

东方标准 http://soft.oneedu.cn

public void handleHelp(); }

public interface PrintHandler{ public void handlePrint(); }

public interface FormatHandler{ public void handleFormat(); }

public class ConcreteHandler

implements HelpHandler,PrintHandler,FormatHandlet{ private HelpHandler helpSuccessor; private PrintHandler printSuccessor; private FormatHandler formatSuccessor;

public ConcreteHandler(HelpHandler helpSuccessor,PrintHandler printSuccessor,

FormatHandler formatSuccessor){

this.helpSuccessor=helpSuccessor; this.printSuccessor=printSuccessor; this.formatSuccessor=formatSuccessor; }

public void handleHelp(){ ....... }

public void handlePrint(){this.printSuccessor=printSuccessor;}

public void handleFormat(){this.formatSuccessor=formatSuccessor;} }

这个办法在增加新的请求request情况下,只是节省了接口的修改量,接口实现ConcreteHandler还需要修改。而且代码显然不简单美丽。

解决方案3: 在Handler接口中只使用一个参数化方法: public interface Handler{

public void handleRequest(String request); }

那么Handler实现代码如下:

public class ConcreteHandler implements Handler{ private Handler successor;

public ConcreteHandler(Handler successor){ this.successor=successor; }

public void handleRequest(String request){ if (request.equals(\

//这里是处理Help的具体代码 }else

//传递到下一个

successor.handle(request);

44

东方标准 http://soft.oneedu.cn

} } }

这里先假设request是String类型,如果不是怎么办?当然我们可以创建一个专门类Request 最后解决方案:接口Handler的代码如下: public interface Handler{ public void handleRequest(Request request); } Request类的定义: public class Request{ private String type; public Request(String type){this.type=type;} public String getType(){return type;} public void execute(){ //request真正具体行为代码 } } 那么Handler实现代码如下: public class ConcreteHandler implements Handler{ private Handler successor; public ConcreteHandler(Handler successor){ this.successor=successor; } public void handleRequest(Request request){ if (request instanceof HelpRequest){ //这里是处理Help的具体代码 }else if (request instanceof PrintRequst){ request.execute(); }else successor.handle(request); //传递到下一个 } } } 这个解决方案就是CoR, 在一个链上,都有相应职责的类,因此叫Chain of Responsibility。

CoR的优点: 因为无法预知来自外界(客户端)的请求是属于哪种类型,每个类如果碰到它不能处理的请求只要放弃就可以。

缺点: 是效率低,因为一个请求的完成可能要遍历到最后才可能完成,当然也可以用树的概念优化。在Java AWT1.0中,对于鼠标按键事情的处理就是使用CoR,到Java.1.1以后,就使用Observer代替CoR扩展性差,因为在CoR中,一定要有一个统一的接口Handler.局限性就在这里。

与Command模式区别: Command 模式需要事先协商客户端和服务器端的调用关系,比如 1 代表 start 2 代表 move 等,这些 都是封装在 request 中,到达服务器端再分解。 CoR 模式就无需这种事先约定,服务器端可以使用 CoR 模式进行客户端请求的猜测,一个个猜测试验。

《===============戏说责任链模式================》

? 在责任链模式中,很多对象由每一个对象对其下家的引用而接 起来形成一条链。请

求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。 ? 晚上去上英语课,为了好开溜坐到了最后一排,哇,前面坐了好几个漂亮的MM哎,

找张纸条,写上“Hi,可以做我的女朋友吗?如果不愿意请向前传”,纸条就一个接一个的传上去了,糟糕,传到第一排的MM把纸条传给老师了,听说是个老处女呀,快跑!

45

东方标准 http://soft.oneedu.cn

16.行为 / 命令模式(Command)

Command模式是最让我疑惑的一个模式,我在阅读了很多代码后,才感觉隐约掌握其大概原理,我认为理解设计模式最主要是掌握起原理构造,这样才对自己实际编程有指导作用.Command模式实际上不是个很具体,规定很多的模式,正是这个灵活性,让人有些confuse。

定义 : 将来自客户端的请求传入一个对象,无需了解这个请求激活的动作或有关接受这个请求的处理细节。

这是一种两台机器之间通讯联系性质的模式,类似传统过程语言的CallBack功能。 优点 : 解耦了发送者和接受者之间联系。 发送者调用一个操作,接受者接受请求执行相应的动作,因为使用Command模式解耦,发送者无需知道接受者任何接口。

不少Command模式的代码都是针对图形界面的,它实际就是菜单命令,我们在一个下拉菜单选择一个命令时,然后会执行一些动作。

将这些命令封装成在一个类中,然后用户(调用者)再对这个类进行操作,这就是Command模式,换句话说,本来用户(调用者)是直接调用这些命令的,如菜单上打开文档(调用者),就直接指向打开文档的代码,使用Command模式,就是在这两者之间增加一个中间者,将这种直接关系拗断,同时两者之间都隔离,基本没有关系了。

显然这样做的好处是符合封装的特性,降低耦合度,Command是将对行为进行封装的典型模式,Factory是将创建进行封装的模式,

从Command模式,我也发现设计模式一个\通病\好象喜欢将简单的问题复杂化, 喜欢在不同类中增加第三者,当然这样做有利于代码的健壮性 可维护性 还有复用性。

如何使用 ? 具体的Command模式代码各式各样,因为如何封装命令,不同系统,有不同的做法.下面事例是将命令封装在一个Collection的List中,任何对象一旦加入List中,实际上装入了一个封闭的黑盒中,对象的特性消失了,只有取出时,才有可能模糊的分辨出:

典型的Command模式需要有一个接口.接口中有一个统一的方法,这就是\将命令/请求封装为对象\

public interface Command {

public abstract void execute ( ); }

具体不同命令/请求代码是实现接口Command,下面有三个具体命令 public class Engineer implements Command { public void execute( ) {

//do Engineer's command } }

public class Programmer implements Command { public void execute( ) {

//do programmer's command } }

public class Politician implements Command { public void execute( ) {

//do Politician's command } }

46

东方标准 http://soft.oneedu.cn

按照通常做法,我们就可以直接调用这三个Command,但是使用Command模式,我们要将他们封装起来,扔到黑盒子List里去: public class producer{

public static List produceRequests() { List queue = new ArrayList();

queue.add( new DomesticEngineer() ); queue.add( new Politician() ); queue.add( new Programmer() ); return queue; } }

这三个命令进入List中后,已经失去了其外表特征,以后再取出,也可能无法分辨出谁是Engineer 谁是Programmer了,看下面客户端如何调用Command模式: public class TestCommand {

public static void main(String[] args) {

List queue = Producer.produceRequests();

for (Iterator it = queue.iterator(); it.hasNext(); )

//客户端直接调用execute方法,无需知道被调用者的其它更多类的方法名。 ((Command)it.next()).execute(); } }

由此可见,调用者基本只和接口打交道,不合具体实现交互,这也体现了一个原则,面向接口编程,这样,以后增加第四个具体命令时,就不必修改调用者TestCommand中的代码了。

理解了上面的代码的核心原理,在使用中,就应该各人有自己方法了,特别是在如何分离调用者和具体命令上,有很多实现方法,上面的代码是使用\从List过一遍\的做法.这种做法只是为了演示。

使用Command模式的一个好理由还因为它能实现Undo功能.每个具体命令都可以记住它刚刚执行的动作,并且在需要时恢复。

Command模式在界面设计中应用广泛.Java的Swing中菜单命令都是使用Command模式,由于Java在界面设计的性能上还有欠缺,因此界面设计具体代码我们就不讨论,网络上有很多这样的示例。

《===============戏说命令模式=================》

? 命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执

行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。

? 俺有一个MM家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信

息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送COMMAND,就数你最小气,才请我吃面。”

47

东方标准 http://soft.oneedu.cn

17.行为 / 状态模式(State)

定义: 不同的状态,不同的行为;或者说,每个状态有着相应的行为。

何时使用 ? State模式在实际使用中比较多,适合\状态的切换\。因为我们经常会使用If elseif else 进行状态切换, 如果针对状态的这样判断切换反复出现,我们就要联想到是否可以采取State模式了。

不只是根据状态,也有根据属性。如果某个对象的属性不同,对象的行为就不一样,这点在数据库系统中出现频率比较高,我们经常会在一个数据表的尾部,加上property属性含义的字段,用以标识记录中一些特殊性质的记录,这种属性的改变(切换)又是随时可能发生的,就有可能要使用State。

是否使用 ? 在实际使用,类似开关一样的状态切换是很多的,但有时并不是那么明显,取决于你的经验和对系统的理解深度。

这里要阐述的是\开关切换状态\和\一般的状态判断\是有一些区别的, \一般的状态判断\也是有 if..elseif结构,例如: if (which==1) state=\ else if (which==2) state=\ else if (which==3) state=\

这是一个 \一般的状态判断\值的不同是根据which变量来决定的,which和state没有关系。如果改成:

if (state.euqals(\ else if (state.euqals(\ else if (state.euqals(\

这就是 \开关切换状态\是将state的状态从\切换到\再切换到\在切换到\好象一个旋转开关,这种状态改变就可以使用State模式了。

如果单纯有上面一种将\这一个方向切换,也不一定需要使

用State模式,因为State模式会建立很多子类,复杂化,但是如果又发生另外一个行为:将上面的 切换方向反过来切换,或者需要任意切换,就需要State了。 请看下例:

public class Context{

private Color state=null; public void push(){

//如果当前red状态 就切换到blue if (state==Color.red) state=Color.blue; //如果当前blue状态 就切换到green

else if (state==Color.blue) state=Color.green; //如果当前black状态 就切换到red

else if (state==Color.black) state=Color.red; //如果当前green状态 就切换到black

else if (state==Color.green) state=Color.black; Sample sample=new Sample(state); sample.operate(); }

public void pull(){

//与push状态切换正好相反

48

东方标准 http://soft.oneedu.cn

if (state==Color.green) state=Color.blue;

else if (state==Color.black) state=Color.green; else if (state==Color.blue) state=Color.red; else if (state==Color.red) state=Color.black; Sample2 sample2=new Sample2(state); sample2.operate(); } }

在上例中,我们有两个动作push推和pull拉,这两个开关动作,改变了Context颜色,至此,我们就需要使用State模式优化它。

另外注意 : 但就上例,state的变化,只是简单的颜色赋值,这个具体行为是很简单的,State适合巨大的具体行为,因此在,就本例,实际使用中也不一定非要使用State模式,这会增加子类的数目,简单的变复杂。

例如: 银行帐户, 经常会在Open 状态和Close状态间转换。

例如: 经典的TcpConnection, Tcp的状态有创建 侦听 关闭三个,并且反复转换,其创建侦听

关闭的具体行为不是简单一两句就能完成的,适合使用State

例如: 信箱POP帐号, 会有四种状态, start HaveUsername Authorized quit,每个状态对应的行

为应该是比较大的。适合使用State

例如: 在工具箱挑选不同工具,可以看成在不同工具中切换,适合使用State。如 具体绘图程序,

用户可以选择不同工具绘制方框 直线 曲线,这种状态切换可以使用State。 如何使用 ? State需要两种类型实体参与:

1. state manager 状态管理器 ,就是开关 ,如上面例子的Context实际就是一个state manager, 在state manager中有对状态的切换动作。

2. 用抽象类或接口实现的父类,,不同状态就是继承这个父类的不同子类。 以上面的Context为例。我们要修改它,建立两个类型的实体。 第一步: 首先建立一个父类: public abstract class State{

public abstract void handlepush(Context c); public abstract void handlepull(Context c); public abstract void getcolor(); }

父类中的方法要对应state manager中的开关行为,在state manager中 本例就是Context中,有两个开关动作push推和pull拉。那么在状态父类中就要有具体处理这两个动作:handlepush() handlepull(); 同时还需要一个获取push或pull结果的方法getcolor() 下面是具体子类的实现:

public class BlueState extends State{ public void handlepush(Context c){

//根据push方法\如果是blue状态的切换到green\ c.setState(new GreenState()); }

public void handlepull(Context c){

//根据pull方法\如果是blue状态的切换到red\ c.setState(new RedState()); }

49

东方标准 http://soft.oneedu.cn

public abstract void getcolor(){ return (Color.blue)} }

同样 其他状态的子类实现如blue一样。

第二步: 要重新改写State manager 也就是本例的Context: public class Context{

private Sate state=null; //我们将原来的 Color state 改成了新建的State state; //setState是用来改变state的状态 使用setState实现状态的切换 pulic void setState(State state){ this.state=state; }

public void push(){

//状态的切换的细节部分,在本例中是颜色的变化,已经封装在子类的handlepush中实现,这里无需关心

state.handlepush(this);

//因为sample要使用state中的一个切换结果,使用getColor() Sample sample=new Sample(state.getColor()); sample.operate(); }

public void pull(){

state.handlepull(this);

Sample2 sample2=new Sample2(state.getColor()); sample2.operate(); } }

至此,我们也就实现了State的refactorying过程。

以上只是相当简单的一个实例,在实际应用中,handlepush或handelpull的处理是复杂的。 状态模式优点:

(1) 封装转换过程,也就是转换规则

(2) 枚举可能的状态,因此,需要事先确定状态种类。 状态模式可以允许客户端改变状态的转换行为,而状态机则是能够自动改变状态,状态机是一个比较独立的而且复杂的机制,具体可参考一个状态机开源项目: http://sourceforge.net/projects/smframework/ ;

状态模式在工作流或游戏等各种系统中有大量使用,甚至是这些系统的核心功能设计,例如政府OA中,一个批文的状态有多种:未办;正在办理;正在批示;正在审核;已经完成等各种状态,使用状态机可以封装这个状态的变化规则,从而达到扩充状态时,不必涉及到状态的使用者。

在网络游戏中,一个游戏活动存在开始;开玩;正在玩;输赢等各种状态,使用状态模式就可以实现游戏状态的总控,而游戏状态决定了游戏的各个方面,使用状态模式可以对整个游戏架构功能实现起到决定的主导作用。

状态模式实质:

使用状态模式前,客户端外界需要介入改变状态,而状态改变的实现是琐碎或复杂的。 使用状态模式后,客户端外界可以直接使用事件Event实现,根本不必关心该事件导致如何状态变化,这些是由状态机等内部实现。

50

本文来源:https://www.bwwdw.com/article/cu5x.html

Top