开闭原则
对修改关闭,对扩展开放。
如何对扩展开放?
- 面向抽象编程,利用实现接口、继承类的方式来进行扩展。
- 在扩展的同时满足之前方法的可用。
依赖倒置原则
抽象不应该依赖于细节,细节应该依赖抽象。(针对接口编程,不要针对实现编程)
如何实现依赖倒置?
- 面向抽象编程。
单一职责原则
不要存在多于一个导致类变更的原因。(一个类或者一个方法,尽可能的只做一件事情)
如何实现单一职责?
- 拆分步骤,解耦。
接口隔离原则
用多个专门的接口,而不是使用单一的总接口,客户端不应该依赖他不需要的接口。
实现接口隔离应该注意:
- 一个类对一个类的依赖应该建立在最小的接口上。
- 建立单一的接口,不要建立庞大臃肿的解耦。(注意适度)
- 要细化接口,接口中的方法尽量少。
迪米特原则(最少知道原则)
一个对象应该对其他对象保持最少的了解。(只和朋友交流,不和陌生人说话)
怎么做?
- 如何做到其他的依赖(代理?委派?)
里氏替换原则
一个软件实体如果适用一个父类的话,那么一定适用于其子类,所有引用父类的地方必须能透明的使用其子类的对象,子类能够替换父类对象,而程序逻辑不变。
怎么做?
- 子类可以扩展父类功能,单不能改变父类原有功能。
- 子类可以实现父类的抽象方法,但是不能覆盖非抽象方法。
- 子类的方法实现父类的方法时(重载、重写、实现抽象),方法的入参要更宽松,出参要更严格。
合成复用原则
尽可能的使用对象组合,聚合而不是继承的关系来达到软件复用的目的。(组合:has-a。聚合:Contains-a。继承:is-a)
设计原则总结
学习了设计模式,才知道之前自己有多蠢,编出的代码又费劲又冗余,给后来的同时维护造成了很多麻烦。
1、不知道什么是开闭原则,来了新业务就新增一段逻辑代码,业务不段的适应市场调整逻辑和参数,自己就不断的修改代码,很傻很无聊。最后类越来越繁琐,自己都看不下去了,别人更头疼。
现在想想如果新业务对原先逻辑又依赖,应该写一个子类继承原来的类,在子类上扩展新业务的实现方法。
2、不知道什么是依赖倒置原则,一上来就想编码实现业务的具体要求,没有想过新业务和新业务之间、新业务和老业务之间的关联关系。结果写了很多重复的代码,以后业务模式变化了,每个类都要修改一遍。
现在想想应该在拿到需求之后,先找下业务的共同的,抽象出接口或者父类出来,各自子类中实现不同的业务逻辑,后面的维护就会变得容易很多,代码更容易阅读。
3、不知道什么是单一原则,某些主要的类文件越来越大,功能越来越杂,简直能包罗万象了。
现在想想应该不同业务不同功能的都单独编一个类或方法。
4、不知道什么是接口隔离原则,类似不知道单一原则,把所有不同方法都放在一个接口中,搞成了一个超级接口
现在想想接口也要分门别类,专门做同一类事情。
5、不知道什么是迪米特法则,一个类中引用了很多无用的jar包,入参和成员变量中引入了不需要的实例对象。
现在想想应该删除用不到的jar包,舍弃无关的引用。
6、不知道什么是里氏替换原则,用不好继承,惧怕用继承,结果代码写的很臃肿和初级,没一点逼格,编程功底一直无法再进一步,一直停留再初级阶段。
现在想想应该善用继承,把继承的规范用到极致,这样的代码会更灵活和健壮。
7、不知道什么是合成复用原则,就是因为之前6条法则做不好,没有做到细化类/接口/方法,到处都是紧耦合,搞得既没法用继承复用,更不能用组合或聚合的原则去编码。
工厂模式(创建型)
简单工厂(产品的工厂)
switch case/ if else / new Instance() 等手段来实现,后边扩展的越多内部越臃肿。
- 通过名称判断应该创建哪个产品。
- 根据传入的Class类型,直接通过反射调用构造函数生成对象实例。
工厂方法(工厂的工厂)
主要是为了解决 简单工厂 代码的臃肿问题。 不再由单一的工厂类生产产品,而事故由工厂类的子类实现具体产品的创建。因此 每增加一类产品,只需要增加一个相应工厂类的子类。
抽象工厂(复杂的工厂)
主要为了解决产品组,产品等级之间复杂的关系。一个产品可以有多个生产厂商,一个生产厂商可以生产多个产品。
单例模式(创建型)
- 私有化构造方法。
- 懒加载。
- 保证线程安全。
- 防止反射破坏单例。
- 防止序列化、反序列化破坏。
延伸: Spring是如何保证单例?如何保证安全?如何实现问题的? 容器式单例,如何保证线程安全,如何保证不被反射以及序列化。
Spring是容器式单例。
IOC 容器使用 ConcurrentHashMap,同时在操作IOC容器时也会使用synchronized加锁。
//存储注册信息的BeanDefinition
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
...
//注册的过程中需要线程同步,以保证数据的一致性
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
if (this.manualSingletonNames.contains(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);
updatedSingletons.remove(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
Spring 默认是懒加载?(getBean)。
Spring 单例能否被反射 序列化破坏取决于自己新建的类(Spring 本身不做控制)。
如何防止反射
首先要理解反射实例化对象也是基于构造函数实现的,那么如果我们要阻止反射,可以再构造函数中进行判断。如果实例不为null,直接返回实例对象,或者直接跑出异常。
// 防止反射
private Test(){
if(object != null){
throw new Exception();
}
}
如何防止序列化
在序列化反序列化过程中,会进行一个判断。判断类是否有readResolve()方法,有的话调用该方法,没有使用newInstance()。
所以
private volatile static Test object;
// 防止反序列化
private Object readResolve(){
return object;
}
饿汉式
- 通过static 变量,或者static 静态块赋值静态变量。
- 初始化就加载。效率高。
- 内存消耗大,不适合大量使用。
懒汉式
使用时再赋值。
节省了内存空间。
出现了线程不安全问题(两个线程同时创建对象,造成出现两个实例情况)。
Synchronized关键字给方法加锁解决。(锁的粒度比较大,效率不高,高并发使用会造成大量阻塞)。
-
减少锁的粒度。(在对象为null需要进行初始化时再去加锁,还是会出现线程安全问题)。
private volatile static Test object; private Test(){ } private Object getInstance(){ if(object == null){ synchronized(Test.class){ // 双重检查锁 if(object == null){ // 这里可能会发生 指令重排序问题 所以需要用volatile 关键字 object = new Test(); } } } return object; }
不安全问题可以使用双重检查锁解决。(双重检查锁,会出现指令重排序问题需要用 volatile关键字来保证有序性,不够优雅~)
静态内部类
利用java语法的特点来实现单例。Java静态属性、静态块、静态方法会在类初始化时就加载分配空间。而静态内部类则是在使用时候才会去加载分配内存。
- 写法优雅,利用了Java本身语法的特点,性能高,避免了内存浪费。
注册式(枚举式)
枚举 官方定义不允许反射创建 。枚举式单例与饿汉式单例一样,不适合大量使用。
容器式单例
将每一个实例都缓存在容器中,使用唯一标识获取。
ThreadLoacl
单线程下能够保证单例。
ThreadLocal底层也是一个map,key为当前的线程。
源码使用
Spring
-
AbstractFactoryBean
@Override public final T getObject() throws Exception { if (isSingleton()) { return (this.initialized ? this.singletonInstance : getEarlySingletonInstance()); } else { return createInstance(); } }
Mybatis
-
ErrorContext
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>(); private ErrorContext() { } public static ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; }
原型模式(创建型)
拷贝创建对象,不是基于构造函数。
- 类初始化消耗资源较多。
- new对象需要非常繁琐的过程(数据准备、访问权限)。
- 构造函数比较复杂。
- 循环中生产大量对象。
浅克隆
浅克隆在做的时候都是将引用地址传递过去,并非是值传递。cloneObjec.setAge(this.getAge());
深克隆
字节码(输入输出流)、JSON
建造者模式(创建型)
建造者模式注重于创建过程,不同的创建过程产生不同的结果。
通过一个Builder类来实现创建过程,形成链式结构。适用于复杂的创建过程对象使用。
Builder 也可以作为一个内部类存在。
源码中:StringBuilder、BeanDefinationBuilder、SqlSessionFactoryBuilder,
代理模式
静态代理
能够代理具体的对象。
动态代理
jdk动态代理
基于接口实现,需要传递目标对象。
- 代理类需要实现InvocationHandler接口。
- 目标类必须实现一个固定的接口。
- 实际是通过method.invoke()调用目标对象方法。
cglib动态代理
动态代理本质
super.h.invoke(); h 是什么?
public class Proxy implements Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler var1) {
Objects.requireNonNull(var1);
this.h = var1;
}
// ....
public static Object newProxyInstance(ClassLoader var0, Class<?>[] var1, InvocationHandler var2) throws IllegalArgumentException {
Objects.requireNonNull(var2);
try {
return var6.newInstance(var2);
} catch (InstantiationException | IllegalAccessException var8) {
} catch (InvocationTargetException var9) {
} catch (NoSuchMethodException var10) {
}
}
}
jdk动态代理,通过输入输出流,生成新的代理对象(class类),对象类中继承了Proxy类,对象中所有方法均调用Proxy类中InvocationHandler属性的invoke()方法。
适配器模式(结构型)
又叫变压器模式,他的功能是将一个类的接口变成客户端所期望的另一种接口,从而使的原本因接口不匹配而无法在一起工作的两个类能够一起工作。
类适配器
对象适配器
接口适配器
桥接模式(结构型)
享元模式(结构型)
享元模式主要为了提升程序性能,通过缓存来避免大量创建重复的对象。一种缓存池。
享元模式看主要是为了控制资源消耗。
例如:数据库连接池(创建连接需要尝试去连接数据库,比较耗时,可以使用享元模式直接缓存),线程池。
特点
享元模式一般配合工厂模式(单例),来去使用。通过工厂对象来获取、重置共享资源。
适用场景
常常应用于系统底层的开发,以便解决系统性能问题。
系统有大量相似的对象需要缓冲池的场景。
- 大量相似对象不代表经常new的对象都应该使用享元模式缓冲池。
- 比如订单,每天都要有很多的订单,每次都需要重新生成新的订单,那么订单适合应用享元模式么?
如果订单生成有一部分重复的耗时操作,建议使用享元模式来去避免,后续订单如何重置为初始状态,可以考虑克隆。
问题点
-
享元模式如何保证每次获取的都是初始化的对象(被拿走使用后会有一部分的赋值、修改操作)。
克隆后使用(一样消耗内存)?或者使用后重置?
享元模式还需要重点关注线程安全问题。
内部状态
一些内部属性,不需要随环境的变化而变化。如数据库连接池中连接地址,用户名、密码。
外部状态
同样是属性,但是会随着外部变化而变化,如连接池中连接是否在用,在被哪个线程使用。