常见的设计模式(可用于面试,持续更新)

一,软件的设计原则

在我们了解设计模式之前,应该对软件的的设计原则先进行一个了解,这样我们才能更清楚的明白,设计模式为我们解决了哪些软件设计上的问题。
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

1.开闭原则

人话:对扩展开放,对修改关闭
在程序需要进行拓展的时候,不能去修改原有的代码,而是通过接口以及抽象的方式,实现功能的增加和迭代。当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

像我们平时写web后端接口的时候,service实现一般是先定义一个接口,再在impl里面实现接口里的方法,很多人都是照葫芦画瓢,没有去思考为什么这样做,其中真实原因是为了实现功能的拓展,当我们有另外一套service逻辑的时候,如果在出入参不改变的时候,可重新去实现一个实现类然后交给spring管理,这样即使新的逻辑不适用想切换为老的版本,只需要注入不同的实现类就行。

2.里氏代换原则

人话: 儿子一定可以代替父亲,且做的更好,但是不能反驳父亲的观点
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较
差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

3.依赖倒转原则

人话:国家领导如果想要发展国家医疗,不需要纠结在哪盖多少所医院,只需要给下面传达重点发展医疗行业以及发展的大体路线(这就是抽象)。
而下属官员则需要根据国家领导的指示和路线去具体实现就可以,比如通过盖医院,或提高医保水平等(这就是细节依赖于抽象)

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

我们要组装一个电脑,需要cpu和内存条和硬盘等,如果将这个机箱cpu,内存条,硬盘等全部以最原始的结构焊死在该主板上,那么下次如果仅仅cpu坏了,得把整个主板换掉,其中主板里包括内存条和硬盘等因为焊死了也得换掉,然后重新买个完整集成好得主板再安装上去。
但是如果我们把主板得每个组件位置做成插拔式的化,即使cpu坏了,我们只需要重新买个cpu换掉就行而不是换掉整个硬件系统。

4.接口隔离原则

人话:我不需要的东西,不要给我
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。

5.迪米特法则

人话:租房尽量找合规平台,不要直接去找房东,防止扯皮没地方投诉。
迪米特法则又叫最少知识原则。只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

二、常用的设计模式方法详解(只包含常用的)

设计模式分为23种,其实大多数的设计模式平时开发中也用不上,顶多也只是应付下面试,包括设计模式的分类一般也不需要过多的了解,这里为了简化仅仅只给出常用常考的设计模式,其他的设计模式不做详述,如果后面有一些常考的设计模式这里没涉及到或者有的冷门的设计模式应该删除,可以评论区踢一脚。

1.单例设计模式(实现方式特多,问的也特多)

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供
了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建。
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

1.1 饿汉式(静态变量方式,静态代码块方式,枚举类)

/**
 * 饿汉1 静态变量方式
 *     静态变量创建类的对象
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance = new Singleton();
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
   }
}
//------------------------------------------------------------------------------------------
/**
 * 饿汉2静态代码块方式
 * 在静态代码块中创建该类对象
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance;
    static {
        instance = new Singleton();
   }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
   }
}
//------------------------------------------------------------------------------------------
/**
 * 枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一
 * 次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是
 * 所用单例实现中唯一一种不会被破坏的单例实现模式。
 */
 /**
 * 饿汉3 枚举方式 枚举方式属于饿汉式方式
 */
public enum Singleton {
    INSTANCE;
}

1.2 懒汉式(包括 线程不安全方式,线程安全方式,双重检查锁方式、静态内部类方式、枚举方式)

/**
 * 懒汉式
 * 线程不安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance;
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}
//-------------------------------------------------------------------------
/**
 * 懒汉式
 * 线程安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    //在成员位置创建该类的对象
    private static Singleton instance;
    //对外提供静态方法获取该对象
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}
//-------------------------------------------------------------------------
/**
 * 双重检查方式
 * 双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检
* 测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问
* 题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
* 要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关
* 键字可以保证可见性和有序性(具体为啥这样可以看JUC的JMM)。
 */
