阅读 2200

阿里架构师讲面试:最easy的设计模式讲解

设计模式作用

模式虽好,但是不会使代码更复杂吗?

代码复杂与否很多时候是主观的。人们对模式的熟悉程度极大地左右他们如何看待基于模式的重构。当他们不熟悉某个模式时,会认为模式过于复杂,而熟悉了某个模式之后,通常就不会这么认为。

模式是前人智慧的结晶,模式是在某一背景下某类问题的一种解决方案。重用这种智慧是非常有益的。通常,实现模式有助于去除重复、简化逻辑、提高灵活性和扩展性。尽量地学习更多的模式,而不要认为模式太复杂而不使用模式。

对于一个场合到底用不用模式,这对所有的开发人员来说都是一个很纠结的问题。有时候,因为预见到需求上会发生的某些变化,为了系统的灵活性和可扩展性而使用了某种设计模式,但这个预见的需求偏偏没有,相反,没预见到的需求倒是来了不少,导致在修改代码的时候,使用的设计模式反而起了相反的作用,以至于整个项目组怨声载道。这样的例子,我相信每个程序设计者都遇到过。所以,基于敏捷开发的原则,我们在设计程序的时候,如果按照目前的需求,不使用某种模式也能很好地解决,那么我们就不要引入它,因为要引入一种设计模式并不困难,我们大可以在真正需要用到的时候再对系统进行改造,引入这个设计模式。

比如客户汇率发布模块优化为观察者模式,可以在迭代过程中实际需要时进行优化(have to)。如果一开始想不清楚必要性,则很容易给系统带来不必要的复杂度。

常见设计模式

本文整理了常见的设计模式共15个,如果觉得不太容易记,可以尝试下情景记忆法。把每种大类下的设计模式编辑到一段小情景里,可能会更容易记忆,且记得更牢固。

创建型

在热火朝天的工厂里,经理突然抽调单身汉小张去建造图纸上的原型

单例

单例有其独有的使用场景,一般是对于那些业务逻辑上限定不能多例只能单例的情况,例如:类似于计数器之类的存在,一般都需要使用一个实例来进行记录,若多例计数则会不准确。

  • 饿汉式

 何为饿?饿者,饥不择食;但凡有食,必急食之。此处同义:在加载类的时候就会创建类的单例,并保存在类中。

public class Singleton {
    // 首先,将 new Singleton() 堵死
    private Singleton() {};
    // 创建私有静态实例,意味着这个类第一次使用的时候就会进行创建
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
    // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
    // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
    public static Date getDate(String mode) {return new Date();}
}
复制代码

缺点:非懒加载,浪费内存空间

  • 懒汉式
//写法1,对getInstance加Synchronized锁,并发访问效率较低。
public class LHanDanli {
    //定义一个私有类变量来存放单例,私有的目的是指外部无法直接获取这个变量,而要使用提供的公共方法来获取
    private static LHanDanli dl = null;
    //定义私有构造器,表示只在类内部使用,亦指单例的实例只能在单例类内部创建
    private LHanDanli(){}
    //定义一个公共的公开方法来返回该类的实例,由于是懒汉式,需要在第一次使用时生成实例,所以为了线程安全,使用synchronized关键字来确保只会生成单例
    public static synchronized LHanDanli getInstance(){
        if(dl == null){
            dl = new LHanDanli();
        }
        return dl;
    }
}

