设计模式学习笔记

设计模式的三个分类

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

创建型模式

对象实例化的模式,创建型模式用于结构对象的实例化过程

  • 单例模式:某个类只能有一个实例,提供一个全局的访问点
  • 工厂模式
    • 简单工厂:一个工厂类根据传入的产量决定创建出哪一种产品类的实例
    • 工厂方法:定义一个创建对象的接口,让子类实例化哪个类
    • 抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类
  • 建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造
  • 原形模式:通过复制现有的实例来创建新的实例
单例模式

单例模式的写法有很多种,一个一个地看一下

  • 饿汉式
  public class Singleton {
  
      private Singleton singleton = new Singleton();
  
      private Singleton() {}
  
      public static Singleton getInstance() {
          return singleton;
      }
  
  }

这段代码的好处是编写简单,但是无法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地延迟加载,从而减少负载,所以还能改进。

  • 单线程写法
  public class Singleton {
  
      private static Singleton singleton = null;
  
      private Singleton() {}
  
      public static Singleton getInstance() {
          if (singleton == null) {
              singleton = new Singleton();
          }
          return singleton;
      }
  
  }

这种写法也是很简单的, 由私有构造方法和一个公有静态工厂方法构成,在工厂方法中对对象进行判空处理,为空时再进行实例对象的操作。这种方式可以延迟加载,但是有一个弱点:线程不安全。

如果有两条线程同时调用 getInstance() 方法,就有很大可能会导致重复创建对象。

  • 考虑线程安全的写法
  public class Singleton {
      private static volatile Singleton singleton = null;
  
      private Singleton() {}
  
      public static Singleton getInstance() {
          synchronized(Singleton.class) {
              if (singleton == null) {
                  singleton = new Singleton();
              }
          }
          return singleton;
      }
  } 

这种写法考虑了线程安全,将对单例对象的判空处理以及实例化部分使用synchronized进行加锁。同时对单例对象使用 volatile关键字进行限制,保证其对所有线程的可见性,并且禁止对其进行指令重排序优化。这样就可以从语义上保证这种单例模式的写法是线程安全的。注意:是语义上的线程安全)

这种写法是可以正确运行的,但是效率低下,几乎无法实际应用。因为在每次调用getInstnce方法的时候,都必须在 synchronized这里进行排队,而真正遇到需要实例化的情况是非常少的,这种写法造成了极大的浪费。

  • 兼顾线程安全和效率的写法
  public class Singleton {
  
      private static volatile Singleton singleton = null;
  
      private Singleton() {}
  
      public static Singleton getInstance() {
          if (singleton == null) {
              synchronized(Singleton.class) {
                  if (singleton == null) {
                      singleton = new Singleton();
                  }
              }
          }
          return singleton;
      }
      
  } 

这种写法被称为“双重检查锁”,就是在 getInstance中进行两次判断操作,看上去很多余,但是实际上极大地提高了效率,提升了并发度,进而提升了性能。

为什么可以提高并发度呢? 在单例中实例化的操作是比较少的,绝大多数都是可以并行的读操作,因此在加锁synchronized前先进行一次判空处理,就可以减少绝大多数的加锁操作,执行效率提高的目的也达到了。

  • 静态内部类写法
  public class Singleton {
      private static class Holder {
          private static Singleton singleton = new Singleton();
      }
  
      private Singleton() {}
  
      public static Singleton getInstance() {
          return Holder.singleton;
      }
  
  }

这么写的原因:

在这个例子里,内部类 Holder 的静态变量 singleton 就是我们需要的唯一的单例对象。

静态内部类与外部类没有什么大的关系,外部类加载的时候,内部类不会被加载,静态内部类只是调用的时候用了外部类的名字而已。

这种方式的优点是:

  1. 不用 synchronized加锁,节省时间
  2. 调用 getInstance()时才会加载内部类(创建对象),延迟加载的时间。
  • 枚举写法
  public enum Singleton {
  
      INSTANCE;
  
      private String name;
  
      public String getName() {
          return this.name;
      }
  
      public void setName(String name) {
          this.name = name;
      }
  
  }
工厂模式
  • 简单工厂模式