public class Singleton { 
    //私有构造方法
    private Singleton() {}
    //这里为什么加volatile可以好好给面试官扯扯
    private static volatile Singleton instance;
    // private static Singleton instance;
   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
 //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
 if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
               }
           }
       }
        return instance;
   }
}
//-------------------------------------------------------------------------

/**
 * 静态内部类方式
 * 第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,
 * 虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
 * (这里不知道为啥可以去看下jvm的类加载机制和过程)
 * ++++++++++++++++++++++++++++++总结+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * 静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任
 * 何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
   }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
   }
}

1.3 存在的问题

破坏单例模式:使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。

1.解决序列化破坏单例:
在Singleton类中添加 readResolve() 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
   }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
   }
    /**
     * java规范,一定要写成readResolve这名字
     * 下面是为了解决序列化反序列化破解单例模式
     */
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
   }
}

2.反射方式破解单例的解决方法:

.......
//私有构造方法
    private Singleton() {
        /*
           反射破解单例模式需要添加的代码
        */
        if(instance != null) {
            throw new RuntimeException();
       }
   }
   .......

2. 工厂模式

在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严
重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的** 开闭原则**。
如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,
直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:** 解耦**。

2.1 工厂模式的分类

  1. 简单工厂模式(不属于GOF的23种经典设计模式)
  2. 工厂方法模式
  3. 抽象工厂模式

2.2 简单工厂

简单工厂模式由一个工厂类根据传入的参数,决定创建哪一种具体产品类的实例。

public class SimpleCoffeeFactory {
    public static Coffee createCoffee(String type) {
        Coffee coffee = null;
        if("americano".equals(type)) {
            coffee = new AmericanoCoffee();
       } else if("latte".equals(type)) {
            coffee = new LatteCoffee();
       }
        return coffee;
   }
}

优点:客户端不需要知道创建对象的具体类,只需知道工厂类和产品类的接口即可。
缺点:工厂类的职责过重,增加新的产品时需要修改工厂类,违反了** 开闭原则**。

2.4 工厂方法模式

针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。
工厂方法模式定义了一个创建对象的接口(创建的对象必须是返回类相同的类型或者是其子类),由子类决定要实例化的类是哪一个。这样,工厂方法模式将实例化操作推迟到子类。

/**
* 工厂方法模式的主要角色:
* 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂
* 方法来创建产品。
* 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
* 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
* 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同
* 具体工厂之间一一对应。
**/

// 产抽象工厂:
public interface CoffeeFactory {
    Coffee createCoffee();
}
//-------------------------------------------------------------------------------
 // 具体工厂:
public class LatteCoffeeFactory implements CoffeeFactory {
    public Coffee createCoffee() {
        return new LatteCoffee();
   }
}
public class AmericanCoffeeFactory implements CoffeeFactory {
    public Coffee createCoffee() {
        return new AmericanCoffee();
   }
}
//-------------------------------------------------------------------------------
// 咖啡店类:
public class CoffeeStore {
    private CoffeeFactory factory;
    public CoffeeStore(CoffeeFactory factory) {
        this.factory = factory;
   }
    public Coffee orderCoffee(String type) {
        Coffee coffee = factory.createCoffee();
        coffee.addMilk();
        coffee.addsugar();
        return coffee;
   }
}
/** 从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,
* 这样就解决了简单工厂模式的缺点。
* 工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的
* 优点,而且克服了它的缺点。
**/

优点:

  1. 遵循开闭原则,可以在不修改现有代码的情况下添加新产品。
  2. 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
    缺点:
    增加了系统的复杂性,需要为每个产品创建一个工厂类。

2.5 抽象工厂模式

前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机、服装厂只生产上衣等。
这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。
抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。
在这里插入图片描述
总结来说:抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生
产多个等级的产品。

/**
* 抽象工厂模式的主要角色如下:
* 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
* 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
* 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
* 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
**/