//写法2,双重判断法(Double Check Lock,DCL)。
public class Singleton {
    // 首先,也是先堵死 new Singleton() 这条路
    private Singleton() {}
    // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
    private static volatile Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            // 加锁
            synchronized (Singleton.class) {
                // 这一次判断也是必须的,不然会有并发问题
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
复制代码

这里的双重指的的双重判断,而加锁单指那个synchronized,为什么要进行双重判断,其实很简单,第一重判断,如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例。如果没有创建,才会进入同步块,同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建。

DCL模式的优点就是,只有在对象需要被使用时才创建,第一次判断 INSTANCE == null为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化。这样既可以节约内存空间,又可以保证线程安全。但是,由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况。具体分析如下:

instance  = new SingleTon();
复制代码

这个步骤,其实在jvm里面的执行分为三步:

1.在堆内存开辟内存空间。

2.在堆内存中实例化SingleTon里面的各个参数。

3.把变量指向堆内存空间。

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,instance已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,即在JDK1.6及以后,只要定义为private volatile static SingleTon INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

  • 静态内部类模式(推荐)
public class SingleTon{
  private SingleTon(){}

  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }

  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
复制代码

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

缺点:无法传参数

参考:blog.csdn.net/mnb65482/ar…

享元

当我们项目中创建很多对象,而且这些对象存在许多相同模块,这时,我们可以将这些相同的模块提取出来采用享元模式生成单一对象,再使用这个对象与之前的诸多对象进行配合使用,这样无疑会节省很多空间。

www.cnblogs.com/V1haoge/p/6…

工厂

工厂,就是生产产品的地方。

在Java设计模式中使用工厂的概念,那就是生成对象的地方了。

本来直接就能创建的对象为何要增加一个工厂类呢?

这就需要了解工厂方法要解决的是什么问题了,如果只有一个类,我们直接new一个对象完事,这是最简单的;但是如果有多个类呢,而且这些类还需要针对不同的情况来创建不同的对象,这时候就需要工厂了,我们可以在工厂中根据条件来创建具体的对象。

这样一来就将调用方和具体的目标类进行了解耦,调用方根本就不知道需要创建那个对象,它只是提出了条件,然后工厂就可以根据给定的条件来决定创建哪一个对象。

  • 简单工厂(工厂生成单类产品,switch case)
/**
 * 桌子接口
 */
public interface Desk {
    String getType();
}

/**
 * 木质桌子
 */
public class WoodenDesk implements Desk{
    private String type = "木质桌";
    @Override
    public String getType() {
        return type;
    }
}

/**
 * 塑料桌
 */
public class PlasticDesk implements Desk {
    private String type = "塑料桌";
    @Override
    public String getType() {
        return type;
    }
}

/**
 * 桌子工厂
 */
public class DeskFactory {
    public static Desk createDesk(Type type) {
        switch (type) {
            case WOODEN:
                return new WoodenDesk();
            case PLASTIC:
                return new PlasticDesk();
            default:
                return null;
        }
    }
}

/**
 * 测试类
 */
public class Client {
    public static void main(String[] args) {
        Desk desk = DeskFactory.createDesk(Type.PLASTIC);
        System.out.println(desk.getType());
    }
}
复制代码

简单地说,简单工厂模式通常就是这样,一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。

  • 工厂方法(工厂生成单类产品,新增产品无需修改原有工厂)

抽象工厂类实现,每个具体工厂类只生产一种产品。简单工厂方法只有一个工厂类来面向多个目标(产品)实现。当目标实现(产品)增多时,我们不得不去修改工厂类的方法,使其兼容新的实现类型,这明显违背了开闭原则,所以出现了工厂方法模式。

  • 抽象工厂模式(工厂生产产品组合)

  • 为什么要引入抽象工厂

抽象工厂模式是对工厂方法模式的再升级,但是二者面对的场景稍显差别。

工厂方法模式面对的目标一般都是单类的,就比如工厂方法中所举的例子,目标就是桌子这一类商品。

如果是这样的呢:生产的是桌椅组合,目标的一套商品,每一套商品中的每类商品的种类的不同的,不同的组合形成不同的套装。这种情况下,就需要使用抽象工厂模式。

  • 抽象工厂和工厂方法的不同

抽象工厂模式面对的是一个组合体,如果将这一点排除的话,其他方面看起来,二者还是相似的。

实例

一个经典的例子是造一台电脑。我们先不引入抽象工厂模式,看看怎么实现。

因为电脑是由许多的构件组成的,我们将 CPU 和主板进行抽象,然后 CPU 由 CPUFactory 生产,主板由 MainBoardFactory 生产,然后,我们再将 CPU 和主板搭配起来组合在一起,如下图:

这个时候的客户端调用是这样的:

// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();
// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();
// 组装 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);
复制代码

单独看 CPU 工厂和主板工厂,它们分别是前面我们说的工厂模式。这种方式也容易扩展,因为要给电脑加硬盘的话,只需要加一个 HardDiskFactory 和相应的实现即可,不需要修改现有的工厂。

但是,这种方式有一个问题,那就是如果 Intel 家产的 CPU 和 AMD 产的主板不能兼容使用,那么这代码就容易出错,因为客户端并不知道它们不兼容,也就会错误地出现随意组合。

下面就是我们要说的产品族的概念,它代表了组成某个产品的一系列附件的集合:

当涉及到这种产品族的问题的时候,就需要抽象工厂模式来支持了。我们不再定义 CPU 工厂、主板工厂、硬盘工厂、显示屏工厂等等,我们直接定义电脑工厂,每个电脑工厂负责生产所有的设备,这样能保证肯定不存在兼容问题。

这个时候,对于客户端来说,不再需要单独挑选 CPU厂商、主板厂商、硬盘厂商等,直接选择一家品牌工厂,品牌工厂会负责生产所有的东西,而且能保证肯定是兼容可用的。

public static void main(String[] args) {
    // 第一步就要选定一个“大厂”
    ComputerFactory cf = new AmdFactory();
    // 从这个大厂造 CPU
    CPU cpu = cf.makeCPU();
    // 从这个大厂造主板
    MainBoard board = cf.makeMainBoard();
      // 从这个大厂造硬盘
      HardDisk hardDisk = cf.makeHardDisk();
    // 将同一个厂子出来的 CPU、主板、硬盘组装在一起
    Computer result = new Computer(cpu, board, hardDisk);
}
复制代码

当然,抽象工厂的问题也是显而易见的,比如我们要加个显示器,就需要修改所有的工厂,给所有的工厂都加上制造显示器的方法。这有点违反了对修改关闭,对扩展开放这个设计原则。

参考:

javadoop.com/post/design…

www.cnblogs.com/V1haoge/p/1…

建造者

www.cnblogs.com/V1haoge/p/1…

建造者模式专门对付属性很多的那种类,为了让代码更优美。感觉没啥用。

原型

原型模式很简单:有一个原型实例,基于这个原型实例产生新的实例,也就是“克隆”了。Object 类中有一个 clone() 方法,它用于生成一个新的对象,当然,如果我们要调用这个方法,java 要求我们的类必须先实现 Cloneable 接口,此接口没有定义任何方法,但是不这么做的话,在 clone() 的时候,会抛出 CloneNotSupportedException 异常。java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。

import java.io.Serializable;
import java.util.Arrays;

/**
 * ClassName: UsersEntity
 * Description: Object类的clone方法
 * ①实现Cloneable接口
 * ②重写clone方法
 * 浅拷贝绝对正确,但是在面对一些复杂类型的属性时候,可能无法进行深拷贝
 * 要实现真正的深拷贝,需要根据特定的实体类类型去做,我还无法深刻理解,所以这里就不列出了
 *
 * 这里还实现了Serializable接口时为了演示另一种克隆方法
 */
public class UsersEntity implements Cloneable,Serializable{
    int age;
    int[] number;

    public UsersEntity(int age, int[] number) {
        this.age = age;
        this.number = number;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "UsersEntity{" +
                "age=" + age +
                ", number=" + Arrays.toString(number) +
                '}';
    }

import java.io.*;

/**
 * ClassName: CloneUtil
 */
public class CloneUtil {

    /**
     * 实现对象克隆方法
     * 1)实体类实现Cloneable接口并重写Object类的clone()方法
     * 2)实体类实现Serializable接口,Serializable接口没有任何抽象方法,只是起到一个标记作用
     *   使得实体类能够被序列号,然后通过对象的序列化和反序列化进行克隆
     */
    public static <T extends Serializable> T clone(T obj) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(obj);

        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);

        return (T) ois.readObject();
    }

    public class TestClone {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        int[] number = {5,3,56};
        int age = 123;

        // 克隆方法①:实现Object类的clone方法,只能进行浅拷贝,如果拷贝失败可能会在运行时抛出异常
        UsersEntity user = new UsersEntity(age,number);
        System.out.println(user);

        // 克隆方法②:基于序列化和反序列化实现的不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化
        // 这项检查是在编译器完成的,不是运行时抛出异常,优于方法①,让问题再编译的时候暴露出来比运行时抛出好
        UsersEntity userClone = CloneUtil.clone(user);
        System.out.println(userClone);
    }
}
复制代码

结构型

房产代理拿出电脑,用适配器连到大屏幕上。“你看下别人家房子的外观装饰”,要装修一下才好卖出去。

适配器(接口转换)

