博文内容根据网络资料整理总结,如有侵权,联系删除!

设计模式

设计模式大体上分为三种:

创建型模式 、 结构型模式 、 行为型模式

创建型模式——5种

工厂模式

需要用到两个以上的工厂

image-20230320161404203

抽象工厂模式

image-20230320161455884

简单的说就是为了方便不同的CPU和主板之间的兼容,在选择好特定的工厂后, 只能在这个工厂中选择相对兼容的CPU主板以及其他硬件。

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

简单工厂模式和工厂模式和抽象工厂模式的不同点

工厂模式其实只生产一个实例,但是抽象工厂模式,按照上面举的例子,是将不同CPU型号组装的电脑的各部分抽象出来,因此一个工厂是可以生产多种实例的,可以是CPU 可以是主板,可以是硬盘,等等。

简单工厂模式是将各种生产实例的方法扔到一个工厂里,没有跟工厂模式一样,有具体的单个特征,比如生产食物,生产手机。简单工厂可以生产不同种的东西,比如生产纸巾,生产杯子,有关生产的东西都可以扔到简单工厂里。

单例模式

显而易见,就是直接new一个对象返回

分为饿汉模式和饱汉模式

饿汉模式

直接new 返回

其中可能有多种通过不同参数构造的实例

例如多线程中的Excutors 线程池创建工具类。

饱汉模式

在静态对象中定义好,只有第一次调用才去创建,否则直接返回该对象

适合于服务类这些不改变其中参数的类创建对象,否则会有线程不安全问题等。

为了防止指令重排,并且保证这个单例在内存中的可见性,必须使用volatile

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;
    }
}

也可使用内部类:

public class Singleton3 {

    private Singleton3() {}
    // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
    private static class Holder {
        private static Singleton3 instance = new Singleton3();
    }
    public static Singleton3 getInstance() {
        return Holder.instance;
    }
}

同理,要让类的实例在编译时即可被实例化,可以写成枚举类,这样在类加载阶段的准备阶段即可设置初始值,在初始化阶段则new出对象,放在方法区中。

建造者模式

套路就是先 new 一个 Builder,然后可以链式地调用一堆方法,最后再调用一次 build() 方法,我们需要的对象就有了。 通常通过

在设置属性的方法中,返回类型为当前的对象上下文, this 。 在最后调用的build()方法中,返回的即是我们想要的创建对象。

return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.Programs3200.controller"))
                .build();

说实话,建造者模式的链式写法很吸引人,但是,多写了很多“无用”的 builder 的代码,感觉这个模式没什么用。不过,当属性很多,而且有些必填,有些选填的时候,这个模式会使代码清晰很多。我们可以在 Builder 的构造方法中强制让调用者提供必填字段,还有,在 build() 方法中校验各个参数比在 User 的构造方法中校验,代码要优雅一些。

当然,如果你只是想要链式写法,不想要建造者模式,有个很简单的办法,User 的 getter 方法不变,所有的 setter 方法都让其 return this 就可以了.

原型模式

原型模式很简单:有一个原型实例,基于这个原型实例产生新的实例,也就是“克隆”了。

Object 类中有一个 clone() 方法,它用于生成一个新的对象,当然,如果我们要调用这个方法,java 要求我们的类必须先实现 Cloneable 接口,此接口没有定义任何方法,但是不这么做的话,在 clone() 的时候,会抛出 CloneNotSupportedException 异常。

protected native Object clone() throws CloneNotSupportedException;

java的克隆是浅克隆,也就是两个引用使用的是同一个实例对象。

要实现深克隆,可以使用序列化和反序列化,或者直接new一个新对象一个个set\get , 或者方便点使用json工具直接序列化成json,然后反序列化成对象,但是其实实际上也是通过反射来获取每个字段的值来构造的。

结构型模式——7种

代理模式

既然说是代理,那就要对客户端隐藏真实实现,由代理来负责客户端的所有请求。当然,代理只是个代理,它不会完成实际的业务逻辑,而是一层皮而已,但是对于客户端来说,它必须表现得就是客户端需要的真实实现。

