设计模式

126 阅读15分钟

六大设计原则

开闭原则

  • 概念

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原(是“原”,指原来的代码)代码的情况下进行扩展。

  • 优点
    • 可复用性
    • 可维护性

里氏替换原则

  • 概念

派生类(子类)对象能够替换其基类(父类)对象被调用

依赖倒转原则

  • 概念

程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

接口隔离原则

  • 概念

客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上

合成/聚合复用原则

  • 概念

合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承。

迪米特法则

  • 概念

一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

工厂模式

1.什么是工厂模式

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且通过使用一个共同的接口来指向新创建的对象。实现了创建者和调用者的分离,工厂模式分为简单工厂、工厂方法、抽象工厂模式

2.工厂模式的好处

  • 利用工厂模式可以降低程序的耦合性,为后期的维护修改提供了很大的便利。
  • 将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦

3.Spring开发中的工厂设计模式

1.Spring IOC

  • 在Spring IOC容器创建bean的过程是使用了工厂设计模式
  • Spring中无论是通过xml配置还是通过配置类还是注解进行创建bean,大部分都是通过简单工厂来进行创建的。
  • 当容器拿到了beanName和class类型后,动态的通过反射创建具体的某个对象,最后将创建的对象放到Map中。

2.为什么Spring IOC要使用工厂设计模式创建Bean呢

  • 在实际开发中,如果我们A对象调用B,B调用C,C调用D的话我们程序的耦合性就会变高。(耦合大致分为类与类之间的依赖,方法与方法之间的依赖。)
  • 在很久以前的三层架构编程时,都是控制层调用业务层,业务层调用数据访问层时,都是是直接new对象,耦合性大大提升,代码重复量很高,对象满天飞
  • 为了避免这种情况,Spring使用工厂模式编程,写一个工厂,由工厂创建Bean,以后我们如果要对象就直接管工厂要就可以,剩下的事情不归我们管了。Spring IOC容器的工厂中有个静态的Map集合,是为了让工厂符合单例设计模式,即每个对象只生产一次,生产出对象后就存入到Map集合中,保证了实例不会重复影响程序效率。

4.工厂模式分类

  • 简单工厂:用来生产同一等级结构中的任意产品。(不支持拓展增加产品)
  • 工厂方法:用来生产同一等级结构中的固定产品。(支持拓展增加产品)
  • 抽象工厂:用来生产不同产品组的全部产品。(不支持拓展增加产品;支持增加产品组)

不使用工厂模式

public class BMW111 {
    public BMW111(){
        System.out.println("生产BMW111");
    }
}

public class BMW222 {
    public BMW222(){
        System.out.println("生产BMW222");
    }
}

public class Customer {
    public static void main(String[] args) {
        //不使用工厂模式
        BMW111 bmw111 = new BMW111();
        BMW222 bmw222 = new BMW222();
    }
}

使用简单工厂模式

//父类
public class BMW {
}

public class BMW111 extends BMW{
    public BMW111(){
        System.out.println("生产BMW111");
    }
}

public class BMW222 extends BMW{
    public BMW222(){
        System.out.println("生产BMW222");
    }
}

//创建工厂类
public class SimpleFactory {
    public BMW createBMW(int i) {
        switch (i) {
            case 111 :
                return new BMW111();
            case 222:
                return new BMW222();
            default:
                break;
        }
        return null;
    }
}

public class Customer {
    public static void main(String[] args) {
       //使用简单工厂模式
        SimpleFactory simpleFactory = new SimpleFactory();
        BMW bmw111 = simpleFactory.createBMW(111);
        BMW bmw222 = simpleFactory.createBMW(222);
    }
}

使用工厂方法模式

//创建工厂接口
public interface Factory {
    BMW createBMW();
}
public class FactoryBMW111 implements Factory {
    @Override
    public BMW createBMW() {
        return new BMW111();
    }
}
public class FactoryBMW222 implements Factory {
    @Override
    public BMW createBMW() {
        return new BMW222();
    }
}
public class Customer {
    public static void main(String[] args) {
        //工厂方法模式
        Factory factoryBMW111 = new FactoryBMW111();
        Factory factoryBMW222 = new FactoryBMW222();
        BMW bmw111 = factoryBMW111.createBMW();
        BMW bmw222 = factoryBMW111.createBMW();
    }
}

抽象工厂模式

抽象工厂简单地说是工厂的工厂,抽象工厂可以创建具体工厂,由具体工厂来产生具体产品。

1637054037019

策略模式

1.定义

定义一组算法,将每一个算法封装起来,从而使它们可以互相切换。

2.特点

  • 一组算法,那就是不同的策略
  • 这组算法都实现了相同的接口或者继承相同的抽象类,所以可以互相切换

3.涉及到的角色

  • 封装角色:上层访问策略的入口,它持有抽象策略角色的引用
  • 抽象策略角色:提供接口或者抽象类,定义策略组必须拥有的方法和属性
  • 具体策略角色:实现抽象策略,定义具体的算法逻辑

