文章目录
- 为什么要学习设计模式
- 单例模式
- 哪些地方使用单例模式
- 懒汉模式和饿汉模式的区别
- 单例的特性
- 饿汉模式与懒汉模式的区别
- 工厂模式
- Spring 工厂模式创建Bean
- 为什么Spring IOC要使用工厂设计模式创建Bean呢
- 各个工厂模式的区别
- 简单工厂(一个工厂生产不同的具体产品)
- 工厂方法模式(不同的工厂生产不同的产品)
- 抽象工厂模式
- 代理模式
- 代理模式分类
- 静态代理
- 动态代理—JDK代理 (实现InvocationHandler接口,Proxy.newProxyInstance(classLoader, interfaces, jdkProxy))
- CGLIB动态代理 (实现MethodInterceptor重写intercept方法)
- 建造者模式
- 观察者模式
- 模板方法模式
- 适配器模式
- 策略模式
- 外观模式
为什么要学习设计模式
- 设计模式是前人大佬总结出来的一套代码设计的规范,好处就是代码可重用、容易理解、最主要的是代码复用很整齐,而不是东一个类西一个类。
- 懂设计模式才能能加容易看懂一些框架源码,更重要的是公司里的大佬写的项目可以接手且能看懂。追求优雅地编程。
单例模式
哪些地方使用单例模式
- 线程连接池
- 日志类、Controller默认单例
单例对象的类必须保证只有一个实例存在,整个系统只能使用一个对象实例,优点:不会频繁地创建和销毁对象,浪费系统资源。缺点是没有抽象层,难以扩展。
单例模式的常见写法:
-
饿汉式单例模式的写法:线程安全 ,顾名思义,类⼀加载就创建对象,这种⽅式⽐较常⽤,但容易产⽣垃圾对象,浪费内存空间。 优点:线程安全,没有加锁,执⾏效率较⾼缺点:不是懒加载(使⽤的时候再创建对象),类加载时就初始化,浪费内存空间。例如:游戏地图还没打开,地图实例已经加载导致内存拉满,手机卡顿。
public class Singleton {// 1、私有化构造⽅法,避免外界直接new对象private Singleton(){}// 2、定义⼀个静态变量指向⾃⼰类型,私有对象避免直接用类获取private final static Singleton instance = new Singleton();// 3、对外提供⼀个公共的⽅法获取实例public static Singleton getInstance() {return instance;} }
-
懒汉式单例模式的写法:非线程安全 ,优点:懒加载;缺点:线程不安全。
-
目前此种方式的单例确实满足了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成一堆人在抢厕所,就会造成多个同样的实例并存,从而没有达到单例的要求。
public class Singleton {// 1、私有化构造⽅法,不允许外部直接使用new创建private Singleton(){}// 2、定义⼀个静态变量指向⾃⼰类型private static Singleton instance;// 3、对外提供⼀个公共的⽅法获取实例public static Singleton getInstance(){//public后加上synchronized 就线程安全了,缺点效率低 // 判断为 null 的时候再创建对象if (instance == null) { //可能有多个线程同时访问到这行代码!instance = new Singleton();}return instance; }
-
-
双重检查锁(DCL,即 double-checked locking )单例模式的写法:优点:懒加载,线程安全,效率较高;缺点:实现较复杂 。关于内部的第二重空判断的作用,当多个线程⼀起到达锁位置时,进行锁竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null,会进⾏单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。
public class Singleton {// 1、私有化构造⽅法private Singleton() {}// 2、定义⼀个静态变量指向⾃⼰类型private volatile static Singleton instance;// 3、对外提供⼀个公共的⽅法获取实例public static Singleton getInstance() {// 第一重检查是否为 null ,如果存在就不需要同步。if (instance != null) {return instance;}// 使⽤ synchronized 加锁synchronized (Singleton.class) {// 第二重检查是否为 nullif (instance == null) {// new关键字创建对象不是原⼦操作,123步骤instance = new Singleton();}}return instance;} }
双重检查锁中使用 volatile 的两个重要特性:可见性、禁止指令重排序;这是因为 new 关键字创建对象不是原子操作,创建⼀个对象会经历下⾯的步骤:
- 在堆内存开辟内存空间
- 调用构造方法,初始化对象
- 引用变量指向堆内存空间
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序,创建对象的执行顺序可能为 123或者132。当我们在引用变量上面添加 volatile 关键字以后,会通过在创建对象指令的前后添加内存屏障来禁止指令重排序,就可以避免这个问题,而且对volatile 修饰的变量的修改对其他任何线程都是可见的。不加volatile 导致DCL失效问题:某个线程乱序运⾏ 1 3 2 指令的时候 ,引用变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 nulll ,导致错误使⽤了没有初始化的⾮ null 实例,这样的话就会出现异常。
第二次判断null的原因,当此时单例为null时,多个线程竞争锁去创建实例,其他线程阻塞;如果没有第二次判断null,当第一个线程创建完单例后其他线程拿到锁对象还会再次进入创建新的单例,这就重复创建了;其实,不进行第二次判断也不是不行,当多个线程同时竞争锁时,如果进入阻塞就立即快速失败(使用reenterLock的tryLock尝试强锁,抢不到就失败),保证只有一个线程执行即可不判断空;
-
枚举单例,优点:简单,⾼效,线程安全,可以避免通过反射破坏枚举单例 。
public enum Singleton {INSTANCE;public void doSomething(String str) {System.out.println(str);} } Singleton singleton = Singleton.INSTANCE;
懒汉模式和饿汉模式的区别
- 懒汉模式不线程安全,饿汉模式线程安全(DCL线程安全)
- 懒汉模式延迟加载,饿汉模式类加载时创建对象。
单例的特性
- 单例类必须自己创建自己的唯一实例
- 单例必须提供给其他对象获取这一实例的方法
饿汉模式与懒汉模式的区别
- 饿汉线程安全,懒汉线程不安全
- 饿汉类加载时加载,浪费内存,懒汉延迟加载
- DCL线程安全,懒加载,但是性能低
注意事项:如果使用反射创建单例,则会重新实例化一个新的对象,防止反射漏洞攻击的做法是使用flag判断;
工厂模式
它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。实现了创建者和调用者分离,工厂模式分为简单工厂、工厂方法、抽象工厂模式。工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式,我们只需要提供参数就能得到产品实例对象。利用工厂模式可以降低程序的耦合性,为后期的维护修改提供了很大的便利。
Spring 工厂模式创建Bean
在Spring IOC容器创建bean的过程是使用了工厂设计模式,当容器拿到了beanName和class类型后,动态的通过反射创建具体的某个对象,最后将创建的对象放到Map中。
为什么Spring IOC要使用工厂设计模式创建Bean呢
在实际开发中,如果我们A对象调用B,B调用C,C调用D的话我们程序的耦合性就会变高。为了避免这种情况,Spring使用工厂模式编程,写一个工厂,由工厂创建Bean,以后我们如果要对象就直接管工厂要就可以,剩下的事情不归我们管了。Spring IOC容器的工厂中有个静态的Map集合,是为了让工厂符合单例设计模式,即每个对象只生产一次,生产出对象后就存入到Map集合中,保证了实例不会重复影响程序效率。
各个工厂模式的区别
- 简单工厂:用来生产同一级结构中的任意产品,不支持拓展增加产品;一个工厂生产多种产品。
- 工厂方法:用来生产同一级结构中的固定产品,支持拓展增加产品;一个工厂生产一种产品;
- 抽象工厂:用来生成不同产品族的全部产品,不支持直接拓展增加产品,支持增加产品族;抽象工厂简单地说是工厂的工厂,抽象工厂可以创建具体工厂,由具体工厂来产生具体产品
简单工厂(一个工厂生产不同的具体产品)
简单工厂模式:简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建 。就是用工厂方法代替new操作的一种模式,可以给系统带来更大的可扩展性和尽量少的修改量(降低耦合)。要什么问工厂拿,而不自己new,如果工厂升级了只需要修改工厂。客户端不需要关注创建逻辑,只需提供传入工厂的参数 。缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则,且产品多的话会使工厂类比较复杂。 例如咖啡机!
- 优点:客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可 ;客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品; 实现了责任分割。
- 缺点:不易拓展,一旦添加新的产品类型,就不得不修改工厂的创建逻辑 。产品类型较多时,工厂的创建逻辑可能过于复杂,一旦出错可能造成所有产品的创建失败,不利于系统的维护
- 写产品接口 2. 实现接口写具体产品类 3. 实现工厂方法create为静态方法根据参数返回产品
//创建产品接口,目的是多态
public interface Car {public void run();
}
//实现接口,具体产品类
public class BMW implements Car{String name = "宝马";@Overridepublic void run() {System.out.println("马中赤兔,我是宝马!能跑180码");}
}
//实现接口,具体产品类
public class Aodi implements Car{String name = "奥迪";@Overridepublic void run() {System.out.println("人中吕布,我是奥迪,能跑200码");}
}
//静态方法根据不同参数生产不同产品
public class CarFactory {public static Car create(String name){if("".equals(name) || name==null){return null;}else if("宝马".equals(name)){return new BMW();} else if ("奥迪".equals(name)) {return new Aodi();}return null;}
}
工厂方法模式(不同的工厂生产不同的产品)
和简单工厂模式中工厂负责生产所有产品相比,工厂方法模式具体的产品工厂生产具体的产品只需要定义⼀个抽象工厂,其定义了产品的生产接口,但不负责具体的产品,将生产任务交给不同的派生类工厂,这样不用通过指定类型来创建对象了。
简单工厂模式是一个工厂类,生产各种产品;工厂方法模式把 CarFactory 作为了一个接口,并各个汽车有各自的工厂都是实现CarFactory 接口。然后需要生产什么产品直接实例化对应的工厂来生产。
//创建产品接口,目的是多态
public interface Car {public void run();
}
//实现接口,具体产品类
public class BMW implements Car{String name = "宝马";@Overridepublic void run() {System.out.println("马中赤兔,我是宝马!能跑180码");}
}
//实现接口,具体产品类
public class Aodi implements Car{String name = "奥迪";@Overridepublic void run() {System.out.println("人中吕布,我是奥迪,能跑200码");}
}
// 汽车工厂接口
public interface CarFactory {public Car createCar();
}
// 具体的产品工厂
public class BMWFactory implements CarFactory{@Overridepublic Car createCar() {return new BMW();}
}
// 具体的产品工厂
public class AodiFactory implements CarFactory{@Overridepublic Car createCar() {return new Aodi();}
}
// 使用具体的产品工厂来生产产品
public static void main(String[] args) {BMWFactory bmwFactory = new BMWFactory();Car car = bmwFactory.createCar();car.run();
}
抽象工厂模式
简单工厂模式和工厂方法模式不管工厂怎么拆分抽象,都只是针对⼀类产品,如果要生成另⼀种产品,就比较难办了!抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让子类去做决定 ,以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。
代理模式
通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。(也就是AO的P微实现)。代理在原有代码乃至原业务流程都不修改的情况下,直接在业务流程中切入新代码,增加新功能,这也和Spring的(面向切面编程)很相似 。代理模式场景:Spring AOP、日志打印、异常处理、事务控制、权限控制等 。
代理模式分类
- 静态代理:简单代理模式,是动态代理的理论基础。常见使用在代理模式
- jdk动态代理:使用反射完成代理。需要有顶层接口才能使用,常见是mybatis的mapper文件是代理。
- cglib动态代理:也是使用反射完成代理,可以直接代理类(jdk动态代理不行),使用字节码技术,不能对 final类进行继承。(需要导入jar包)
静态代理
所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了 。也就是说先继承以前的类,然后把以前的类的对象作为参数传进去,然后在这个对象前面后面进行一些操作。也就是说 代理对象是主动送来的,而不是通过反射被动拿到的。
public class UserDao{public void save() {System.out.println("保存数据方法");}
}//代理类
public class UserDaoProxy extends UserDao {private UserDao userDao;public UserDaoProxy(UserDao userDao) {this.userDao = userDao;} public void save() {System.out.println("开启事物...");userDao.save();System.out.println("关闭事物...");}
}
动态代理—JDK代理 (实现InvocationHandler接口,Proxy.newProxyInstance(classLoader, interfaces, jdkProxy))
通过实现InvocationHandler接口,重写invoke方法来动态获取对象并调用对象的方法,如下:
public class JDKProxy implements InvocationHandler {// 这其实业务实现类对象,用来调用具体的业务方法private Object target;// 通过构造函数传入目标对象public JDKProxy(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("前置增强");//下面invoke()方法是以反射的方式来创建对象,第一个参数是要创建的对象,第二个是构成方法的参数,由第二个参数来决定创建对象使用哪个构造方法Object result = method.invoke(target, args);System.out.println("后置增强");return result;}
}
使用代理,调用 Proxy.newProxyInstance(classLoader, interfaces, jdkProxy);
传入目标对象的类加载器、接口数组、和代理对象即可实例化代理对象。
public static void main(String[] args) {// 获取被代理对象UserDaoImpl userDaoImpl = new UserDaoImpl();JDKProxy jdkProxy = new JDKProxy(userDaoImpl);// 获取类加载器,接口对象ClassLoader classLoader = userDaoImpl.getClass().getClassLoader();Class<?>[] interfaces = userDaoImpl.getClass().getInterfaces();// 主要装载器、一组接口及调用处理动态代理实例UserDao newProxyInstance = (UserDao) Proxy.newProxyInstance(classLoader, interfaces, jdkProxy);newProxyInstance.save();}
CGLIB动态代理 (实现MethodInterceptor重写intercept方法)
CGLIB动态代理和jdk代理一样,使用反射完成代理,不同的是他可以直接代理类(jdk动态代理不行,他必须目标业务类必须实现接口),CGLIB动态代理底层使用字节码技术,CGLIB动态代理不能对 final类进行继承。(CGLIB动态代理需要导入jar包)
//代理主要类
public class CglibProxy implements MethodInterceptor {private Object targetObject;// 这里的目标类型为Object,则可以接受任意一种参数作为被代理类,实现了动态代理public Object getInstance(Object target) {// 设置需要创建子类的类this.targetObject = target; }//代理实际方法public Object intercept(Object obj, Method method, Object[] args,MethodProxy proxy) throws Throwable {System.out.println("开启事物");Object result = proxy.invoke(targetObject, args);System.out.println("关闭事物");// 返回代理对象return result;}
}public static void main(String[] args) {CglibProxy cglibProxy = new CglibProxy();UserDao userDao = (UserDao) cglibProxy.getInstance(new UserDaoImpl());userDao.save();
}
建造者模式
可以理解为分步骤创建一个复杂的对象,允许使用相同的创建代码生成不同类型和形式的对象。例如lombk中的build来设置对象属性。主要有四个模块:
- product,产品类,具体需要生成的类对象
- builder,建造者接口,给产品对象设置属性
- concreteBuilder,具体的建造者类,实现接口类中的所有方法,并返回一个组建好的对象。
- director,一般不用这个角色,调用具体建造者来创建复杂对象的各个部分。
与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。JAVA 中的 StringBuilder就是建造者模式创建的,他把一个单个字符的char数组组合起来。
观察者模式
观察者模式又叫做发布-订阅(Publish/Subscribe)模式 。观察者模式主要用于处理对象间的⼀对多的关系,是⼀种对象行为模式。该模式的实际应用场景比较容易确认,当⼀个对象状态发⽣变化时,所有该对象的关注者均能收到状态变化通知,以进行相应的处理。
优点:被观察者和观察者之间是抽象耦合的 ;被观察者无需关心他的观察者 ;支持广播通信;
缺点:观察者只知道被观察对象发生了变化,但不知变化的过程和缘由 ;被观察者也可能是观察者,如果观察者和被观察者之间产生循环依赖,或者消息传递链路形成闭环,会导致无限循环。
示例:支付场景:用户购买⼀件商品,当支付成功之后三方会回调自身,在这个时候系统可能会有很多需要执行的逻辑(如:更新订单状态,发送邮件通知,赠送礼品…),这些逻辑之间并没有强耦合,因此天然适合使用观察者模式去实现这些功能,
模板方法模式
模板方法模式是指定义一个算法骨架,将具体内容延迟到子类去实现。 Spring 中 jdbcTemplate 、 hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使⽤到了模板模式。 例如:喝茶分为三步:烧水、放入茶叶泡茶、品茶;那么烧水和品茶相同代码可以父类实现,而具体泡什么茶交给子类实现。
public abstract class TeaTemplate {public void wash() {System.out.println("专业洗茶壶18年,洗刷刷!");}abstract void make();public void drink() {System.out.println("品茶,一茶品春秋!");}
}public class RedTea extends TeaTemplate{@Overridevoid make() {System.out.println("找红茶...");System.out.println("红茶只煮15分钟...");}
}public static void main(String[] args) {RedTea redTea = new RedTea();redTea.wash();redTea.make();redTea.drink();
}
优点:提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中; 实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制并且符合开闭原则
适配器模式
在我们的应用程序中我们可能需要将两个不同接口的类来进行通信,在不修改这两个的前提下我们可能会需要某个中间件来完成这个衔接的过程。这个中间件就是适配器。所谓适配器模式就是将⼀个类的接口,转换成客户期望的另⼀个接口:有点像欧标充电器适配器。它可以让原本两个不兼容的接口能够无缝完成对接。通过类继承实现适配,继承 Target 的接口,继承 Adaptee 的实现。
Target:定义 Client 真正需要使⽤的接口;Adaptee: 其中定义了⼀个已经存在的接口,也是我们需要进行适配的接口。Adapter: 对 Adaptee 和 Target 的接口进⾏适配,保证对 target 中接口的调⽤可以间接转换为对 Adaptee 中接口进行调用。
优点:组合若⼲关联对象形成对外提供统⼀服务的接口;提高了类的复用(不用再造新类);
缺点:过多使⽤适配模式容易造成代码功能和逻辑意义的混淆。
策略模式
定义了一系列的算法 或 逻辑 或 相同意义的操作,并将每一个算法、逻辑、操作封装起来,而且使它们还可以相互替换。我觉得主要是为了 简化 if…else 所带来的复杂和难以维护。 针对一组算法或逻辑,将每一个算法或逻辑封装到具有共同接口的独立的类中,从而使得它们之间可以相互替换。
- 优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性非常良好。
- 缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露
public abstract class PayStrategy {abstract void algorithmInterface();
}public class AlipayStrategy extends PayStrategy{@Overridevoid algorithmInterface() {System.out.println("支付宝支付");}
}public class BankStrategy extends PayStrategy{@Overridevoid algorithmInterface() {System.out.println("银联支付");}
}public class WepayStrategy extends PayStrategy {@Overridevoid algorithmInterface() {System.out.println("微信支付");}
}public class Context {PayStrategy payStrategy;public Context(PayStrategy payStrategy){this.payStrategy = payStrategy;}public void pay(){payStrategy.algorithmInterface();}
}public static void main(String[] args) {AlipayStrategy alipayStrategy = new AlipayStrategy();Context context = new Context(alipayStrategy);context.pay();
}
外观模式
外观模式:也叫门面模式,隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。它向现有的系统添加一个接口,用这一个接口来隐藏实际的系统的复杂性。使用外观模式,他外部看起来就是一个接口,其实他的内部有很多复杂的接口已经被实现。
例如:用户注册完之后,依次需要调用阿里短信接口、邮件接口、微信推送接口。
public static void main(String[] args) {//普通模式需要这样AliSmsService aliSmsService = new AliSmsServiceImpl();EamilSmsService eamilSmsService = new EamilSmsServiceImpl();WeiXinSmsService weiXinSmsService = new WeiXinSmsServiceImpl();aliSmsService.sendSms();eamilSmsService.sendSms();weiXinSmsService.sendSms();//利用外观模式简化方法new Computer().sendMsg();
}public class Computer {AliSmsService aliSmsService;EamilSmsService eamilSmsService;WeiXinSmsService weiXinSmsService;public Computer() {aliSmsService = new AliSmsServiceImpl();eamilSmsService = new EamilSmsServiceImpl();weiXinSmsService = new WeiXinSmsServiceImpl();} //只需要调用它public void sendMsg() {aliSmsService.sendSms();eamilSmsService.sendSms();weiXinSmsService.sendSms();}
}