实际上就是在Service层套个接口,然后其中有共用的方法,对外开放,而我们具体的实现类可以实现这个接口,然后具体去实现其中的方法。一些内容使用的方法可以用private修饰,主要要实现接口暴露的方法,这个才是主要的。

代理模式说白了就是做 “方法包装” 或做 “方法增强”。在面向切面编程中,算了还是不要吹捧这个名词了,在 AOP 中,其实就是动态代理的过程。比如 Spring 中,我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理,然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。

说到动态代理,又可以展开说 …… Spring 中实现动态代理有两种,一种是如果我们的类定义了接口,如 UserService 接口和 UserServiceImpl 实现,那么采用 JDK 的动态代理,感兴趣的读者可以去看看 java.lang.reflect.Proxy 类的源码;另一种是我们自己没有定义接口的,Spring 会采用 CGLIB 进行动态代理,它是一个 jar 包,性能还不错。

适配器模式

适配器模式做的就是,有一个接口需要实现,但是我们现成的对象都不满足,需要加一层适配器来进行适配。

适配器模式总体来说分三种:默认适配器模式、对象适配器模式、类适配器模式。

默认适配器模式

就是有一个有很多方法的接口,我们只想实现这个接口的两个方法,其他不需要用,但是接口是默认需要实现全部方法的,因此这个时候就可以用一个类实现空方法,然后另一个类继承这个作为适配器的中间的类,然后我们调用的类覆写自己需要用到的方法就可。

对象适配器模式

我们需要一只鸭,但是我们只有一只鸡,这个时候就需要定义一个适配器,由这个适配器来充当鸭,但是适配器里面的方法还是由鸡来实现的。

image-20230331223202111

类适配器

image-20230331223259447

小结

类适配器和对象适配器的异同

一个采用继承,一个采用组合;

类适配属于静态实现,对象适配属于组合的动态实现,对象适配需要多实例化一个对象。

总体来说,对象适配用得比较多。

适配器模式和代理模式的异同

两者都是需要一个具体的实现类的实例。但是代理模式做的是增强原方法的活;适配器做的是适配的活,为的是提供“把鸡包装成鸭,然后当做鸭来使用”,而鸡和鸭它们之间原本没有继承关系。

桥梁模式

image-20230331232656422

装饰器模式

创建一个新类,包装原始类 , 从而在新类中提升原来类的功能

image-20221028183342150

1、定义父类

2、定义原始类 , 继承父类 , 定义功能

3、定义装饰类, 继承父类 , 包装原始类 , 增强功能

image-20230331234255522

image-20230331234212224

我们知道 InputStream 代表了输入流,具体的输入来源可以是文件(FileInputStream)、管道(PipedInputStream)、数组(ByteArrayInputStream)等,这些就像前面奶茶的例子中的红茶、绿茶,属于基础输入流。FilterInputStream 承接了装饰模式的关键节点,其实现类是一系列装饰器,比如 BufferedInputStream 代表用缓冲来装饰,也就使得输入流具有了缓冲的功能,LineNumberInputStream 代表用行号来装饰,在操作的时候就可以取得行号了,DataInputStream 的装饰,使得我们可以从输入流转换为 java 中的基本类型值。

门面模式

门面模式(也叫外观模式,Facade Pattern)在许多源码中有使用,比如 slf4j 就可以理解为是门面模式的应用。
门面模式的优点显而易见,客户端不再需要关注实例化时应该使用哪个实现类,直接调用门面提供的方法就可以了,因为门面类提供的方法的方法名对于客户端来说已经很友好了。
简而言之,就是用一个类集合多个实现同一个类的属性,然后写这些属性的调用方法。
三个图形类实现了shape
然后门面类定义三个方法,分别画出三个类的图像

public class ShapeMaker {
   private Shape circle;
   private Shape rectangle;
   private Shape square;

   public ShapeMaker() {
      circle = new Circle();
      rectangle = new Rectangle();
      square = new Square();
   }

  /**
   * 下面定义一堆方法,具体应该调用什么方法,由这个门面来决定
   */