  • 为什么引入适配器模式?

将一个类的接口转换成客户希望的另一个接口。通常情况下,客户端可以通过目标类的接口访问它所提供的服务。有时,现有的类可以满足客户类的功能需要,但是它所提供的接口不一定是客户类所期望的,这可能是因为现有类中方法名与目标类中定义的方法名不一致等原因所导致的。

  • 类图

  • 对象适配器实例
//目标接口(客户端需要使用的接口)
public interface Target {
    //客户端需要请求处理的方法
    public void request();
}

public class Adapter implements Target {

    //持有源接口对象
    private Adaptee adaptee;

    /**
     * 构造方法,传入需要被适配的对象
     * @param adaptee
     */
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    /**
     * 重写目标接口的方法,以适应客户端的需求
     */
    @Override
    public void request() {
        //调用源接口的方法
        System.out.println("适配器包装源接口对象,调用源接口的方法");
        adaptee.specifiRequest();
    }
}

public class Client {
    public static void main(String[] args){
        //创建源对象(被适配的对象)
        Adaptee adaptee = new Adaptee();
        //利用源对象对象一个适配器对象,提供客户端调用的方法
        Adapter adapter = new Adapter(adaptee);
        System.out.println("客户端调用适配器中的方法");
        adapter.request();

    }
}
//客户端调用适配器中的方法
//适配器包装源接口对象,调用源接口的方法
//源接口对象调用源接口中的方法
复制代码
  • 接口适配器实例

当存在这样一个接口,其中定义了N多的方法,而我们现在却只想使用其中的一个到几个方法,如果我们直接实现接口,那么我们要对所有的方法进行实现,哪怕我们仅仅是对不需要的方法进行置空(只写一对大括号,不做具体方法实现)也会导致这个类变得臃肿,调用也不方便,这时我们可以使用一个抽象类作为中间件,即适配器,用这个抽象类实现接口,而在抽象类中所有的方法都进行置空,那么我们在创建抽象类的继承类,而且重写我们需要使用的那几个方法即可。

public interface A {
    void a();
    void b();
    void c();
    void d();
    void e();
    void f();
}

public abstract class Adapter implements A {
    public void a(){}
    public void b(){}
    public void c(){}
    public void d(){}
    public void e(){}
    public void f(){}
}
public class Ashili extends Adapter {
    public void a(){
        System.out.println("实现A方法被调用");
    }
    public void d(){
        System.out.println("实现d方法被调用");
    }
}
public class Client {
    public static void main(String[] args) {
        A a = new Ashili();
        a.a();
        a.d();
    }

}
复制代码

www.cnblogs.com/V1haoge/p/6…

segmentfault.com/a/119000001…

外观(Facade)