/**
* 现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法
* 模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类
* 爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等
* 级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品
* 族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。
**/
//抽象工厂:
public interface DessertFactory {
    Coffee createCoffee();
    Dessert createDessert();
}
//具体工厂:
//美式甜点工厂
public class AmericanDessertFactory implements DessertFactory {
    public Coffee createCoffee() {
        return new AmericanCoffee();
   }
    public Dessert createDessert() {
        return new MatchaMousse();
   }
}
//意大利风味甜点工厂
public class ItalyDessertFactory implements DessertFactory {
    public Coffee createCoffee() {
        return new LatteCoffee();
   }
    public Dessert createDessert() {
        return new Tiramisu();
   }
}
//如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。

优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

使用场景:

  1. 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空
    调等。
  2. 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
  3. 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
    如:
    a. 输入法换皮肤,一整套一起换(包括颜色,卡通人偶,主题样式等全部都会换掉)。
    b. 生成不同操作系统的程序。

3. 建造者模式

将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。
分离了部件的构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。
说人话就是,你想要一个自行车,你只需要提供一堆自行车的原材料就行(铁和胶,或者碳纤维和纯钛合金),然后交给自行车零部件建造的作坊(由Builder来负责)去建造好零部件(比如车轮,座椅,踏板等),建造好之后交给自行车组装工人(由Director负责)来组装好最后交给你使用。

3.1 案例说明

创建共享单车
生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和OfoBuilder是具体的建造者;Director是指挥者。

/**
* 建造者(Builder)模式包含如下角色:
* 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。
* 具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体
* 创建方法。在构造过程完成后,提供产品的实例。
* 产品类(Product):要创建的复杂对象。
* 指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产
* 品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
 **/
 
//自行车类
public class Bike {
    private String frame;
    private String seat;
    public String getFrame() {
        return frame;
   }
    public void setFrame(String frame) {
        this.frame = frame;
   }
    public String getSeat() {
        return seat;
   }
    public void setSeat(String seat) {
        this.seat = seat;
   }
}
//------------------------------------------------------------------------------------------------------------------
// 抽象 builder 类
public abstract class Builder {
    protected Bike mBike = new Bike();
    public abstract void buildFrame();
    public abstract void buildSeat();
    public abstract Bike createBike();
}
//------------------------------------------------------------------------------------------------------------------
//摩拜单车Builder类
public class MobikeBuilder extends Builder {
    @Override
    public void buildFrame() {
        mBike.setFrame("铝合金车架");
   }
    @Override
    public void buildSeat() {
        mBike.setSeat("真皮车座");
   }
    @Override
    public Bike createBike() {
        return mBike;
   }
}
//------------------------------------------------------------------------------------------------------------------
//ofo单车Builder类
public class OfoBuilder extends Builder {
    @Override
    public void buildFrame() {
        mBike.setFrame("碳纤维车架");
   }
    @Override
    public void buildSeat() {
        mBike.setSeat("橡胶车座");
   }
    @Override
    public Bike createBike() {
        return mBike;
   }
}
//------------------------------------------------------------------------------------------------------------------
//指挥者类
public class Director {
    private Builder mBuilder;
    public Director(Builder builder) {
        mBuilder = builder;
   }
    public Bike construct() {
        mBuilder.buildFrame();
        mBuilder.buildSeat();
        return mBuilder.createBike();
   }
}
//------------------------------------------------------------------------------------------------------------------
//测试类
public class Client {
    public static void main(String[] args) {
        showBike(new OfoBuilder());
        showBike(new MobikeBuilder());
   }
    private static void showBike(Builder builder) {
        Director director = new Director(builder);
        Bike bike = director.construct();
        System.out.println(bike.getFrame());
        System.out.println(bike.getSeat());
   }
}
//---------------------------------------------------------------------------------------------
/**
* 上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它
* 用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况
* 下需要简化系统结构,可以把指挥者类和抽象建造者进行结合
**/
// 抽象 builder 类
public abstract class Builder {
    protected Bike mBike = new Bike();
    public abstract void buildFrame();
    public abstract void buildSeat();
    public abstract Bike createBike();
    