简单工厂模式角色分配:

  1. 工厂角色(Factory):简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂可以被外部直接调用,创建所需要的产品的对象。
  2. 抽象产品角色(Product):简单工厂模式缩创建的所有对象的父类(或者接口),它负责描述所有实例所共有的公共接口。
  3. 具体产品角色(Concrete Product):简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。

创建抽象产品角色

public interface Shape {
    void draw();
}

创建具体产品角色

public class Circle implements Shape {
    public Circle() {
        System.out.println('Circle');
    }

    @Override
    public void draw() {
        System.out.println("Draw Circle");
    }
}

public class Rectangle implements Shape {
    public Rectangle() {
        System.out.println('Rectangle');
    }

    @Override
    public void draw() {
        System.out.println('Draw Rectangle');
    }
}

创建工厂角色

public class ShapeFactory {
    public static Shape getShape(String shapeType) {
        switch(shapeType) {
            case "Circle":
                return new Circle();

            case "Rectangle":
                return new Rectangle();
        }
        return null;
    }
}

使用工厂

Shape circle = ShapeFactory.getShape("Circle");
circle.draw();

Shape reactangle = ShapeFactory.getShape("Rectangle");
rectangle.draw();

这种实现有个问题,当需要新增产品类的时候,就需要修改工厂类中的 getShape方法,这明显不符合

开发(对扩展开放)-封闭(对修改封闭) 原则

使用反射机制改善简单工厂

工厂角色

public class ShapeFactory2 {

    public static Shape getShape(Class<? extends Shape> clazz) {
        Object obj = null;

        try {
            obj = Class.forName(clazz.getName()).newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        return obj;
    }

}

使用工厂

Circle circle = (Circle) ShapeFactory2.getShape(Circle.class);
circle.draw();

Rectangle rectangle = (Rectangle) ShapeFactpory2.getShape(Rectangle.class);
rectangle.draw();
  • 工厂方法模式

工厂方法模式角色分配:

  1. 抽象工厂角色(Abstract Factory): 是工厂方法模式的核心,与应用程序无关。任何在模式中创建的工厂类必须实现这个接口。
  2. 具体工厂角色(Concrete Factory): 这是实现抽象工厂接口的具体工厂类,包含于应用程序密切相关的逻辑,并且受到应用程序调用以创建某一种产品对象。
  3. 抽象产品角色(Abstract Product):工厂方法模式缩创建的对象的超类型,也就是产品对象的共同父类或者共同拥有的接口。
  4. 具体产品角色(Concrete Product):这个角色实现了抽象产品角色缩定义的接口。某具体产品有专门的具体共产创建,他们直接往往一一对应。

抽象工厂角色

  public interface Factory {
      public Shape getShape();
  }

具体工厂角色

  public CircleFactory implements Factory {
  
      @Override
      public Shape getShape() {
          return new Circle();
      }
  }
  
  public RectangleFactory implements Factory {
  
      @Override
      public Shape getShape() {
          return new Rectangle();
      }
  }

使用工厂

  Factory circleFactory = new CircleFactory();
  Shape circle = circleFactory.getShape();
  circle.draw();
  • 抽象工厂模式

抽象工厂模式角色分配

  1. 抽象工厂角色(Abstract Factory): 是工厂方法模式的核心,与应用程序无关。任何在模式中创建的工厂类必须实现这个接口。
  2. 具体工厂角色(Concrete Factory): 这是实现抽象工厂接口的具体工厂类,包含于应用程序密切相关的逻辑,并且受到应用程序调用以创建某一种产品对象。
  3. 抽象产品角色(Abstract Product):工厂方法模式缩创建的对象的超类型,也就是产品对象的共同父类或者共同拥有的接口。
  4. 具体产品角色(Concrete Product):抽象工厂模式所创建的任何产品对象都是某一个具体产品类的实例。在抽象工厂中创建的产品属于同一产品族,这不同于工厂方法模式中的工厂只创建单一产品。

那工厂方法中的工厂和抽象工厂中的工厂有什么区别呢?

抽象工厂是生产一整套有产品的(至少要生产两个产品),这些产品必须是相互有关系或有依赖的,而工厂方法中的工厂是生产单一产品的工厂。

以游戏中的各种枪和子弹作为实例,枪与子弹是相互有关系相互依赖的。

抽象工厂角色

