设计原则:
1、开闭原则:对扩展开放,对修改关闭。为了使程序的扩展性好,易于维护和升级,想要达到就使用接口和抽象类,即抽象化
2、里氏替换原则:开闭原则的补充,任何基类可以出现的地方,子类一定可以出现,LSP是继承复用的基石,对开闭原则的补充,是对实现抽象化的具体步骤的规范。
例:类B继承类A时,除了添加新的方法来实现新的功能外,应该尽量不要重写父类A的方法,也尽量不要重载父类A的方法。若重载,则入参要更宽松,返回要更严格。
3、依赖倒转原则:开闭原则的基础,直接对接口编程,依赖于抽象而不依赖具体,降低客户需求和实现的耦合。
4、接口隔离原则:使用多个隔离的接口,比使用单个接口要好,即降低类之间的耦合度。尽量细化接口,接口中的方法尽量少。
例一个接口只服务于一个子模块或业务逻辑,服务定制;
5.、迪米特法则(最少知道原则):一个实体应该尽量少的与其他实体相互作用,使得系统功能模块相对独立。
降低类与类间的耦合度
6、单一职责:一个类负责一项职责
例:用户属性接口和用户行为接口分离,只有逻辑足够简单时才可不遵循
7、合成复用原则:尽量是用合成/聚合的方式,不适用继承
设计模式三大类:
创建型设计模式:工厂、简单工厂、单例*、建造在、原型...
结构型设计模式:适配器、装饰器、代理、桥接、外观、组合、亨元...
行为型设计模式:策略、模板方法、观察者、访问中、责任链...
补充:并发型模式和线程池模式
1、普通工厂模式
建立一个工厂类,对实现同一接口的类进行实例的创建。
例:
1)创建共有接口A,内含接口方法F
2)创建2个实现类B.C,分别实现接口A并实现接口方法F
3)创建一个工厂类D,内有返回值为接口A的方法,方法内判断传入String参数equals B or C 来决定new B() or new C( );
2、抽象工厂模式
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以就有了该模式,可以直接通过新增工厂类来扩展程序
例:
1)创建共有接口A,内含接口方法F
2)创建2个实现类B.C,分别实现接口A并实现接口方法F
3)创建一个工厂共有接口,并创建2个工厂类实现接口并实现接口方法,方法内有返回值为接口A的方法,根据不同方法new B or C
3、单例模式
单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
单例模式一般特征:私有的构造器,私有静态的实例,public静态的get()方法
方法演进:
1.0版本:饿汉模式,直接私有的构造器+私有静态的实例(懒汉时直接new对象)+public静态的get()方法
优点:用类加载机制避免了多线程问题 缺点:即使该单例没被使用也被预选加载,造成内存浪费。
2.0版本:懒汉模式,延迟加载即用到该单例时才加载,加载时考虑多线程问题。若用synchronized锁住 get方法保证线程安全,则会锁住整个对象,会造成每次获取实例性能开销太大
3.0版本:事实上,只有在第一次创建对象的时候需要加锁,所以引入了双重检查锁定,只有在第一重检验实例为null时才用synchronized上锁,上锁后进行第二重检验实例是否为null来加载实例(因为其他线程最终也会获取到该锁所以进行二重检验)。看似解决了问题,但是在Java指令中创建对象和赋值操作是分开进行的,也就是说一个new操作时分两步进行的,在JVM里并不保证这两个操作的先后顺序(指令重排序)也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,如下图是在单线程情况下:
即在分配完对象内存空间后,设置实例指向该内存空间和初始化该对象是会被重排序的,这样的重排序在单线程环境下是不会有负面影响的,Q:为什么在单线程环境下2/4不会重排序?A:java内存模型的intra-thread semantics将确保2一定会排在4前面执行。
但是实际开发中,我们会接触和使用到多线程
即在并发环境下会出现如下错误↓,红色虚线为错误原因,即该对象并没有去初始化而线程B访问了该对象
在明白了发生错误的原因后,可以从两个角度去解决该问题:
1)禁止JVM指令重排序
2)允许JVM指令重排序,但是禁止其他线程知道该线程进行过重排序,即其他线程对重排序不可见
解决方案:
1)基于volatie的双重检查锁定的解决方案(在JDK5时使用新的内存模型规范,该规范增强了volatie的语义)
使用volatie关键字修饰私有静态实例
volatie会禁止2/3的指令重排序
这个方案是通过禁止指令重排序来达到线程安全的延迟初始化(懒汉模式)
即私有volatile静态实例+public静态get双重检查锁定
2)基于类初始化的解决方案
JVM在类的初始化阶段(即class被加载后,被线程使用之前),会执行类的初始化,在执行类初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对一个类的初始化,保证了线程安全。
基于这个特性,可以实现另一种解决方案:
即私有静态内部类(new一个public静态实例)+public静态get类(直接用内部类名调用返回内部类实例)
当两个线程并发执行get方法时,都会去尝试获取内部类class对象的初始化锁
Q:为什么会触发内部类的初始化
A:因为根据JAVA语言规范,声明的静态非常量字段被使用将触发类或接口的初始化
补充:JAVA语言规范还规定了触发初始化的情况,如当类的实例被创建时,类声明的静态方法被调用,类或接口的静态字段被复制,声明的静态非常量字段被使用,当类是顶级类时且断言语句嵌套在类中被执行时。
事实上,JVM会让试图获取锁的线程最终都获取到初始化锁,即当获取失败时会排队等待获取锁,为了确保该类被初始化过了。
这里即保证了其他线程B对获取到初始化锁的线程A的重排序不可见,即线程A进入类的初始化并将status设置为正在初始化,当线程B进入初始化队列时,会获取到A设置的status并乖乖等待,直到A初始化结束后将status设置为初始化结束释放锁并唤醒其他等待线程,到这里A的初始化过程完成,B才被唤醒并获取锁接着读取status为初始化结束,B直接释放锁,到这里线程B的初始化过程完成。(这里的status标记位虚构,JVM的具体实现为实现类似功能)
这里存在一个happen-before关系,即线程A执行类的初始化时的写入操作(执行类中静态初始化和初始化类中静态字段),线程B一定能看到。
两种解决方案对比总结:
延迟初始化降低了初始化类和创建实例的开销,但增加了访问被延迟初始化的字段的开销。在正常情况下,正常初始化要优于延迟初始化,若要对实例字段进行线程安全的延迟初始化则用volatile,若要对静态字段进行线程安全的延迟初始化则使用基于类初始化的方案。
4.0版本:枚举
public enum Singleton{
instance;
public void whateverMethod(){}
}
枚举实际上为类对象,依然使用的类加载机制保证线程安全
之前版本的弊端:
1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例(重写反序列化方法,直接获取单例对象)。
2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
4、适配器模式
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:
1)类的适配器模式
例:
目的:使Targetable目标接口的实现类具有待适配类Source类的功能。
实现:一个待适配的Source类(拥有方法A),目标接口Targetable(拥有抽象方法A和B),通过Adapte类(继承Source并实现接口Targetable,且实现方法B),将Source类的功能扩展到Targetable里。Targetable t = new Adapte(); 这个t拥有方法A和B。这样Targetable目标接口的实现类就具有了待适配Source类的功能。
2)对象的适配器模式
例:
目的:将一个对象转换成满足另一个新接口的对象
基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Source类,而是持有Source类的实例,以达到解决兼容性的问题。只需要修改Adapter类,Adapter类不继承Source类,而是构造Source的实例,通过入参为Source实例的构造方法构造Adapter类实例,然后重写Source类中的方法A,通过实例source.A()调用。
3)接口的适配器模式*
例:
目的:为了只实现接口中定义的部分抽象方法(默认必须全部实现),引入了接口的适配器模式。
实现:借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行
适配器模式使用场景:spring中AOP中有Adapte,即由于Advice链需要拦截器对象,所以为每一个Advisor中的advice都适配了拦截器对象。
5、装饰器模式
目的:给一个对象增加一些新的功能,而且是动态的(继承是静态的)。缺点:为创建过多相似对象
实现:要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。共有接口Sourceable(内有抽象方法A),被装饰类Source(实现了A)和装饰类Decorater(持有Source的实例且有入参为该实例的构造方法)。在装饰类Decorate重写方法A的具体实现过程中调用Soucer类实例source并调用source.A动态地添加功能。
private Sourceable source;
public Decorator(Sourceable source){
super();
this.source = source;
}
使用场景:JAVA中的IO流中装饰器如BufferedReader in = new BufferedReader(new FileReader("foo.in"));
其中BufferedReader为装饰类,FileReader为被装饰类。另:spring中对于不同数据库,DAO访问SessionFactory的applicationContext中配置的dataSource也用了装饰器模式。
6、代理模式
目的:相比于装饰器模式,更多的是对功能的控制,即专职专干。例如我们需要租房子时去找中介,需要打官司时去找律师,即找更熟悉的人帮你处理事情。
实现:实现和装饰器模式相似,区别在于装饰器通过被装饰类实例入参构造,代理类直接在构造函数中new出被代理对象。
private Source source;
public Proxy(){
super();
this.source = new Source();
}
使用场景:JDK动态代理。JDK提供基于接口的代理,不提供基于类的代理。即其对象必须是某个接口的实现,使用java.lang.reflect.Proxy类来根据被代理类生成代理对象,通过Proxy类静态方法newProxyInstance产生(通过为 Proxy 类指定 ClassLoader 对象和一组 interface创建)。以及需要实现java.lang.reflect.InvavationHandler接口的调用处理器,实现invoke方法调用原对象方法并在方法前后处理需要处理的业务逻辑。
7、模板方法
目的:在不改变模板结构的前提下在子类中重新定义模板中的内容,提高代码的重用性和实现反向控制。
实现:一个抽象类中,有一个主方法,再定义1…n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用被继承的抽象类的主方法,实现对子类的调用。即一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现,各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复;
优点:
- 实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 & 符合“开闭原则”
缺点:
引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。
8、策略模式
目的:定义一系列算法,将每个算法封装到具有公共接口的一系列策略类中,从而使它们可以相互替换 & 让算法可在不影响客户端的情况下发生变化,简单来说:准备一组算法 & 将每一个算法封装起来,让外部按需调用 & 使得互换,策略模式仅仅封装算法(包括添加 & 删除),但策略模式并不决定在何时使用何种算法,算法的选择由客户端来决定。将算法的责任和本身进行解耦,算法可独立于外部变化,客户端根据外部条件不同策略选择不同算法。
实现:一个抽象策略类(规范策略种类),一个具体策略类(策略的具体实现),一个环境角色(用于连接上下文,负责将具体策略传递给客户端,即当客户端传入A则该角色就把A的具体策略返回,客户端也用该角色实例进行接收返回值)
优点:策略类之间可以自由切换由于策略类都实现同一个接口,所以使它们之间可以自由切换。
易于扩展增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
缺点:客户端必须先知道所具有的策略类别,并自行决定使用哪一种策略。