    public Bike construct() {
        this.buildFrame();
        this.BuildSeat();
        return this.createBike();
   }
}
/**
* 这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果
* construct() 过于复杂,建议还是封装到 Director 中。
**/

3.2 建造者的优缺点

优点:

  1. 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
  2. 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  3. 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  4. 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。

缺点:
造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

3.3 建造者的使用场景

建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。

  1. 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
  2. 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。

3.4 建造者模式和工厂模式对比

  1. 工厂方法模式VS建造者模式
    工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。
    我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。
  2. 抽象工厂模式VS建造者模式
    抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
    建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
    如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。

3. 代理模式

代理模式可以说是我们学框架了解的第一个设计模式,因为spring的一个重要的功能-----面向切面编程,就是用代理模式的,这也是初中级java面试的常问的问题。
动态代理模式属于结构型模式,结构型模式描述的是如何将类或对象按某种布局组成更大的结构(说人话就是怎么增强一个类或对象的功能)。它分为类结构型模式对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:

  1. 代理模式
  2. 适配器模式
  3. 装饰者模式
  4. 桥接模式
  5. 外观模式
  6. 组合模式
  7. 享元模式

但是我们目前重点关注代理模式适配器模式 即可,其他的根据情况可以自己去网上学习相关的资料。

3.1 动态代理模式

代理模式分为动态代理模式静态代理模式,其中,静态代理可以理解为:新建一个类AProxy,AProxy去继承需要被代理的类A,AProxy去重写该类A相关的方法,最终实现方法增强。理解起来还是比较简单的,不会的小伙伴可以网上去找找,这里为了节约篇幅不做赘述,本节的重点讲述的是动态代理的作用和实现。
代理(Proxy)模式分为三种角色:

抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

动态代理其实是没法完全自己实现的,需要借助jdk的一些内置api(类库)去实现,接下来我们讲述jdk的动态代理。

3.1.1 JDK内置的动态代理

Java中提供了一个动态代理类Proxy,Proxy并不是代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象(也就是上面所说的AProxy)。
我们以买票的案例来实现下动态代理:

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火 车站是目标对象,代售点是代理对象。

//卖票接口
public interface SellTickets {
    void sell();
}
//----------------------------------------------------------------------------------------------------------
//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {
    public void sell() {
        System.out.println("火车站卖票");
   }
}
//----------------------------------------------------------------------------------------------------------
//代理工厂,用来创建代理对象
public class ProxyFactory {
    private SellTickets station = new TrainStation();
    public SellTickets getProxyObject() {
//使用Proxy获取代理对象(代售点售卖火车票)
/**
* newProxyInstance()方法参数说明:
* ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
* Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
* InvocationHandler h : 代理对象的调用处理程序
**/
SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(
                                                station.getClass().getClassLoader(),
                                                station.getClass().getInterfaces(),
                                                //匿名内部类,有些会将这个类作为一个单独的类去书写,这里为了方便才这样
                                                new InvocationHandler() { 
/**
* InvocationHandler中invoke方法参数说明:
* proxy : 代理对象
* method : 对应于在代理对象上调用的接口方法的 Method 实例
* args : 代理对象调用接口方法时传递的实际参数
**/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
   //执行真实对象(理解收取代理费之后,代售点买火车站的票给乘客)
   Object result = method.invoke(station, args); 
   return result;
   }
});//匿名内部类结束
 return sellTickets;
}
}
//----------------------------------------------------------------------------------------------------------
//测试类
public class Client {
public static void main(String[] args) {
        //获取代理对象
        ProxyFactory factory = new ProxyFactory();
        SellTickets proxyObject = factory.getProxyObject();
        proxyObject.sell();
   }
}
/**
* 值得注意的是:ProxyFactory是代理类吗?
* ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生
* 成的类(一般是这样的:public final class $Proxy0 extends Proxy implements SellTickets{}。)
* 这个动态生成的类是我们看不到的,他是动态生成的。
* 通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)可以查看到动态生成的代理类的结构。
**/
3.2.1 CGLIB动态代理

