设计模式总结(3)【模板方法模式、迭代器模式、代理模式、桥接模式】

模板方法设计模式

一、基本概念

模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。

例如: 在现实生活中,完成某件事情是需要 n 个固定步骤的。如”在淘宝进行购物”这件事情的完成一般需要三个步骤: 登录网站、挑选商品、 付款。但对于登录网站与付款这两步,每个人几乎都是相同的操作。但不同的地方是,每个人所挑选的商品是不同的。
在软件开发过程中同样存在这样的情况。某类的某个方法的实现,需要几个固定步骤。在这些固定步骤中,对于该类的不同对象, 有些步骤的实现是固定不变的,有些步骤的实现是大相径庭的,有些步骤的实现是可变可不变的。对于这种情况,就适合使用模板方法设计模式编程。

二、结构

基本结构:

在模板方法设计模式中,存在一个父类(一般是抽象的)。其中包含 两类方法: 模板方法 和 3 种步骤方法;

  • 模板方法: 即实现某种算法的方法步骤。这些步骤都是调用的步骤方法实现的。
  • 步骤方法:即完成模板方法的每个阶段性方法。
    • 抽象方法: 子类必须实现的方法。即子类的个性化定义;
    • 最终方法(固定方法): 子类不能重写的方法,即所有子类都要做的步骤;
    • 钩子方法: 父类给了默认实现,子类可以重写,也可以不重写的方法;

结构:

09_template_02.png

三、案例

实现在网站上购物的案例。

基本逻辑图:

09_template_01.png

代码:

首先看Shopping类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Shopping {

// 模板方法
public void buyGoods(){
useLogin(); //固定的方法,子类不能重写
buy(); //抽象方法,子类必须实现
pay(); //钩子方法(hook),子类可以重写
}

// 固定方法(最终方法)
public final void useLogin() {
System.out.println("用户登录");
}

//抽象方法
public abstract void buy();

// 钩子方法
public void pay(){
System.out.println("使用银联支付");
}
}

然后是两个子类:

1
2
3
4
5
6
public class ShoesShopping extends Shopping {
@Override
public void buy() {
System.out.println("购买Nike鞋子!!!!");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class ClothesShopping extends Shopping {
@Override
public void buy() {
System.out.println("购买海澜之家男装!!!!!");
}

// 重写了钩子方法
@Override
public void pay() {
System.out.println("使用支付宝支付");
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
public class MyTest {

public static void main(String[] args){
Shopping shoesShopping = new ShoesShopping();
shoesShopping.buyGoods();

System.out.println("-----------------------");
Shopping clothesShopping = new ClothesShopping();
clothesShopping.buyGoods();
}
}

输出:

1
2
3
4
5
6
7
用户登录
购买Nike鞋子!!!!
使用银联支付
-----------------------
用户登录
购买海澜之家男装!!!!!
使用支付宝支付

四、总结

有时候为了防止恶意操作,模板方法也会加上 final 关键词。

五、实际使用

Spring对数据库的操作在jdbc上面做了深层次的封装,使用spring的注入功能,可以把DataSource注册到JdbcTemplate之中。
这个JdbcTemplate用的是一种更高级的模板思想,具体说来,它引入了一种回调模式
对于JdbcTemplate,它是个抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class JdbcTemplate {  
public final Object execute(String sql){
Connection con=null;
Statement stmt=null;
try{
con=getConnection();
stmt=con.createStatement();
Object retValue=executeWithStatement(stmt,sql);
return retValue;
}catch(SQLException e){
...
}finally{
closeStatement(stmt);
releaseConnection(con);
}
}
protected abstract Object executeWithStatement(Statement stmt, String sql);
}

要是想使用,就必须要实现,但是其中的方法太多
因此就引出了中间态的回调

1
2
3
public interface StatementCallback{  
Object doWithStatement(Statement stmt);
}

然后在进行具体的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JdbcTemplate {  
public final Object execute(StatementCallback callback){
Connection con=null;
Statement stmt=null;
try{
con=getConnection();
stmt=con.createStatement();
Object retValue=callback.doWithStatement(stmt);
return retValue;
}catch(SQLException e){
...
}finally{
closeStatement(stmt);
releaseConnection(con);
}
}

...//其它方法定义
}

迭代器模式

一、基本概念

迭代器模式属于行为型模式。

这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。

如果我们需要统一遍历不同的集合(容器),也可以利用迭代器模式解决(下面的案例就是解决这个问题)。

二、结构

结构:

10_iterator_01.png

  • Container 是聚合类,其中 getIterator() 方法可以产生一个 Iterator
  • Iterator 主要定义了 hasNext()next() 方法;
  • 每一个Container的实现类都会有一个具体的Iterator实现类来帮助实现迭代;

三、案例

实现两个容器能够统一遍历。(或者两个以上)

先看代码结构: (这里我将两个迭代器实现类写在两个容器内部)

10_iterator_02.png

代码:

1
2
3
public interface Container {
Iterator getIterator();
}
1
2
3
4
public interface Iterator<E> {
boolean hasNext();
E next();
}

两个容器类,他们内部的容器不同,内部有自带不同的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ConcreteContainer1 implements Container {

public Integer[] arr;

public ConcreteContainer1() {
arr = new Integer[10];
for(int i = 0; i < 10; i++) arr[i] = i;
}

@Override
public Iterator getIterator() {
return new ConcreteIterator1<Integer>();
}

private class ConcreteIterator1<E> implements Iterator{

int index;

@Override
public boolean hasNext() {
if(index < arr.length)
return true;
return false;
}

@Override
public Object next() {
if(hasNext())
return arr[index++];
return null;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ConcreteContainer2 implements Container {

public ArrayList arr;

public ConcreteContainer2() {
arr = new ArrayList();
for(int i = 0; i < 10; i++) arr.add(i);
}

@Override
public Iterator getIterator() {
return new ConcreteIterator1<Integer>();
}

private class ConcreteIterator1<E> implements Iterator{

int index;

@Override
public boolean hasNext() {
if(index < arr.size())
return true;
return false;
}

@Override
public Object next() {
if(hasNext())
return arr.get(index++);
return null;
}
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 如果不这么做,需要区分c1和c2的遍历方式, 一个是数组,一个是List
public class MyTest {

public static void main(String[] args){
Container c1 = new ConcreteContainer1();
Container c2 = new ConcreteContainer2();

ArrayList<Iterator>its = new ArrayList<>();
its.add(c1.getIterator()); // 将c1迭代器加入its
its.add(c2.getIterator()); // 将c1迭代器加入its

// 实现了统一遍历, 只需要将迭代器加入到 its中即可
for(Iterator it : its) {
while(it.hasNext())
System.out.print( it.next() + " ");
System.out.println();
}
}
}

输出:

1
2
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9

四、总结

优点:

  • 在同一个聚合上可以有多个遍历;
  • 在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码;

代理模式

一、基本概念

代理模式是对象的结构模式。

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用(接口的引用)

二、静态代理

静态代理是指,代理类在程序运行前就已经定义好,其与目标类(被代理类)的关系在程序运行前就已经确立。

静态代理类似于企业与企业的法律顾问间的关系。法律顾问与企业的代理关系,并不是在“官司“发生后才建立的,而是之前就确立好的一种关系

而动态代理就是外面打官司一样,是官司发生了之后临时请的律师。

代理可以看做就是在被代理对象外面包裹一层(和装饰者类似但又不同):

案例: 比如我们有一个可以移动的坦克,它的主要方法是move(),但是我们需要记录它移动的时间,以及在它移动前后做日志,其静态代理的实现模式就类似下面的图:

11_proxy_01.png

两个代理类以及结构关系:

11_proxy_02.png

代码:

1
2
3
public interface Movable {
void move();
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Tank implements Movable {
@Override
public void move() {
// 坦克移动
System.out.println("Tank Moving......");
try {
Thread.sleep(new Random().nextInt(5000)); // 随机产生 1~5秒, 模拟坦克在移动 
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

两个代理类: TankTimeProxyTankLogProxy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TankTimeProxy implements Movable {

private Movable tank;

public TankTimeProxy(Movable tank) {
this.tank = tank;
}

@Override
public void move() {
// 在前面做一些事情: 记录开始时间
long start = System.currentTimeMillis();
System.out.println("start time : " + start);

tank.move();

// 在后面做一些事情: 记录结束时间,并计算move()运行时间
long end = System.currentTimeMillis();
System.out.println("end time : " + end);
System.out.println("spend all time : " + (end - start)/1000 + "s.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TankLogProxy implements Movable {

private Movable tank;

public TankLogProxy(Movable tank) {
this.tank = tank;
}

@Override
public void move() {
// tank 移动前记录日志
System.out.println("Tank Log start.......");

tank.move();

// tank 移动后记录日志
System.out.println("Tank Log end.......");
}
}

测试:

1
2
3
4
5
6
7
public class Client {
public static void main(String[] args){
Movable target = new TankLogProxy(new TankTimeProxy(new Tank())); //先记录时间,再记录日志
// Movable target = new TankTimeProxy(new TankLogProxy(new Tank())); //先记录日志,再记录时间
target.move();
}
}

输出:

1
2
3
4
5
6
Tank Log start.......
start time : 1551271511619
Tank Moving......
end time : 1551271514522
spend all time : 2s.
Tank Log end.......

这其中有两个很重要的点,那就是:

  • 两个代理对象内部都有着被代理对象(target)实现的接口的引用
  • 且两个代理对象都实现了被代理对象(target)实现的接口

三、基本动态代理

上面静态代理的缺点在哪?

现在单看做时间这个代理,如果我们现在多了一个飞机,飞机里面的方法是fly(),现在要给飞机做代理,那么我们不能用之前写的TankTimeProxy,我们需要额外的写一个PlaneTimeProxy,这明显是冗余代码,所以这就是静态代理最大的缺点,这可以用动态代理解决

动态代理是指,程序在整个运行过程中根本就不存在目标类的代理类(在JDK内部叫$Proxy0,我们看不到),目标对象的代理对象只是由代理生成工具(如代理工厂类) 在程序运行时由 JVM 根据反射等机制动态生成的。代理对象与目标对象的代理关系在程序运行时才确立。

对比静态代理,静态代理是指在程序运行前就已经定义好了目标类的代理类。代理类与目标类的代理关系在程序运行之前就确立了。

首先看动态代理的一些特点:

  • 动态代理不需要写出代理类的名字,你要的代理对象我直接给你产生,是使用的时候生成的;
  • 只需要调用Proxy.newProxyInstance()就可以给你产生代理类;

JDK动态代理相关API:

12_proxy_07.png

下面看使用动态代理解决上面的问题(可以用TimeProxy代理一切对象):

1
2
3
public interface Movable {
void move();
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Tank implements Movable {
@Override
public void move() {
// 坦克移动
System.out.println("Tank Moving......");
try {
Thread.sleep(new Random().nextInt(5000)); // 随机产生 1~5秒, 模拟坦克在移动 
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

新增的飞机:

1
2
3
public interface Flyable {
void fly();
}
1
2
3
4
5
6
7
8
9
10
11
public class Plane implements Flyable{
@Override
public void fly() {
System.out.println("Plane Flying......");
try {
Thread.sleep(new Random().nextInt(5000)); // 随机产生 1~5秒, 飞机在飞行 
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

我们的关键处理,即编写MyTimeProxyInvocationHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 静态代理做不到既为飞机做时间代理,又为坦克做时间代理,但是动态代理可以为所有对象做代理
public class MyTimeProxyInvocationHandler implements InvocationHandler {

private Object target;//注意这里是 Object ,不是Movable或者Flyable

public MyTimeProxyInvocationHandler(Object target) {
this.target = target;
}

// proxy : 代理对象 可以是一切对象 (Object)
// method : 目标方法
// args : 目标方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// 在前面做一些事情: 记录开始时间
long start = System.currentTimeMillis();
System.out.println("start time : " + start);

method.invoke(target, args); // 调用目标方法 invoke是调用的意思, 可以有返回值的方法(我们这里move和fly都没有返回值)

// 在后面做一些事情: 记录结束时间,并计算move()运行时间
long end = System.currentTimeMillis();
System.out.println("end time : " + end);
System.out.println("spend all time : " + (end - start)/1000 + "s.");

return null;
}
}

最后测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Client {
public static void main(String[] args){
Movable tank = new Tank();
//可以为所有对象产生时间代理的 InvocationHandler
MyTimeProxyInvocationHandler myInvocationHandler = new MyTimeProxyInvocationHandler(tank);
Movable tankProxy = (Movable) Proxy.newProxyInstance(
tank.getClass().getClassLoader(),
tank.getClass().getInterfaces(),
myInvocationHandler
);
tankProxy.move();

System.out.println("--------------------");

Flyable plane = new Plane();
myInvocationHandler = new MyTimeProxyInvocationHandler(plane);
// 为飞机产生代理, 为..产生代理,这样可以为很多东西产生代理,静态代理做不到
Flyable planeProxy = (Flyable) Proxy.newProxyInstance(
plane.getClass().getClassLoader(),
plane.getClass().getInterfaces(),
myInvocationHandler
);
planeProxy.fly();
}
}

输出(同时为TankPlane做了代理):

1
2
3
4
5
6
7
8
9
start time : 1551275526486
Tank Moving......
end time : 1551275531193
spend all time : 4s.
--------------------
start time : 1551275531195
Plane Flying......
end time : 1551275532996
spend all time : 1s.

我们分析一下这个代理过程:

11_proxy_03.png

调用过程(重要):

  • JDK内部的Proxy类在内部创建了一个$Proxy0的代理对象(它实现了目标对象所在接口Movable
  • $Proxy0内部有InvocationHandler接口的引用,然后在$Proxy中调用了接口的invoke()方法;
  • 而我们将InvocationHandler接口的实现类传入了Proxy,所以我们在实现类中加入的前后逻辑就会得到执行;

如果这里还不够理解,可以看代理模式(二),会模拟实现JDK的底层实现。

四、CGLIB动态代理

问题: 使用 JDK 的 Proxy 实现代理,要求目标类与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现

可以用 CGLIB 来解决上面的问题。

CGLIB 代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象

所以,使用CGLIB 生成动态代理,要求目标类必须能够被继承,即不能是 final 的类

基本结构:

12_proxy_06.png

代码:

Tank类(没有接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 没有实现接口
public class Tank {

public void move() {
// 坦克移动
System.out.println("Tank Moving......");
try {
Thread.sleep(new Random().nextInt(5000)); // 随机产生 1~5秒, 模拟坦克在移动 
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

MyCglibFactory类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

//需要实现MethodInterceptor, 当前这个类的对象就是一个回调对象
// MyCglibFactory 是 类A,它调用了Enhancer(类B)的方法: setCallback(this),而且将类A对象传给了类B
// 而类A 的 方法intercept会被类B的 setCallback调用,这就是回调设计模式
public class MyCglibFactory implements MethodInterceptor { //public interface MethodInterceptor extends Callback

private Tank target;

public MyCglibFactory(Tank target) {
this.target = target;
}

public Tank myCglibCreator() {
Enhancer enhancer = new Enhancer();

// 设置需要代理的对象 : 目标类(target) , 也是父类
enhancer.setSuperclass(Tank.class);

// 设置代理对象, 这是回调设计模式: 设置回调接口对象 :
enhancer.setCallback(this); // this代表当前类的对象,因为当前类实现了Callback

return (Tank) enhancer.create();
}

// 这个就是回调方法(类A的方法)
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 在前面做一些事情: 记录开始时间
long start = System.currentTimeMillis();
System.out.println("start time : " + start);

method.invoke(target, args);

// 在后面做一些事情: 记录结束时间,并计算move()运行时间
long end = System.currentTimeMillis();
System.out.println("end time : " + end);
System.out.println("spend all time : " + (end - start)/1000 + "s.");
return null;
}
}

测试:

1
2
3
4
5
6
public class Client {
public static void main(String[] args){
Tank proxyTank = new MyCglibFactory(new Tank()).myCglibCreator();
proxyTank.move();
}
}

输出(进行了时间代理TimeProxy):

1
2
3
4
start time : 1551327522964
Tank Moving......
end time : 1551327526214
spend all time : 3s.

上面的设计模式用到了回调设计模式:
在 Java 中,类 A调用类 B 中的某个方法 b(),然后类 B 又在某个时候反过来调用类 A中的某个方法 a(),对于 A来说,这个a() 方法便叫做回调方法。

Java 的接口提供了一种很好的方式来实现方法回调。这个方式就是定义一个简单的接口,在接口之中定义一个我们希望回调的方法。这个接口称为回调接口。(Callback)
在前面的例子中,我们定义的 MyCglibFactory 类就相当于前面所说的 A类,而 Enhancer 类则是 B 类。A 类中调用了Enhancer 类的 setCallback(this)方法,并将回调对象 this 作为实参传递给了Enhancer 类。Enhancer 类在后续执行过程中,会调用A类中的intercept()方法,这个 intercept()方法就是回调方法

五、实际应用

Spring的AOP
https://www.cnblogs.com/flowwind/p/4782606.html

桥接模式

一、基本概念

桥接模式属于结构型模式。

桥接模式其实只需要记住关键的一点: 解决的是不同的子类之间排列组合可以构成巨多的类的问题

比如:

13_brige_01.png

举个例子,你要画画了,画画本身是一个类,你画画要考虑画什么形状(Shape) (A),你画画还需要考虑用什么颜色去画(Color)(B)。

形状可以有圆、长方形、正方形….,而颜色可以用红、蓝、绿…..。

那我现在要用蓝笔画长方形,这是一种新的组合,用红笔画长方形,又是一种组合,这样会产生很多的排列组合,如果我们都写一个类,那就是不好的设计了。

二、案例

就用上面那个画画的例子来引出我们的桥接模式。

我们的改造就是。不需要那个A类了,而是在形状类B类这边有这C类(这里改成接口, 也可以用类)的引用。

具体结构如下:

13_bridge_02.png

然后还有一个关键的点:

注意一定要在Shape中有ColorAPI的引用,而且子类也要调用父类的构造函数。

1
2
3
public interface ColorAPI {
void draw(String name);
}

颜色(B)这边的三个实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BluePen implements ColorAPI {
@Override
public void draw(String name) {
System.out.println("用蓝笔画 " + name);
}
}
public class GreenPen implements ColorAPI {
@Override
public void draw(String name) {
System.out.println("用绿笔画 " + name);
}
}
public class RedPen implements ColorAPI {
@Override
public void draw(String name) {
System.out.println("用红笔画 " + name);
}
}

形状这边的大父类(A):

1
2
3
4
5
6
7
8
9
public abstract class Shape {

protected ColorAPI colorAPI; //这个父类必须要有 另一边的引用

protected Shape(ColorAPI colorAPI) {
this.colorAPI = colorAPI;
}
public abstract void dw(); // color
}

形状的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Circle extends Shape {

private String name;

public Circle(String name, ColorAPI colorAPI) {
super(colorAPI);
this.name = name;
}
@Override
public void dw() {
colorAPI.draw(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Rectangle extends Shape {

private String name;

public Rectangle(String name, ColorAPI colorAPI) {
super(colorAPI);
this.name = name;
}

@Override
public void dw() {
colorAPI.draw(name);
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
//桥接模式简单的说:  解决排列组合导致组合类巨多的问题
public class Test {

public static void main(String[] args){
Shape blueRectangle = new Rectangle("长方形", new BluePen());
blueRectangle.dw();

Shape redCircle = new Circle("圆", new RedPen());
redCircle.dw();
}
}

输出:

1
2
用蓝笔画 长方形
用红笔画 圆

再看一下整体框架图:

13_bridge_03.png

三、总结

对于两个独立变化的维度,使用桥接模式再适合不过了。实现减少排列组合减少的类。
可以这样理解:当想要使用某一模块的完整功能时,就把这一模块的基类和接口扩展到这个类中,这个类就充当了一个桥的作用(对于要使用的方法)