设计模式

教程: https://www.runoob.com/design-pattern/design-pattern-intro.html

简介

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。 项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题, 以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

类型

  1. 创建型:隐藏对象的构建逻辑。比如,工厂模式、抽象工厂模式、单例模式、构建者模式、原型模式。
  2. 结构型:这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
    比如,适配器模式、桥接模式、过滤器模式、组合模式、装饰者模式、外观模式、享元模式、代理模式
  3. 行为型:这些设计模式特别关注对象之间的通信。比如,责任链模式、策略模式、命令模式、模板模式、观察者模式、解释器模式、 迭代器模式、中介者模式、备忘录模式、状态模式、空对象模式、访问者模式

1.单例模式

  1. 什么是单例模式? 在所有的设计模式中,单例模式是我们在项目开发中最为常见的设计模式之一。单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

  2. 使用场景?

    • 在整个项目中需要一个共享访问点或共享数据, 比如我们在程序中进行计数、配置文件的信息等
    • 创建对象会很消耗资源,比如线程和数据库资源等,比如我们自己控制线程池。

这种方式极大地简化了在复杂环境 下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。

  1. 问题? 高并发下如何保证单例模式的线程安全性呢?如何保证序列化后的单例对象在反序列化后任然是单例的呢?

为了解决并发的问题,我们提出了单例的多种实现方式:

  • 普通单例(线程不安全),教学案例
  • 懒汉(线程不安全, getInstance 方法需要使用 synchronized 修饰才是安全的)
  • 饿汉(线程安全)
  • 静态内部类(线程安全),其实内部类使用饿汉模式
  • 枚举(线程安全)。这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
  • 双检查锁机制(线程安全),单例的对象需要加volatile的话,即使有DCL(Double Check Locking),但是对象获得的不是最新的,一样可能导致线程不安全

防止通过反射创建对象

public class Singleton7 {

    private static Singleton7 singleton = new Singleton7();

    private static boolean flag = true;

    private Singleton7() {
        if (flag) {
            flag = !flag;
        } else {
            throw new RuntimeException("禁止反射生成对象");
        }
    }
}

2.工厂模式

主要分为三类

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式
  1. 简单工厂
    需要什么,生产什么 。
    public class Factory1 {  
      
    public static void main(String[] args) {  
        Car baoMao = getCar("BaoMao");  
        baoMao.run();  
      
        Car benChi = getCar("BenChi");  
        benChi.run();  
    }  
      
    public static Car getCar(String type) {  
        if ("BaoMao".equals(type)) {  
            return new BaoMa();  
        } else if ("BenChi".equals(type)) {  
            return new BenChi();  
        } else {  
            throw new RuntimeException("不支持type=" + type);  
        }  
    }}
    
  2. 工厂方法模式 只有一个工厂,通过不同的方法,生产多种 ```java public class Factory2 {

    public static void main(String[] args) {
    Factory2 factory = new Factory2();
    Car baoMa = factory.getBaoMa();
    Car benChi = factory.getBenChi();

     baoMa.run();  
     benChi.run();    }  
    

    public Car getBaoMa() {
    return new BaoMa();
    }

    public Car getBenChi() {
    return new BenChi();
    }

}


3. 抽象工厂模式  
使用不用的工厂,生产具有相同特点的,每个工厂只生产一种。
可以配合策略模式实现不要需求下获得不同的bean

案例: spring 的 FactoryBean
```java
public class Factory3 {  
  
    public static void main(String[] args) {  
        Car baoMa = new BaoMaFactory().getInstance();  
        Car benChi = new BenChiFactory().getInstance();  
  
        baoMa.run();  
        benChi.run();  
    }  
  
    private static interface CarFactory {  
  
        Car getInstance();  
    }  
  
  
    private static class BaoMaFactory implements CarFactory {  
  
        @Override  
        public Car getInstance() {  
            return new BaoMa();  
        }  
    }  
    private static class BenChiFactory implements CarFactory {  
  
        @Override  
        public Car getInstance() {  
            return new BenChi();  
        }  
    }
}

3.构建者模式

也就是我们平常说的builder模式,比如在 mybatis、spring 中大量使用了 builder 模式。

使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。

使用场景:
1、需要生成的对象具有复杂的内部结构。
2、需要生成的对象内部属性本身相互依赖。

与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。

优点:

  1. 建造者独立,易扩展。
  2. 便于控制细节风险。

缺点:

  1. 产品必须有共同点,范围有限制。
  2. 如内部变化复杂,会有很多的建造类。