同样是上面的案例,我们再次使用CGLIB代理实现。如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
CGLIB是第三方提供的包,所以需要引入jar包的坐标:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

其中代码如下:

//火车站
public class TrainStation {
    public void sell() {
        System.out.println("火车站卖票");
   }
}
//代理工厂
public class ProxyFactory implements MethodInterceptor {
    private TrainStation target = new TrainStation();
    public TrainStation getProxyObject() {
        //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
        Enhancer enhancer =new Enhancer();
        //设置父类的字节码对象
        enhancer.setSuperclass(target.getClass());
        //设置回调函数
        enhancer.setCallback(this);
        //创建代理对象
        TrainStation obj = (TrainStation) enhancer.create();
        return obj;}
/*
* intercept方法参数说明:
* o :代理对象
* method :真实对象中的方法的Method实例
* args :实际参数
* methodProxy :代理对象中的方法的method实例
* */
public TrainStation intercept(Object o, Method method, Object[] args, 
MethodProxy methodProxy) throws Throwable {
        System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)");
        TrainStation result = (TrainStation) methodProxy.invokeSuper(o, args);
        return result;
   }
}
//测试类
public class Client {
    public static void main(String[] args) {
        //创建代理工厂对象
        ProxyFactory factory = new ProxyFactory();
        //获取代理对象
        TrainStation proxyObject = factory.getProxyObject();
        proxyObject.sell();
   }
}

3.2 三种代理的对比

  1. jdk代理和CGLIB代理:
    使用CGLIB实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。

  2. 动态代理和静态代理
    动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。

4.适配器模式

所谓适配器,就是一个中间的转换装置,比如下图这个不同接口转换的的转换器,它就是一个典型的适配器。适配器的作用是能够使用两种不兼容的东西通过适配器的转换变得兼容,而适配器就作为一种中间的调和装置。适配器在我们生活中无处不咋,包括不同插孔类型的转换器等。
那么我们可以思考一下,假如有一个类的方法F的入参需要传入一个对象A,然后调用A的方法FA。为了能够使更多自定义的对象可以传入进去,那我我们可以将A作为适配器,然后我们其他的自定义方法去继承这个适配器A,再重写FA,那么所有的自定义类的对象就可以传入进去,且能够调用自己重写的方法。
安卓接口转typec接口

4.1 适配器模式的定义:

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类适配器模式对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

4.2 适配器模式结构

适配器模式(Adapter)包含以下主要角色:
目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成(就像上图的type-c转换器)
目标接口,让客户按目标接口的格式访问适配者。

4.3 类适配器

实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
读卡器的例子:
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡
器,将TF卡中的内容读取出来。

//SD卡的接口(目标接口)
public interface SDCard {
    //读取SD卡方法
    String readSD();
    //写入SD卡功能
    void writeSD(String msg);
}

//SD卡实现类
public class SDCardImpl implements SDCard {
    public String readSD() {
        String msg = "sd card read a msg :hello word SD";
        return msg;
   }
      public void writeSD(String msg) {
        System.out.println("sd card write msg : " + msg);
   }
}
//电脑类
public class Computer {
    public String readSD(SDCard sdCard) {
        if(sdCard == null) {
            throw new NullPointerException("sd card null");
       }
        return sdCard.readSD();
   }
}
//TF卡接口
public interface TFCard {
    //读取TF卡方法
    String readTF();
    //写入TF卡功能
    void writeTF(String msg);
}
//TF卡实现类 (适配者)
public class TFCardImpl implements TFCard {
    public String readTF() {
        String msg ="tf card read msg : hello word tf card";
        return msg;
   }
    public void writeTF(String msg) {
        System.out.println("tf card write a msg : " + msg);
   }
}
//定义适配器类(SD兼容TF)(适配器)
public class SDAdapterTF extends TFCardImpl implements SDCard {
    public String readSD() {
        System.out.println("adapter read tf card ");
        return readTF();
   }
    public void writeSD(String msg) {
        System.out.println("adapter write tf card");
        writeTF(msg);
   }
}
//测试类
public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer();
        SDCard sdCard = new SDCardImpl();
        System.out.println(computer.readSD(sdCard));
        System.out.println("------------");
        SDAdapterTF adapter = new SDAdapterTF();
        System.out.println(computer.readSD(adapter));
   }
}