  • 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入门面模式,客户代码将变得很简单,与之关联的对象也很少。
  • 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。

www.kancloud.cn/digest/xing…

www.cnblogs.com/V1haoge/p/6…

装饰

  • 为什么引入装饰模式

装饰器模式就是使用在对已有的目标功能存在不足,需要增强时,前提是目标存在抽象接口。动态地给一个对象增加一些额外的职责,增加对象功能来说,装饰模式比生成子类实现更为灵活。

装饰模式能够实现动态的为对象添加功能,是从一个对象外部来给对象添加功能。通常有两种方式可以实现给一个类或对象增加行为:

  • 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。

  • 组合机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)。

  • 实例

/**
 * 目标接口:房子
 */
public interface House {
    void output();
}
/**
 * 房子实现类
 */
public class DonghaoHouse implements House {
    @Override
    public void output() {
        System.out.println("这是董浩的房子");
    }
}
/**
 * 房子实现类
 */
public class DongliangHouse implements House {
    @Override
    public void output() {
        System.out.println("这是董量的房子");
    }
}

public class Decorator implements House {
    private House house;
    public Decorator(House house){
        this.house = house;
    }
    @Override
    public void output() {
        System.out.println("这是针对房子的前段装饰增强");
        house.output();
        System.out.println("这是针对房子的后段装饰增强");
    }
}

public class Clienter {
    public static void main(String[] args) {
        House donghaoHouse = new DonghaoHouse();
        House decorator = new Decorator(donghaoHouse);
        decorator.output();
    }
}
复制代码

juejin.im/post/5ba0fb…

代理

  • 为什么要引入代理模式?

  代理(Proxy)是为其他对象提供一个代理以控制对某个对象的访问,即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

  这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。

  代理模式算是我接触较早的模式,代理就是中介,中间人。法律上也有代理,比如代理律师之类,委托人将自己的一部分权限委托给代理者,代理者就拥有被代理者(委托人)的部分权限,并且可以以被代理人的名义来实行这些权限,此时代理者与委托人等同,当然代理人也可以在实行权限时配合自己的能力来进行,当然不能超出这个权限。

  Java中的代理模式类似于上面的代理,我们也是为一个类(委托类)创建一个代理类,来代表它来对外提供功能。

  • 如何在Java中创建一个类的代理类呢?

  很简单,我们需要创建一个公共接口,委托类要实现这个接口,再创建一个接口的实现类作为代理类,在这个类中的方法中可以直接调用委托类中的同名方法,外部类要进行访问时,可以使用接口指向代理类实例,调用代理类中的方法,从而间接调用委托类中的具体方法实现。

代理模式的一个好处就是对外部提供统一的接口方法,而代理类在接口中实现对真实类的附加操作行为,从而可以在不影响外部调用情况下,进行系统扩展。也就是说,我要修改真实角色操作的时候,尽量不要修改他,而是在外部在“包”一层进行附加行为,即代理类。例如:接口A有一个接口方法operator(),真实角色:RealA实现接口A,则必须实现接口方法operator()。客户端Client调用接口A的接口方法operator()。现在新需求来了,需要修改RealA中的operator()的操作行为。怎么办呢?如果修改RealA就会影响原有系统的稳定性,还要重新测试。这是就需要代理类实现附加行为操作。创建代理ProxyA实现接口A,并将真实对象RealA注入进来。ProxyA实现接口方法operator(),另外还可以增加附加行为,然后调用真实对象的operator()。从而达到了“对修改关闭,对扩展开放”,保证了系统的稳定性。我们看客户端Client调用仍是接口A的接口方法operator(),只不过实例变为了ProxyA类了而已。也就是说代理模式实现了ocp原则。

