设计模式
设计模式的主题是可维护性和可复用性
可维护性
理解、改正、适应、扩展
可复用性
重复使用的难易程度
七大原则
SOLID+C
S_单一职责 Single Responsibility Principle SRP
承担的越多, 复用的越少
解耦职责
一个类只负责一个功能; 一个模块只负责一个功能;一个函数只负责一个功能
- 主要指的是大部分的具体类s
- 抽象类和接口需要依照具体情况
不同的职责放到不同的类中
一个对象只包含单一职责
被完整的封装在一个类中
O_ 开闭原则 Open-Closed Principle OCP
为系统提供抽象层, 提高扩展性
要求尽量面向抽象类、接口进行编程
基于实体——软件模块、一个或多个类
- 对扩展开放
- 对修改关闭
如果对模块功能的拓展修改了源码,如果不考虑重构的情况,那么这就是违背开闭原则的
可变封装原则 Principle of Encapsulation of Variation EVP
- 将可变因素,比如变量,可变交互数据独立进行封装
L_ 里氏代换原则 Liskov Substitution Principle LSP
设计上:父类 实现上:子类替代父类——父类与子类的关系
- 引用基类,必然可以使用子类的对象
父类替换为子类,功能不受影响;
- 子类的所有方法在父类中声明
- 父类尽可能设置为抽象类、接口
I _依赖倒转原则 Dependence Inversion Principle DIP
(开闭原则的进阶原则)
抽象层指导编写
高层模块不依赖低层模块
高层模块依赖于抽象
抽象不依赖于细节
细节依赖抽象(先有抽象,再有细节)
依赖指的是父类与子类之间的依赖;具体类依赖抽象类,而不能反过来;
- 具体类只负责实现,而不进行扩充
常见概念
类间耦合
- 零耦合
- 具体耦合
- 抽象耦合
依赖注入
一个类的对象传入另一个类;注入时尽量传入父类对象;程序运行时通过子类覆盖父类;
- 构造注入
- 设值注入
- 接口注入
I _接口隔离原则 Interface Segregation Principle ISP
将大接口转化为小接口;每个接口承担一个独立的角色,也称为角色隔离原则;一个接口中只包含某一类用户定制的方法;
控制接口的粒度(职责、大小)
- 客户端不依赖其不需要的接口
私有的方法在类中实现,interface中的接口均为所需接口
如果不遵循,会看到不属于自己的方法。
定制服务:为不同的功能提供具体接口
D _迪米特法则 Law of Demeter LoD
限制实体之间的通信宽度和深度;控制访问权限;信息隐藏;降低类之间的耦合,不过会增加大量小方法;
尽量创建松耦合的类
降低成员变量 && 方法的访问权限
只要是类,尽量设计成不变类
只要可能,一个对象对其他对象的引用程度降到最低
适当中介,减少耦合
- 每个软件单位对其他的单位只有最少的认知
- 仅在与本单位相关联的软件单位
中介者模式
C _合成复用原则 Composite Reuse Principle CRP
使用组合/聚合而非继承;将一个类的对象作为另一个类的对象的一部分;
继承的问题
- 破坏系统封装性——白箱复用
- 继承的是静态,运行时也不改变——灵活性差
- 仅能在有限环境中使用——不能声明为final
组合的优势
- 已有对象纳入新对象——黑箱复用
- 运行时动态进行
继承是静态的, 组合是动态的
优先组合
而非继承
结合里氏替换,将extends的方法改为引用的私有属性对象
创造型
工厂方法——CLASS
抽象工厂
生成器 (Builder)
原型
单例
基本概念
单例模式属于设计模式中的创建型模式之一。
特点
使用单例模式的对象只有一个实例。 具体应用场景,比如QQ、Broswer这种单应用程序和插件、客户端,资源管理器等
实现方法
实现方法目前主流的有:
- 饿汉式单例
- 懒汉式单例
- 双检锁单例
- 内部静态类单例
之所以分为这么多类型,主要围绕的是两个方面的问题,一是线程安全,二是内存占用。
不同类型的实现方式有利有弊,主要会围绕其实现难度,锁,懒加载,性能开销这四个方面评估,适合使用场景的就是最好的。
饿汉(Eager Initialization)
- 无锁 - 类加载时就创建实例,不存在多线程同时访问的情况,避免了同步问题,但会产生内存浪费 public class Singleton{ private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance(){ return instance; } }
懒汉(Synchronized Lazy Initialization)
- 无锁,线程不安全 - 懒加载,显式调用时才会进行实例加载,避免内存浪费 public class Singleton{ private static Singleton instance; private Singleton(){} public static Singleton getInstance(){ if(instance == null) { instance = new Singleton(); return instance; }else { return instance; } } } - 有锁, 线程安全 - 懒加载 - 效率低,高并发不适用 public class Singleton{ private static Singleton instance; private Singleton(){} public static Singleton synchronized getInstance(){ if(instance == null) { instance = new Singleton(); return instance; }else { return instance; } } }
双检锁(Double-Checked Locking)
- 实例化检查+锁检查,线程安全,在多线程环境下避免加锁的性能开销,性能较高 - 具体性能高低取决于getInstance如何实现 public class Singleton{ /** * volatile关键字用于确保多线程环境下的可见性。 * volatile 是 Java 中的关键字,用于声明变量,确保多个线程能够正确地处理该变量。主要有两个作用: * 可见性(Visibility): 当一个线程修改了被 volatile 修饰的变量的值,变化会对其它线程立即可见。背后的实现机制锁会直接控制内存,而非缓存。 * 禁止指令重排序(Preventing Instruction Reordering): volatile 修饰的变量的读写操作不能被重排序,确保了在多线程环境下的正确执行顺序。 */ private static volatile Singleton instance; private Singleton(){} public static Singleton getInstance(){ // 第一次检查,若实例不存在则进入同步 if (instance == null) { // 第二次检查,进入锁检查。实现线程安全,确保只有一个实例创建 synchronized (Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return singleton; } }
登记式 / 静态内部类(Static Inner Class)
- 使用static inner class既实现了Lazy Init 又实现了Lazy Loading,与双检锁不同的是,双检锁会在类装载的同时进行初始化; 登记式将其分离,只有显式调用getIns时才会装载Holder并实例化ins,而不是Singleton加载时就完成了实例化。 - 通过final定义常量实现单例 - 通过内部类加载实现线程安全与延迟加载 /** * 对于内部类的加载,Java虚拟机会保证在同一时刻只有一个线程对类进行初始化。通过类加载器的锁(Class Initialization Lock)实现。类加载器锁是对类进行初 * 始化的唯一入口,使得多线程环境下只有一个线程能够操作类的初始化操作。类加载是由类加载器负责的,而类加载器在加载类时会采用一种单一的全局锁,锁是全局单 * 一的,所以不存在竞争问题。 */ public class Singleton{ private static class Holder{ private static final Singleton instance = new Singleton(); } private Singleton(){} public static final Singleton getInstance(){ return Holder.instance; } }