单例设计模式

1.概念

单例对象的类必须保证只有一个实例存在

2.适用场景

单例模式只允许创建一个对象,因此节省内存,加快对象的访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等,如:

  • 需要频繁实例化然后销毁的对象
  • 创建对象时耗时过多或者耗资源过多,但又常用到的对象
  • 有状态的工具类对象
  • 频繁访问数据库或文件的对象

3.饿汉式

public class Singleton {
  private static final Singleton instance = new Singleton();
  private Singleton(){};
  public static Singleton getInstance() {
    return instance;
  }
}

优点

没有线程安全问题,简单

缺点

  • 提前初始化会延长类加载器的时间
  • 如果不使用会浪费内存空间
  • 不能传递参数

4.懒汉式

public class Singleton {
  private Singleton(){}
  
  public static Singleton getInstance() {
    return Holder.SINGLE_TON;
  }
  
  private static class Holder {
    private static final Singleton SINGLE_TON = new Sinleton();
  }
}

优点

解决线程安全问题,延迟初始化,在初始化内部类的过程中,JVM保证同一时刻只有一个线程运行

5.双重检查锁懒汉式

public class Singleton {
  private volatile static Singleton uniqueSingleton;
  
  private Singleton() {}
  
  public Singleton getInstance() {
    if (null == uniqueSingleton) {
      synchronized (Singleton.class) {
        if (null == uniqueSingleton) {
        	uniqueSingleton = new Singleton();
        }
      }
    }
    return uniqueSingleton;
	}	
}

1.为什么使用volatile指令重排序

使用volatile关键字后,重排序被禁止,所有的写操作都将发生在读操作之后

2.为什么要双重检查null

  • 第一个null:为了避免给非空对象上锁,浪费性能
  • 第二个null:当一个线程执行实例化之后,退出锁定区域,如果有第二个判断,剩下的线程还是可以实例化

6.单例模式的破坏

Singleton sc1 = Singleton.getInstance();
Singleton sc2 = Singleton.getInstance();
System.out.println(sc1); // sc1,sc2是同一个对象
System.out.println(sc2);
/*通过反射的方式直接调用私有构造器*/

Class<Singleton> clazz = (Class<Singleton>)
Class.forName("Singleton");

Constructor<Singleton> c = clazz.getDeclaredConstructor(null);

c.setAccessible(true); // 跳过权限检查

Singleton sc3 = c.newInstance();
Singleton sc4 = c.newInstance();

System.out.println("通过反射的方式获取的对象sc3:" + sc3); // sc3,sc4不是同一个对象
System.out.println("通过反射的方式获取的对象sc4:" + sc4);

7.spring中bean的单例

1.有状态与无状态对象

  • 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
  • 无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。

2.实现

public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{
  /**
  * 充当了Bean实例的缓存,实现方式和单例注册表相同
  */
  private final Map singletonCache=new HashMap();
  
  public Object getBean(String name)throws BeansException{
  return getBean(name,null,null);
}
...
public Object getBean(String name,Class requiredType,Object[] args)throws
BeansException{
  //对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码)
  String beanName=transformedBeanName(name);
  Object bean=null;
  //手工检测单例注册表
  Object sharedInstance=null;
  //使用了代码锁定同步块,原理和同步方法相似,但是这种写法效率更高
  synchronized(this.singletonCache){
  	sharedInstance=this.singletonCache.get(beanName);
  }
  if(sharedInstance!=null){
  ...
    //返回合适的缓存Bean实例
    bean=getObjectForSharedInstance(name,sharedInstance);
  }else{
  ...
    //取得Bean的定义
    RootBeanDefinition
    mergedBeanDefinition=getMergedBeanDefinition(beanName,false);
  	...
  //根据Bean定义判断,此判断依据通常来自于组件配置文件的单例属性开关
  //<bean id="date" class="java.util.Date" scope="singleton"/>
  //如果是单例,做如下处理
  if(mergedBeanDefinition.isSingleton()){
    synchronized(this.singletonCache){
      //再次检测单例注册表
      sharedInstance=this.singletonCache.get(beanName);
      if(sharedInstance==null){
      ...
      try {
      //真正创建Bean实例
          				sharedInstance=createBean(beanName,mergedBeanDefinition,args);
        //向单例注册表注册Bean实例
        addSingleton(beanName,sharedInstance);
      }catch (Exception ex) {
      ...
      }finally{
      ...
      }
    }
    }
    bean=getObjectForSharedInstance(name,sharedInstance);
    }
    //如果是非单例,即prototpye,每次都要新创建一个Bean实例
    //<bean id="date" class="java.util.Date" scope="prototype"/>
  else{
  	bean=createBean(beanName,mergedBeanDefinition,args);
  }
  }
  ...
  return bean;
  }
}

