优雅のJava(零)—— 面向问题的学习
何谓代码的简洁,只剩下思想,而没有重复,当然了这是理想情况,但我们尽量做到DRY(Don’t repeat yourself)
为啥要DRY?除了看着牙碜,还有个大问题在于,难以测试,debug以及更新,如果重复代码多了,面对日新月异的需求,你在原来基础上改一下,那就是牵一发而动全身,可不得裂开?另外,如果这个重复的代码有bug,那你要debug岂不是全部代码都要看一遍?可能几万行代码慢慢看~
举个栗子
我们想去深圳玩,用面向对象的思维来做的话,我们要找旅游社,而旅游社给我们提供坐火车去深圳的服务,那么这个服务的类,代码如下:
public class GoToSZByTrain{
String name;
GoToSZByTrain(String name){
this.name = name;
}
protected void register(){
System.out.println(name+" is registered for a travel to SZ by Train");
}
protected void pay(){
System.out.println(name+" is payed for a travel to SZ by Train");
}
protected void setOut(){
System.out.println("Have a nice journey, Mr."+name);
}
protected void journey(){
System.out.println("Mr."+name+" has taken a journey to SZ");
}
}
很快旅游社又提供 坐飞机去深圳旅游的服务,这样的话旅游社程序员想到我可以CV一下:
public class GoToSZByPlane{
protected String name;
GoToSZByPlane(String name){
this.name = name;
}
protected void register(){
System.out.println(name+" is registered for a travel to SZ by Plane");
}
protected void pay(){
System.out.println(name+" is payed for a travel to SZ by Plane");
}
protected void setOut(){
System.out.println("Have a nice journey, Mr."+name);
}
protected void journey(){
System.out.println("Mr."+name+" has taken a journey to SZ");
}
}
好景不长,事实上还有很多去深圳的方式,比如飞机也分直达的与需要中转的(较便宜),火车也分直达与需要中转的,还有轮船、汽车、动车、高铁等各种方式,这么说来得CV特别多的类。
没事,这个程序猿也不嫌牙碜,他就是头铁,愣头小青年,硬CV十多个类。过了几天,然而问题来了,国家要求实名制,这意味着每个服务类的流程中要添加实名步骤,这个就裂开了,因为刚刚CV的这么十多个类都要添加一遍
其实你们早发现了——这些流程步骤特别相近,有些甚至是一模一样的,这就是代码重复冗余,那么我们第一个想法,就是用模板思维,这些服务都是一个模板,一个模子刻出来的,那只需要一个模板+各种变种即可,
这里我们可以用类的继承来实现,我们看到,setOut方法和journey方法其实是完全一样,不变的,而其他方法有变化,那么,父类放上,已经抽离出来的,完全相同的方法,而有变化的方法则推迟到子类再去具体实现
如下:
protected abstract class GoToSZ{
protected String name;
GoToSZ(String name){
this.name = name;
}
protected void register(){
System.out.println(name+" is registered for a travel to SZ");
}
protected void pay(){
System.out.println(name+" is payed for a travel to SZ");
}
protected final void setOut(){
System.out.println("Have a nice journey, Mr."+name);
}
protected final void journey(){
System.out.println("Mr."+name+" has taken a journey to SZ");
}
}
public class GoToSZByTrain extends GoToSZ{
protected String name;
GoToSZByTrain(String name){
super(name);
}
@Override
protected void register(){
System.out.println(name+" is registered for a travel to SZ by Train");
}
@Override
protected void pay(){
System.out.println(name+" is payed for a travel to SZ by Train");
}
}
直观的来看,我们省下来两个完全一样的步骤,不必再重复了,而且更重要的是,即变更该需求,添加“实名认证”这个步骤,由于步骤相同,我们也只需要在父类那边加上即可,子类代码都不用动。
另外补充一个细节,假设你不想让子类放飞自我的override覆盖父类的方法,可以在父类设置final属性。
这就是一种很简单的思想模式——模板方法(Template Method),父类就是模板。
想过一个问题,这里我们可以抽离出一个模板,作为父类,可是实际情况有这么简单嘛
我们可以把模板看作一种“特性”,毕竟我们之所以能抽离成一个模板父类,是因为他们有共同的特征,那问题来了,特性只能有一个吗?其实不然,可能我们需要一个杂交产物,或者说集百家之长的对象,那该怎么办?熟悉java的话你知道,java的继承只能单继承,只能有一个爸爸,所以通过继承是不可行的,所以该怎么办呢?
我们总结一下,第一层是一个模板对应多个子类,一对多,
第二层是多个模板创造多个子类,但是这个多个模板的继承,创造过程只有一层,所以是单层的多对多,也就是排列组合的数量为 A n n A_n^n Ann
第三层,则更加有意思——多层的多对多,因为原来的类执行的各个环节也可以新增需求,那么各个环节都有变数,如果我们环节的变式数量分别为 n , m , k n,m,k n,m,k,那么最终可能的结果就是 A n n ⋅ A m m ⋅ A k k . . . , n , m , k A_n^n·A_m^m·A_k^k...,n,m,k Ann⋅Amm⋅Akk...,n,m,k均为每层环节模板(特性)的种类。
这个我称之为特性杂交问题的实现。
无论多少层,实际上我们想要就是一种模式——装饰器模式,因为我们是根据新的需求,在原有的每个环节,或者说每一层,基于原有的类上添加特性与功能(不是修改与删除),那么,只需要一个可以添加特性的对象,也就是可以装饰原有的类,这样一方面比继承要灵活太多,也完美解决单层的或者多层的多对多。
这里有个细节,我们说装饰,也就是这是可拆卸的,也是可叠加的,就好像包快递包裹,你想包多少层就包多少层,比如像下面这个代码所揭示的一样:
MyClass originInstance = new MyClass();
MyClass afterDecorationInstance = new Decorator1(new Decorator2(new Decorator3(originInstance)));
这个初始的对象实例(originInstance)被连续三个装饰器装饰完,变成afterDecorationInstance,就像被包装了三层包装的包裹。
值得注意的是,这里虽然说添加特性,但不是增加方法的意思,只是原有的方法做增强,换言之,其接口规范没有改变,只是每个接口的实现方法做的功能增强。
所以我们也能大概猜出来,这个装饰器应该也需要遵守接口规范(interface)
下面举个栗子,
我们先写个简单的接口:
public interface MyInterface {
public void myOperation();
}
然后准备一个初始类,他只能自我介绍一下,打印一句话而已
public class MyClass implements MyInterface{
private String name;
MyClass(String name){
this.name = name;
}
public void myOperation(){
System.out.println("I'm "+name);
}
}
然后我们用于增强的装饰器decoration:
public class Decorator1 implements MyInterface{
private MyInterface myInstance;
Decorator1(MyInterface myInstance){
this.myInstance = myInstance;
}
public void myOperation() {
System.out.println("Functional enhancement by decoration 1");
myInstance.myOperation();
System.out.println("Functional enhancement by decoration 1");
}
}
其他的装饰器也类似,
然后我们在主class那边运行main线程:
public class MainClass {
public static void main(String[] args) {
MyInterface myInstance = new MyClass("Ryan");
System.out.println("--before enhancement");
myInstance.myOperation();
System.out.println("\n--After enhancement");
MyInterface afterDecoration = new Decorator3(new Decorator2(new Decorator1(myInstance)));
afterDecoration.myOperation();
}
}
最终结果就是
--before enhancement
I'm Ryan
--After enhancement
Functional enhancement by decoration 3
Functional enhancement by decoration 2
Functional enhancement by decoration 1
I'm Ryan
Functional enhancement by decoration 1
Functional enhancement by decoration 2
Functional enhancement by decoration 3
可以看到,实际实现的方法看起来也没啥了不起的——传入你想要增强的对象,调用其方法的同时,在前后做些增强即可,也就是装饰了原对象。
看吧,其实设计模式没这么玄乎,当然灵活运用很难,但不至于是那种不可能学会的东西,你即便写不出来,至少能看出来吧,吃不到猪肉,我得见过猪跑~
那么我们用装饰器来试试之前的例子,现在提个需求,
所有交通工具会分为直达与中转类的,这两种的费用,执行的流程有不同,
另外,到了深圳后的服务分为“经济套餐”、“标准套餐”、“豪华套餐”,每种套餐对应的服务也不一样,计费也不同。
最后,旅行社想要拓展业务,不只是到深圳,而是到全国任意城市,也就是大约300个,因为到的城市不同,当地的办事处不同,当地景点旅游的内容、风土人情也不同,所以服务流程也有变化。
我们算算如果全部用继承来做,会有多少排列组合,有多少子类:
N = C 2 1 ⋅ C 3 1 ⋅ C 300 1 = 1800 N = C_2^1·C_3^1·C_{300}^1 = 1800 N=C21⋅C31⋅C3001=1800
如果可以选择多个城市都旅游,那可就恐怖了, A 300 300 A_{300}^{300} A300300是非常巨大的数字
还记得之前说的,多层,多对多嘛,这就算个例子,这里有三层,第一层decorator有两个,第二层有三个,第三层有300个,其实相对于1800已经好太多了
当然了 实际使用,还需要改点东西,比如结合继承来使用,我们可以设置一个装饰器基类,比如全国的办事处的相同特性,抽象出来的国家级的抽象基类,然后再根据各个省的办事处相同特性,抽象成省级的抽象基类,最后弄到然后再弄300个市级装饰器子类。这样就避免了装饰器的更新不便,以及代码重复冗余的问题。其实这里甚至可以嵌套装饰器:)装饰器的装饰器
其实,刚刚的特性杂交问题的一个孪生问题就是子类泛滥问题,假设用继承的方法来完成子类泛滥,抛开其不能实现特性杂交的问题,如果硬是要实现,也必定会造成子类泛滥,想一下,这么多变式,你要用 A n n A_n^n Ann个子类来实现(单层多对多),岂不裂开,人都蒙了,而且还陷入代码重复——因为你做的功能增强是重复的,你相当于往不同的子类上做了相同的功能增强,而且假设这个功能增强又需要更新或者删除,那么所有的经过功能增强的子类都需要修改,健壮性很差,耦合度太高。
所以,装饰器模式很好的做到了实现特性杂交的同时还避免了子类泛滥问题。
这个来自java.io包的OutputStream家族,其实也用到了装饰器模式。
我们看代码就知道了:
BufferedOutputStream bis = new BufferedOutputStream(
new FileOutputStream(
new File("/home/user/abc.txt")));
BufferedOutputStream bis2 = new BufferedOutputStream(
new FilterOutputStream(
new FileOutputStream(
new File("/home/user/abc.txt"))));
很明显 能这么玩,很装饰器:)这里第三级就是第二级类的装饰器
这里我想问个问题,比如对于第一种解决方案,模板方法,如果父类不是抽象类,你觉得妥不妥当?
其实抽象类被认为特别具有模板的含义,算是约定吧,
另外,我们说了,正是因为单单考虑那些共性的方法时,我们没法确定子类的,个性的那些方法该怎么实现,在这个时候我们只能推迟,把个性化的东西留给后边的子类去具体实现,这是自顶而下的设计。但是真的就不能继承自,普通的,可以有自己实例的类嘛?
当然不是,不然那些多级继承的,比方说我们刚刚提到的那个java.io里面的OutputStream家族
第三级并非继承自抽象类,也能用,因为其父类本身就能独立使用,而我们旅游社的装饰器类的多层级继承,就不行,因为省级的类并不能独立使用(指的是可以实例化,并且完成服务),仅此而已。所以不用把自己限定的太死,模板方法关键点在于父类作为模板,抽离出子类共性的东西,这点是思想的关键。
同样的,我们看到这个类家族也运用了装饰器模式,但是他是那种像传统教材写的,无论原始的,待装饰的类还是装饰器类,都实现自同一个接口(等价于遵守统一规范)这种方式嘛?也不是,他直接略过接口,采用层层继承的方式来应用装饰器模式,第三级是第二级类的装饰器。
当然了,我相信为什么这么设计,他们还有考虑到别的因素,我猜测,首先是自顶而下的设计,还有就是单一职责原则。
试想,如果这个stream类的这个树要是都实现同一个接口,岂不是这个接口会很乱?应该说它的功能未免太复杂了吧,粒度不够细,什么才叫细?细到分解成Closeable接口和Flushable接口,那为什么这么细?还是为了代码重用,这里的接口也有模板的味道,别忘了我们除了OutputStream,还有InputStream呢!
public abstract class OutputStream implements Closeable, Flushable
public abstract class InputStream implements Closeable
所以说要抓住思想,我认为装饰器思想的实现关键在于那个待装饰类实例的引入,这个传入的类实例,其规范,无论是利用接口还是抽象类实现,都必须是一样的,否则没法实现层层装饰的套娃效果。
既然关键部分说了,那其他非关键的部分其实是可以更改得咯(比起之前我们举的范例)?没错!我们经过对OutputStream家族的学习,发现可以打破前面所说的,因为接口规范的限制,装饰器的实现方法只能是接口所规定的,而不能新增,这个说法:
我们看
首先传入的对象实例确实是OutputStream,也就是大家共同遵守的规范,但是装饰器本身却是继承自FilterOutputStream,这样相对于我们都实现同一个接口这种方式,明显的好处就是他的功能更强了,继承自的FilterOutputStream本身就可以在OutputStream上加东西,它还可以进一步加东西,不受约束,甚至你愿意的话,装饰器所继承的类可以和OutputStream无关。所以装饰器本身核心在于传入的对象实例的规范。
其实说实话道理挺简单的,我BB这么多只想提醒屏幕前的你和我自己——千万别照着教科书的例程敲过去就完事了,你要想,这个设计模式要解决的问题是什么,它能解决的核心原因是什么,而不是盲目的照猫画虎,这样就陷入了教条主义,限制了自己,之后再想灵活应用就非常困难了。
子类泛滥
另外,关于子类泛滥的诱因很多,我们这里是避开其中一种——功能增强时导致的子类泛滥,下一节我们将聊聊另一种我认为的诱因——输入输出参数类型不定,导致的子类泛滥。
AOP?
细心的朋友可能发现,这个装饰器模式有点面向切面编程AOP的味道,都是在操作的前后加点共同的东西(特性),以切面实现功能增强,实际上AOP是指一种思想,或者说是种“接口”,具体怎么实现,仁者见仁智者见智,比如说我们的装饰器就是AOP的简单实现方式之一,所以你的感觉没有错:)
预告
另外,设计模式都是特定场合能有用的套路,但实际上换个场合都有些问题,比方说,我们的装饰器能够实现特性杂交,已经很强了,但是如果接口的规范改变,那可能所有的装饰器都需要改变,特定情况时候也不是那么好用,所以可能需要一种方法,可以不受接口的约束,这就是我们第四篇也会介绍的代理模式。
当然这里你会觉得这有点钻牛角尖,其实我们还是需要抱着学徒的心,以包容的心态,去尽力理解那些大牛宣扬的设计模式,而不要自己觉得没用就一开始心理上拒绝,至少我的话,永远是个菜鸡,想先听,先看,再去实践中验证,最终取其精华去其糟粕,融为自己的东西。
下一篇
下一篇我们来看看 单例的创建是怎么实现的
专栏导航
优雅のJava(零)—— 面向问题的学习