4. 原型模式

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。
例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

主要解决:在运行期建立和删除原型。

何时使用:

  1. 当一个系统应该独立于它的产品创建,构成和表示时。
  2. 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
  3. 为了避免创建一个与产品类层次平行的工厂类层次时。
  4. 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。

关键代码:

  1. 实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。
  2. 原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。

应用实例:

  1. 细胞分裂。
  2. JAVA 中的 Object clone() 方法。

优点:

  1. 性能提高。
  2. 逃避构造函数的约束。

缺点:

  1. 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
  2. 必须实现 Cloneable 接口。

使用场景:

  1. 资源优化场景。
  2. 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
  3. 性能和安全要求的场景。
  4. 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
  5. 一个对象多个修改者的场景。
  6. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
  7. 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。

比如在spring中bean的创建类型就可以分为 singleton 和 prototype, prototype就是原型模式,说简单就是每次调用的时候复制出一个新的对象

注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。

5.适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。

主要解决:
主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。

使用场景:

  1. 系统需要使用现有的类,而此类的接口不符合系统的需要。
  2. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
  3. 通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)

关键代码:
适配器继承或依赖已有的对象,实现想要的目标接口。

应用实例:

  1. JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。
  2. JAVA 中的 jdbc。

优点:

  1. 可以让任何两个没有关联的类一起运行。
  2. 提高了类的复用。
  3. 增加了类的透明度。
  4. 灵活性好。

缺点:

  1. 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
  2. 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。

注意事项:
适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。

6.装饰器模式

也叫做增强模式,是一种动态增强对象方式。
使用场景:

  1. 需要扩展一个类的功能,或给一个类增加附加功能。
  2. 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
  3. 需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式。

应用实例:

  1. 动态代理(AOP) 。被增强的对象可以切换:Service , 增强的内容也可以切换:事务处理
  2. FileInputSteram:它是节点流!就是和一个资源绑定在一起的!文件!
    BufferedInputStream:它是装饰流!创建我是一定要给我一个底层对象,然后我不管你给我的是什么流,我都会给它添加缓冲区!

缺点:

  1. 增强的内容是死的,不能动!
  2. 被增强的对象也是死的!

7.代理模式

优点:

  1. 职责清晰。
  2. 高扩展性。
  3. 智能化。

缺点:

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

使用场景:

  1. 远程代理。
  2. 虚拟代理。
  3. Copy-on-Write 代理。
  4. 保护(Protect or Access)代理。
  5. Cache代理。
  6. 防火墙(Firewall)代理。
  7. 同步化(Synchronization)代理。
  8. 智能引用(Smart Reference)代理。

注意事项:

  1. 和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
  2. 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。

这里我们指的是我们自己使用的静态代理,动态代理可以参考Jdk, cglib

8.中介者模式

用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
与装饰者模式很像,但是中介者的作用更倾向于整合一些类对象,完成一件事。
比如,spring 中 service 整合了多个 dao 层对象,实现一次数据库的操作。

9.命令模式

10.责任链模式

责任链模式的定义:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这个对象连成一条链, 并沿着这条链传递该请求,直到有一个对象处理他为止。这里就不再过多的介绍什么是责任链模式,主要来说说java中如何编写。 主要从下面3个框架中的代码中介绍。

实现方式:列表 和 链表