   public void drawCircle(){
      circle.draw();
   }
   public void drawRectangle(){
      rectangle.draw();
   }
   public void drawSquare(){
      square.draw();
   }
}

组合模式

组合模式用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。

每个员工都有姓名、部门、薪水这些属性,同时还有下属员工集合(虽然可能集合为空),而下属员工和自己的结构是一样的,也有姓名、部门这些属性,同时也有他们的下属员工集合。
public class Employee {
   private String name;
   private String dept;
   private int salary;
   private List<Employee> subordinates; // 下属

   public Employee(String name,String dept, int sal) {
      this.name = name;
      this.dept = dept;
      this.salary = sal;
      subordinates = new ArrayList<Employee>();
   }

   public void add(Employee e) {
      subordinates.add(e);
   }

   public void remove(Employee e) {
      subordinates.remove(e);
   }

   public List<Employee> getSubordinates(){
     return subordinates;
   }

   public String toString(){
      return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]");
   }   
}

享元模式

英文是 Flyweight Pattern,不知道是谁最先翻译的这个词,感觉这翻译真的不好理解,我们试着强行关联起来吧。Flyweight 是轻量级的意思,享元分开来说就是 共享 元器件,也就是复用已经生成的对象,这种做法当然也就是轻量级的了。复用对象最简单的方式是,用一个 HashMap 来存放每次新生成的对象。每次需要一个对象的时候,先到 HashMap 中看看有没有,如果没有,再生成新的对象,然后将这个对象放入 HashMap 中。

结构型模式总结

代理模式是做方法增强的,适配器模式是把鸡包装成鸭这种用来适配接口的,桥梁模式做到了很好的解耦,装饰模式从名字上就看得出来,适合于装饰类或者说是增强类的场景,门面模式的优点是客户端不需要关心实例化过程,只要调用需要的方法即可,组合模式用于描述具有层次结构的数据,享元模式是为了在特定的场景中缓存已经创建的对象,用于提高性能。

代理模式是获取代理类对象,然后通过代理类做他原本该做的事情,然后我们在其前或者其后做一些动态方法增强

适配器模式有三个,默认的是我们不想实现一个接口的所有方法,那就用一个类实现接口,但是不写方法具体的内容,然后第三个类则继承第二个类,实现覆写想实现的几个方法。而对象适配器模式,就是在适配器的构造方法中注入一个能干活的类,然后其他方法调用该类的方法干活。类适配器跟对象适配器其实差不多,只不过类适配器是通过继承这个能干活的类,来获取他的干活的方法的。这样就不用多搞个对象了。

桥梁模式,就是一个接口可能有多个实现类,这个接口就是桥梁,然后另一个接口注入这个桥梁,可以调用这个桥梁的各种方法。

装饰模式,就是其实方法跟其他类类似,但是这个装饰器可以在其他类的基础上,再优化一下性能或者其他功能,比如输入流可以将字节缓存到管道里,防止多次开关IO还得等处理机调度完才能处理,一次性到缓存流就很快了。还有取得输入行号的流,总之其实都是在普通的字节流的基础上加强的,这就是装饰。

门面模式,有一堆类需要调用,有一堆属性需要自己写?不需要,直接封装一个工具类,然后里面放着其他类的实例,方法里再调用这些类的方法来封装自己的方法,这样使用者只需要使用这个类就行了,这就是门面模式:装饰一个门面,用户不需要进去看,只需要关心门面提供的方法。

组合模式,没啥好说的,就是一个类里还有其他类的对象,可能是列表装着,可能是Map装着。比如一个教室类,有教师列表,有学生列表,有班级属性xxxxx还有很多。

享元模式,听名字很怪,联想不到啥,但是其实就是一起享受一个元素,一个对象?一个Map数组存着相同的多个对象,如果在Map里找得到需要的对象,则直接拿来用,找不到那就重新new一个,然后放到Map里。

行为型模式——11种

策略模式

image-20230401201837721

跟桥梁模式类似:

image-20230401201853851

但是桥梁模式的左侧多了一层抽象解耦,结构更为复杂。

观察者模式

