创建者模式:
- 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式
结构型模式:
- 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式、
行为者模式:
- 模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式
面向对象有三大特征继承封装多态的同时,还具有这七大原则
OOP七大原则
一、开-闭原则
(Open-Closed Principle, OCP)
开闭原则是这七大设计原则中最常见、最基本的
开闭原则定义:软件实体对扩展是开放的,但对修改是关闭的。意思就是说在不修改软件实体的基础上去扩展其他功能。
开闭原则实例:
比如实现一个绘制图线的功能
设计方案如下图所示
用户类中直接调用画直线类,但是如果有一个新需求,要求我们画斜线或者曲线的话,这时就需要修改画直线类中的代码(使用switch,else if),这样就违背了开闭原则,于是我需要在不修改实体的基础上去扩展画斜线和曲线功能
重构后的代码设计方案如下图所示
这样我们在没有改动代码的情况下,而是添加代码的情况下完成功能扩展
二、里氏替换原则
(Liskov Substitution Principle,LSP)
是继承复用的基石,说白了就是继承与派生的规则
里氏替换原则核心:在软件系统中,一个可以接受父类对象的地方必然可以接受子类对象
里氏替换原则实例:某系统需要实现画直线功能,现在有DrawLineA和DrawLineB两种画图方式,在操作类中提供了这两种画图方法选择画直线
现在我需要改变或添加一种画直线方式来画直线,比如原先使用DrawLineA方式进行画直线现在更换为DrawLineB方式来画直线,如果直接修改操作类的代码就违背了开闭原则。现在使用里氏替换原则重构代码, 既可以方便了系统扩展,又遵循了开闭原则
重构后的方案图如下图所示
所以说里氏替换原则是实现开闭原则的重要方法之一
三、依赖倒置原则
(Dependence Inversion Principle)
依赖倒置也叫依赖注入、依赖倒转
要针对抽象层编程,不要针对具体类编程
依赖倒置原则核心:要依赖于抽象,不要依赖于具体的实现。
分开来说:(注:抽象:接口或抽象类;细节:具体实现;如果把模块层次关系比作基础关系的话:高层模块和底层模块对应于父类和子类)
一、高层模块不应该依赖底层模块,这两者应该依赖与其抽象
二、抽想不应该依赖细节
三、细节应依赖抽象
依赖倒置实例:
比如某系统可以从本地获取和服务器获取数据,后将数据可以为转化配置成XML文件和XLS文件
假设我们增加了数据源或者有新的转化格式,需要修改操作类里面的源代码,这样就违背了开闭原则
现在使用依赖倒置原则对其进行重构
重构方案图如下图所示
四、接口隔离原则
(Interface Segregation Principle, ISP)
使用多个专门接口来取代一个统一的接口,
一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口中。
接口隔离原则核心:不应该强迫客户端程序依赖他们 不需要的使用方法
接口隔离原则实例:
比如一个系统中有一个大的接口,这个接口包含了GameobjectA、GameobjecB、GameobjectC三个对象的接口
但是这三个对象中有些接口未必需要实现,现在使用接口隔离原则将接口隔离,这样又满足单一职责原则
重新构造的方案如下图所示
五、合成/聚合复用原则
(Composite/Aggregate Reuse Principle,CARP)
在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合或通过继承。
继承复用:实现简单,便于扩展。但是破坏系统的封装性。
组合复用:耦合性相对较低,选择性的调用成员对象的操作。可以再运行时动态运。.
合成复用核心:尽量使用对象组合而不是继承达到复用的目的
六、迪米特法则
(Law of Demeter LoD)
又叫做最少知识原则,一个软件实体对其他实体的引用越少越好,或者说如果两个类不必直接通信,那么这两个类就不应当发生直接的相互作用,而是通过一个第三者发生间接性的交互。
迪米特原则核心:高内聚,低耦合
低迷特原理实例:
比如某一系统有多个系统和多个数据源,他们之间的联系图如下:
这样比较杂乱,耦合性较高,使用迪米特原则后的构造方案如下图所示:
七、单一职责原则
(Simple responsibility pinciple SRP)
- 一个类只负责一个领域的内容,简而言之就是自己的类负责自己的事情,与别的类互不干涉
如果一个类具有多个职责,应该把这多个职责分离出去,再分别创建一些类去一一完成这些职责
换句话说就是一个类的职责要单一,不能将太多的职责放在一个类中
单一职责核心:高内聚、低耦合
如下图是一个类画线的实现:
但是功能太过集中,严重违背了单一职责原则,重构后如下图所示
单例模式(常用)
-
单例模式使⽤场景:
- 业务系统全局只需要⼀个对象实例,⽐如发号器、 redis 连接对象等
- Spring IOC容器中的 bean 默认就是单例
- spring boot 中的controller、service、dao层中通过 @autowire的依赖注⼊对象默认都是单例的
-
单例模式分类:
- 懒汉:就是所谓的懒加载,延迟创建对象,需要用的时候再创建对象
- 饿汉:与懒汉相反,提前创建对象
-
单例模式实现步骤:
- 私有化构造函数
- 提供获取单例的⽅法
懒汉式
懒汉式有以下⼏种实现⽅式:
/**
* @Description: 单例设计模式-懒汉式
*/
public class SingletonLazy {
// 当需要用到该实例的时候再创建实例对象
private static SingletonLazy instance;
/**
* 构造函数私有化
* 不能通过 new SingletonLazy() 的方式创建实例
*
* 当需要用到该实例的时候在加载
* 只能通过 SingletonLazy.getInstance() 这种方式获取实例
*/
private SingletonLazy() {
}
/**
* 单例对象的方法
*/
public void process() {
System.out.println("方法实例化成功!");
}
/**
* 方式一:
* <p>
* 对外暴露一个方法获取该类的对象
* <p>
* 缺点:线程不安全,多线程下存在安全问题
*
* @return
*/
public static SingletonLazy getInstance() {
if (instance == null) {
// 实例为null时候才创建
/**
* 线程安全问题:
* 当某一时刻,两个或多个线程同时判断到instance == null成立的时候
* 这些线程同时进入该if判断内部执行实例化
* 则会新建出不止一个SingletonLazy实例
*/
instance = new SingletonLazy();// 当需要的时候再进行实例化对象
}
return instance;
}
/**
* 方式二:
* 通过加synchronized锁 保证线程安全
*
* 采用synchronized 对方法加锁有很大的性能开销
* 因为当getInstance2()内部逻辑比较复杂的时候,在高并发条件下
* 没获取到加锁方法执行权的线程,都得等到这个方法内的复杂逻辑执行完后才能执行,等待浪费时间,效率比较低
*
* @return
*/
public static synchronized SingletonLazy getInstance2() {
if (instance == null) {
// 实例为null时候才创建
// 方法上加synchronized锁后可以保证线程安全
instance = new SingletonLazy();// 当需要的时候再进行实例化对象
}
return instance;
}
/**
* 方式三:
* 在getInstance3()方法内,针对局部需要加锁的代码块加锁,而不是给整个方法加锁
*
* 也存在缺陷:
* @return
*/
public static SingletonLazy getInstance3() {
if (instance == null) {
// 实例为null时候才创建
// 局部加锁后可以保证线程安全,效率较高
// 缺陷:假设线程A和线程B
synchronized (SingletonLazy.class){
// 当线程A获得锁的执行权的时候B等待 A执行new SingletonLazy();实例化
// 当A线程执行完毕后,B再获得执行权,这时候还是可以实例化该对象
instance = new SingletonLazy();// 当需要的时候再进行实例化对象
}
}
return instance;
}
}
懒汉实现+双重检查锁定+内存模型
对于上面方式三存在的缺陷,我们可以使用双重检查锁定的方式对其进行改进:
/**
* 方式三改进版本:
* 在getInstance3()方法内,针对局部需要加锁的代码块加锁,而不是给整个方法加锁
*
* DCL 双重检查锁定 (Double-Checked-Locking) 在多线程情况下保持高性能
*
* 这是否安全? instance = new SingletonLazy(); 并不是原子性操作
* jvm中 instance实例化内存模型流程如下:
* 1.分配空间给对象
* 2.在空间内创建对象
* 3.将对象赋值给instance引用
*
* 假如出现如下顺序错乱的情况:
* 线程的执行顺序为:1 -> 3 -> 2, 那么这时候会把值写回主内存
* 则,其他线程就会读取到instance的最新值,但是这个是不完全的对象
* (指令重排现象)
*
* @return
*/
public static SingletonLazy getInstance3plus() {
if (instance == null) {
// 实例为null时候才创建
// 局部加锁后可以保证线程安全,效率较高
// 假设线程A和线程B
synchronized (SingletonLazy.class){
// 第一重检查
// 当线程A获得锁的执行权的时候B等待 A执行new SingletonLazy();实例化
// 当A线程执行完毕后,B再获得执行权,这时候再判断instance == null是否成立
// 如果不成立,B线程无法 实例化SingletonLazy
if (instance == null){
// 第二重检查
instance = new SingletonLazy();// 当需要的时候再进行实例化对象
}
}
}
return instance;
}
再次升级方式三,来解决内存模型中的指令重排问题:
// 添加volatile 关键字,禁止实例化对象时,内存模型中出现指令重排现象
private static volatile SingletonLazy instance;
/**
* 方式三再次升级版本:
* 在getInstance3()方法内,针对局部需要加锁的代码块加锁,而不是给整个方法加锁
*
* DCL 双重检查锁定 (Double-Checked-Locking) 在多线程情况下保持高性能
*
* 解决指令重排问题——禁止指令重排
* @return
*/
public static SingletonLazy getInstance3plusplus() {
if (instance == null) {
// 实例为null时候才创建
// 局部加锁后可以保证线程安全,效率较高
// 假设线程A和线程B
synchronized (SingletonLazy.class){
// 第一重检查
// 当线程A获得锁的执行权的时候B等待 A执行new SingletonLazy();实例化
// 当A线程执行完毕后,B再获得执行权,这时候再判断instance == null是否成立
// 如果不成立,B线程无法 实例化SingletonLazy
if (instance == null){
// 第二重检查
instance = new SingletonLazy();// 当需要的时候再进行实例化对象
}
}
}
return instance;
}
懒汉式调用:
@Test
public void testSingletonLazy(){
SingletonLazy.getInstance().process();
}
什么是指令重排 ? 可以参考 https://blog.csdn.net/qq_51998352/article/details/117451728
饿汉式
/**
* @Description: 单例设计模式-饿汉式
*/
public class SingletonHungry {
// 当类加载的时候就直接实例化对象
private static SingletonHungry instance = new SingletonHungry();
private SingletonHungry(){
}
/**
* 单例对象的方法
*/
public void process() {
System.out.println("方法实例化成功!");
}
public static SingletonHungry getInstance(){
return instance;// 当类加载的时候就直接实例化对象
}
}
饿汉式调用:
@Test
public void testSingletonHungry(){
SingletonHungry.getInstance().process();
}
- 饿汉式单例模式,当类加载的时候就直接实例化对象,因此不需要考虑线程安全问题。
- 优点:实现简单,不需要考虑线程安全问题
- 缺点:不管有没有使用该对象实例,instance对象一直占用着这段内存
- 懒汉与饿汉式如何选择?
- 如果对象内存占用不大,且创建不复杂,直接使用饿汉的方式即可
其他情况均采用懒汉方式(优选)
- 如果对象内存占用不大,且创建不复杂,直接使用饿汉的方式即可
工厂模式(常用)
-
⼯⼚模式介绍:
- 它提供了⼀种创建对象的最佳⽅式,我们在创建对象时 不会对客户端暴露创建逻辑,并且是通过使⽤⼀个共同 的接⼝来指向新创建的对象。
-
例⼦:
- ⼯⼚⽣产电脑,除了A品牌、还可以⽣产B、C、D品牌 电脑;
- 业务开发中,⽀付很常⻅,⾥⾯有统⼀下单和⽀付接 ⼝,具体的⽀付实现可以微信、⽀付宝、银⾏卡等;
-
⼯⼚模式有 3 种不同的实现⽅式:
- 简单工厂模式:通过传⼊相关的类型来返回相应的类,这 种⽅式⽐较单 ⼀,可扩展性相对较差;
- 工厂方法模式:通过实现类实现相应的⽅法来决定相应 的返回结果,这种⽅式的可扩展性⽐较强;
- 抽象工厂模式:基于上述两种模式的拓展,且⽀持细化 产品;
-
应⽤场景:
- 解耦:分离职责,把复杂对象的创建和使⽤的过程分开
- 复⽤代码、降低维护成本:
- 如果对象创建复杂且多处需⽤到,如果每处都进⾏编写,则很多重复代码,如果业务逻辑发⽣了改 变,需⽤四处修改;
- 使⽤⼯⼚模式统⼀创建,则只要修改⼯⼚类即可, 降低成本;
简单工厂模式
-
简单⼯⼚模式(静态工厂)
- ⼜称静态⼯⼚⽅法, 可以根据参数的不同返回不同类的实例,专⻔定义⼀个类来负责创建其他类的实例,被创建的实例通常都具有共同的⽗类;
- 由于⼯⼚⽅法是静态⽅法,可通过类名直接调⽤,⽽且只需要传⼊简单的参数即可;
-
核⼼组成
- Factory:⼯⼚类,简单⼯⼚模式的核⼼,它负责实现 创建所有实例的内部逻辑
- IProduct:抽象产品类,简单⼯⼚模式所创建的所有对象的⽗类,描述所有实例所共有的公共接⼝
- Product:具体产品类,是简单⼯⼚模式的创建⽬标
-
实现步骤
- 创建抽象产品类,⾥⾯有产品的抽象⽅法,由具体的产 品类去实现
- 创建具体产品类,继承了他们的⽗类,并实现具体⽅法
- 创建工厂类,提供了⼀个静态⽅法
createXXX()
⽤来⽣产产品,只需要传⼊你想产品名称
-
优点:
- 将对象的创建和对象本身业务处理分离可以降低系统的 耦合度,使得两者修改起来都相对容易。
-
缺点:
- ⼯⼚类的职责相对过重,增加新的产品需要修改⼯⼚类的判断逻辑,这⼀点与开闭原则是相违背
- 开闭原则(Open Close Principle):对扩展开放,对 修改关闭,程序需要进⾏拓展的时候,不能去修改原有 的代码,实现⼀个热拔插的效果
- 将会增加系统中类的个数,在⼀定程度上增加了系统的复杂度和理解难度,不利于系统的扩展和维护,创建简单对象就不⽤模式
代码实现
创建IProduct 抽象产品接口——IPay
public interface IPay {
/**
* 统一下单
*/
void unifiedOrder();
}
创建Product具体产品类——AliPay/WeChatPay
//AliPay.java
//@Description: 支付宝支付具体实现类
public class AliPay implements IPay{
@Override
public void unifiedOrder() {
System.out.println("支付宝支付统一下单...");
}
}
//WeChatPay.java
//@Description: 微信支付具体实现类
public class WeChatPay implements IPay{
@Override
public void unifiedOrder() {
System.out.println("微信支付统一下单...");
}
}
创建Factory工厂类——SimplePayFactory
//@Description: 简单支付工厂类(静态工厂类)
public class SimplePayFactory {
/**
* 工厂创建方法:
* 根据参数返回对应的支付对象
*/
public static IPay createPay(String payType) {
if (payType == null) {
return null;
} else if (payType.equalsIgnoreCase("WECHAT_PAY")) {
return new WeChatPay();
} else if (payType.equalsIgnoreCase("ALI_PAY")) {
return new AliPay();
}
// 如果需要扩展,可以编写更多
return null;
}
}
测试使用简单支付工厂
@Test
public void testSimplePayFactory(){
IPay wechat_pay = SimplePayFactory.createPay("WECHAT_PAY");
IPay ali_pay = SimplePayFactory.createPay("ALI_PAY");
wechat_pay.unifiedOrder();
ali_pay.unifiedOrder();
}
// 输出结果:
// 微信支付统一下单...
// 支付宝支付统一下单...
上述就是工厂设计模式——简单工场(静态工厂的一个简单使用例子),那么我们来分析下其缺点与不足之处:
需求:
-
如果我需要额外再添加一个A银行的银行卡支付,那么就需要在
SimplePayFactory
类中添加响应的判断逻辑,比如再加一个if
判断,添加一个A银行支付的逻辑 -
而如果再需要一个B银行的银行卡支付,那么还需要再添加一个
if
判断 添加一个B银行支付的逻辑,依次加下去… -
那么这就违背了⼯⼚类要遵循的开闭原则(Open Close Principle)(对扩展开放,对修改关闭,程序需要进⾏拓展的时候,不能去修改原有的代码,实现⼀个热插拔的效果),这样就导致,每次扩展功能的时候都需要添加新的逻辑,并且需要对工厂类进行修改,如果是真实复杂的业务,这就增加了成本。
下面我们来看一下工厂方法模式是如何解决简单工厂模式的这一缺点
工厂方法模式
⼯⼚⽅法模式
-
⼜称⼯⼚模式,是对简单⼯⼚模式的进⼀步抽象化,其 好处是可以使系统在不修改原来代码的情况下引进新的 产品,即满⾜开闭原则
-
通过⼯⼚⽗类定义负责创建产品的公共接⼝,通过⼦类 来确定所需要创建的类型
-
相⽐简单⼯⼚⽽⾔,此种⽅法具有更多的可扩展性和复用性,同时也增强了代码的可读性
-
将类的实例化(具体产品的创建)延迟到⼯⼚类的⼦类 (具体⼯⼚)中完成,即由子类来决定应该实例化哪⼀ 个类
核⼼组成
-
IProduct:抽象产品接口,描述所有实例所共有的公共接⼝
-
Product:具体产品类,实现抽象产品类的接⼝,⼯⼚ 类创建对象,如果有多个需要定义多个
-
IFactory:抽象⼯⼚接口,描述具体⼯⼚的公共接⼝
-
Factory:具体⼯⼚类,实现创建产品类对象,实现抽 象⼯⼚类的接⼝,如果有多个需要定义多个
要实现工厂方法模式,只需要在原来的简单工厂模式基础上,做出改进,而之前我们创建的IPay
抽象产品接口和AliPay
WeChatPay
两个具体产品类不需要改动
首先创建IPayFactory
抽象⼯⼚接口:
//@Description: 抽象⼯⼚接口
public interface IPayFactory {
IPay getPay();
}
然后创建AliPayFactory
和WeChatFactory
两个具体⼯⼚类:
//@Description: 具体工厂类 AliPayFactory
public class AliPayFactory implements IPayFactory{
@Override
public IPay getPay() {
return new AliPay();
}
}
//@Description: 具体工厂类 WeChatFactory
public class WeChatFactory implements IPayFactory{
@Override
public IPay getPay() {
return new WeChatPay();
}
}
进行测试:
@Test
public void testMethodPayFactory(){
AliPayFactory aliPayFactory = new AliPayFactory();
IPay ali_pay = aliPayFactory.getPay();
ali_pay.unifiedOrder();// 输出