我们在写一个功能函数时,经常需要在其中写入与功能不是直接相关但很有必要的代码,如日志记录,信息发送,安全和事务支持等,这些枝节性代码虽然是必要的,但它会带来以下麻烦:

  1. 枝节性代码游离在功能性代码之外,它不是函数的目的,这是对OO是一种破坏。
  2. 枝节性代码会造成功能性代码对其它类的依赖,加深类之间的耦合,可重用性降低。
  3. 从法理上说,枝节性代码应该监视着功能性代码,然后采取行动,而不是功能性代码通知枝节性代码采取行动。

这种场景下,可以把枝节性代码放到代理类中作为功能增强出现。

  • 静态代理
  1. 优点:可以做到在不修改目标对象的功能前提下,对目标功能扩展。

  2. 缺点:代理类和委托类实现相同的接口,同时要实现相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

public interface IUserDao {
    void doSth();
}
public class UserDao implements IUserDao {
    public void doSth() {
        System.out.println("----已经保存数据!----");
    }
}

public class UserDaoProxy implements IUserDao{
    //接收保存目标对象
    private IUserDao iUserDao;
    public UserDaoProxy(IUserDao iUserDaoiUserDao){
        this.iUserDao = iUserDao;
    }

    public void save() {
        System.out.println("开始事务...");
        iUserDao.doSth();//执行目标对象的方法
        System.out.println("提交事务...");
    }
}

public class Test {
    public static void main(String[] args) {
        //委托对象
        UserDao userDao = new UserDao();
        //代理对象,把委托对象传给代理对象,建立代理关系
        UserDaoProxy proxy = new UserDaoProxy(userDao);
        proxy.doSth();//执行的是代理的方法
    }
}
复制代码
  • 装饰模式和代理模式的区别

  • 代理是全权代理,目标根本不对外,全部由代理类来完成。
  • 装饰是增强,是辅助,目标仍然可以自行对外提供服务,装饰器只起增强作用。
  • 代理模式和装饰器模式虽然都依赖于目标接口,但是代理针对的目标实现类是固定的,而装饰器模式可以随意指定,也就是说目标是可以自有扩展的。(即装饰器中方法可以扩展,而代理类中方法要和委托类原方法保持一致)

www.cnblogs.com/V1haoge/p/1…

行为型

教练下了命令:“打的太乱了,今天状态怎么这么差!”接下来的策略就是他妈的观察对方球队,把他们当模板,看看对方是怎么按照责任各司其职的!。

模板(抽象类)

  • 为什么要有模板方法模式?

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

1)良好的封装性:把公有的不变的方法封装在父类,而子类负责实现具体逻辑。

2)良好的扩展性:增加功能由子类实现基本方法扩展,符合单一职责原则和开闭原则。

3)复用代码。

  • 类图

  • 实例-小黄车开锁
public abstract class AbstractClass {

    protected boolean isNeedUnlock = true;  // 默认需要开锁

    /**
     * 基本方法,子类需要实现
     */
    protected abstract void unlock();
    /**
     * 基本方法,子类需要实现
     */
    protected abstract void ride();
    /**
     * 钩子方法,子类可实现
     *
     * @param isNeedUnlock
     */
    protected void isNeedUnlock(boolean isNeedUnlock) {
        this.isNeedUnlock = isNeedUnlock;
    }

    /**
     * 模板方法,负责调度基本方法,子类不可实现
     */
    public final void use() {
        if (isNeedUnlock) {
            unlock();
        } else {
            System.out.println("========锁坏了,不用解锁========");
        }
        ride();
    }

}
// 扫码开锁的单车
public class ScanBicycle extends AbstractClass {
    @Override
    protected void unlock() {
        System.out.println("========" + "扫码开锁" + "========");
    }

    @Override
    protected void ride() {
        System.out.println(getClass().getSimpleName() + "骑起来很拉风");
    }

    protected void isNeedUnlock(boolean isNeedUnlock) {
        this.isNeedUnlock = isNeedUnlock;
    }
}

// 密码开锁的单车
public class CodeBicycle extends AbstractClass {
    @Override
    protected void unlock() {
        System.out.println("========" + "密码开锁" + "========");
    }

    @Override
    protected void ride() {
        System.out.println(getClass().getSimpleName() + "骑起来很拉风");
    }

    protected void isNeedUnlock(boolean isNeedUnlock) {
        this.isNeedUnlock = isNeedUnlock;
    }
}
//调用测试1
public class Client {
    public static void main(String[] args) {
        ScanBicycle scanBicycle = new ScanBicycle();
        scanBicycle.use();

        CodeBicycle codeBicycle = new CodeBicycle();
        codeBicycle.use();
    }
}
//调用测试2,测试钩子方法
public class Client {
    public static void main(String[] args) {
        ScanBicycle scanBicycle = new ScanBicycle();
        scanBicycle.isNeedUnlock(false);
        scanBicycle.use();

        CodeBicycle codeBicycle = new CodeBicycle();
        codeBicycle.isNeedUnlock(true);
        codeBicycle.use();
    }
}
复制代码