类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。

4.4 对象适配器模式

实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前
系统的业务接口。
我们使用对象适配器模式将读卡器的案例进行改写:
代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类:

//创建适配器对象(SD兼容TF)(适配器)
public class SDAdapterTF  implements SDCard {
    private TFCard tfCard;
    public SDAdapterTF(TFCard tfCard) {
        this.tfCard = tfCard;
   }
   public String readSD() {
        System.out.println("adapter read tf card ");
        return tfCard.readTF();
   }
    public void writeSD(String msg) {
        System.out.println("adapter write tf card");
        tfCard.writeTF(msg);
   }
}
//测试类
public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer();
        SDCard sdCard = new SDCardImpl();
        System.out.println(computer.readSD(sdCard));
        System.out.println("------------");
        TFCard tfCard = new TFCardImpl();
        SDAdapterTF adapter = new SDAdapterTF(tfCard);
        System.out.println(computer.readSD(adapter));
   }
}
// 注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创
// 建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。

4.5 应用场景

以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。

5.策略模式

该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分
割开来,并委派给不同的对象对这些算法进行管理。

举个例子,我们想去上海,可以使用不同的方式,到达上海是目的,中间的这个到达方式可以有多种,比如:打车,坐飞机,坐火车等。这个到达的方式就是不同的策略方式。再比如,我们开发java程序,写一个helloworld,写helloword是目的,但是我们可以通过Idea写,也可以通过eclipse写,这个写的方式就是不同的策略。

5.1 策略模式的结构

策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

(策略不是我们随便定义的,需要有相关的规范,这个抽象策略就类似于相关的规范,我们必须根据这个接口要求去实现。就好像,我们去上海选择的交通工具必须是能够搭乘人且是运输工具,而不能是耕地的车或者挖土的车等。)

具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。

具体的我们选择的交通工具

环境(Context)类:持有一个策略类的引用,最终给客户端调用。

最后我们坐上车走上国道,最后到达上海客运中心或者火车站或者飞机场。这个具体的策略需要给谁用,这个使用策略的类就是环境类

5.2 策略模式的案例描述

案例1:
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动(打折、满减、买一送一等策略),由促销员将促销活动展示给客户。
代码如下:
定义百货公司所有促销活动的共同接口

public interface Strategy {
    void show();
}

定义具体策略角色(Concrete Strategy):每个节日具体的促销活动

//为春节准备的促销活动A
public class StrategyA implements Strategy {
    public void show() {
        System.out.println("买一送一");
   }
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
    public void show() {
        System.out.println("满200元减50元");
   }
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
    public void show() {
        System.out.println("满1000元加一元换购任意200元以下商品");
   }
}

定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员

public class SalesMan {                        
    //持有抽象策略角色的引用                              
    private Strategy strategy;                                                        
    public SalesMan(Strategy strategy) {       
        this.strategy = strategy;              
   }                                                                                   
    //向客户展示促销活动                                
    public void salesManShow(){                
        strategy.show();                       
   }                                          
}  
/**
* 策略模式还是比较好理解的。就是我们根据策略模式给我们提供的接口去实现我们需要的策略,然后再将策略对象传递给环境类(也就是执行接口方法的那个类,* 在调用(或者类本身自己调用)相关方法(eg:salesManShow()),就能够去按照我们定义的算法去执行得到最终的结果了(通过满减折扣还是通过买一送一或打
* 折等)。
**/

案例 2:
相信大家都用过Comparator方法,当我们给数组排序的时候,我们可以通过Arrays的静态方法sort进行排序,我们有时候就需要传入一个Comparator对象进行定制排序(正序、倒序、根据对象的特定字段排序等),通过实现Comparator接口的方法去指定我们想要怎么排序,其中这个定制的Comparator就是相关的策略,Arrays.sort(…)就是相关的环境,

