目录
使用多重策略,有一种基本场景:一个大的策略由多个小的策略步骤组合得到。例如一个投资策略,要考虑投资方向(房地产、实业等),再考虑投资地点,再考虑投资金额。一个最后的投资策略结果,是3个小的选择的数学组合。类似的例子比比皆是。
多次决策而非多继承
【实验6:一系列决策】假设类Client要选择一个代表人物/Person演讲/lecture()。Person的筛选的条件,包括职业/Profession,例如从教师、医生、律师……中选择了教师;地域/Region,例如从北京、上海、武汉……中选择了武汉;年龄段/Ages,例如从中年、青年、少年……中选择了青年。
在考虑设计方案的时候,有人将目光聚焦到类型层面,认为Profession、Region和Ages分别描述了人/Person的一个方面,或者说不同的变化维度。在类型层面上,Person通过多继承机制将成为Profession、Region和Ages的子类型,是一种不现实的实现方式,因为多继承的方式处理,将出现“武汉-年轻-教师”这样的随意组合的难以计数的类型,事实上,类型爆炸是显而易见的。关注类型层面没有抓住问题的关键,虽然当作引子来讨论桥接模式没什么大问题,但容易将人的关注点带歪,他们忽略了真正应该关注的是多次策略选择——函数级别。Client需要的是某个人完成lecture(),其代码要反映出不同的选项。能否如下体现多次策略选择呢?
foo(){
p. lecture (); // Profession的子类的代码
r. say (); // Region的子类的代码
a. talk (); // Ages的子类的代码
}
不可。这是模板方法模式的应用方式,模板方法模式中各个抽象函数彼此独立。而演讲时一个函数如r. say ()应该成为另一个函数a. talk ()的一部分。可以用伪代码表示为:
p. lecture (string , r. say ( a. talk () ))
下面讨论实验6的实现。例程并不需要出现Person这个类(它让人联想到多继承),而Client只需要依赖Profession、Region和Ages三者之一。这三者的依赖关系可以是无序的,例程4-8中随意地采取了一种依赖关系Profession→Region→Ages。
在Profession→Region的实现中,可以使得Profession有一个成员变量Region,也可以将后者作为Profession的一个函数的参数。
在使用spring/God等依赖注入的场合,成员变量比较方便,如
public interface Profession{
//下一次的策略,Region由配置文件任意选择
Region next = (Region)God.create("Region"); // import yqj2065.util.God;
abstract public String lecture(String s);//必须将自己的特色融入到Region. say ()中
}
在[4.3.3参数化]中考虑Region作为Profession的一个函数的参数。
虽然Profession、Region和Ages都可以有很多的子类型,简单起见,例程中和配置文件中仅仅使用图4-4提供的3个具体类如Teacher等(因为后面将通过lambda来创建它们的对象)。
此时,Client仅仅依赖一个接口Profession,Profession表现为一个大策略选择——找到了一个代表人物。该大策略如何组合而成,或者说如何实现,Client不感兴趣。对于Client而言,大策略的选择,就是选择了Profession的子类型如Teacher,而Teacher将自己的特色,融入到Region选项的代码中。换言之,Region的子类型的代码中,都拥有Profession的代码。
package chap4.bridgeP; //后面的类型中,包语句略
public interface Ages{
public String talk(String s);
}
public class Youth implements Ages{
public String talk(String s){
String myStyle="带着青年的朝气,";
return myStyle + s ;
}
}
import yqj2065.util.God;
public interface Region{
Ages next = (Ages)God.create("Ages");
public String say(String s);
}
public class Wuhaner implements Region{
public String say(String s){
return next.talk ("带着武汉的腔调,"+s);
}
}
public interface Profession{
Region next = (Region)God.create("Region");
public String lecture(String s);;//必须将自己的特色融入到Region中
}
public class Teacher implements Profession{
//String myStyle =this.getClass().getSimpleName();
@Override public String lecture(String s){
String myStyle = "带着粉笔灰的味道,说到:"+s;
next.say(myStyle);
}
}
public class Client{
public static void main(String[] args){
Profession p = (Profession)God.create("Profession");
p.lecture("hello world");
}
}
运行Client的输出为:
带着青年的朝气,带着武汉的腔调,带着粉笔灰的味道,说到:hello world
一个代表人物,如果是“中年-北京-律师”对象,将输出另外的模板式演讲。所谓模板式演讲,意味着两个“中年-北京-律师”对象的演讲肯定一样;不同的组合出来的对象,都按照固定的格式输出,如:Ages风格+ Region风格+Profession风格+说到:+内容。
可以通过代码更改输出的顺序。【如果你想初步理解桥接模式,可以到此为止】
小模板方法和IStyle
如果编写多个Profession的子类型,就会发现Profession的各种子类型如医生,不仅要给出自己的风格,还要考虑与Region的融合。代码如下:
@Override public void lecture(String s){
String myStyle = "带着消毒水的味道,说到:"+s;
next.say(myStyle);
}
模板方法模式在面向对象领域的用法,就是将子类的公共代码——如与Region的融合的代码,从各种子类型中提取出来,作为父类型Profession的小模板方法。而子类型Teacher仅仅给出自己的风格。对例程4-8进行的改进,如例程4-9所示。同理,在Region和Ages类层次上也采用小模板方法。
package chap4.bridgeP;
import yqj2065.util.God;
public interface Profession{
Region next = (Region)God.create("Region");
//小模板方法
public default String lecture(String s) {
s = ",说到:"+s;
String myStyle = this.style();//特意使用this
return next.say(myStyle +s);
}
public default String style() { return "";} // public abstract String style();
}
package chap4.bridgeP;
public class Teacher implements Profession{
//代码向上集中后
//String myStyle = "带着粉笔灰的味道"
String myStyle =this.getClass().getSimpleName();
@Override public String style(){ return myStyle; }
}//Region和Ages类层次 略;Client代码不变
现在考虑一个有趣的问题。当Profession、Region和Ages都使用了小模板方法,而且各自的小模板方法所调用的抽象方法,在3个接口中都是style()时,一个意想不到的类型诞生了。在上一节中提到,设计时要避免某个类型如Person继承3个接口Profession、Region和Ages,而现在出现了3个接口的父类型IStyle。
在介绍桥接模式时,突兀的IStyle并没有什么作用。假设出现下面的代码,会有什么意味呢?
package chap4.bridgeP;
public interface IStyle{
public default String style () { return "";}
}
public interface Profession extends IStyle{
IStyle base; //setter方法略
//小模板方法
public default String lecture(String s) {
s = ",说到:"+s;
String myStyle = this.style();//特意使用this
return base.say(myStyle +s);
}
}
不要诧异,装饰模式是特殊情况下的桥接模式。
参数化
比较Profession有一个成员变量Region,将Region作为参数更体现桥接模式的本质,Profession的行为lecture(String s,Region next, Ages end)被Region和Ages参数化。
package chap4.bridgeP;
public interface Region /* extends IStyle*/ {
//参数化
public default String say(String s,Ages next) {
s = ","+s;
String myStyle = this.style();//特意使用this
return next.talk(myStyle +s);
}
public abstract String style();
}
public interface Profession {
public default String lecture(String s, Region next, Ages end) {
s = ",说到:"+s;
String myStyle = this.style();//特意使用this
return next.say(myStyle + s,end );
}.
public abstract String style();
}
package chap4.bridgeP;
import yqj2065.util.God;
public class Client{
public static void main(String[] args){
//p.lecture("hello world");
//参数化之后
Profession p = (Profession)God.create("Profession");
Region next = (Region)God.create("Region");
Ages end = (Ages)God.create("Ages");
p.lecture("hello world",next,end);
((Profession)()->"律师").lecture("hello world",()->"北京",()->"中年");
}
}
★桥接模式,将2次以上的策略选择串接起来形成大策略。
参数化更体现桥接模式的本质,Client需要一个教师-武汉-青年的组合人物的lecture(),而该方法包含了多个类层次的代码的融合。
[吐槽GoF]
[GoF]关于桥接模式意图的描述:"decouple anabstraction from its implementation so that the two can varyindependently".(将抽象与它的实现分离,使它们都可以独立地变化)。
这是一种较难理解的描述方式,“抽象与它的实现”是很少组队在一起的夫妻,通常使用的成对词汇是接口-实现,抽象类-实现类。
当依赖关系为Client→Profession→Region→Ages时,Client其实可以依赖Profession、Region和Ages三者中任何一个,例子中仅仅碰巧将Profession作为了“抽象”;而把Region作为“抽象”即Profession的lecture(String s)方法的实现的参与者(被GoF称为Profession的实现),Ages则递归地成为Profession的“实现的实现”。从封装的角度看,GoF的说法有一定的道理,就是说,把Profession、Region和Ages看成一个整体/大类型,而Client只看见Profession,可以将Profession视为抽象部分(不如称为接口),而Profession背后隐藏的类型可以称为实现部分。然而,Client关注的是多次策略选择,而非封装技巧。
在参数化的代码中,Client依同时依赖Profession、Region 和Ages三者。而所谓“抽象”,不过是有方法将“实现”和“实现的实现”作为参数。换言之,Profession、Region和Ages分别描述了人不同的变化维度,通常谁作为“抽象”时随机的。
此外,所有被依赖的都是抽象类型。自然地,任何一次策略选择都可以独立地变化。
更加浓郁的函数式风格