一、六大设计原则
1.单一职责
一个类或者模块只负责完成一个职责;
2.里氏替换原则
继承中子类可以使用父类的功能,但不能替换父类的功能,若要替换建议使用组合,少用继承;
3.依赖倒置原则
下层模块引入上层模块的依赖;
4.接口隔离原则
建立单一接口,不要建立臃肿庞大的接口。接口尽量细化,同时接口中的方法尽量少;
5.迪米特法则/最少知识原则
只和你的密友谈话
一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂,那是你的事儿,和我没关系,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关系。
6.开闭原则
对扩展开放,对修改关闭
在已有的代码基础上进行扩展,而不是修改已有的代码
二、23种设计模式
1.创建型设计模式
1.1单例(Singleton)
确保一个类只有一个实例,并提供一个全局访问点
1.1.1饿汉式
在类中直接new一个对象,在需要使用时直接返回该对象,需要将构造方法私有化,饿汉式优势为可以确保线程安全,缺点为不能延时加载
/**
* @description 饿汉式(线程安全,调用效率高,但是不能延时加载)
* @author: muse
**/
public class SingletonDemo1 {
private static SingletonDemo1 instance = new SingletonDemo1();
private SingletonDemo1(){}
public static SingletonDemo1 getInstance(){
return instance;
}
}
1.1.2懒汉式
在类初始化时,不创建对象,在需要使用时进行判断,若创建过该对象则直接返回,若没有创建则先创建再返回,在创建过程中,会出现线程安全问题,所以在方法上加线程锁,确保线程安全,缺点为在方法上加锁会导致调用效率低
/**
* @description 懒汉式(线程安全,调用效率不高,但是能延时加载)
* @author: muse
**/
public class SingletonDemo2 {
// 类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo2 instance;
// 构造器私有化
private SingletonDemo2(){}
// 方法同步,调用效率低
public static synchronized SingletonDemo2 getInstance() {
if(instance == null) {
instance = new SingletonDemo2();
}
return instance;
}
}
1.1.3双重校验
为解决懒汉式线程安全需要在方法上加锁导致调用效率低的问题,所以使用双重校验的方式,先判断是否创建该对象,若没有创建则进行加锁再次判断后创建该对象进行返回,若已经创建则直接进行返回该对象,不用再次进行加锁判断,提升懒汉式的效率
/**
* @description 双重校验
* @author: muse
**/
public class SingletonDemo3 {
private volatile static SingletonDemo3 singletonDemo3;
private SingletonDemo3() {}
public static SingletonDemo3 newInstance() {
if (singletonDemo3 == null) {
synchronized (SingletonDemo3.class) {
if (singletonDemo3 == null) {
singletonDemo3 = new SingletonDemo3();
}
}
}
return singletonDemo3;
}
}
1.1.4静态内部类
由于双重校验代码比较臃肿,并需要多次判断,所以可以使用静态内部类进行单例对象的创建
- 问题:问什么这种内部静态类的方式,是线程安全的?
- 答:首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的方法。那么什么是方法?
这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的【静态成员的的赋值语句】和【静态代码块】组成的。JVM内部会保证一个类的
方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的方法,其他的线程
都要阻塞等待,直到这个线程执行完方法。然后执行完方法后,其他线程唤醒,但是不会再进入方法。也就是说同一个加载器下,一个类型只会初始化一次。
那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个代码,当我们执行getInstance方法的时候,会导致SingletonClassInstance
类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的代码也只会被执行一次,所以他只会有一个实例。
/**
* @description 静态内部类(线程安全,调用效率高,可以延时加载)
* @author: muse
**/
public class SingletonDemo4 {
private SingletonDemo4(){
System.out.println("SingletonDemo4");
}
/** 静态内部类 */
private static class SingletonClassInstance {
private static final SingletonDemo4 instance = new SingletonDemo4();
}
/** 只有在第一次调用时,才会被创建,可以认为是懒加载的升级版本 */
public static SingletonDemo4 getInstance(){
return SingletonClassInstance.instance;
}
public static void main(String[] args) {
System.out.println("args = " + Arrays.deepToString(args));
SingletonDemo4.getInstance();
SingletonDemo4.getInstance();
}
}
1.2原型(Prototype)
- 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。也就是说,这种不通过new关键字来产生一个对象,而是通过对象复制(Java中的clone或反序列化)来实现的模式,就叫做原型模式
- 由于是通过clone创建的对象,所以使用浅拷贝对于非基本类型的属性(如:List等)通过浅拷贝会存在修改原本对象的值,所以需要通过深拷贝,重写clone方法,new一个新的属性,而不是通过复制对象地址的方式来进行clone
1.3工厂方法(FactoryMethod)
- 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
- 通过抽象方法提供对象生成入口,可直接通过调用抽象方法的实现类来进行判断
1.4抽象工厂(AbstractFactory)
- 通过接口,来创建一组产品,将产品属性提取为接口,若新增属性内容则完善接口即可,增加可拓展性,可在不修改原代码的前提下进行功能的拓展
1.5建造者/生成器模式(Builder)
- 使用生成器模式,可以封装一个产品的构造过程,并允许按步骤构造产品。
- 将一个复杂对象的创建过程封装起来。允许对象通过多个步骤来创建,并且可以改变过程。向客户隐藏产品内部的表现。产品的实现可以被替换,因为客户只看到一个抽象接口。例如买车的选配,可根据自身需求进行选配,实现自定义的组装,而非所有人都一样
2.结构型设计模式
2.1代理(Proxy)
- 当我们想要对一个业务类进行某些横切性的增强时,例如:增加请求与响应的日志、增加权限校验、增加远程请求对象封装等等。我们可以采用代理模式去实现,而不需要修改原有的类。
- 代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。使用代理模式创建代理对象,让代理对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。SpringAOP中采用的JDK动态代理,就是最典型的例子。
2.2适配器(Adapter)
- 将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
2.3桥接(Bridge)
- 将抽象部分和实现部分,分离解耦,使得两者可以独立地变化。桥接模式通过将实现和抽象放在两个不同的类层次中而使它们可以独立变化。
- 为一个类包含另一个类的接口或方法,通过接口与抽象类进行解耦进行实现。抽象和实现可以独立扩展,不会影响到对方。
2.4装饰(Decorator)
- 动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更弹性的替代方案
- 适用于有比较多的排列组合甚至会重复多次组合的情况,若对每种组合都进行实现,类会多的爆炸,所以此类情况可以使用装饰方法,增加可扩展性。
2.5门面(Facade)
- 提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
- 通过对流程的封装,来简化使用,比如启动一辆汽车,需要启动发动机、中控、音响、空调等,作为驾驶员只需要按下车上的启动键即可,代码层面也是如此,对内部复杂的逻辑进行封装,可通过简单的门面来实现复杂的功能
2.6享元/蝇量模式(Flyweight)
- 享元模式是池技术的重要实现方式,使共享对象可以有效地支持大量的细粒度的对象。
- 比如在表单收集中,有大量的重复元素:男、女、地址等,可以通过key/value的形式进行存储,将某些会重复出现的属性,通过K/V等的方式进行映射存储。我们就可以采取将这些通用实例池化,然后直接去池里获取即可。
2.7组合(Composite)
- 将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
- 通过不同抽象类区分层级,组合出树型的上下级关系
3.行为型设计模式
3.1模板方法(Template Method)
- 在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
- 在骨架中对可变的内容进行抽象,如1、2、3、4步骤,可以在骨架中将2、4步骤进行抽象,下放到子类中进行实现,骨架即可理解为抽象类,将一些不变的内容写在抽象类中,需要变化的内容即可在子类中进行实现
3.2策略(Strategy)
- 定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
- 通过接口和接口的实现类组合为一个算法族,当需要需要调用或者切换某个算法时,将接口的实现类进行切换即可完成逻辑的变化,对其他业务没有影响并可快速完成逻辑更替。
3.3命令(Command)
- 将“请求”封装成命令对象,以便使用不同的请求、队列或日志来参数化其他对象。
- 例如:有一个四行两列的遥控器,将每行的左右分为开关按钮,设定开和关的命令,第一行左侧则为0的开按钮,右侧为0的关按钮,只需对开关方法传入对应下标即可完成命令
public class CommandTest {
public static void main(String[] args) {
// 创建遥控器和灯
RemoteController controller = new RemoteController();
Light light = new Light();
// 向遥控器中初始化指令
controller.addCommand(0, new LightOnCommand(light), new LightOffCommand(light));
controller.pushOnButton(0); // 按下遥控器中第1排的ON按钮
controller.pushOffButton(0); // 按下遥控器中第1排的OFF按钮
}
}
3.4职责链(Chain of Responsibility)
- 使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
- 该设计模式思想类似于链条,通过不同的职责将对象串起来并执行该对象需要处理的内容,当该对象处理完成之后,移交下一个对象直到最终该链条中没有对象为止。
3.5状态(State)
- 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
- 通过设置状态来区分在当前状态下需要做什么操作。
3.6观察者(Observer)
- 定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
- 观察者建造一个池,将需要被观察到对象存入池中统一操作,观察者可以对被观察的对象进行统一操作,也可从池中添加对象或删除对象。
3.7中介者(Mediator)
- 使用中介者模式来集中相关对象之间复杂的沟通和控制方式。
- 中介者通过逻辑处理将业务进行串联,降低各业务之间的耦合性
3.8迭代器(Iterator)
- 提供一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示
- 通过迭代器模式可以将多个不同的元素进行遍历,无需关心内部如何实现的迭代,只需传入多个元素的集合即可。
3.9访问者(Visitor)
- 表示一个作用于某个对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
3.10备忘录(Memento)
- 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态。当你需要让对象返回之前的状态时(例如:你的用户请求“撤销”),就使用备忘录模式。
3.11解释器(Interpreter)
- 提供如何定义语言的文法,以及对语言句子的解释方法,即解释器