观察者模式对于我们来说,真是再简单不过了。无外乎两个操作,观察者订阅自己关心的主题和主题有数据变化后通知观察者们。

首先,需要定义主题,每个主题需要持有观察者列表的引用,用于在数据变更的时候通知各个观察者:

public class Subject {

   private List<Observer> observers = new ArrayList<Observer>();
   private int state;

   public int getState() {
      return state;
   }

   public void setState(int state) {
      this.state = state;
      // 数据已变更,通知观察者们
      notifyAllObservers();
   }

   public void attach(Observer observer){
      observers.add(observer);        
   }

   // 通知观察者们
   public void notifyAllObservers(){
      for (Observer observer : observers) {
         observer.update();
      }
   }     
}

观察者接口:

public abstract class Observer {
   protected Subject subject;
   public abstract void update();
}

其实如果只有一个观察者类的话,接口都不用定义了,不过,通常场景下,既然用到了观察者模式,我们就是希望一个事件出来了,会有多个不同的类需要处理相应的信息。比如,订单修改成功事件,我们希望发短信的类得到通知、发邮件的类得到通知、处理物流信息的类得到通知等。

我们来定义具体的几个观察者类:

public class BinaryObserver extends Observer {

      // 在构造方法中进行订阅主题
    public BinaryObserver(Subject subject) {
        this.subject = subject;
        // 通常在构造方法中将 this 发布出去的操作一定要小心
        this.subject.attach(this);
    }

      // 该方法由主题类在数据变更的时候进行调用
    @Override
    public void update() {
        String result = Integer.toBinaryString(subject.getState());
        System.out.println("订阅的数据发生变化,新的数据处理为二进制值为:" + result);
    }
}

public class HexaObserver extends Observer {

    public HexaObserver(Subject subject) {
        this.subject = subject;
        this.subject.attach(this);
    }

    @Override
    public void update() {
          String result = Integer.toHexString(subject.getState()).toUpperCase();
        System.out.println("订阅的数据发生变化,新的数据处理为十六进制值为:" + result);
    }
}

总之就是,有一个主题类,里面用List存着观察者的列表,其实主题是被观察的,观察者类有提供主题调用的通知的回调方法,然后在new观察者类的时候,要在观察者类的构造方法里,把观察者加入主题的List存放的观察者列表中,当有主题有新消息的时候,会遍历观察者列表,然后调用观察者列表提供的通知方法来通知观察者。

责任链模式

责任链通常需要先建立一个单向链表,然后调用方只需要调用头部节点就可以了,后面会自动流转下去。比如流程审批就是一个很好的例子,只要终端用户提交申请,根据申请的内容信息,自动建立一条责任链,然后就可以开始流转了。有这么一个场景,用户参加一个活动可以领取奖品,但是活动需要进行很多的规则校验然后才能放行,比如首先需要校验用户是否是新用户、今日参与人数是否有限额、全场参与人数是否有限额等等。设定的规则都通过后,才能让用户领走奖品。

有这么一个场景,用户参加一个活动可以领取奖品,但是活动需要进行很多的规则校验然后才能放行,比如首先需要校验用户是否是新用户、今日参与人数是否有限额、全场参与人数是否有限额等等。设定的规则都通过后,才能让用户领走奖品。

这里通过责任链,定义多个规则的继承责任链的类,然后只需要调用第一次的apply,后继的链也会不断调用

首先,我们要定义流程上节点的基类:

public abstract class RuleHandler {

      // 后继节点
    protected RuleHandler successor;

    public abstract void apply(Context context);

    public void setSuccessor(RuleHandler successor) {
        this.successor = successor;
    }
    public RuleHandler getSuccessor() {
        return successor;
    }
}

是否新用户

public class NewUserRuleHandler extends RuleHandler {

    public void apply(Context context) {
        if (context.isNewUser()) {
              // 如果有后继节点的话,传递下去
            if (this.getSuccessor() != null) {
                this.getSuccessor().apply(context);
            }
        } else {
            throw new RuntimeException("该活动仅限新用户参与");
        }
    }

}

是否所在地区能够参加