juejin.im/post/5a2e42…

命令(Command)

汇率生成策略执行模块可以采用该种模式,以上下文作为统一参数。

/定义一个命令接口
public interface Command{
    public void execute();
    public void undo();
}

//其中的一个命令继承自这个接口
public class addPatCommand implements Command{
    public void execute(){
        doSomething...
    }
    public void undo(){
        undo...
    }
}

//执行者,用来执行命令
public class Executer{
    //命令队列
    ArrayList<Command>commandList = new ArrayList<Command>();
    //记录当前已经执行的命令
    int executed = 0;
    //执行命令
    public void execute(){
        if(commandList.size()==0)
            System.out.println("Please add a command");
        else{
            for(int i=executed; i<commandList.size(); i++){
                commandList.get(i).execute();
            }
            executed = commandList.size();
        }
    }
    //添加命令
    public void addCommand(Command mCommand){
        commandList.add(mCommand);
    }
    //执行撤销命令,回滚到执行前
    public void undo(){
        if(executed==0)
            System.out.println("Do Nothing");
        else{
            for(int i=executed; i>=0; i--){
                commandList.get(i).undo();
            }
        }
    }
}
复制代码

segmentfault.com/a/119000000…

my.oschina.net/xianggao/bl…

策略(田忌赛马或者节日营销)

  • 为什么要有策略模式

策略模式是对象的行为模式,其实就是对一系列级别平等的算法的封装,它不关心算法实现,让客户端去动态的依靠 “环境” 类去选择需要的算法,因为他们能互相替换,可以说策略模式使能一系列算法可以平滑的切换。

  • 类图

//结合工厂模式
public class StrategyFactory {
    private Strategy strategy;

    // 把创建策略放在封装角色内,客户端只需要知道结果
    public void build(String strategyType) {
        if (strategyType.equals("WIN")) {
            strategy = new ConcreteStrategyB();
        } else if (strategyType.equals("LOSE")) {
            strategy = new ConcreteStrategyA();
        }
    }

    /**
     * 调用策略
     */
    public void strategyExecute() {
        strategy.algorithmLogic();
    }
}

public interface Strategy {
    public void algorithmLogic();

}
public class ConcreteStrategyA implements Strategy{
    @Override
    public void algorithmLogic() {
        // 具体的算法逻辑(输了比赛)
        System.out.println("第一场:上等马vs上等马  第二场:中等马vs中等马  第三场:下等马vs下等马  赛果:输!");
    }
}
public class ConcreteStrategyB implements Strategy{
    @Override
    public void algorithmLogic() {
        // 赢
        System.out.println("第一场:下等马vs上等马  第二场:上等马vs中等马  第三场:中等马vs下等马  赛果:赢!");
    }
}
public class Client {
    public static void main(String[] args) {
        StrategyFactory strategyFactory = new StrategyFactory();
        strategyFactory.build("LOSE");
        strategyFactory.strategyExecute();
    }
}
复制代码
  • 策略模式实例

  • 报价中心算法执行

从DB中加载算法模板,利用反射创建对应算法对象,然后将StrategyExecuteContext送入执行。

segmentfault.com/a/119000001…

segmentfault.com/a/119000001…

状态(我的一天)

策略模式和状态模式都是消除含有大量 if…else 或 switch…case 这类硬编码结构的良策。

  • 为什么引入状态模式

状态模式的使用目的就是控制一个对象状态转换的条件表达式过于复杂时的情况——把状态的判断逻辑转译到表现不同状态的一系列类当中,可以把复杂的判断逻辑简化。

  • 类图

状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式的示意性类图如下所示:

  • 环境(Context)角色,也称上下文:定义客户端所感兴趣的接口,并且保留一个具体状态类的实例。这个具体状态类的实例给出此环境对象的现有状态。
  • 抽象状态(State)角色:定义一个接口,用以封装环境(Context)对象的一个特定的状态所对应的行为。
  • 具体状态(ConcreteState)角色:每一个具体状态类都实现了环境(Context)的一个状态所对应的行为。
public class Context {
    //持有一个State类型的对象实例
    private State state;

    public void setState(State state) {
        this.state = state;
    }
    /**
     * 用户感兴趣的接口方法
     */
    public void request(String sampleParameter) {
        //转调state来处理
        state.handle(sampleParameter);
    }
}

//把研究对象的处理方法放到状态对象中实现
public interface State {
    /**
     * 状态对应的处理
     */
    public void handle(String sampleParameter);
}