public class Arrays{
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
       } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
       }
   }
}

5.3 策略模式的优缺点

1,优点:
策略类之间可以自由切换。由于策略类都实现同一个接口,所以使它们之间可以自由切换。
易于扩展。增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
避免使用多重条件选择语句(if else),充分体现面向对象设计思想。 (这点我们平时再大量if-else的时候,我们除了可以改成switch,还可以尝试将其替换为策略模式,方便拓展)
2,缺点:
客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
策略模式将造成产生很多策略类,一个策略一个类。

6.观察者模式

观察者模式又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
简单理解就是:我们军训的时候,我们选择一个教官,然后我们这群人就订阅了这个教官。当军训开始了,教官发布口令(Publish),我们可以根据不同的口令做出不同的动作(Subscribe)。

6.1 观察者模式的结构

在观察者模式中有如下角色:
Subject: 抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

6.2 案例

在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。

定义抽象观察者类,里面定义一个更新的方法:

public interface Observer {
    void update(String message);
}

定义具体观察者类,微信用户是观察者,里面实现了更新的方法:

public class WeixinUser implements Observer {
    // 微信用户名
    private String name;
    public WeixinUser(String name) {
        this.name = name;
   }
    @Override
    public void update(String message) {
        System.out.println(name + "-" + message);
   }
}

定义抽象主题类,提供了attach、detach、notify三个方法:

public interface Subject {
    //增加订阅者
    public void attach(Observer observer);
    //删除订阅者
    public void detach(Observer observer);
    
    //通知订阅者更新消息
    public void notify(String message);
}

微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法:

public class SubscriptionSubject implements Subject {
    //储存订阅公众号的微信用户
    private List<Observer> weixinUserlist = new ArrayList<Observer>();
    @Override
    public void attach(Observer observer) {
        weixinUserlist.add(observer);
   }
    @Override
    public void detach(Observer observer) {
        weixinUserlist.remove(observer);
   }
    @Override
    public void notify(String message) {
        for (Observer observer : weixinUserlist) {
            observer.update(message);
       }
   }
}

客户端程序:

public class Client {
     public static void main(String[] args) {
        SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
        //创建微信用户
        WeixinUser user1=new WeixinUser("孙悟空");
        WeixinUser user2=new WeixinUser("猪悟能");
        WeixinUser user3=new WeixinUser("沙悟净");
        //订阅公众号
        mSubscriptionSubject.attach(user1);
        mSubscriptionSubject.attach(user2);
        mSubscriptionSubject.attach(user3);
        //公众号更新发出消息给订阅的微信用户
        mSubscriptionSubject.notify("传智黑马的专栏更新了");
   }
}

6.3 优缺点

  1. 优点:
    降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】。
  2. 缺点:
    如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时,如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃。

6.4 使用场景

  1. 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  2. 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。
面试中常问的设计模式有很多,以下是一些常见设计模式: 1. 单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点。 2. 工厂模式(Factory Pattern):定义一个用于创建对象的接口,由子类决定实例化哪个类。 3. 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无需指定具体类。 4. 建造者模式(Builder Pattern):将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。 5. 原型模式(Prototype Pattern):通过复制现有对象来生成新对象,避免了使用new关键字创建对象。 6. 适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的另一个接口。 7. 装饰器模式(Decorator Pattern):动态地给对象添加额外的职责,同时又不改变其结构。 8. 观察者模式(Observer Pattern):定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会得到通知并自动更新。 9. 策略模式(Strategy Pattern):定义了一系列的算法,并将每个算法封装起来,使它们可以互相替换。 10. 模板方法模式(Template Method Pattern):定义了一个算法的骨架,将一些步骤延迟到子类中实现。 这些只是一些常见设计模式,具体还会根据面试的要求和职位不同而有所变化。在面试中重要的是理解每个设计模式的原理、适用场景以及优缺点,并能够灵活运用到实际问题中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值