实现案例:

  1. servlet中的filter
    Servlet中分别定义了一个 Filter和FilterChain的接口,核心代码如下
    public final class ApplicationFilterChain implements FilterChain {
     private int pos = 0;
     private int n;
     private ApplicationFilterConfig[] filters; 
     private Servlet servlet
    
     @Override
     public void doFilter(ServletRequest request, ServletResponse response) {
         if (pos < n) {
             ApplicationFilterConfig filterConfig = filters[pos++];
             Filter filter = filterConfig.getFilter();
             filter.doFilter(request, response, this);
         } else {
             servlet.service(request, response);
         }
     }
    }
    

    代码还算简单,结构也比较清晰,定义一个Chain,里面包含了Filter列表和servlet,达到在调用真正servlet之前进行各种filter逻辑。

  1. dubbo中的filter
    Dubbo在创建Filter的时候是另外一个方法,通过把Filter封装成 Invoker的匿名类,通过链表这样的数据结构来完成责任链,核心代码如下:
    private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
     Invoker<T> last = invoker;
     List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class)
         .getActivateExtension(invoker.getUrl(), key, group);
     if (filters.size() > 0) {
         for (int i = filters.size() - 1; i >= 0; i --) {
             final Filter filter = filters.get(i);
             // 链表结构
             final Invoker<T> next = last;
             last = new Invoker<T>() {
                 ...
                 public Result invoke(Invocation invocation) throws RpcException {
                     return filter.invoke(next, invocation);
                 }
                 ...
             };
         }
     }
     return last;
    }
    

    Dubbo的责任链就没有类似FilterChain这样的类吧Filter和调用Invoker结合起来,而是通过创建一个链表,调用的时候我们只知道第一个节点, 每个节点包含了下一个调用的节点信息。 这里的虽然Invoker封装Filter没有显示的指定next,但是通过java匿名类和final的机制达到同样的效果。

  1. mybatis中的plugin 这3个框架在实现责任链方式不尽相同
    Mybatis可以配置各种Plugin,无论是官方提供的还是自己定义的,Plugin和Filter类似,就在执行Sql语句的时候做一些操作。
    Mybatis的责任链则是通过动态代理的方式,使用Plugin代理实际的Executor类。(这里实际还使用了组合模式,因为Plugin可以嵌套代理),核心代码如下:
    public class Plugin implements InvocationHandler{
     private Object target;
     private Interceptor interceptor;
    
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {      
    
         if (满足代理条件) {
             return interceptor.intercept(new Invocation(target, method, args));
         }
         return method.invoke(target, args);     
     }
    
     public static Object wrap(Object target, Interceptor interceptor) {
         Class<?> type = target.getClass();
         Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
         if (interfaces.length > 0) {
             return Proxy.newProxyInstance(
                     type.getClassLoader(),
                     interfaces,
                     new Plugin(target, interceptor, signatureMap));
         }
         return target;
     }
    } 
    

11.策略模式

定义一组算法,将每个算法都封装起来,并且使它们之间根据条件可以互换
其实策略模式,就是通过对一个接口进行多种实现,在不同的场景使用不同的实现,都是在Java的基础特性的基础上总结出来的

主要解决:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。

关键代码:实现同一个接口。

优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。

缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

使用场景:

  1. 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  2. 一个系统需要动态地在几种算法中选择一种。
  3. 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

12.迭代器模式

它提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。
比如jdk提供的Iterator,实现了对容器内部对象的操作

13.组合模式

组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。
组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

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+" ]");
   }   
}

14.观察者模式

又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式、从属者(Dependents)模式,

作用:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。而且要考虑到易用和低耦合,保证高度的协作。

何时使用: 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

应用案例:

  1. spring中的listener
  2. swing中的event事件
  3. mq

优点:

  1. 观察者和被观察者是抽象解耦合的。
  2. 建立一套触发机制。

缺点:

  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

使用场景:

  1. 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  2. 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  3. 一个对象必须通知其他对象,而并不知道这些对象是谁。

注意事项: 1、JAVA 中已经有了对观察者模式的支持类。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。

15.门面模式(外观模式)

要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
案例: log4j

16.备忘录模式

保存一个对象的某个状态,以便在适当的时候恢复对象

17.访问者模式

18.状态模式

在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

19.解释器模式

这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等

20.享元模式

使用共享对象的方法,用来尽可能减少内存使用量以及分享数据。
设计思想就是:对象如果已经new过一次,我们就可以把他保存其他,等待下次使用。

使用场景:

  1. 系统中存在大量的相似对象。
  2. 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
  3. 需要缓冲池的场景(对象池,连接池等)。

21.桥接模式

将抽象和实现解耦,使得两者可以独立地变化

22.模板方法模式

实现方式: a)父类模板类(规定要执行的方法和顺序,只关心方法的定义及顺序,不关心方法实现)
b)子类实现类(实现a规定要执行的方法,只关心方法实现,不关心调用顺序)

优点: 1)封装不变部分,扩展可变部分:把认为不变部分的算法封装到父类实现,可变部分则可以通过继承来实现,很容易扩展。
2)提取公共部分代码,便于维护。
3)行为由父类控制,由子类实现。

缺点:
模板方法模式颠倒了我们平常的设计习惯:抽象类负责声明最抽象、最一般的事物属性和方法,实现类实现具体的事物属性和方法。在复杂的项目中可能会带来代码阅读的难度。

案例:

  1. AQS
  2. spring template

23.空对象模式


文章信息

张雪随笔
master
Homepage
Issues
Download

This Software is under the terms of The Unlicense.