public class ConcreteStateA implements State {

    @Override
    public void handle(String sampleParameter) {

        System.out.println("ConcreteStateA handle :" + sampleParameter);
    }

}
public class ConcreteStateB implements State {

    @Override
    public void handle(String sampleParameter) {

        System.out.println("ConcreteStateB handle :" + sampleParameter);
    }

}

//客户端调用
public class Client {

    public static void main(String[] args){
        //创建状态
        State state = new ConcreteStateB();
        //创建环境
        Context context = new Context();
        //将状态设置到环境中
        context.setState(state);
        //请求
        context.request("test");
    }
}
复制代码

从上面可以看出,环境类Context的行为request()是委派给某一个具体状态类的。通过使用多态性原则,可以动态改变环境类Context的属性State的内容,使其从指向一个具体状态类变换到指向另一个具体状态类,从而使环境类的行为request()由不同的具体状态类来执行。

  • 实例2-有限状态机实现

如果用一个有限状态机(Finite-state machine, FSM)来表示目前的状态转换,那大概是这样的:

对于状态不多、转换也不是很复杂的情况,用状态判断(if else方式)来处理还也算简洁明了。但一旦状态变多,操作变复杂,那么业务代码就会充斥各种条件判断,各种状态处理逻辑散落在各处。这时如果要新增一种状态,或者调整一些处理逻辑,就会比较麻烦,还很容易出错。

例如本例中,实际处理时可能还存在取消订单、支付失败/超时、退款失败/超时等情况,如果再加上物流以及一些内部状态,那处理起来就极其复杂了,而且一不小心还会出现支付失败了还能给用户退款,或者已经退款了还给用户发货等不应该出现的情况。这其实是一种坏味道,会造成代码不易维护和扩展。

汇率生成状态机控制。

segmentfault.com/a/119000000…

www.cnblogs.com/java-my-lif…

juejin.im/entry/5a26b…

观察者(妈妈通知小朋友吃饭)

  • 为什么要有观察者模式?

以购票为核心业务(此模式不限于该业务),但围绕购票会产生不同的其他逻辑,如:

  1. 购票后记录文本日志
  2. 购票后记录数据库日志
  3. 购票后发送短信
  4. 购票送抵扣卷、兑换卷、积分
  5. 其他各类活动等

传统解决方案:

在购票逻辑等类内部增加相关代码,完成各种逻辑。

存在问题:

  1. 一旦某个业务逻辑发生改变,如购票业务中增加其他业务逻辑,需要修改购票核心文件、甚至购票流程。
  2. 日积月累后,文件冗长,导致后续维护困难。

存在问题原因主要是程序的"紧密耦合",使用观察模式将目前的业务逻辑优化成"松耦合",达到易维护、易修改的目的。

  • 应用场景

  • 报价中心生成对客汇率后,需要做一系列的报价后处理。

  • 购票后处理

blog.csdn.net/swengineer/…

  • 类图

  • 实例-吃饭了
public class Mother implements Subject{
    /**
     * 她要通知的孩子。孩子是观察者,妈妈是被观察者。
     */
    private ArrayList<Observer> children = new ArrayList<>();
    /**
     * 通知的内容
     */
    private String message;

    public Mother(String name) {
        super(name);
    }

    @Override
    public void registerObserver(Observer observer) {
        children.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        children.remove(observer);
    }

    @Override
    public void notifyObserver() {
        children.forEach(observer -> observer.message(message));
    }

    //做事情
    public void sendMessage(String message) {
        this.message = message;
        // 通知她们
        notifyObserver();
    }
}

public class Child implements Observer{

    public Child(String name) {
        super(name);
    }

    @Override
    public void message(String m) {
        System.out.println(getName() + "收到的消息:"  + m);
    }
}

public class Main {
    public static void main(String[] args) {
        Mother mother = new Mother("妈妈");
        Child xiaoBing = new Child("小冰");
        Child xiaoAi = new Child("小爱");

        // 孩子都是亲生的,吃饭时叫她们
        mother.registerObserver(xiaoBing);
        mother.registerObserver(xiaoAi);

        mother.sendMessage("饭煮好了,回来吃饭,买了你们想吃的鸡腿");

        System.out.println("------------------分割线-----------------------");
        // 小爱说不回来吃了,取消通知她
        mother.removeObserver(xiaoAi);
        mother.sendMessage("饭煮好了,回来吃饭,买了你们想吃的鸡腿");
    }
}
复制代码
  • 观察者模式和发布订阅模式区别

  • 观察者模式中,Subject是知道观察者的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。

  • 发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。

  • 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。

segmentfault.com/a/119000001…

责任链(报销审批)熟悉

  • 为什么要有责任链模式?