  public interface Factory {
      public Gun produceGun();
      public Bullet produceBullet();
  }

具体工厂角色

  public interface Factory {
      public Gun produceGun();
      public Bullet produceBullet();
  }
  
  public class AK_Factory implements Factory {
  
      @Override
      public Gun produceGun() {
          return new AK();
      }
  
      @Override
      public Bullet produceBullet() {
          rteurn new  AK_Bullet();
      }
  
  }
  
  public class M416_Factory implements Factory {
      @Override
      public Gun produceGun() {
          return new M416();
      }
  
      @Override
      public Bullet produceBullet() {
          rteurn new  M416_Bullet();
      }
  }

抽象产品角色

  public interface Gun {
      void shooting();
  }
  
  public interface Bullet {
      void load();
  }

具体产品角色

  public class AK implements Gun {
  
      @Override
      public void shooting() {
          System.out.println("shooting with AK");
      }
  
  }
  
  public class AK_Bullet implements Bullet {
  
      @Override
      public void load() {
          System.out.println("load bullets with AK");
      }
  }
  
  public class M416 implements Gun {
  
      @Override
      public void shooting() {
          System.out.println("shooting with M416");
      }
  
  }
  
  public class M416_Bullet implements Bullet {
  
      @Override
      public void load() {
          System.out.println("load bullets with M416");
      }
  
  }
  

使用工厂

  Factory factory;
  Gun gun;
  Bullet bullet;
  
  factory = new AK_Factory();
  bullet = factory.produceBullet();
  bullet.load();
  gun = factory.produceGun();
  gun.shooting();

在项目实际使用过程中,有一定的局限性,需要具体情况具体分析。

与工厂方法最大的不同点:抽象工厂模式的工厂不单单可以创建一个对象,而是可以创建一组对象。

建造者模式

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

建造者模式的角色分配

  1. 建造者角色:定义生成实例所需要的所有方法
  2. 具体的建造者角色:实现生成实例所需要的所有方法,并且定义获取最终生成实例的方法。
  3. 监工角色:定义使用建造者角色中的方法来生成实例的方法

建造者角色

public abstract class Builder {
    public abstract void buildPart1();
    public abstract void buildPart2();
    public abstract void buildPart3();
}

具体建造者角色

public class ConcreteBuilder extends Builder {

    private StringBuffer buffer = new StringBuffer(); // 假设最终产品为 buffer.toString();

    @Override
    public void buildPart1() {
        buffer.append("BuilderPart1");
    }

    @Override
    public void buildPart2() {
        buffer.append("BuilderPart2");
    }

    @Override
    public void buildPart3() {
        buffer.append("BuilderPart3");
    }

    public String getResult() {
        return buffer.toString();
    }
}

监工角色

public class Director {

    private Builder builder;

    public Director(Builder builder) {
        this.builer = builder;
    }

    public void setBuilder(Builder builder) {
        this.builder = builder;
    }

    public void construct() {
        builder.buildPart1();
        builder.buildPart2();
        builder.buildPart3();
    }

}

使用建造者模式

ConcreteBuilder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.construct();
String result = builder.getResult();

建造者模式的主要作用是在用户不知道对象如何建造(建造过程和细节)的情况下就可以直接创建复杂的对象,

  1. 用户只需要给出指定复杂对象的类型和内容
  2. 建造者模式负责按 复杂对象的构造流程创建复杂对象(把复杂的建造过程和细节隐藏起来)

优点

  1. 易于解耦

将产品本身与产品创建过程进行解耦,可以使用相同过程的创建过程来得到不同的产品。也就是说细节依赖抽象。

  1. 易于精确控制对象的创建

将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰

  1. 易于扩展

增加新的具体建造者无需修改原有类库的代码,易于扩展,符合“开闭原则”

缺点

  1. 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品直接的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
  2. 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

应用场景

  1. 需要生成的对象有复杂的内部结构,同时这些产品对象具备共性
  2. 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

结构型模式

把类和对象结合在一起形成一个更大的结构

