设计模式
一、设计模式六大原则
最终目的:抽象构建框架、实现扩展细节。(高内聚低耦合)
- 单一职责原则
- 里氏替换原则
- 接口隔离原则
- 迪米特法则
- 依赖倒置原则
- 开闭原则
讲解:https://blog.csdn.net/qq_33507618/article/details/111866527?spm=1001.2014.3001.5501
二、不使用工厂模式
根据构造方法来创建类,耦合度较高,不易扩展。
三、简单工厂模式
分为工厂类、抽象产品类(抽象类或者接口)、具体产品类。
工厂类使用ifelse或者switch来创建不同的产品类。
四、工厂方法模式
分为抽象工厂类(抽象类或者接口)、具体工厂类、抽象产品类(抽象类或者接口)、具体产品类。
具体工厂类去实例化相应的产品类。
当有新的产品加入进来时,只需要创建新的具体工厂类和相应的具体产品类来重写或实现抽象工厂类和抽象产品类,不需要去修改之前的代码,易扩展。
五、策略模式
分为封装角色、抽象策略角色(抽象类或者接口)、具体策略角色。
策略模式的优点:
- 策略模式提供管理相关算法族的方法。策略类的等级结构定义了一个算法或者行为族。恰当使用继承可以把公共代码一刀父类中,避免代码重复。
- 策略模式可以避免多重ifelse或者switch的判断语句。
策略模式的缺点:
- 调用时必须先知道有哪些具体的策略。意味着调用者必须知道所有策略的区别,从而选择 其中一个合适的策略。
- 由于把每个具体的策略实现都单独封装成类,如果备选策略较多的话,那么对象的数目就会很可观。
六、单例模式
单例对象的类必须保证只有⼀个实例存在。
适⽤场景:
单例模式只允许创建⼀个对象,因此节省内存,加快对象访问速度,因此对象需要被公⽤的场合适合使⽤,如多个模块使⽤同⼀个数据源连接对象等等。如:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但⼜经常⽤到的对象。
- 有状态的⼯具类对象。
- 频繁访问数据库或⽂件的对象。
单例模式分以下几种: 饿汉式、懒汉式、双重检查锁。
无状态的对象才能进行单例模式设计。
- 有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数 据,是⾮线程安全的。在不同⽅法调⽤间不保留任何状态。
- ⽆状态就是⼀次操作,不能保存数据。⽆状态对象(Stateless Bean),就是没有实例变量的对象 . 不能保存数据,是不变类,是线程安全的。
1、饿汉式:
优点:没有线程安全问题,简单。
缺点:
- 提前初始化会延⻓类加载器加载类的时间;
- 如果不使⽤会浪费内存空间;
- 不能传递参数。
- 使用了静态变量,静态变量和静态代码块是类加载的时候执行。
/**
* @ClassName SingletonHungry
* @Description 饿汉式单例模式
* @Author 张龙飞
* @Date 2021/2/7 13:18
*/
public class SingletonHungry {
private static final SingletonHungry instance = new SingletonHungry();
private SingletonHungry() {
}
public static SingletonHungry getInstance() {
return instance;
}
}
2、懒汉式:
线程不安全的懒汉式写法
/**
* @ClassName SingletonLazyUnSafety
* @Description 线程不安全的懒汉式单例
* @Author 张龙飞
* @Date 2021/2/7 13:31
*/
public class SingletonLazyUnSafety {
private static SingletonLazyUnSafety singletonLazyUnSafety;
private SingletonLazyUnSafety() {
}
public static SingletonLazyUnSafety getInstance() {
if (null == singletonLazyUnSafety) {
singletonLazyUnSafety = new SingletonLazyUnSafety();
}
return singletonLazyUnSafety;
}
}
线程安全的懒汉式单例写法(静态内部类的单例写法)
优点:解决线程安全,延迟初始化(Effective Java推荐写法)。
/**
* @ClassName SingletonLazySafety
* @Description 线程安全的懒汉式单例
* @Author 张龙飞
* @Date 2021/2/7 13:33
*/
public class SingletonLazySafety {
private SingletonLazySafety() {
}
public static SingletonLazySafety getInstance() {
return Holder.SINGLE_TON;
}
public static class Holder {
private static final SingletonLazySafety SINGLE_TON = new SingletonLazySafety();
}
}
3、双检锁:
除了静态内部类实现线程安全的懒汉式外,还可以使用双重检查锁来保证单例的线程安全。
/**
* @ClassName SingletonDouble
* @Description 双重检查锁单例
* @Author 张龙飞
* @Date 2021/2/7 13:38
*/
public class SingletonDouble {
// volatile只能保证可见性,不能保证线程安全;
private volatile static SingletonDouble singletonDouble;
// 不对外提供构造方法
private SingletonDouble() {
}
public static SingletonDouble getInstance() {
// 检查是否有引用指向对象(高并发情况下会有多个线程同时进入);
// 摒除大多数的处理,避免每次调用都加锁实例化,从而导致效率降低。
if (null == singletonDouble) {
synchronized (SingletonDouble.class) {
/*
避免多线程时,同时获取到锁而创建了多个对象。
(防止线程A执行完检查一后,线程调度到线程B,
这时候线程B可以创建完实例并释放锁。
再切回线程A时,加完锁还需要再判断一次。)
*/
if (null == singletonDouble) {
/*
实例化对象其实进行了三个步骤:(volatile禁止了指令重排序)
1. 分配内存空间
2. 初始化对象
3. 将对象指向刚分配的内存空间
*/
singletonDouble = new SingletonDouble();
}
}
}
return singletonDouble;
}
}
volatile的作用
在执行程序的时候,为了提高性能,处理器和编译器常常会对指令进行重排序,但是不能随意排序,重排序满足以下两个条件:
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序。
使用了volatile关键字后,禁止重排序,所有的写操作都将发生在读操作之前。
4、单例模式的破坏
为什么反射会破坏单例?
因为反射时,获取到构造器类后,可以通过构造器类强行设置可获取私有成员,便可以通过单例模式代码中的private修饰的构造函数进行初始化。
Class clazz = (Class) Class.forName(“com.learn.example.Singleton”);
Constructor < Singleton > c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 强行设置可访问private修饰的属性及方法,跳过权限检查。
Singleton sc3 = c.newInstance();
七、命令模式
分为抽象命令类、具体命令类、调用者类、接受者类
命令模式的优点
- 降低耦合度;
- 新的命令可以很容易的加入到系统中;
- 可以比较容易的设计一个命令队列和宏命令(组合命令);
- 可以方便的实现队请求的undo和redo;
命令模式的缺点
- 使⽤命令模式可能会导致某些系统有过多的具体命令类。因为针对每⼀个命令都需要设计⼀个具体命 令类, 因此某些系统可能需要⼤量具体命令类,这将影响命令模式的使⽤。
八、代理模式
1、静态代理
分为抽象角色、真实角色、代理角色(引用真实角色,在调用真实角色前后增强代码)。
优点:可以做到在不修改目标对象功能的基础上,对目标对象进行扩展。
缺点:
- 每一个代理类都要实现一遍目标类的接口,如果目标类增加了一个接口,那么代理类也必须跟着添加。
- 每一个代理类对应一个目标类对象,如果要代理的类对象比较多,那么代理类也会与很多,代码会比较臃肿。
2、jdk动态代理
通过反射实现,能有效解决静态代理中代理类过多而代码臃肿的问题。
利⽤拦截器(拦截器必须实现InvocationHanlder)加上反射机制⽣成⼀个实现代理接⼝的匿名类,在调⽤具体⽅法前调⽤InvokeHandler来处理。只能对实现了接⼝的类⽣成代理只能对实现了接⼝的类⽣成代理。
- 动态代理类实现InvocationHandler接⼝,并重写该invoke⽅法。
Student student = (Student) Proxy.newProxyInstance(classLoader,
new Class[]{Student.class,
proxy);
- 代理的类必须实现一个接口。
源码判断如下:
// 因为生成的代理类的父类是Proxy类,通过jdk代理生成的类都继承Proxy类。
// 因为Java是单继承的,
// 而代理类又必须继承自Proxy类,所以通过jdk代理的类必须实现接口。
//确认代理的Class是接⼝,从这可看出JDK动态代理的劣势
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
3、cglib动态代理
利⽤ASM开源包,对代理对象类的class⽂件加载进来,通过修改其字节码⽣成⼦类来处理。主 要是对指定的类⽣成⼀个⼦类,覆盖其中的⽅法,并覆盖其中⽅法实现增强,但是因为采⽤的是继承, 对于final类或⽅法,是⽆法继承的。
- 动态代理类实现MethodInterceptor接口
- final修饰的类、方法不能使用cglib代理;
总结:只有实现类接口的类才能使用jdk动态代理。