public class LocationRuleHandler extends RuleHandler {
    public void apply(Context context) {
        boolean allowed = activityService.isSupportedLocation(context.getLocation);
          if (allowed) {
            if (this.getSuccessor() != null) {
                this.getSuccessor().apply(context);
            }
        } else  {
            throw new RuntimeException("非常抱歉,您所在的地区无法参与本次活动");
        }
    }
}

奖品是否领完

public class LimitRuleHandler extends RuleHandler {
    public void apply(Context context) {
          int remainedTimes = activityService.queryRemainedTimes(context); // 查询剩余奖品
        if (remainedTimes > 0) {
            if (this.getSuccessor() != null) {
                this.getSuccessor().apply(userInfo);
            }
        } else {
            throw new RuntimeException("您来得太晚了,奖品被领完了");
        }
    }
}

客户端操作:

public static void main(String[] args) {
    RuleHandler newUserHandler = new NewUserRuleHandler();
      RuleHandler locationHandler = new LocationRuleHandler();
      RuleHandler limitHandler = new LimitRuleHandler();

   	//全部责任链都要
    newUserHandler.setSuccessor(locationHandler);
    locationHandler.setSuccessor(limitHandler);
    newUserHandler.apply(context);
    
      // 假设本次活动仅校验地区和奖品数量,不校验新老用户
      locationHandler.setSuccessor(limitHandler);
      locationHandler.apply(context);
}

优点对比直接设置一个列表,然后存放各种过滤器的类,进行遍历取出然后apply,优点有啥,可能是比较清晰?然后能够随时调换规则 , 好像没啥。

模板方法模式

在含有继承结构的代码中,模板方法模式是非常常用的,这也是在开源代码中大量被使用的。

通常会有一个抽象类:

public abstract class AbstractTemplate {
    // 这就是模板方法
      public void templateMethod(){
        init();
        apply(); // 这个是重点
        end(); // 可以作为钩子方法
    }
    protected void init() {
        System.out.println("init 抽象层已经实现,子类也可以选择覆写");
    }
      // 留给子类实现
    protected abstract void apply();
    protected void end() {
    }
}

模板方法中调用了 3 个方法,其中 apply() 是抽象方法,子类必须实现它,其实模板方法中有几个抽象方法完全是自由的,我们也可以将三个方法都设置为抽象方法,让子类来实现。也就是说,模板方法只负责定义第一步应该要做什么,第二步应该做什么,第三步应该做什么,至于怎么做,由子类来实现。

随便写一个实现类

public class ConcreteTemplate extends AbstractTemplate {
    public void apply() {
        System.out.println("子类实现抽象方法 apply");
    }
      public void end() {
        System.out.println("我们可以把 method3 当做钩子方法来使用,需要的时候覆写就可以了");
    }
}

然后使用

public static void main(String[] args) {
    AbstractTemplate t = new ConcreteTemplate();
      // 调用模板方法 , 然后魔板方法里会按顺序调用那三个方法,其中有自己写的,有原本定义好的
    //比如发送消息的环境,模板方法里会调用初始化,发送消息,消息回显,其中消息回显的内容可以作为抽象类由我们自己实现,然后系统调用的时候会传参,进一个字符串,那么我们可以根据这个字符串来做我们自己的操作,可以把这个字符串记录到数据库,如果字符串带着错误信息,也可以加载到数据库后记录错误日志,通知管理员来处理 .....
      t.templateMethod();
}

因此,这个模式还是十分常用的。特别适用于框架技术,定义一个传入信息让用户自定处理方式的方法,然后在执行完框架的方法操作后也会调用这个用户自己实现的回调函数。

状态模式

库存中心有减库存和 补库存

public interface State {
   public void doAction(Context context);
}

定义减库存的状态

public class DeductState implements State {

   public void doAction(Context context) {
      System.out.println("商品卖出,准备减库存");
      context.setState(this);

      //... 执行减库存的具体操作
   }

   public String toString(){
      return "Deduct State";
   }
}

定义补库存的状态