  • 适配器模式:将一个类的方法接口转换成客户希望的另外一个接口
  • 桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立地变化
  • 组合模式:将对象组合成树形结构以表示 “部分-整体”的层次结构
  • 装饰模式:动态地给对象添加新的功能
  • 外观模式:对外提供一个统一的方法,来访问子系统中的一群接口
  • 亨元(蝇量)模式:通过共享技术来有效地支持大量细粒度的对象
  • 代理模式:为其他对象提供一个代理以便控制这个对象的访问
适配器模式

适配器模式把一种借口变换成客户端所期待的另一种借口,从而使原本因借接口不匹配而无法一起工作的两个类能够在一起工作。

使用场景

  1. 系统需要使用现有的类,而此类的接口不符合系统的需要,即接口不兼容
  2. 想要建立一个可以重复使用的类,用于与一些彼此直接没有太大关联的一些类,包括一些可能在将来引进的一些类一起工作
  3. 需要一个统一的输出接口,而输入端的接口不可预知

适配器角色分配

  1. 目标角色,也就是所期待得到的接口
  2. 需要适配的接口
  3. 适配器角色,是适配器模式的核心。适配器将源接口转换成目标接口

目标角色

public interface Target {
    void Request();
}

需要适配的接口

public class Adaptee {
    public void SpecifiRequest() {
        
    }
}

适配器角色

public class Adapter extends Adaptee implements Target {

    @Override
    public void Request() {
        this.SpecifiRequest();
    }

}

*使用适配器模式

Target adapter = new Adapter();
adapter.Request();
代理模式

定义:起到中介对象,连接客户端和目标对象

作用:通过引用代理对象的方式来间接访问目标对象

解决的问题:防止直接访问目标对象给系统带来的不必要复杂性

代理模式角色分配

  1. 抽象主题角色
  2. 代理角色
  3. 真实对象角色

抽象主题角色

public interface Subject {
    void doSomething();
}

真实对象角色

public class RealSubject implements Subject {

    @Override
    public void doSomething() {
        System.out.println("doSomething");
    }
}

代理角色

public class Proxy implements Subject {

    @Override
    public void doSomething() {

        RealSubject realSubject = new RealSubject();
        realSubject.doSomething();

        System.out.println("doSomething");
        this.doAnotherThing();
    }

    private void doAnotherThing() {
        System.out.println("doAnotherThing");
    }
}

使用代理模式

Subject proxy = new Proxy();
proxy.doSomething();

优点

  1. 协调调用者和被调用者,降低了系统的耦合度
  2. 代理对象作为客户端和目标对象的中介,起到了保护目标对象的作用

缺点

  1. 由于在客户端和真实主题之间增加了代理对象,因此会造成请求的速度变慢
  2. 实现代理模式需要额外的工作(有些代理模式实现非常复杂),从而增加了实现的复杂度

行为型模式

类和对象如何交互,及划分责任和算法。

  • 访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能
  • 模板模式:定义一个算法结构,而将一些步骤延迟到子类实现
  • 策略模式:定义一系列算法,把它们封装起来,并且使它们可以相互替换
  • 状态模式:允许一个对象在其对象内部状态改变时改变它的行为
  • 观察者模式:对象间的一对多的依赖关系
  • 备忘录模式:在不破坏封装的前提下,保持对象的内部状态
  • 中介者模式:用一个中介对象来封装一系列的对象交互
  • 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构
  • 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器
  • 命令模式:将命令请求封装为一个对象,使得可以使用不同的请求来进行参数化
  • 责任链模式:将请求的发送者和接受者解耦,使得多个对象都有处理这个请求的机会
观察者模式

观察者模式(Observer Pattern)也叫做 发布-订阅模式(Publish/Subscribe)、模型-视图模式(Model/View)。

这个模式最重要的作用就是解耦。也就是将观察者和被观察者进行解耦,使得它们之间的依赖性更小,甚至做到毫无依赖。

在观察者模式中它定义了一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

观察者接口

public interface Observer {
    void update(Object obj);
}

被观察者基类

public abstract class Observable {

    private Vector<Observer> obVector = new Vector();

    public void addObserver(Observer observer) {
        obVector.add(observer);
    }

    public void delObserver(Observer observer) {
        obVector.remove(observer);
    }

    public void notifyObservers(Object obj) {
        for (Observer observer: obVector) {
            observer.update(obj);
        }
    }

}