public void test(int i, Request request){
    if(i==1){
        Handler1.handle(request);
    }else if(i == 2){
        Handler2.handle(request);
    }else if(i == 3){
        Handler3.handle(request);
    }else if(i == 4){
        Handler4.handle(request);
    }else{
        Handler5.handle(request);
    }
}
复制代码

代码的业务逻辑是这样的,方法有两个参数:整数i和一个请求request,根据i的值来决定由谁来处理request,如果i==1,由Handler1来处理,如果i==2,由Handler2来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类有if…else…条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题:

  • 代码臃肿:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。

  • 耦合度高:如果我们想继续添加处理请求的类,那么就要继续添加else if判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。

  • 应用场景

  1. 有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
  2. 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求.
  3. 可动态指定一组对象处理请求.

报价中心策略执行模块可以使用责任链模式,统一传递StrategyExecuteContext:

  1. 策略配置初始化
  2. 源汇率加载
  3. 算法约束校验
  4. 算法执行
  • 类图

  • 实例-报销处理
//抽象处理类
public abstract class Handler {
    protected Handler nextHandler = null;

    public Handler getNextHandler() {
        return nextHandler;
    }

    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public abstract String dispose(String user , double fee);
}

//报销处职员,只能报销小于500的金额
public class StaffMember extends Handler {
    @Override
    public String dispose(String user, double fee) {
        if(fee < 500){
            System.out.println("报销处职员 给了 "+user+" "+fee+"元");
        }else if (super.getNextHandler() == null){
            System.out.println("处理不了 "+user+" 要 "+fee+"元的事情");
        }else {
            super.getNextHandler().dispose(user,fee);
        }
        return null;
    }
}

//处长类
public class SectionChief extends Handler {
    @Override
    public String dispose(String user, double fee) {
        if(fee < 1000){
            System.out.println("小主管 给了 "+user+" "+fee+"元");
        }else if (super.getNextHandler() == null){
            System.out.println("处理不了 "+user+" 要 "+fee+"元的事情");
        }else {
            super.getNextHandler().dispose(user,fee);
        }
        return null;
    }
}

//老板类
public class Director extends Handler {
    @Override
    public String dispose(String user, double fee) {
        if(fee < 5000){
            System.out.println("老大 给了 "+user+" "+fee+"元");
        }else if (super.getNextHandler() == null){
            System.out.println("处理不了 "+user+" 要 "+fee+"元的事情");
        }else {
            super.getNextHandler().dispose(user,fee);
        }
        return null;
    }
}

public class Main {
    public static void main(String[] args){
        StaffMember staffMember = new StaffMember();
        SectionChief sectionChief = new SectionChief();
        Director director = new Director();

        //set Handler
        staffMember.setNextHandler(sectionChief);
        sectionChief.setNextHandler(director);

        staffMember.dispose("小王",400);
        staffMember.dispose("小张",800);
        staffMember.dispose("小李",1200);

        staffMember.dispose("小明",10000);
    }
}
复制代码

在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。

segmentfault.com/a/119000000…

通过spring实现

Spring 配置优惠券筛查的责任链,注入检查链对象

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="byName">
  <!-- 优惠券展示链 -->
  <bean id="displayCouponCheckChain" class="com.isudox.service.coupon.CheckChain">
    <property name="checkList">
      <list>
        <ref bean="timeCheckHandlerNode"/>
        <ref bean="riskCheckHandlerNode"/>
        <ref bean="receivedCheckHandlerNode"/>
        <ref bean="stockCheckHandlerNode"/>
      </list>
    </property>
  </bean>
  <!-- 优惠券发放链 -->
  <bean id="sendCouponCheckChain" class="com.isudox.service.coupon.CheckChain">
    <property name="checkList">
      <list>
        <ref bean="receivedCheckHandlerNode"/>
        <ref bean="stockCheckHandlerNode"/>
        <ref bean="timeCheckHandlerNode"/>
        <ref bean="riskCheckHandlerNode"/>
      </list>
    </property>
  </bean>
</beans>
复制代码

这样就可以对优惠券部分的业务灵活配置,如果需要新增逻辑,不用更改已有的代码,再实现一个 CheckChainNode 接口就可以了。另外,如果想更改筛查链,也只需要对 Spring 的配置进行修改,重启实例就能生效,无需再次编译发布。简直轻松愉快!

作者简介

2012年本科毕业,2016年硕士毕业。曾供职于IBM中国研发中心,国企,蚂蚁金服等多家企业。接触java开发10余年,目前专注于分布式应用架构师相关知识系统化总结和分享。希望对需要的朋友们系统化得学习和积累相关领域有所帮助。

觉得有收获的话帮忙点个赞吧,让有用的知识分享给更多的人

## 欢迎关注掘金号:五点半社

## 关注微信公众号:五点半社(工薪族的财商启蒙)##