Java 设计模式(零)—— 面向问题的学习 代码重复问题 模板方法 装饰器模式

2021/6/17 1:24:14

本文主要是介绍Java 设计模式(零)—— 面向问题的学习 代码重复问题 模板方法 装饰器模式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

    • 前言
    • 设计模式的来源
    • 什么是设计模式 design pattern
    • DRY 代码重复问题
    • 解决方案:模板方法 Template Method
    • 特性杂交问题
    • 解决方案:装饰器模式 Decorator
    • 旅游社的例子
    • 子类泛滥
    • 后记

前言

说实话一直想学设计模式,感觉酷酷的,但问题是官方的定义不讲人话,而类似尚硅谷的教程,虽然已经是梳理得很开了,但是感觉学的还是有点死记硬背的味道,即,学一个新模式,敲一下代码,看一下优点,下一个,

然后自己写的时候还是选择综合征+畏畏缩缩担心代码不够好+最后写的自己都不知道是什么

最要命的是,模式多了开始晕,组合起来运用更加不可能,然后大佬们夸夸其谈将自己用了啥模式啥模式balabala,你只能装作自己听懂了然后尴尬而不失礼貌的微笑,生怕大佬瞧不起你。

我不装了,这么学和背课本差不多,就感觉是被动的接受但永远不能内化成自己的东西,如果,换个思路,我来针对某些问题,“创造”模式,有种主人翁的心理,而非被填充知识的鸭子,那又如何?带着这个思路,打算重新审视一下,设计模式

这个我称为,面向问题的学习(Question Oriented Learning),接下来我们短暂的唠一下嗑,说一下设计模式的起源,然后进入本次文章的核心——模板方法。

设计模式的来源

历史来源就是,最早有哥们总结了建筑领域的253个模式,然后90年代,四人帮(GoF)站在前人的肩膀上,将前面那253用到了编程领域,然后写了那本书,专门为了解决面向对象设计开发的问题。
当然了,设计模式真正流行,我猜想是,来到IT行业,流水线生产,多人配合的时代,就好比服务端程序,有些人之前写PHP也行啊,你甚至还可以用c实现啊,为什么后面主流是java,因为应用场景错综复杂,需求变化多端,需要多人合作,这时代码的艺术体现出来了,有人开始琢磨怎么样才能让多人合作项目更加高效,估计就这么翻出来90年代还有个设计模式这种说法,用一下还挺好使,于是流传到今天。

什么是设计模式 design pattern

说白了 固定套路,那什么套路?用来解决一类问题的套路

比方说你写代码思考到,未来万一需求改变了,或者在原有基础上增加了,有没有优雅的方式能够高效更新代码的时候,这时如果你犯难了,说明你应对这类问题还没有套路,而如果用了相应的设计模式,相当于借助前人的思考成果,想出合适的解决方案,这就说明你学会了这一模式。

假设,你要是这次绞尽脑汁想出来个法子,瞎写,然后苟着苟着勉强达到设计目标,下次遇到同样的问题,又凉了,因为没有总结,那就是最低效的,也是没学会设计模式的结果。

讲完这些我们说定义:针对某一上下文环境中一个问题的常用 通用解决方案

这告诉我们什么?通解是有,但是个性化的运用就看本事了。

实际上发明设计模式的Gof也没有定死这模式的标准,后人在此基础上,如果发现相似儿常见的问题,在Gof设计模式上做了点变式,那当然也是ok,
可以说这是好事,时代在发展,只会使用几十年前的方法论应对现在的问题,恐怕心有余而力不足。

DRY 代码重复问题

何谓代码的简洁,只剩下思想,而没有重复,当然了这是理想情况,但我们尽量做到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的这么十多个类都要添加一遍

解决方案:模板方法 Template Method

其实你们早发现了——这些流程步骤特别相近,有些甚至是一模一样的,这就是代码重复冗余,那么我们第一个想法,就是用模板思维,这些服务都是一个模板,一个模子刻出来的,那只需要一个模板+各种变种即可,
这里我们可以用类的继承来实现,我们看到,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均为每层环节模板(特性)的种类。

这个我称之为特性杂交问题的实现。

解决方案:装饰器模式 Decorator

无论多少层,实际上我们想要就是一种模式——装饰器模式,因为我们是根据新的需求,在原有的每个环节,或者说每一层,基于原有的类上添加特性与功能(不是修改与删除),那么,只需要一个可以添加特性的对象,也就是可以装饰原有的类,这样一方面比继承要灵活太多,也完美解决单层的或者多层的多对多。

这里有个细节,我们说装饰,也就是这是可拆卸的,也是可叠加的,就好像包快递包裹,你想包多少层就包多少层,比如像下面这个代码所揭示的一样:

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 设计模式(零)—— 面向问题的学习 代码重复问题 模板方法 装饰器模式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程