3.controller默认是单例的

不要使用非静态的成员变量,否则会发生数据逻辑混乱。

8.单例对象的生命周期

  • 出生:容器创建时对象出生 (立即创建)
  • 活着:只要容器在,对象一直活着
  • 死亡:容器销毁,对象消亡
  • 总结:单例对象与容器共存亡

9.多例对象生命周期

  • 出生:当我们使用对象时,Spring框架为我们创建对象
  • 活着:对象只要在使用过程中就一直活着
  • 死亡:当对象长时间没有别的对象进行引用时,有Java的垃圾回收器回收

10.工具类使用单例模式还是静态方法

  • 没有配置信息的工具类,使用静态类
  • 如果有配置信息的工具类,最好用单例模式

命令模式

1.定义

将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志

2.模式结构

Command:抽象命令类

ConcreteCommand:具体命令类

Invoker:调用者

Receiver:接收者

Client:客户类

3.优点

  • 降低系统的耦合度
  • 新的命令可以很容易的加入到系统中
  • 可以比较容易地设计一个命令队列和宏命令
  • 可以方便地实现对请求的Undo和Redo

4.缺点

使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用

代理模式

1.什么是代理模式

  • 通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。(AOP的微实现)
  • 代理在原有代码乃至原业务流程都不修改的情况下,直接在业务流程中切入新代码,增加新功能

2.应用场景

SpringAO、日志打印、异常处理、事务控制、权限控制

3.代理的分类

1.静态代理

优点

可以做到在不修改目标功能的情况下,对目标功能的扩展

缺点

每一个代理类都必须实现一遍委托类(也就是realsubject)的接口,如果接口增加方法,则代理类也必须跟着修改。其次,代理类每一个接口对象对应一个委托对象,如果委托对象非常多,则静态代理类就非常臃肿,难以胜任。

2.jdk动态代理

步骤

  1. 编写一个委托类的接口,即静态代理的(Subject接口)
  2. 实现一个真正的委托类,即静态代理的(RealSubject类)
  3. 创建一个动态代理类,实现InvocationHandler接口,并重写该invoke方法
  4. 在测试类中,生成动态代理的对象。
public class DynamicProxy implements InvocationHandler {
  private Object object;
  
  public DynamicProxy(Object object) {
  	this.object = object;
  }
  
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws
  Throwable {
    Object result = method.invoke(object, args);
    return result;
  }
}

创建动态代理的对象

Subject realSubject = new RealSubject();
DynamicProxy proxy = new DynamicProxy(realSubject);
ClassLoader classLoader = realSubject.getClass().getClassLoader();
Subject subject = (Subject) Proxy.newProxyInstance(classLoader, new Class[]
{Subject.class}, proxy);

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces,

InvocationHandler handler)方法的三个参数

  1. loader,指定代理对象的类加载器
  2. interfaces,代理对象需要实现的接口,可以同时指定多个接口
  3. handler,方法调用的实际处理者,代理对象的方法调用都会转发到这里

cglib动态代理

实现

public class HelloConcrete {
  public String sayHello(String str) {
  	return "HelloConcrete: " + str;
  }
}

// 1. 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
public class MyMethodInterceptor implements MethodInterceptor{
  ...
  @Override
    public Object intercept(Object obj, Method method, Object[] args,
    MethodProxy proxy) throws Throwable {
    logger.info("You said: " + Arrays.toString(args));
    return proxy.invokeSuper(obj, args);
  }
}

// 2. 然后在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());
HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));

通过CGLIB的Enhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是HelloConcrete的具体方法。

原理

CGLIB是一个强大的高性能的代码生成包,底层是通过使用一个小而快的字节码处理框架ASM,它可以在运行期扩展Java类与实现Java接口,Enhancer是CGLIB的字节码增强器,可以很方便的对类进行拓展

创建代理对象的步骤

  1. 生成代理类的二进制字节码文件
  2. 加载二进制字节码,生成Class对象
  3. 通过反射机制获得实例构造,并创建代理类对象

4.区别

  1. jdk动态代理:利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。只能对实现了接口的类生成代理
  2. cglib:利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法实现增强,但是因为采用的是继承,对于final类或方法,是无法继承的。

5.选择

  1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
  2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
  3. 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。

模板方法模式

意图

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。

优点

  1. 封装不变部分,扩展可变部分。
  2. 提取公共代码,便于维护。
  3. 行为由父类控制,子类实现。

缺点

每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

适配器模式

意图

将一个类的接口转换成客户希望的另外一个接口

优点

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

缺点

  1. 过多地使用适配器,会让系统非常零乱,不易整体进行把握。
  2. 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。

装饰器模式

意图

允许向一个现有的对象添加新的功能,同时又不改变其结构

优点

装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能

缺点

多层装饰比较复杂。

观察者模式

意图

一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

优点

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

缺点

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