public class RevertState implements State {
    public void doAction(Context context) {
        System.out.println("给此商品补库存");
          context.setState(this);

          //... 执行加库存的具体操作
    }
      public String toString() {
        return "Revert State";
    }
}

定义的Context类

public class Context {
    private State state;
      private String name;
      public Context(String name) {
        this.name = name;
    }

      public void setState(State state) {
        this.state = state;
    }
      public void getState() {
        return this.state;
    }
}

调用

public static void main(String[] args) {
    // 我们需要操作的是 iPhone X
    Context context = new Context("iPhone X");

    // 看看怎么进行补库存操作
      State revertState = new RevertState();
      revertState.doAction(context);

    // 同样的,减库存操作也非常简单
      State deductState = new DeductState();
      deductState.doAction(context);

      // 如果需要我们可以获取当前的状态
    // context.getState().toString();
}

读者可能会发现,在上面这个例子中,如果我们不关心当前 context 处于什么状态,那么 Context 就可以不用维护 state 属性了,那样代码会简单很多。

不过,商品库存这个例子毕竟只是个例,我们还有很多实例是需要知道当前 context 处于什么状态的。

备忘录模式

备忘录模式( Memento Pattern )又称为快照模式( Snapshot Pattern )或令牌模式( Token
Pattern),是指在不破坏封装的前提下,捕获一个对象的内部状态,井在对象之外保存这个状
态。这样以后就可将该对象恢复到原先保存的状态,属于行为型模式。

比如我们经常使用的svn、git就是个典型的备忘录模式实现,可以提交代码,可以回滚代码。(实际的开发中该模式用的还是很少的)

发起者:记录自身的状态,由自己定义需要记录什么信息,然后return一个备忘录类

@Getter
@Setter
@ToString
public class Originator {
    private String state;

    public Memento saveToMemento(){
        return new Memento(state);
    }

    public void restoreMemento(Memento memento){
        this.state = memento.getState();
    }
}

备忘录类

@Getter
@Setter
@ToString
public class Memento {
    private String state;
    //其实可以有很多属性的
    //比如 状态号 , 当前日志栈, 当前数据库信息, 最近操作时间 等等 ,其实完全可以抽象出来,当做一个状态类

    public Memento(String state){
        this.state = state;
    }
}

操作记录备忘录的内容:

public class Caretaker {
    //本质用一个栈来维护每次加入的内容,其实都一样,也可以用队列,用数组,我们可以任选需要恢复的节点就是了
    private final Stack<Memento> stack = new Stack<Memento>();

    public void addMemento(Memento memento) {
        stack.push(memento);
    }

    public Memento getMemento() {
        return stack.pop();
    }
}

客户端操作

public class Test {
    public static void main(String[] args) {
        //备忘录信息记录者
        Caretaker caretaker = new Caretaker();
        
        //创建一个干活的人,可能会需要记载备忘录信息,也就是自身当前的状态
        Originator originator = new Originator();
        //在操作后,当前状态为1
        originator.setState("1");
        
  		//此时我们保存一个备忘信息,因为我们不知道这个干活的人需要保存什么信息,因为一些信息是初始化的,每次new出来其实都是那个值,这种信息我们没必要保存,我们需要保存有更改的信息,但是我们不知道,因此让干活的人自己挑选要保持的信息,然后return出来是最好的
        caretaker.addMemento(originator.saveToMemento());
        
        //然后继续干活,继续更改状态
        originator.setState("2");
        originator.setState("3");
        
       
       	//此时 ,碰到异常,因为有备忘录,我们不需要跑路! 通过备忘录的一个节点,然后调用干活的人的子集内部的回溯方法,来回溯状态 , 这个节点是从保存备忘录的信息的栈里取出来的一个最近的
        originator.restoreMemento(caretaker.getMemento());
    }
}

命令模式

迭代子模式

访问者模式

中介者模式

解释器模式

剩下五种遇到再补充

参考文章:

https://juejin.cn/post/6844903695667167240#heading-1

https://juejin.cn/post/7185458264098734137

 posted on 2023-04-02 00:21    阅读(29)  评论(0编辑  收藏  举报