单例设计模式
- 单例设计模式:
- 目的是保证一个类只能有一个实例,而且自行实例化并向整个系统提供这个实例,避免频繁创建对象,节约内存。
- 单例意思只包含一个对象被称为单例的特殊类
- 使用场景
- 业务系统全局只需要一个对象实例,比如发号器、redis连接对象等
- Spring IOC容器中的bean默认就是单例
- spring boot 中的controller、service、dao层中通过@autowire的依赖注入对象默认都是单例的
- 分类:
- 懒汉:就是所谓的懒加载,延迟创建对象
- 饿汉:与懒汉相反,提前创建对象
- 实现步骤
- 私有化构造函数
- 提供获取单例的方法
饿汉/懒汉选择
- 饿汉方式:提前创建好对象
- 优点:实现简单,没有多线程同步问题
- 缺点:不管有没使用,instance对象一直占着这段内存
- 如何选择:
- 如果对象不大,且创建不复杂,直接用饿汉的方式即可
- 其他情况则采用懒汉实现方式
懒汉式:
/**
* 构造函数私有化
*/
private SingletonLazy() {
};
/**
* volatile 是java提供的关键字,可以禁止指令重排
* DCL 双重检查锁定(Double-Check-Locking) 可以在多线程情况下保持高性能
*/
private static volatile SingletonLazy instance;
public static SingletonLazy getInstance() {
// 第一重检查
if (instance == null) {
// A、B,锁定
synchronized (SingletonLazy.class) {
// 第二重检查
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
饿汉式
private SingletonHungry(){};
private static SingletonHungry instance = new SingletonHungry();
public static SingletonHungry getInstance(){
return instance;
}
工厂模式
- 它提供了一种创建对象的最佳方式,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象
工厂模式有 3 种不同的实现方式
- 简单工厂模式:通过传入相关的类型来返回相应的类,这种方式比较单 一,可扩展性相对较差;
- 工厂方法模式:通过实现类实现相应的方法来决定相应的返回结果,这种方式的可扩展性比较强;
- 抽象工厂模式:基于上述两种模式的拓展,且支持细化产品
应用场景
- 解耦:分离职责,把复杂对象的创建和使用的过程分开
- 复用代码 降低维护成本:
- 如果对象创建复杂且多处需用到,如果每处都进行编写,则很多重复代码,如果业务逻辑发生了改变,需用四处修改;
- 使用工厂模式统一创建,则只要修改工厂类即可,降低成本
实现步骤
- 创建抽象产品类,里面有产品的抽象方法,由具体的产品类去实现
- 创建具体产品类,继承了他们的父类,并实现具体方法
- 创建工厂类,提供了一个静态方法createXXX用来生产产品,只需要传入你想产品名称
优点
- 将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。
缺点
- 工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背
- 即开闭原则(Open Close Principle)对扩展开放,对修改关闭,程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果
- 将会增加系统中类的个数,在一定程度上增加了系统的复杂度和理解难度,不利于系统的扩展和维护,创建简单对象就不用模式
代码实现
// 1.创建抽象产品类(这里就用一个支付接口作示例)
public interface Pay {
/**
* 统一下单
*/
void unifiedOrder();
}
// 2.实现具体方法
public class ALiPay implements Pay{
/**
* 阿里统一下单
*/
public void unifiedOrder() {
System.out.println("阿里支付");
}
}
/* ******************************* */
public class WECHATPay implements Pay{
/**
* 统一下单
*/
public void unifiedOrder() {
System.out.println("微信支付");
}
}
// 3.创建工厂类,提供一个静态方法用来确定支付方式。
public class SimpleFactory {
public static Pay creatOrder(String payMethod){
if (payMethod == null){
return null;
}
if (payMethod.equalsIgnoreCase("WECHAT_PAY")){
return new WECHATPay();
}else if (payMethod.equalsIgnoreCase("ALi_PAY")){
return new ALiPay();
}
return null;
}
}
// 4.调用执行
工厂方法模式
- 又称工厂模式,是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则
- 通过工厂父类定义负责创建产品的公共接口,通过子类来确定所需要创建的类型
- 相比简单工厂而言,此种方法具有更多的可扩展性和复用性,同时也增强了代码的可读性
- 将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子类来决定应该实例化哪一个类。
- 核心组成
- IProduct:抽象产品类,描述所有实例所共有的公共接口
- Product:具体产品类,实现抽象产品类的接口,工厂类创建对象,如果有多个需要定义多个
- IFactory:抽象工厂类,描述具体工厂的公共接口
- Factory:具体工场类,实现创建产品类对象,实现抽象工厂类的接口,如果有多个需要定义多个
优点:
- 符合开闭原则,增加一个产品类,只需要实现其他具体的产品类和具体的工厂类;
- 符合单一职责原则,每个工厂只负责生产对应的产品
- 使用者只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则
- 迪米特法则:最少知道原则,实体应当尽量少地与其他实体之间发生相互作用
- 依赖倒置原则:针对接口编程,依赖于抽象而不依赖于具体
- 里氏替换原则:俗称LSP, 任何基类可以出现的地方,子类一定可以出现, 对实现抽象化的具体步骤的规范
缺点
- 增加一个产品,需要实现对应的具体工厂类和具体产品类;
- 每个产品需要有对应的具体工厂和具体产品类
编码实现
/**
* 抽象工厂方法
*/
public interface PayFactory {
Pay getPay();
}
/**
* 具体产品工厂
*/
public class AliPayFactory implements PayFactory {
@Override
public Pay getPay() {
return new AliPay();
}
}
/**
* 抽象产品
*/
public interface Pay {
/**
* 统一下单
*/
void unifiedorder();
}
/**
* 具体产品
*/
public class AliPay implements Pay {
@Override
public void unifiedorder() {
System.out.println("支付宝支付 统一下单接口");
}
}
抽象工厂模式
抽象工厂模式:基于上述两种模式的拓展,是工厂方法模式的升级版,当需要创建的产品有多个产品线时使用抽象工厂模式是比较好的选择。
背景
- 工厂方法模式引入工厂等级结构,解决了简单工厂模式中工厂类职责过重的问题
- 但工厂方法模式中每个工厂只创建一类具体类的对象,后续发展可能会导致工厂类过多,因此将一些相关的具体类组成一个“具体类族”,由同一个工厂来统一生产,强调的是一系列相关的产品对象!!!
实现步骤
1、定义两个接口 Pay、Refund
2、创建具体的Pay产品、创建具体的Refund产品
3、创建抽象工厂 OrderFactory 接口
里面两个方法 createPay/createRefund
4、创建支付宝产品族AliOderFactory,实现OrderFactory抽象工厂
5、创建微信支付产品族WechatOderFactory,实现OrderFactory抽象工厂
6、定义一个超级工厂创造器,通过传递参数获取对应的工厂
优点
- 当一个产品族中的多个对象被设计成一起工作时,它能保证使用方始终只使用同一个产品族中的对象
- 产品等级结构扩展容易,如果需要增加多一个产品等级,只需要增加新的工厂类和产品类即可, 比如增加银行支付、退款
缺点
- 产品族扩展困难,要增加一个系列的某一产品,既要在抽象的工厂和抽象产品里修改代码,不是很符合开闭原则
- 增加了系统的抽象性和理解难度
原型设计模式
- 是一种对象创建型模式,使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,主要用于创建重复的对象,同时又能保证性能
- 工作原理是将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程
- 应该是最简单的设计模式了,实现一个接口,重写一个方法即完成了原型模式
核心组成
- Prototype: 声明克隆方法的接口,是所有具体原型类的公共父类,Cloneable接口
- ConcretePrototype : 具体原型类
- Client: 让一个原型对象克隆自身从而创建一个新的对象
public class Person implements Cloneable, Serializable {
private String name;
private int age;
private List<String> info;
get and set....
@Override
protected Person clone() throws CloneNotSupportedException {
return (Person)super.clone();
}
/**
* 解决原型模式 copy出现的浅拷贝问题:只能copy基本数据类型,而对引用数据类型则指向同一个地址。因为经过序列化跟反序列化的对象内存地址会变更
* @return
*/
public Object deepClone(){
try{
// 输出 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
// 输入 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Person copyObj = (Person)ois.readObject();
return copyObj;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
public class client {
// 如果未经序列化,此时list中的数据为 aaa,ccc
public static void main(String[] args) throws CloneNotSupportedException {
Person lucy = new Person();
lucy.setName("lucy");
lucy.setAge(11);
lucy.getInfo().add("aaa");
Person jack = person1.clone();
jack.setName("jack");
person1.setAge(11);
person1.getInfo().add("ccc");
}
}
应用场景
- 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得
- 如果系统要保存对象的状态,做备份使用
- 遗留问题:
- 通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的
- 浅拷贝实现 Cloneable,深拷贝是通过实现 Serializable 读取二进制流
- 拓展
- 浅拷贝
如果原型对象的成员变量是基本数据类型(int、double、byte、boolean、char等),将复制一份给克隆对象;
如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址
通过覆盖Object类的clone()方法可以实现浅克隆
- 深拷贝
无论原型对象的成员变量是基本数据类型还是引用类型,都将复制一份给克隆对象,如果需要实现深克隆,可以通过序列化(Serializable)等方式来实现
原型模式是内存二进制流的拷贝,比new对象性能高很多,使用的时候记得注意是选择浅拷贝还是深拷贝
优点
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,可以提高新实例的创建效率
- 可辅助实现撤销操作,使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用恢复到历史状态
缺点
- 需要为每一个类配备一个克隆方法,对已有的类进行改造时,需要修改源代码,违背了“开闭原则”
- 在实现深克隆时需要编写较为复杂的代码,且当对象之间存在多重的嵌套引用时,需要对每一层对象对应的类都必须支持深克隆
建造者模式
- 使用多个简单的对象一步一步构建成一个复杂的对象,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
- 允许用户只通过指定复杂对象的类型和内容就可以构建它们,不需要知道内部的具体构建细节
场景举例
- KFC创建套餐:套餐是一个复杂对象,它一般包含主食如汉堡、烤翅等和饮料 如果汁、 可乐等组成部分,不同的套餐有不同的组合,而KFC的服务员可以根据顾客的要求,一步一步装配这些组成部分,构造一份完整的套餐
- 电脑有低配、高配,组装需要CPU、内存、电源、硬盘、主板等
核心组成
- Builder:抽象建造者,定义多个通用方法和构建方法
- ConcreteBuilder:具体建造者,可以有多个
- Director:指挥者,控制整个组合过程,将需求交给建造者,由建造者去创建对象
- Product:产品角色
/**
* 声明了建造者的公共方法
*/
public interface Builder {
/**
*细节方法
*/
void buildCpu();
void buildMainboard();
void buildDisk();
void buildPower();
void buildMemory();
Computer createComputer();
}
/**
*
* @Description 将产品和创建过程进行解耦,使用相同的创建过程创建不同的产品,控制产品生产过程
* Director是全程指导组装过程,具体的细节还是builder去操作
* @Version 1.0
**/
public class Director {
public Computer craete(Builder builder){
builder.buildMemory();
builder.buildCpu();
builder.buildMainboard();
builder.buildDisk();
builder.buildPower();
return builder.createComputer();
}
}
public class HighComputerBuilder implements Builder{
private Computer computer = new Computer();
@Override
public void buildCpu() {
computer.setCpu("高配 CPU");
}
@Override
public void buildMainboard() {
computer.setMainboard("高配 主板");
}
@Override
public void buildDisk() {
computer.setDisk("高配 磁盘");
}
@Override
public void buildPower() {
computer.setPower("高配 电源");
}
@Override
public void buildMemory() {
computer.setMemory("高配 内存");
}
@Override
public Computer createComputer() {
return computer;
}
}
优点
- 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,更加精细地控制产品的创建过程
- 增加新的具体建造者无须修改原有类库的代码,符合开闭原则
- 建造者模式结合链式编程来使用,代码上更加美观
缺点
- 建造者模式所创建的产品一般具有较多的共同点,如果产品差异大则不建议使用
- JDK里面的应用
- tcp传输协议 protobuf 生成的api、java中的StringBuilder(不完全一样,思想一样)
- 建造者模式与抽象工厂模式的比较:
- 建造者模式返回一个组装好的完整产品 , 抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族
适配器模式
- 见名知意,是作为两个不兼容的接口之间的桥梁,属于结构型模式
- 适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
- 常见的几类适配器
- 类的适配器模式
- 想将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可
- 对象的适配器模式
- 想将一个对象转换成满足另一个新接口的对象时,可以创建一个适配器类,持有原类的一个实例,在适配器类的方法中,调用实例的方法就行
- 接口的适配器模式
- 不想实现一个接口中所有的方法时,可以创建一个Adapter,实现所有方法,在写别的类的时候,继承Adapter类即
应用场景
- 系统需要使用现有的类,而这些类的接口不符合系统的需要
- JDK中InputStreamReader就是适配器
- JDBC就是我们用的最多的适配器模式
接口的适配器案例实战
- 设计模式的疑惑
- 会感觉到好像是理解了模式的功能,但是一到真实的系统开发中,就不知道如何使用这个模式了
- 前面每个案例都有讲实际的编码操作,大家一定要充分领悟
- 接口适配器
有些接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要实现部分接口就可以了
// 网关接口
public interface PayGeteway {
/**
* 下单
*/
void unifiedOrder();
/**
* 退款
*/
void refund();
/**
* 查询
*/
void query();
/**
* 发红包
*/
void sendRedPack();
}
*************************
// 适配器实现
public class PayGatewayAdapter implements PayGeteway{
public void unifiedOrder() {
}
public void refund() {
}
public void query() {
}
public void sendRedPack() {
}
}
*****************************
// 继承适配器,并选择需要实现的接口
public class ProductVideoOrder extends PayGatewayAdapter{
@Override
public void unifiedOrder() {
System.out.println("统一下单");
}
@Override
public void refund() {
System.out.println("退款");
}
@Override
public void query() {
System.out.println("查询");
}
}
*****************************
public class ProductVipOrder extends PayGatewayAdapter {
@Override
public void unifiedOrder() {
System.out.println("统一下单");
}
@Override
public void refund() {
System.out.println("退款");
}
@Override
public void query() {
System.out.println("查询");
}
@Override
public void sendRedPack() {
System.out.println("发红包");
}
}
总结
- 在使用一些旧系统或者是类库时,经常会出现接口不兼容的问题,适配器模式在解决这类问题具有优势
- 学习设计模式一定不要局限代码层面,要从软件系统整体去考虑,而不是为了使用设计模式,而去使用设计模式
优点
- 可以让任何两个没有关联的类一起运行,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
- 增加灵活度, 提高复用性,适配器类可以在多个系统使用,符合开闭原则
缺点
- 整体类的调用链路增加,本来A可以直接调用C,使用适配器后 是A调用B,B再调用C
类适配器
想将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可
public class OldModule {
public void methodA(){
System.out.println("methodA执行了");
}
}
public interface TargetModule {
/**
* 和需要适配的类方法名(OldModule)一样
*/
void methodA();
/**
* 新的方法,如果有多个方法的话,直接编写就行
*/
void methodB();
void methodC();
}
// 继承旧模块 并实现目标模块
public class Adapter extends OldModule implements TargetModule{
public void methodB() {
System.out.println("执行了B");
}
public void methodC() {
System.out.println("执行了C");
}
}
// 测试类
TargetModule targetModule = new Adapter();
targetModule.methodA();
targetModule.methodB();
targetModule.methodC();
// 输出
methodA执行了
执行了B
执行了C