`
喻红叶
  • 浏览: 39546 次
  • 性别: Icon_minigender_1
  • 来自: 哈尔滨
社区版块
存档分类
最新评论

Java与模式-模板方法模式

 
阅读更多

引言

激情火热的世界杯已经进行到了四分之一决赛,相信各位球迷都已熬夜熬的心肝脾肺肾俱虚。一场足球比赛包括以下几个部分:上半场比赛,中场休息,下半场,加时赛,点球大战。这五部分构成了一场完整的杯赛比赛,这是现行规则下的固定套路,不会因为其他因素而改变。将足球比赛转换成Java代码如下:

public abstract class WorldCupMatch {
	/**
	 * 比赛流程,不可更改,是程序的顶级执行逻辑
	 * 注意是final修饰的
	 */
	public final void match() {
		//上半场比赛
		firstHalf();
		//中场休息
		halfTime();
		//下半场
		secondHalf();
		
		//加时赛
		overtime();
		//点球大战
		penalty();
	}
	
	//不同比赛的上半场比赛是不同的,留给具体的比赛去实现
	public abstract void firstHalf();
	//不同的比赛下半场比赛是不同的,留给具体的比赛区实现
	public abstract void secondHalf();
	
	//所有的比赛都要中场休息,中场休息的行为是相同的
	public void halfTime() {
		System.out.println("中场休息15分钟,双方球员返回更衣室休息!");
	}
	
	//不是所有的比赛都有加时赛,如果没有加时赛的比赛就不需要实现overtime(),由父类给出一个默认实现
	public void overtime() {
		System.out.println("90分钟结束战斗,无需加时,洗洗睡!");
	}
	
	//不是所有的比赛都有点球大战,如果没有点球大战的比赛就不需要实现penalty(),由父类给出一个默认实现
	public void penalty() {
		System.out.println("已经分出胜负,无需残酷的点球大战,洗洗睡!");
	}
}

一场比赛要遵循FIFA的规定,具体的比赛必须要完成上半场和下半场,中场休息也是固定的套路,但是一场比赛可能没有加时赛和点球大战,至于加时和点球则要根据实际情况,下面以阿根廷VS瑞士为例:

class ArgVSSu extends WorldCupMatch {

	/**
	 * 比赛的组成部分,必须由子类实现
	 */
	public void firstHalf() {
		System.out.println("上半场双方比较平稳,毫无亮点");		
	}

	/**
	 * 比赛的组成部分,必须由子类实现
	 */
	public void secondHalf() {
		System.out.println("下半场阿根廷攻势狂暴,可惜中锋不给力");
	}
	
	/**
	 * 有父类实现的hook方法,子类可以改变其行为
	 */
	public void overtime() {
		System.out.println("118分钟时梅西助攻迪玛利亚破门,阿根廷取胜");
	}
}

模板方法模式

在上面的两段代码中有如下鲜明的特点:

(1)父类WorldCupMatch规定了程序执行的逻辑,也就是match()方法,它定义了一个算法的步骤,每一个步骤都被一个方法所代表,允许子类为一个或多个步骤提供实现。

(2)父类将由子类实现的方法设置为abstract,将这些具体行为推迟到子类中实现,子类也必须实现这些步骤;

(3)父类对某些步骤提供了空实现或者默认实现,这就给子类选择的余地,是否覆盖父类的行为完全由子类决定。这些由父类默认实现的方法叫做钩子方法(hook)。

将上面的三个特点再进行抽象,就得到了一个应用极广的设计模式,模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。在上面的代码中,match()就是一个模板方法,这也是这个设计模式的名字由来。这个模式涉及两个角色:

(1)抽象模板角色:定义并实现一个模板方法,它给出一个顶级逻辑的骨架,顶级逻辑的组成步骤放在相应的抽象操作中;定义组成逻辑步骤的方法,需要推迟到子类实现的定义为抽象方法,还可以提供默认实现的钩子方法,当然了也可提供具体方法;

(2)具体模板角色:实现父类定义的抽象方法,根据实际情况,还可以覆盖父类的钩子方法,这样在不改变算法逻辑的情况下给出不同的实现。

上面的例子已经很能代表模板方法模式了,还是再给出一个示意性代码:

abstract class AbstractTemplate {
	/**
	 *模板方法
	 */
	public void templateMethod() {
		//调用基本方法,由子类实现
		doOperation1();
		doOperation2();
		
		//自己实现的基本方法
		doOperation3();
	}
	
	//抽象方法,推迟到子类实现
	public abstract void doOperation1();
	public abstract void doOperation2();
	
	//钩子方法
	public void doOperation3() {
		//实现代码
	}
}

class ConcreteTemplate extends AbstractTemplate {
	//必须实现父类中的抽象方法,逻辑的组成部分
	public void doOperation1() {
		//实现代码
	}
	public void doOperation2() {
		//实现代码
	}
	
	//可以有选择的覆盖父类钩子方法
	//public void doOperation3(){ 实现代码}
}
好莱坞原则

艺人在将简历提交给好莱坞的娱乐公司后,他们所能做的就是等待,因为娱乐告诉他们:不要给我们打电话,我们会打给你。这就是好莱坞原则,这个原则的关键之处是:高层拥有对低层的完全控制,低层只是整个流程的一部分实现。好莱坞原则体现了模板模式的关键:子类可以置换掉父类的可变部分,但是不可以改变模板方法所代表的顶级逻辑

设计理念

模板方法模式使用是极广的,但是很多人在使用的时候没有意识到自己已经使用了这个模式。模板方法模式是基于继承的代码复用技术,它的设计理念是尽量减少必须由子类覆盖的基本方法的数目。我们来看看模板方法模式中的方法

模板方法

定在在抽象模板类中,把基本操作方法组合在一起形成一个总算法或总行为。

基本方法

在模板方法中有三类基本方法:

抽象方法,总行为的组成步骤,定义在抽象模板类中,由子类实现,且子类必须实现。

具体方法,由抽象模板类实现,子类并不对覆盖,也就是最普通的方法。

钩子方法,由抽象模板类提供空实现或者默认实现,子类可以选择是否覆盖此方法。钩子方法还能对某些将要发生或者已经发生的步骤做出反应,有能力为其父类做一些决定。

在写模板类时,怎么知道什么时候该使用抽象方法,什么时候使用钩子方法呢?如果这个方法是算法的必选部分,同时又是可变部分,那么就必须是抽象方法,子类必须提供该方法的实现;如果这个方法是算法的可选部分,那么就可以设置为钩子方法,同时提供默认实现。

在写模板类时要时刻记住:尽量减少必须由子类覆盖的方法数目,如果某些步骤是可选的,那么就实现成钩子方法,而不是抽象方法。

重构

在对一个继承的等级结构做重构时,一个应当遵从的原则是将行为尽量移动到结构的高端,而将状态移动到结构的低端。Auer指出:

(1)应当根据行为而不是状态定义一个类。也就是说,一个类的实现首先建立在行为的基础上,而不是建立在状态的基础上。

(2)在实现行为时,是用抽象状态而不是用具体状态。如果一个行为涉及到对象的状态时,用间接引用而不是直接引用。换言之,应当是用取值方法而不是直接引用属性。

(3)给操作划分层次。一个类的行为应当放到方法里面(对应抽象模板中的基本方法),这些方法可以很方便地在子类中加以置换。

(4)将状态属性的确认推迟到子类中。不要在抽象类中过早地声明属性变量,应当将它们尽量地推迟到子类中去声明。如果在抽象类中需要使用状态属性的话,可以调用抽象取值方法,将抽象的取值方法推迟到子类中实现。

Auer指出的这些原则,其实就将设计引导到了模板方法模式里,我们使用模板方法模式来对一段代码进行重构。

将大方法打破

关于一个方法多大才算合适,有一个较为直观的指导:一个方法的长度不应超过一页。对于一个很长的方法,应该将它拆分成若干个小的方法,将这些小的方法作为原来方法的组成部分。可以看出,拆分后的原方法就是一个模板方法。下面的方法就是一个很大的方法:

public void bigMethod() {
	...
	代码块1
	...
	代码块2
	...
	代码块3
	...
	代码块4
	...
	代码块5
}
应用模板方法模式拆分后:
public void bigMethod() {
	step1();
	step2();
	if(...)
		step3();
	else if(...)
		step4()
		else
		 step5();
}

public void step1(); {
	代码块1
}
public void step2(); {
	代码块2
}
...
public void step5(); {
	代码块5
}
建立取值方法

拆分之后,原本各个代码块共享的局部变量无法再被共享,有两种解决办法,一是将原本共享的局部变量改为父类的私有变量;二是使用抽象取值方法,在原本需要这个变量的地方调用抽象取值方法,这样可以将状态的声明推迟到子类中,使得等级结构的高端与状态分离,符合Auer提出的第(4)条规则。常量也应该建立取值方法,这样将常量的声明推迟到子类中。

反复进行

如果有一些特征相同,功能相同而细节不同的方法,那么就可以利用继承和多态继续重构下去。重构完成后,所有的基本方法都是合适的细粒度,所有的常数都放到了常数方法里面。这时候就得到一个类,里面有一个模板方法和一系列基本方法。

多态取代条件转移

在上面拆分后的代码中,有一个条件转移块,这样的条件转移块在大而不当的方法中常常见到,,可以使用多态取代条件转移,这是另一个重要的重构原则。将所考虑的类当做抽象模板类,设计一些具体子类,再将基本方法中特征相同、功能相同而细节不同的方法划分到不同子类中。在上面的例子中,step3(),step4(),step5()就符合这些条件,因此,我们可以将这三个类当做抽象方法newMethod()在不同子类的具体实现,这样根据多态性,便可以调用相应的方法了。

public abstract class AbstractClass {
	/**
	 * 模板方法
	 */
	public void bigMethod() {
		step1();
		step2();
		newMethod();
	}
	
	//抽象方法
	public abstract void step1();
	public abstract void step2();
	public abstract void newMethod();
}

class ConcreteClass1 extends AbstractClass {
	public void newMethod() {
		代码块3
	}
}
...
重构完成后,这其实是一个标准的模板方法模式。在需要的情况下,建立独立的类负责独立的行为,从而可以将独立的行为委派到独立的对象里面。

转载请注明:喻红叶《Java与模式-模板方法模式》

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics