设计模式

Posted on Aug 5, 2021

个人对设计模式的一点理解,由于没有专研于开发,可能会有不恰当之处,欢迎批评

策略模式

策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。——《HEAD FIRST 设计模式》

就我个人对 OO 的理解来看,抽象是 OO 中最重要的思想之一,也就是从多个具体的对象中抽象出相似之处。有的时候相似之处可能不够相似,抽象就会变得困难,策略模式就是一种抽象具有相似“算法”的一族对象的一种模式。《HEAD FIRST 设计模式》中举的鸭子的例子挺有意思的,不够既然这里在定义里面说到了“算法”,我就用狭义的算法来举例。

假设我们要解决一个最速问题——比如从城市的一个点到另一个点怎么样才能最快到达。首先我们会把城市抽象成一张图,然后对它求最短路,这再明显不过。那么我们很容易得出这样一个继承关系

糟糕的设计

这个设计是非常糟糕的,因为我们在选择最短路算法时,必然要考虑到算法的效率和功能性问题,比如 Dijkstra 算法可能是平均表现最好的算法,但是它不能处理负权边和负环(虽然我想不出来哪个道路会是负权边,但是这里为了举例就不管了),SPFA 在某些特殊情况下可能会死掉,但是它可以处理负环,Floyd 可以处理多源最短路。那么这里 Dijkstra,SPFA,Floyd 就组成了一个最短路算法族,对于不同的情况我们选择不同的算法(也就是不同的策略):

  • 没有负权边和负环:Dijkstra
  • 有负权边:SPFA
  • 要求求得多个点到点的最短路径:Floyd

打一个比方,上面的 HangZhou 继承类,(假设)由于杭州市有负权边,所以必须用 SPFA,如果杭州没有了负权边,那么可能把它换成 Dijkstra 才是最好的选择,这样在维护的时候就不得不重写杭州类的 calcFastest 函数,假设我们有一百个城市要维护,今天北京获得了外星人的支持造出来一条通过时间为负数的道路,明天杭州的负权路被反科技恐怖分子炸了,维护者就不得不一直修改代码,这样维护起来肯定是非常痛苦的。使用策略模式就可以解决这个问题,使对象甚至可以在运行时改变其使用的算法。具体的做法,就是把使用算法计算“委托”给别的对象来做:

使用策略模式的设计

使用这样的方式,我们就可以在运行时修改使用的算法,调用下面这个函数即可

changeCalcWay(Calcer newWay)
{
    this.calcer = newWay;
}

事实上,我们可以在运行时让对象自己选择最合适的算法

if (hasNegtiveEdge(hangZhou))
{
    newWay = new Spfa();
    hangZhou.changeCalcWay(newWay);
}
else if (hasLotsFastestProblems(hangZhou))
{
    newWay = new Floyd();
    hangZhou.changeCalcWay(newWay);
}
else
{
    newWay = new Dijkstra();
    hangZhou.changeCalcWay(newWay);
}

如果我们需要添加新的最短路计算方式,只需要再继承一个 Calcer 即可。如果我们对 Spfa 进行了改进,所有使用 Spfa 的对象都可以获得这个改进,并且对象自身甚至不需要知道这一点。

和《HEAD FIRST 设计模式》中的鸭子的飞行方法和叫法一样,这里的计算最短路就是解决问题的策略,我们有很多策略可选,策略模式就提供给了我们一个良好的切换策略的设计方法。

这个设计模式体现了这些 OO 设计原则

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程

所谓的针对接口编程,其实就是 OO 的运行时多态的体现。

观察者模式

观察者模式在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。

观察者模式是松耦合的,主题(即被依赖的对象)和观察者互相不知道也不关心对方的具体实现,更具有弹性,更能应对变化。

实现观察者模式其实比较容易,所有的观察者只需要实现一个统一的接口让主题进行 update 操作,主题本身只要维护一个观察者列表并且提供注册为观察者和注销的方法即可。每次主题发生变化时只要遍历观察者列表并调用 update 操作就可以让所有的观察者获得更新。

观察者模式

那么每次 CentralData 中的私有数据变化时,只要调用 notifyObserver 方法,就会依次调用每个观察者的 update 方法。

观察者模式其实比较容易理解,这种设计模式最重要的是实现了互相依赖的对象间的松耦合,同时建立了一套触发机制,使数据的更改可以根据需求传递给观察者。

考虑到观察者本身也可以成为主题,当出现循环观察的情况时,可能会造成严重的错误。

观察者模式体现的 OO 原则:

为交互对象之间的松耦合设计而努力。

装饰者模式

装饰者模式动态的将责任附加到对象上。想要拓展功能,装饰者提供有别于继承的另一种选择。

OO 中如果需要扩展一个类的功能,继承是一有力机制,但是继承是编译期完成的,无法在运行时动态完成功能的拓展。装饰者模式提供了一种动态的拓展功能的设计方法。

java.io 包就是使用装饰者模式的典型,举例而言

java.io

FilterInputStream 虚类是一个抽象装饰器,通过继承该虚类可以装饰它左边三个具体的继承自 InputStream 的类。

装饰者模式虽然说是有别于继承的,但是这里仍然继承了 InputStream 超类,这是因为每一个装饰器和被装饰对象都需要是同一个类型,这样才可以用统一的方法来访问装饰器,也可以保证装饰器本身可以再被装饰。也就是说这里的继承并非继承功能,而是继承结构。

当我们需要添加新的功能时,只需要建立新的装饰者并用装饰器装饰需要被继承的对象即可,而不需要修改原先的代码,做到了“对扩展开放,对修改关闭”。

比如如果我们需要添加一个将所有的输入都转为小写的功能,就只需要建立一个对应的装饰器

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

public class LowerCaseInputStream extends FilterInputStream {

    public LowerCaseInputStream(InputStream in) {
        super(in);
    }

    public int read() throws IOException {
        int c = super.read();
        return (c == -1 ? c : Character.toLowerCase((char) c));
    }

    public int read(byte[] b, int offset, int len) throws IOException {
        int result = super.read(b, offset, len);
        for (int i = 0; i < result; i++) {
            b[i] = (byte) Character.toLowerCase((char) b[i]);
        }
        return result;
    }
}

然后用该装饰器装饰别的输入流即可

InputStream in =
                new LowerCaseInputStream(
                    new BufferedInputStream(
                        new FileInputStream("test.txt")));

之后都会总结在时光机里面,具体的写出来似乎没什么必要,网络上已有大量的资料。我只在时光机里面简单地记录一下自己的心得。