当前位置: 首页 > news >正文

【设计模式】(万字总结)深入理解Java中的创建型设计模式

1. 前言

在软件开发的世界里,设计模式是一种被广泛接受并应用的解决方案。它们不仅仅是代码的设计,更是对问题的思考和解决的方法论。在Java开发中,特别是在面向对象的编程中,设计模式尤为重要。创建型设计模式,作为设计模式的重要组成部分,专注于如何有效地创建对象,同时尽可能减少耦合度,提高代码的可复用性和可维护性。

本文将带领您深入探索Java中的创建型设计模式。我们将从简单的概念入手,逐步深入到每种模式的具体实现方式和实际应用场景。通过学习和理解这些设计模式,您将能够更加灵活和高效地设计和开发Java应用程序。

创建型模式提供创建对象的机制,能够提升已有代码的灵活性和复用性

  • 常用的有:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式。

  • 不常用的有:原型模式。

2.  单例模式

创建型模式提供创建对象的机制,能够提升已有代码的灵活性和复用性

  • 常用的有:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式。

  • 不常用的有:原型模式。

2.1. 单例模式介绍

1 ) 定义

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,此模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类。

单例模式也比较好理解,比如一个人一生当中只能有一个真实的身份证号,一个国家只有一个政府,类似的场景都是属于单例模式。

2 ) 使用单例模式要做的两件事

  1. 保证一个类只有一个实例

  2. 为该实例提供一个全局访问节点

3 ) 单例模式结构

2.2.  饿汉式

在类加载期间初始化静态实例,保证 instance 实例的创建是线程安全的 ( 实例在类加载时实例化,有JVM保证线程安全).

特点: 不支持延迟加载实例(懒加载) , 此中方式类加载比较慢,但是获取实例对象比较快

问题: 该对象足够大的话,而一直没有使用就会造成内存的浪费。

public class Singleton_01 {
​//1. 私有构造方法private Singleton_01(){
​}
​//2. 在本类中创建私有静态的全局对象private static Singleton_01 instance = new Singleton_01();
​
​//3. 提供一个全局访问点,供外部获取单例对象public static  Singleton_01 getInstance(){
​return instance;}
​
}

2.3. 懒汉式(线程不安全)

此种方式的单例实现了懒加载,只有调用getInstance方法时 才创建对象.但是如果是多线程情况,会出现线程安全问题.

public class Singleton_02 {
​//1. 私有构造方法private Singleton_02(){
​}
​//2. 在本类中创建私有静态的全局对象private static Singleton_02 instance;
​
​//3. 通过判断对象是否被初始化,来选择是否创建对象public static  Singleton_02 getInstance(){
​if(instance == null){
​instance = new Singleton_02();}return instance;}
​
}

假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程A在执行完if (instance == null) 后,线程调度机制将 CPU 资源分配给线程B,此时线程B在执行 if (instance == null)时也发现单例类还没有被实例化,这样就会导致单例类被实例化两次。为了防止这种情况发生,需要对 getInstance() 方法同步处理。改进后的懒汉模式.

2.4. 懒汉式(线程安全)

原理: 使用同步锁 synchronized锁住 创建单例的方法 ,防止多个线程同时调用,从而避免造成单例被多次创建

  1. 即,getInstance()方法块只能运行在1个线程中

  2. 若该段代码已在1个线程中运行,另外1个线程试图运行该块代码,则 会被阻塞而一直等待

  3. 而在这个线程安全的方法里我们实现了单例的创建,保证了多线程模式下 单例对象的唯一性

public class Singleton_03 {
​//1. 私有构造方法private Singleton_03(){
​}
​//2. 在本类中创建私有静态的全局对象private static Singleton_03 instance;
​
​//3. 通过添加synchronize,保证多线程模式下的单例对象的唯一性public static synchronized  Singleton_03 getInstance(){
​if(instance == null){instance = new Singleton_03();}return instance;}
​
}

懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

2.5. 双重校验

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。

实现步骤:

  1. 在声明变量时使用了 volatile 关键字,其作用有两个:

保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。

屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但 是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。

  1. 将同步方法改为同步代码块. 在同步代码块中使用二次检查,以保证其不被重复实例化 同时在调用getInstance()方法时不进行同步锁,效率高。

/*** 单例模式-双重校验* @date 2022/9/5**/
public class Singleton_04 {
​//使用 volatile保证变量的可见性private volatile static Singleton_04 instance = null;
​private Singleton_04(){}
​//对外提供静态方法获取对象public static Singleton_04 getInstance(){//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例if(instance == null){synchronized (Singleton_04.class){//抢到锁之后再次进行判断是否为nullif(instance == null){instance = new Singleton_04();}}}
​return instance;}
}

在双重检查锁模式中为什么需要使用 volatile 关键字?

在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

  • 第一步是给 singleton 分配内存空间;

  • 第二步开始调用 Singleton 的构造函数等,来初始化 singleton;

  • 第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。

这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。

如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错.

详细流程如下图所示:

线程 1 首先执行新建实例的第一步,也就是分配单例对象的内存空间,由于线程 1 被重排序,所以执行了新建实例的第三步,也就是把 singleton 指向之前分配出来的内存地址,在这第三步执行之后,singleton 对象便不再是 null。

这时线程 2 进入 getInstance 方法,判断 singleton 对象不是 null,紧接着线程 2 就返回 singleton 对象并使用,由于没有初始化,所以报错了。最后,线程 1 “姗姗来迟”,才开始执行新建实例的第二步——初始化对象,可是这时的初始化已经晚了,因为前面已经报错了。

使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在 JDK 5 以及后续版本所使用的 JMM 中,在使用了 volatile 后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的读取到不完整对象的问题的发生。

2.6. 静态内部类

  • 原理 根据 静态内部类 的特性(外部类的加载不影响内部类),同时解决了按需加载、线程安全的问题,同时实现简洁

  1. 在静态内部类里创建单例,在装载该内部类时才会去创建单例

  2. 线程安全:类是由 JVM加载,而JVM只会加载1遍,保证只有1个单例

public class Singleton_05 {private static class SingletonHandler{private static Singleton_05 instance = new Singleton_05();}private Singleton_05(){}public static Singleton_05 getInstance(){return SingletonHandler.instance;}
}

2.7. 反射对于单例的破坏

反射的概念: JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

反射技术过于强大,它可以通过setAccessible()来修改构造器,字段,方法的可见性。单例模式的构造方法是私有的,如果将其可见性设为public,那么将无法控制对象的创建。

public class Test_Reflect {public static void main(String[] args) {try {//反射中,欲获取一个类或者调用某个类的方法,首先要获取到该类的Class 对象。Class<Singleton_05> clazz = Singleton_05.class;//getDeclaredXxx: 不受权限控制的获取类的成员.Constructor c = clazz.getDeclaredConstructor(null);//设置为true,就可以对类中的私有成员进行操作了c.setAccessible(true);Object instance1 = c.newInstance();Object instance2 = c.newInstance();System.out.println(instance1 == instance2);} catch (Exception e) {e.printStackTrace();}}
}

解决方法之一: 在单例类的构造方法中 添加判断 instance != null 时,直接抛出异常

public class Singleton_05 {private static class SingletonHandler{private static Singleton_05 instance = new Singleton_05();}private Singleton_05(){if(SingletonHandler.instance != null){throw new RuntimeException("不允许非法访问!");}}public static Singleton_05 getInstance(){return SingletonHandler.instance;}
}

上面的这种方式使代码简洁性遭到破坏,设计不够优雅.

2.8. 序列化对于单例的破坏

/*** 序列化对单例的破坏* @date 2022/9/6**/
public class Test_Serializable {
​@Testpublic void test() throws Exception{
​//序列化对象输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile.obj"));oos.writeObject(Singleton.getInstance());
​//序列化对象输入流File file = new File("tempFile.obj");ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));Singleton Singleton = (Singleton) ois.readObject();
​System.out.println(Singleton);System.out.println(Singleton.getInstance());
​//判断是否是同一个对象System.out.println(Singleton.getInstance() == Singleton);//false
​}
}
​
​
/*** 单例类实现序列化接口*/
class Singleton implements Serializable {
​private volatile static Singleton singleton;
​private Singleton() {
​}
​public static Singleton getInstance() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();}}}return singleton;}
​
}

输出结构为false,说明:

通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性

解决方案

/**
* 解决方案:只要在Singleton类中定义readResolve就可以解决该问题
* 程序会判断是否有readResolve方法,如果存在就在执行该方法,如果不存在--就创建一个对象
*/
private Object readResolve() {return singleton;
}

问题是出在ObjectInputputStream 的readObject 方法上, 我们来看一下ObjectInputStream的readObject的调用栈:

ObjectInputStream中readObject方法的代码片段try {Object obj = readObject0(false); //最终会返回一个object对象,其实就是序列化对象return obj;
} finally {passHandle = outerHandle;if (closed && depth == 0) {clear();}
}

ObjectInputStream中readObject0方法的代码片段

private Object readObject0(boolean unshared) throws IOException {case TC_OBJECT: //匹配如果是对象return checkResolve(readOrdinaryObject(unshared));
}

readOrdinaryObject方法的代码片段

private Object readOrdinaryObject(boolean unshared)throws IOException{//此处省略部分代码
​Object obj;try {//通过反射创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。//isInstantiable:如果一个serializable的类可以在运行时被实例化,那么该方法就返回true//desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。obj = desc.isInstantiable() ? desc.newInstance() : null;} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}
​return obj;}

到目前为止,也就可以解释,为什么序列化可以破坏单例了:?

答: 序列化会通过反射调用无参数的构造方法创建一个新的对象。

我们是如何解决的呢?

答: 只要在Singleton类中定义readResolve就可以解决该问题

//只要在Singleton类中定义readResolve就可以解决该问题
private Object readResolve() {return singleton;
}

实现原理

if (obj != null &&handles.lookupException(passHandle) == null &&desc.hasReadResolveMethod()){Object rep = desc.invokeReadResolve(obj);if (unshared && rep.getClass().isArray()) {rep = cloneArray(rep);}if (rep != obj) {handles.setObject(passHandle, obj = rep);}}

hasReadResolveMethod:如果实现了serializable 接口的类中包含readResolve则返回true

invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。

总结: Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

2.9. 枚举(推荐方式)

枚举单例方式是<<Effective Java>>作者推荐的使用方式,这种方式

在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例;默认枚举实例的创建是线程安全的,即使反序列化也不会生成新的实例,任何情况下都是一个单例(暴力反射对枚举方式无效)。

特点: 满足单例模式所需的 创建单例、线程安全、实现简洁的需求

public enum Singleton_06{INSTANCE;private Object data;public Object getData() {return data;}public void setData(Object data) {this.data = data;}public static Singleton_06 getInstance(){return INSTANCE;}
}

问题1: 为什么枚举类可以阻止反射的破坏?

  1. 首先枚举类中是没有空参构造方法的,只有一个带两个参数的构造方法.

  2. 真正原因是: 反射方法中不予许使用反射创建枚举对象

    异常: 不能使用反射方式创建enum对象

问题2: 为什么枚举类可以阻止序列化的破坏?

Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。

在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。

比如说,序列化的时候只将INSTANCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

public enum Singleton_06{INSTANCE;
}

2.10. 单例模式总结

1 ) 单例的定义 单例设计模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类。

2 ) 单例的实现

饿汉式

  • 饿汉式的实现方式,在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。

懒汉式

  • 相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。

双重检测

  • 双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。

静态内部类

  • 利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。

枚举方式

  • 最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性(同时阻止了反射和序列化对单例的破坏)。

3.  工厂方法模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

  • 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

《设计模式》一书中,工厂模式被分为了三种:简单工厂、工厂方法和抽象工厂。(不过,在书中作者将简单工厂模式看作是工厂方法模式的一种特例。)

接下来我会介绍三种工厂模式的原理及使用

  • 简单工厂模式(不属于GOF的23种经典设计模式)

  • 工厂方法模式

  • 抽象工厂模式

3.1. 需求: 模拟发放奖品业务

需求: 为了让我们的案例更加贴近实际开发, 这里我们来模拟一下互联网电商中促销拉新下的业务场景, 新用户注册立即参与抽奖活动 ,奖品的种类有: 打折券, 免费优酷会员,小礼品.

3.2. 原始开发方式

不考虑设计原则,不使用设计模式的方式进行开发

在不考虑任何代码的可扩展性的前提下,只为了尽快满足需求.我们可以这样去设计这个业务的代码结构:

1) 实体类

名称描述
AwardInfo获奖信息对应实体类
DiscountInfo打折券信息对应实体类
YouKuMember优酷会员对应实体类
SmallGiftInfo小礼品信息对应实体类
DiscountResult打折券操作响应结果封装类
public class AwardInfo {
​private String uid; //用户唯一ID
​private Integer awardType; //奖品类型: 1 打折券 ,2 优酷会员,3 小礼品
​private String awardNumber; //奖品编号
​Map<String, String> extMap; //额外信息}
​
public class DiscountInfo {
​//属性信息省略......
}
​
public class YouKuMember {
​//属性信息省略......
}
​
public class SmallGiftInfo {
​private String userName;              // 用户姓名private String userPhone;             // 用户手机private String orderId;               // 订单IDprivate String relAddress;            // 收货地址}
​
public class DiscountResult {
​private String status; // 状态码private String message; // 信息
}

2) 服务层

名称功能描述
DiscountServiceDiscountResult sendDiscount(String uid,String number)模拟打折券服务
YouKuMemberServicevoid openMember(String bindMobile , String number)模拟赠送优酷会员服务
SmallGiftServiceBoolean giveSmallGift(SmallGiftInfo smallGiftInfo)模拟礼品服务
public class DiscountService {
​public DiscountResult sendDiscount(String uid, String number){
​System.out.println("向用户发放打折券一张: " + uid + " , " + number);return new DiscountResult("200","发放打折券成功");}
}
​
public class YouKuMemberService {
​public void openMember(String bindMobile , String number){
​System.out.println("发放优酷会员: " + bindMobile + " , " + number);}
}
​
public class SmallGiftService {
​public Boolean giveSmallGift(SmallGiftInfo smallGiftInfo){
​System.out.println("小礼品已发货,获奖用户注意查收! " + JSON.toJSON(smallGiftInfo));return true;}
}

3) 控制层

名称功能描述
DeliverControllerResponseResult awardToUser(AwardInfo awardInfo)按照类型的不同发放商品 奖品类型: 1 打折券 ,2 优酷会员,3 小礼品
public class DeliverController {/*** 按照类型的不同发放商品*     奖品类型: 1 打折券 ,2 优酷会员,3 小礼品*/public void awardToUser(AwardInfo awardInfo){if(awardInfo.getAwardType() == 1){ //打折券DiscountService discountService = new DiscountService();DiscountResult result = discountService.sendDiscount(awardInfo.getUid(), awardInfo.getAwardNumber());System.out.println("打折券发放成功!"+ JSON.toJSON(result));}else if(awardInfo.getAwardType() == 2){ //优酷会员//获取用户手机号String bindMobile = awardInfo.getExtMap().get("phone");//调用serviceYouKuMemberService youKuMemberService = new YouKuMemberService();youKuMemberService.openMember(bindMobile,awardInfo.getAwardNumber());System.out.println("优酷会员发放成功!");}else if(awardInfo.getAwardType() == 3){ /*小礼品封装收货用户信息*/SmallGiftInfo smallGiftInfo = new SmallGiftInfo();smallGiftInfo.setUserName(awardInfo.getExtMap().get("username"));smallGiftInfo.setOrderId(UUID.randomUUID().toString());smallGiftInfo.setRelAddress(awardInfo.getExtMap().get("adderss"));SmallGiftService smallGiftService = new SmallGiftService();Boolean isSuccess = smallGiftService.giveSmallGift(smallGiftInfo);System.out.println("小礼品发放成功!" + isSuccess);}}}

4) 测试

通过单元测试,来对上面的接口进行测试,验证代码质量.

public class TestApi01 {//测试发放奖品接口@Testpublic void test01(){DeliverController deliverController = new DeliverController();//1. 发放打折券优惠AwardInfo info1 = new AwardInfo();info1.setUid("1001");info1.setAwardType(1);info1.setAwardNumber("DEL12345");deliverController.awardToUser(info1);//2. 发放优酷会员AwardInfo info2 = new AwardInfo();info2.setUid("1002");info2.setAwardType(2);info2.setAwardNumber("DW12345");Map<String,String> map = new HashMap<>();map.put("phone","13512341234");info2.setExtMap(map);deliverController.awardToUser(info2);//2. 发放小礼品AwardInfo info3 = new AwardInfo();info3.setUid("1003");info3.setAwardType(3);info3.setAwardNumber("SM12345");Map<String,String> map2 = new HashMap<>();map2.put("username","大远");map2.put("phone","13512341234");map2.put("address","北京天安门");info3.setExtMap(map2);deliverController.awardToUser(info3);}
}

对于上面的实现方式,如果我们有想要添加的新的奖品时,势必要改动DeliverController的代码,违反开闭原则.而且如果有的抽奖接口出现问题,那么对其进行重构的成本会非常高.

除此之外代码中有一组if分支判断逻辑,现在看起来还可以,但是如果经历几次迭代和拓展,后续ifelse肯定还会增加.到时候接手这段代码的研发将会十分痛苦.

3.3. 简单工厂模式

3.3.1. 简单工厂模式介绍

简单工厂不是一种设计模式,反而比较像是一种编程习惯。简单工厂模式又叫做静态工厂方法模式(static Factory Method pattern),它是通过使用静态方法接收不同的参数来返回不同的实例对象.

实现方式:

定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。

适用场景:   (1)需要创建的对象较少。   (2)客户端不关心对象的创建过程。

3.3.2. 简单工厂原理

简单工厂包含如下角色:

  • 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。

  • 具体产品 :实现或者继承抽象产品的子类

  • 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。

3.3.3. 简单工厂模式重构代码

1) service

/*** 免费商品发放接口* @date 2022/9/8**/
public interface IFreeGoods {
​ResponseResult sendFreeGoods(AwardInfo awardInfo);
​
}
/*** 模拟打折券服务* @date 2022/9/8**/
public class DiscountFreeGoods implements IFreeGoods {
​@Overridepublic ResponseResult sendFreeGoods(AwardInfo awardInfo) {
​System.out.println("向用户发放一张打折券: " + awardInfo.getUid() + " , " + awardInfo.getAwardNumber());return new ResponseResult("200","打折券发放成功!");}
}
​
/*** 小礼品发放服务* @date 2022/9/8**/
public class SmallGiftFreeGoods implements IFreeGoods {
​@Overridepublic ResponseResult sendFreeGoods(AwardInfo awardInfo) {
​SmallGiftInfo smallGiftInfo = new SmallGiftInfo();smallGiftInfo.setUserPhone(awardInfo.getExtMap().get("phone"));smallGiftInfo.setUserName(awardInfo.getExtMap().get("username"));smallGiftInfo.setAddress(awardInfo.getExtMap().get("address"));smallGiftInfo.setOrderId(UUID.randomUUID().toString());
​System.out.println("小礼品发放成,请注意查收: " + JSON.toJSON(smallGiftInfo));return new ResponseResult("200","小礼品发送成功",smallGiftInfo);}
}
​
/*** 优酷 会员服务* @date 2022/9/8**/
public class YouKuMemberFreeGoods implements IFreeGoods {
​@Overridepublic ResponseResult sendFreeGoods(AwardInfo awardInfo) {
​String phone = awardInfo.getExtMap().get("phone");System.out.println("发放优酷会员成功,绑定手机号: " + phone);return new ResponseResult("200","优酷会员发放成功!");}
}

2) factory

/*** 具体工厂: 生成免费商品* @date 2022/9/9**/
public class FreeGoodsFactory {public static IFreeGoods getInstance(Integer awardType){IFreeGoods iFreeGoods = null;if(awardType == 1){  //打折券iFreeGoods = new DiscountFreeGoods();}else if(awardType == 2){ //优酷会员iFreeGoods = new YouKuMemberFreeGoods();}else if(awardType == 3){ //小礼品iFreeGoods = new SmallGiftFreeGoods();}return iFreeGoods;}
}

3)controller

public class DeliverController {//发放奖品public ResponseResult awardToUser(AwardInfo awardInfo){try {IFreeGoods freeGoods = FreeGoodsFactory.getInstance(awardInfo.getAwardTypes());ResponseResult responseResult = freeGoods.sendFreeGoods(awardInfo);return responseResult;} catch (Exception e) {e.printStackTrace();return new ResponseResult("201","奖品发放失败!");}}
}

4) 测试

通过单元测试,来对上面的接口进行测试,验证代码质量.

public class TestApi02 {
​DeliverController deliverController = new DeliverController();
​@Testpublic void test01(){
​//1. 发放打折券优惠AwardInfo info1 = new AwardInfo();info1.setUid("1001");info1.setAwardTypes(1);info1.setAwardNumber("DEL12345");
​ResponseResult result = deliverController.awardToUser(info1);System.out.println(result);
​}
​@Testpublic void test02(){
​//2. 发放优酷会员AwardInfo info2 = new AwardInfo();info2.setUid("1002");info2.setAwardTypes(2);info2.setAwardNumber("DW12345");Map<String,String> map = new HashMap<>();map.put("phone","13512341234");info2.setExtMap(map);
​ResponseResult result1 = deliverController.awardToUser(info2);System.out.println(result1);
​}
​@Testpublic void test03(){
​//3. 发放小礼品AwardInfo info3 = new AwardInfo();info3.setUid("1003");info3.setAwardTypes(3);info3.setAwardNumber("SM12345");Map<String,String> map2 = new HashMap<>();map2.put("username","大远");map2.put("phone","13512341234");map2.put("address","北京天安门");info3.setExtMap(map2);
​ResponseResult result2 = deliverController.awardToUser(info3);System.out.println(result2);}
}
3.3.4. 简单工厂模式总结

优点:

封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。

缺点:

增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。

3.4. 工厂方法模式

3.4.1. 工厂方法模式介绍

工厂方法模式 Factory Method pattern,属于创建型模式.

概念: 定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。

3.4.2. 工厂方法模式原理

工厂方法模式的目的很简单,就是封装对象创建的过程,提升创建对象方法的可复用性

工厂方法模式的主要角色:

  • 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。

  • 具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。

  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。

  • 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

我们直接来看看工厂方法模式的 UML 图:

3.4.3. 工厂方法模式重构代码

为了提高代码扩展性,我们需要将简单工厂中的if分支逻辑去掉,通过增加抽象工厂(生产工厂的工厂)的方式,让具体工厂去进行实现,由具体工厂来决定实例化哪一个具体的产品对象.

抽象工厂

public interface FreeGoodsFactory {IFreeGoods getInstance();
}

具体工厂

public class DiscountFreeGoodsFactory implements FreeGoodsFactory {@Overridepublic IFreeGoods getInstance() {return new DiscountFreeGoods();}
}public class SmallGiftFreeGoodsFactory implements FreeGoodsFactory {@Overridepublic IFreeGoods getInstance() {return new SmallGiftFreeGoods();}
}

Controller

public class DeliverController {/*** 按照类型的不同发放商品*/public ResponseResult awardToUser(AwardInfo awardInfo){FreeGoodsFactory freeGoodsFactory = null;if(awardInfo.getAwardType() == 1){freeGoodsFactory = new DiscountFreeGoodsFactory();}else if(awardInfo.getAwardType() == 2){freeGoodsFactory = new SmallGiftFreeGoodsFactory();}IFreeGoods freeGoods = freeGoodsFactory.getInstance();System.out.println("=====工厂方法模式========");ResponseResult result = freeGoods.sendFreeGoods(awardInfo);return result;}}

从上面的代码实现来看,工厂类对象的创建逻辑又耦合进了 awardToUser() 方法中,跟我们最初的代码版本非常相似,引入工厂方法非但没有解决问题,反倒让设计变得更加复杂了。

那怎么 来解决这个问题呢?

我们可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。

/*** 用简单方法模式实现: 工厂的工厂,作用是不需要每次创建新的工厂对象* @date 2022/9/9**/
public class FreeGoodsFactoryMap {private static final Map<Integer,FreeGoodsFactory> cachedFactories = new HashMap<>();static{cachedFactories.put(1, new DiscountFreeGoodsFactory());cachedFactories.put(2, new SmallGiftFreeGoodsFactory());}public static FreeGoodsFactory getParserFactory(Integer type){if(type == 1){FreeGoodsFactory freeGoodsFactory = cachedFactories.get(1);return freeGoodsFactory;}else if(type ==2){FreeGoodsFactory freeGoodsFactory = cachedFactories.get(2);return freeGoodsFactory;}return null;}
}

Controller

/*** 发放奖品接口* @date 2022/9/7**/
public class DeliverController {/*** 按照类型的不同发放商品*/public ResponseResult awardToUser(AwardInfo awardInfo){//根据类型获取工厂FreeGoodsFactory goodsFactory = FreeGoodsFactoryMap.getParserFactory(awardInfo.getAwardType());//从工厂中获取对应实例IFreeGoods freeGoods = goodsFactory.getInstance();System.out.println("=====工厂方法模式========");ResponseResult result = freeGoods.sendFreeGoods(awardInfo);return result;}
}

现在我们的代码已经基本上符合了开闭原则,当有新增的产品时,我们需要做的事情包括:

  1. 创建新的产品类,并且让该产品实现抽象产品接口

  2. 创建产品类对应的具体工厂,并让具体工厂实现抽象工厂

  3. 将新的具体工厂对象,添加到FreeGoodsFactoryMap的cachedFactories中即可,需要改动的代码改动的非常少.

3.4.4. 工厂方法模式总结

工厂方法模优缺点

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;

  • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;

缺点:

  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

什么时候使用工厂方法模式

  • 需要使用很多重复代码创建对象时,比如,DAO 层的数据对象、API 层的 VO 对象等。

  • 创建对象要访问外部信息或资源时,比如,读取数据库字段,获取访问授权 token 信息,配置文件等。

  • 创建需要统一管理生命周期的对象时,比如,会话信息、用户网页浏览轨迹对象等。

  • 创建池化对象时,比如,连接池对象、线程池对象、日志对象等。这些对象的特性是:有限、可重用,使用工厂方法模式可以有效节约资源。

  • 希望隐藏对象的真实类型时,比如,不希望使用者知道对象的真实构造函数参数等。

4. 抽象工厂模式

4.1. 抽象工厂模式介绍

抽象工厂模式比工厂方法模式的抽象程度更高. 在工厂方法模式中每一个具体工厂只需要生产一种具体产品,但是在抽象工厂模式中一个具体工厂可以生产一组相关的具体产品,这样一组产品被称为产品族.产品族中的每一个产品都分属于某一个产品继承等级结构.

1) 产品等级结构与产品族

为了更好的理解抽象工厂, 我们这里先引入两个概念:

  • 产品等级结构 :产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。

  • 产品族 :在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。

在上图中,每一个具体工厂可以生产属于一个产品族的所有产品,例如海尔工厂生产海尔电视机、海尔空调和海尔冰箱,所生产的产品又位于不同的产品等级结构中. 如果使用工厂方法模式,上图所示的结构需要提供9个具体工厂,而使用抽象工厂模式只需要提供3个具体工厂,极大减少了系统中类的个数.

2) 抽象工厂模式概述

抽象工厂模式(Abstract Factory Pattern) 原始定义:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

抽象工厂模式为创建一组对象提供了解决方案.与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,而是负责创建一个产品族.如下图:

4.2. 抽象工厂模式原理

在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法,用于产生多种不同类型的产品.这些产品构成了一个产品族.

抽象工厂模式的主要角色如下:

  • 抽象工厂(Abstract Factory):它声明了一种用于创建一族产品的方法,每一个方法对应一种产品.

  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建.

  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。

  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。

4.3. 抽象工厂模式实现

抽象工厂

/*** 抽象工厂: 在一个抽象工厂中可以声明多个工厂方法,用于创建不同类型的产品* @date 2022/9/15**/
public interface AppliancesFactory {AbstractTV createTV();AbstractFreezer createFreezer();
}

具体工厂: 每一个具体工厂方法,可以返回一个特定的产品对象,而同一个具体工厂所创建的产品对象构成了一个产品族.

public class HairFactory implements AppliancesFactory {@Overridepublic AbstractTV createTV() {return new HairTV();}@Overridepublic AbstractFreezer createFreezer() {return new HairFreezer();}
}public class HisenseFactory implements AppliancesFactory {@Overridepublic AbstractTV createTV() {return new HisenseTV();}@Overridepublic AbstractFreezer createFreezer() {return new HisenseFreezer();}
}

抽象产品

public interface AbstractFreezer {}
public interface AbstractTV {}

具体产品

public class HairFreezer implements AbstractFreezer {}
public class HisenseFreezer implements AbstractFreezer {}
public class HairTV implements AbstractTV {}
public class HisenseTV implements AbstractTV {}

客户端

public class Client {private AbstractTV tv;private AbstractFreezer freezer;public Client(AppliancesFactory factory){//在客户端看来就是使用抽象工厂来生产家电this.tv = factory.createTV();this.freezer = factory.createFreezer();}public AbstractTV getTv() {return tv;}public void setTv(AbstractTV tv) {this.tv = tv;}public AbstractFreezer getFreezer() {return freezer;}public void setFreezer(AbstractFreezer freezer) {this.freezer = freezer;}public static void main(String[] args) {Client client = new Client(new HisenseFactory());AbstractTV tv = client.getTv();System.out.println(tv);AbstractFreezer freezer = client.getFreezer();System.out.println(freezer);}
}

4.4. 抽象工厂模式总结

从上面代码实现中我们可以看出,抽象工厂模式向使用(客户)方隐藏了下列变化:

  • 程序所支持的实例集合(具体工厂)的数目;

  • 当前是使用的实例集合中的哪一个实例;

  • 在任意给定时刻被实例化的具体类型;

所以说,在理解抽象工厂模式原理时,你一定要牢牢记住“如何找到某一个类产品的正确共性功能”这个重点。

抽象工厂模式优点

  1. 对于不同产品系列有比较多共性特征时,可以使用抽象工厂模式,有助于提升组件的复用性.

  2. 当需要提升代码的扩展性并降低维护成本时,把对象的创建和使用过程分开,能有效地将代码统一到一个级别上

  1. 解决跨平台带来的兼容性问题

抽象工厂模式缺点

增加新的产品等级结构麻烦,需要对原有结构进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大不变,违背了开闭原则.

5. 建造者模式

5.1. 建造者模式介绍

建造者模式 (builder pattern), 也被称为生成器模式 , 是一种创建型设计模式.

  • 定义: 将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式要解决的问题

建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

比如: 一辆汽车是由多个部件组成的,包括了车轮、方向盘、发动机等等.对于大多数用户而言,并不需要知道这些部件的装配细节,并且几乎不会使用单独某个部件,而是使用一辆完整的汽车.而建造者模式就是负责将这些部件进行组装让后将完整的汽车返回给用户.

 

5.2. 建造者模式原理

建造者(Builder)模式包含以下4个角色 :

  • 抽象建造者类(Builder):这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的部件对象的创建。

  • 具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供一个方法,返回创建好的负责产品对象。

  • 产品类(Product):要创建的复杂对象 (包含多个组成部件).

  • 指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建(客户端一般只需要与指挥者进行交互)。

5.3. 建造者模式实现方式1

创建共享单车

生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。

这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和HelloBuilder是具体的建造者;Director是指挥者。类图如下:

具体产品

public class Bike {
​//车架private String frame;
​//座椅private String seat;
​public String getFrame() {return frame;}
​public void setFrame(String frame) {this.frame = frame;}
​public String getSeat() {return seat;}
​public void setSeat(String seat) {this.seat = seat;}
}

构建者类

public abstract class Builder {protected Bike mBike = new Bike();public abstract void buildFrame();public abstract void buildSeat();public abstract Bike createBike();
}public class HelloBuilder extends Builder {@Overridepublic void buildFrame() {mBike.setFrame("碳纤维车架");}@Overridepublic void buildSeat() {mBike.setSeat("橡胶车座");}@Overridepublic Bike createBike() {return mBike;}
}public class MobikeBuilder extends Builder {@Overridepublic void buildFrame() {mBike.setFrame("铝合金车架");}@Overridepublic void buildSeat() {mBike.setSeat("真皮车座");}@Overridepublic Bike createBike() {return mBike;}
}

指挥者类

public class Director {private Builder mBuilder;public Director(Builder builder) {this.mBuilder = builder;}public Bike construct() {mBuilder.buildFrame();mBuilder.buildSeat();return mBuilder.createBike();}
}

客户端

public class Client {public static void main(String[] args) {showBike(new HelloBuilder());showBike(new MobikeBuilder());}private static void showBike(Builder builder) {Director director = new Director(builder);Bike bike = director.construct();System.out.println(bike.getFrame());System.out.println(bike.getSeat());}
}

5.4. 建造者模式实现方式2

建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。

1 ) 构造方法创建复杂对象的问题

  • 构造方法如果参数过多,代码的可读性和易用性都会变差. 在使用构造函数时,很容易搞错参数的顺序,传递进去错误的参数值,导致很有隐蔽的BUG出现.

/*** MQ连接客户端* @date 2022/9/19**/
public class RabbitMQClient1 {private String host = "127.0.0.1";private int port = 5672;private int mode;private String exchange;private String queue;private boolean isDurable = true;int connectionTimeout = 1000;//构造方法参数过多,代码的可读性和易用性太差,在使用构造函数时,很容易搞错顺序,传递错误的参数值,导致很有隐蔽的BUGpublic RabbitMQClient1(String host, int port, int mode, String exchange, String queue, boolean isDurable, int connectionTimeout) {this.host = host;this.port = port;this.mode = mode;this.exchange = exchange;this.queue = queue;this.isDurable = isDurable;this.connectionTimeout = connectionTimeout;if(mode == 1){ //工作队列模式不需要设计交换机,但是队列名称一定要有if(exchange != null){throw new RuntimeException("工作队列模式无需设计交换机");}if(queue == null || queue.trim().equals("")){throw new RuntimeException("工作队列模式名称不能为空");}if(isDurable == false){throw new RuntimeException("工作队列模式必须开启持久化");}}else if(mode == 2){ //路由模式必须设计交换机,但是不能设计队列if(exchange == null){throw new RuntimeException("路由模式下必须设置交换机");}if(queue != null){throw new RuntimeException("路由模式无须设计队列名称");}}//其他验证方式,}public void sendMessage(String msg){System.out.println("发送消息......");}public static void main(String[] args) {//每一种模式,都需要根据不同的情况进行实例化,构造方法会变得过于复杂.RabbitMQClient1 client1 = new RabbitMQClient1("192.168.52.123",5672,2,"sample-exchange",null,true,5000);client1.sendMessage("Test-MSG");}
}

2) set方法创建复杂对象的问题

  • set方式设置对象属性时,存在中间状态,并且属性校验时有前后顺序约束,逻辑校验的代码找不到合适的地方放置.

    比如下面的代码, 创建对象后使用set 的方式,那就会导致在第一个 set 之后,对象处于无效状态

    Rectangle r = new Rectangle (); //无效状态

    r.setWidth(2); //无效状态

    r.setHeight(3); //有效状态

  • set方法还破坏了"不可变对象"的密闭性 .

    不可变对象: 对象创建好了,就不能再修改内部的属性值,下面的client类就是典型的不可变对象,创建好的连接对象不能再改动

​
/*** MQ连接客户端* @date 2022/9/19**/
public class RabbitMQClient2 {
​private String host = "127.0.0.1";
​private int port = 5672;
​private int mode;
​private String exchange;
​private String queue;
​private boolean isDurable = true;
​int connectionTimeout = 1000;
​//私有化构造方法private RabbitMQClient2() {
​}
​public String getExchange() {return exchange;}
​public void setExchange(String exchange) {
​if(mode == 1){ //工作队列模式不需要设计交换机,但是队列名称一定要有if(exchange != null){throw new RuntimeException("工作队列模式无需设计交换机");}if(queue == null || queue.trim().equals("")){throw new RuntimeException("工作队列模式名称不能为空");}if(isDurable == false){throw new RuntimeException("工作队列模式必须开启持久化");}}else if(mode == 2){ //路由模式必须设计交换机,但是不能设计队列if(exchange == null){throw new RuntimeException("路由模式下必须设置交换机");}if(queue != null){throw new RuntimeException("路由模式无须设计队列名称");}}
​//其他验证方式,
​this.exchange = exchange;}
​public String getHost() {return host;}
​public void setHost(String host) {this.host = host;}
​public int getPort() {return port;}
​public void setPort(int port) {this.port = port;}
​public int getMode() {return mode;}
​public void setMode(int mode) {
​if(mode == 1){ //工作队列模式不需要设计交换机,但是队列名称一定要有if(exchange != null){throw new RuntimeException("工作队列模式无需设计交换机");}if(queue == null || queue.trim().equals("")){throw new RuntimeException("工作队列模式名称不能为空");}if(isDurable == false){throw new RuntimeException("工作队列模式必须开启持久化");}}else if(mode == 2){ //路由模式必须设计交换机,但是不能设计队列if(exchange == null){throw new RuntimeException("路由模式下必须设置交换机");}if(queue != null){throw new RuntimeException("路由模式无须设计队列名称");}}
​this.mode = mode;}
​public String getQueue() {return queue;}
​public void setQueue(String queue) {this.queue = queue;}
​public boolean isDurable() {return isDurable;}
​public void setDurable(boolean durable) {isDurable = durable;}
​public int getConnectionTimeout() {return connectionTimeout;}
​public void setConnectionTimeout(int connectionTimeout) {this.connectionTimeout = connectionTimeout;}
​public void sendMessage(String msg){
​System.out.println("发送消息......");}
​/*** set方法的好处是参数的设计更加的灵活,但是通过set方式设置对象属性时,对象有可能存在中间状态(无效状态),* 并且进行属性校验时有前后顺序约束.* 怎么保证灵活设置参数又不会存在中间状态呢? 答案就是: 使用建造者模式*/public static void main(String[] args) {
​RabbitMQClient2 client2 = new RabbitMQClient2();client2.setHost("192.168.52.123");client2.setQueue("queue");client2.setMode(1);client2.setDurable(true);client2.sendMessage("Test-MSG2");}
}

3) 建造者方式实现

建造者使用步骤如下:

  1. 目标类的构造方法要传入Builder对象

  2. Builder建造者类位于目标类内部,并且使用static修饰

  3. Builder建造者对象提供内置的各种set方法,注意set方法返回的是builder对象本身

  4. Builder建造者类提供build()方法实现目标对象的创建

public class 目标类{//目标类的构造方法需要传入Builder对象public 目标类(Builder builder){}public 返回值 业务方法(参数列表){}//Builder建造者类位于目标类内部,并且使用static修饰public static class Builder(){//Builder建造者对象提供内置的各种set方法,注意set方法返回的是builder对象本身private String xxx;public Builder setXxx(String xxx){this.xxx = xxx;return this;}//Builder建造者类提供build()方法实现目标对象的创建public 目标类 build(){//校验return new 目标类(this);}}
}

重写案例代码

/*** 建造者模式* @date 2022/9/19**/
public class RabbitMQClient {
​//私有构造方法private RabbitMQClient(Builder builder) {
​}
​public static class Builder{//属性密闭性,保证对象不可变private String host = "127.0.0.1";private int port = 5672;private int mode;private String exchange;private String queue;private boolean isDurable = true;int connectionTimeout = 1000;
​public Builder setHost(String host) {this.host = host;return this;}
​public Builder setPort(int port) {this.port = port;return this;}
​public Builder setMode(int mode) {this.mode = mode;return this;}
​public Builder setExchange(String exchange) {this.exchange = exchange;return this;}
​public Builder setQueue(String queue) {this.queue = queue;return this;}
​public Builder setDurable(boolean durable) {isDurable = durable;return this;}
​public Builder setConnectionTimeout(int connectionTimeout) {this.connectionTimeout = connectionTimeout;return this;}
​
​//返回构建好的复杂对象public RabbitMQClient build(){//首先进行校验if(mode == 1){ //工作队列模式不需要设计交换机,但是队列名称一定要有if(exchange != null){throw new RuntimeException("工作队列模式无需设计交换机");}if(queue == null || queue.trim().equals("")){throw new RuntimeException("工作队列模式名称不能为空");}if(isDurable == false){throw new RuntimeException("工作队列模式必须开启持久化");}}else if(mode == 2){ //路由模式必须设计交换机,但是不能设计队列if(exchange == null){throw new RuntimeException("路由模式下必须设置交换机");}if(queue != null){throw new RuntimeException("路由模式无须设计队列名称");}}
​return new RabbitMQClient(this);}}
​public void sendMessage(String msg){System.out.println("发送消息......");}
}

测试

public class MainAPP {public static void main(String[] args) {//使用链式编程设置参数RabbitMQClient client = new RabbitMQClient.Builder().setHost("192.168.52.123").setMode(2).setExchange("text-exchange").setPort(5672).setDurable(true).build();client.sendMessage("Test");}
}

5.5. 建造者模式总结

1) 建造者模式与工厂模式区别

  • 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。

  • 建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

举例: 顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比 如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起 司,我们通过建造者模式根据用户选择的不同配料来制作披萨。

2) 建造者模式的优缺点

  • 优点

    • 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。

    • 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。

    • 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。

    • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。

  • 缺点

    • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

3) 应用场景

  • 建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。

    • 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。

    • 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。

6. 原型模式

6.1. 原型模式介绍

定义: 原型模式(Prototype Design Pattern)用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

西游记中的孙悟空 拔毛变小猴,孙悟空这种根据自己的形状复制出多个身外化身的技巧,在面向对象软件设计领域被称为原型模式.孙悟空就是原型对象.

原型模式主要解决的问题

  • 如果创建对象的成本比较大,比如对象中的数据是经过复杂计算才能得到,或者需要从RPC接口或者数据库等比较慢的IO中获取,这种情况我们就可以使用原型模式,从其他已有的对象中进行拷贝,而不是每次都创建新对象,进行一些耗时的操作.

6.2. 原型模式原理

原型模式包含如下角色:

  • 抽象原型类(Prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口.

  • 具体原型类(ConcretePrototype):实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象.

  • 客户类(Client):在客户类中,让一个原型对象克隆自身从而创建一个新的对象.由于客户类针对抽象原型类Prototype编程.因此用户可以根据需要选择具体原型类,系统具有较好的扩展性,增加或者替换具体原型类都比较方便.

6.3. 深克隆与浅克隆

根据在复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量 这个条件,原型模式的克隆机制分为两种,即浅克隆(Shallow Clone)和深克隆(Deep Clone)

1) 什么是浅克隆

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象(克隆对象与原型对象共享引用数据类型变量)。

2) 什么是深克隆

除去那些引用其他对象的变量,被复制对象的所有变量都含有与原来的对象相同的值。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

Java中的Object类中提供了 clone() 方法来实现浅克隆。需要注意的是要想实现克隆的Java类必须实现一个标识接口 Cloneable ,来表示这个Java类支持被复制.

Cloneable接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。代码如下:

3) 浅克隆代码实现:

public class ConcretePrototype implements Cloneable {
​public ConcretePrototype() {System.out.println("具体的原型对象创建完成!");}
​@Overrideprotected ConcretePrototype clone() throws CloneNotSupportedException {System.out.println("具体的原型对象复制成功!");return (ConcretePrototype)super.clone();}
}

测试

    @Testpublic void test01() throws CloneNotSupportedException {ConcretePrototype c1 = new ConcretePrototype();ConcretePrototype c2 = c1.clone();
​System.out.println("对象c1和c2是同一个对象?" + (c1 == c2));}

4) 深克隆代码实现

在ConcretePrototype类中添加一个对象属性为Person类型

public class ConcretePrototype implements Cloneable {
​private Person person;
​public Person getPerson() {return person;}
​public void setPerson(Person person) {this.person = person;}
​void show(){System.out.println("嫌疑人姓名: " +person.getName());}
​public ConcretePrototype() {System.out.println("具体的原型对象创建完成!");}
​@Overrideprotected ConcretePrototype clone() throws CloneNotSupportedException {System.out.println("具体的原型对象复制成功!");return (ConcretePrototype)super.clone();}
​
}
​
public class Person {
​private String name;
​public Person() {}
​public Person(String name) {this.name = name;}
​public String getName() {return name;}
​public void setName(String name) {this.name = name;}
}

测试

    @Testpublic void test02() throws CloneNotSupportedException {ConcretePrototype c1 = new ConcretePrototype();Person p1 = new Person();c1.setPerson(p1);
​//复制c1ConcretePrototype c2 = c1.clone();//获取复制对象c2中的Person对象Person p2 = c2.getPerson();p2.setName("xfc");
​//判断p1与p2是否是同一对象System.out.println("p1和p2是同一个对象?" + (p1 == p2));
​c1.show();c2.show();}

如果有需求场景中不允许共享同一对象,那么就需要使用深拷贝,如果想要进行深拷贝需要使用到对象序列化流 (对象序列化之后,再进行反序列化获取到的是不同对象). 代码如下:

  
  @Testpublic void test03() throws Exception {
​ConcretePrototype c1 = new ConcretePrototype();Person p1 = new Person("xfc");c1.setPerson(p1);
​//创建对象序列化输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c.txt"));
​//将c1对象写到文件中oos.writeObject(c1);oos.close();
​//创建对象序列化输入流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c.txt"));
​//读取对象ConcretePrototype c2 = (ConcretePrototype) ois.readObject();Person p2 = c2.getPerson();p2.setName("凡哥");
​//判断p1与p2是否是同一个对象System.out.println("p1和p2是同一个对象?" + (p1 == p2));
​c1.show();c2.show();}

注意:ConcretePrototype类和Person类必须实现Serializable接口,否则会抛NotSerializableException异常。

其实现在不推荐大家用Cloneable接口,实现比较麻烦,现在借助Apache Commons或者springframework可以直接实现:

  • 浅克隆:BeanUtils.cloneBean(Object obj);BeanUtils.copyProperties(S,T);

  • 深克隆:SerializationUtils.clone(T object);

BeanUtils是利用反射原理获得所有类可见的属性和方法,然后复制到target类。 SerializationUtils.clone()就是使用我们的前面讲的序列化实现深克隆,当然你要把要克隆的类实现Serialization接口。

6.4. 原型模式应用实例

模拟某银行电子账单系统的广告信发送功能,广告信的发送都是有一个模板的,从数据库查出客户的信息,然后放到模板中生成一份完整的邮件,然后交给发送机进行发送处理.

发送广告信邮件UML类图

代码实现

  • 广告模板代码

/*** 广告信模板代码* @date 2022/9/20**/
public class AdvTemplate {
​//广告信名称private String advSubject = "xx银行本月还款达标,可抽iPhone 13等好礼!";
​//广告信内容private String advContext = "达标用户请在2022年3月1日到2022年3月30参与抽奖......";
​public String getAdvSubject() {return this.advSubject;}
​public String getAdvContext() {return this.advContext;}
}
  • 邮件类代码

/*** 邮件类* @date 2022/9/20**/
public class Mail {
​//收件人private String receiver;//邮件名称private String subject;//称谓private String appellation;//邮件内容private String context;//邮件尾部, 一般是"xxx版权所有"等信息private String tail;
​
​//构造函数public Mail(AdvTemplate advTemplate) {this.context = advTemplate.getAdvContext();this.subject = advTemplate.getAdvSubject();}
​public String getReceiver() {return receiver;}
​public void setReceiver(String receiver) {this.receiver = receiver;}
​public String getSubject() {return subject;}
​public void setSubject(String subject) {this.subject = subject;}
​public String getAppellation() {return appellation;}
​public void setAppellation(String appellation) {this.appellation = appellation;}
​public String getContext() {return context;}
​public void setContext(String context) {this.context = context;}
​public String getTail() {return tail;}
​public void setTail(String tail) {this.tail = tail;}
}
  • 客户类

/*** 业务场景类* @date 2022/9/20**/
public class Client {//发送信息的是数量,这个值可以从数据库获取private static int MAX_COUNT = 6;//发送邮件public static void sendMail(Mail mail){System.out.println("标题: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()+ "\t..发送成功!");}public static void main(String[] args) {//模拟邮件发送int i = 0;//把模板定义出来,数据是从数据库获取的Mail mail = new Mail(new AdvTemplate());mail.setTail("xxx银行版权所有");while(i < MAX_COUNT){//下面是每封邮件不同的地方mail.setAppellation(" 先生 (女士)");Random random = new Random();int num = random.nextInt(9999999);mail.setReceiver(num+"@"+"liuliuqiu.com");//发送 邮件sendMail(mail);i++;}}
}
  • 运行结果

上面的代码存在的问题:

  • 发送邮件需要重复创建Mail类对象,而且Mail类的不同对象之间差别非常小,这样重复的创建操作十分的浪费资源.

  • 这种情况我们就可以使用原型模式,从其他已有的对象中进行拷贝,而不是每次都创建新对象,进行一些耗时的操作.

代码重构

  • Mail类

/*** 邮件类 实现Cloneable接口,表示该类的实例可以被复制* @date 2022/9/20**/
public class Mail implements Cloneable{
​//收件人private String receiver;//邮件名称private String subject;//称谓private String appellation;//邮件内容private String context;//邮件尾部, 一般是"xxx版权所有"等信息private String tail;
​
​//构造函数public Mail(AdvTemplate advTemplate) {this.context = advTemplate.getAdvContext();this.subject = advTemplate.getAdvSubject();}
​public String getReceiver() {return receiver;}
​public void setReceiver(String receiver) {this.receiver = receiver;}
​public String getSubject() {return subject;}
​public void setSubject(String subject) {this.subject = subject;}
​public String getAppellation() {return appellation;}
​public void setAppellation(String appellation) {this.appellation = appellation;}
​public String getContext() {return context;}
​public void setContext(String context) {this.context = context;}
​public String getTail() {return tail;}
​public void setTail(String tail) {this.tail = tail;}
​@Overridepublic Mail clone(){Mail mail = null;try {mail = (Mail)super.clone();} catch (CloneNotSupportedException e) {e.printStackTrace();}return mail;}
}
  • Client类

/*** 业务场景类* @date 2022/9/20**/
public class Client {//发送信息的是数量,这个值可以从数据库获取private static int MAX_COUNT = 6;//发送邮件public static void sendMail(Mail mail){System.out.println("标题: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()+ "\t..发送成功!");}public static void main(String[] args) {//模拟邮件发送int i = 0;//把模板定义出来,数据是从数据库获取的Mail mail = new Mail(new AdvTemplate());mail.setTail("xxx银行版权所有");while(i < MAX_COUNT){//下面是每封邮件不同的地方Mail cloneMail = mail.clone();cloneMail.setAppellation(" 先生 (女士)");Random random = new Random();int num = random.nextInt(9999999);cloneMail.setReceiver(num+"@"+"liuliuqiu.com");//发送 邮件sendMail(cloneMail);i++;}}
}

6.5. 原型模式总结

原型模式的优点

  1. 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程, 通过复制一个已有实例可以提高新实例的创建效率.

    比如,在 AI 系统中,我们经常需要频繁使用大量不同分类的数据模型文件,在对这一类文件建立对象模型时,不仅会长时间占用 IO 读写资源,还会消耗大量 CPU 运算资源,如果频繁创建模型对象,就会很容易造成服务器 CPU 被打满而导致系统宕机。通过原型模式我们可以很容易地解决这个问题,当我们完成对象的第一次初始化后,新创建的对象便使用对象拷贝(在内存中进行二进制流的拷贝),虽然拷贝也会消耗一定资源,但是相比初始化的外部读写和运算来说,内存拷贝消耗会小很多,而且速度快很多

  2. 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构(具体工厂对应具体产品),而原型模式就不需要这样,原型模式的产品复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品.

  3. 可以使用深克隆的方式保存对象状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用,比如恢复到某一历史状态,可以辅助实现撤销操作.

    在某些需要保存历史状态的场景中,比如,聊天消息、上线发布流程、需要撤销操作的程序等,原型模式能快速地复制现有对象的状态并留存副本,方便快速地回滚到上一次保存或最初的状态,避免因网络延迟、误操作等原因而造成数据的不可恢复。

原型模式缺点

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时需要修改源代码,违背了开闭原则.

使用场景

原型模式常见的使用场景有以下六种。

  • 资源优化场景。也就是当进行对象初始化需要使用很多外部资源时,比如,IO 资源、数据文件、CPU、网络和内存等。

  • 复杂的依赖场景。 比如,F 对象的创建依赖 A,A 又依赖 B,B 又依赖 C……于是创建过程是一连串对象的 get 和 set。

  • 性能和安全要求的场景。 比如,同一个用户在一个会话周期里,可能会反复登录平台或使用某些受限的功能,每一次访问请求都会访问授权服务器进行授权,但如果每次都通过 new 产生一个对象会非常烦琐,这时则可以使用原型模式。

  • 同一个对象可能被多个修改者使用的场景。 比如,一个商品对象需要提供给物流、会员、订单等多个服务访问,而且各个调用者可能都需要修改其值时,就可以考虑使用原型模式。

  • 需要保存原始对象状态的场景。 比如,记录历史操作的场景中,就可以通过原型模式快速保存记录。

7.  结语

通过本文的学习,相信您已经对Java中的创建型设计模式有了更深入的理解和掌握。设计模式不仅仅是一种技术,更是一种思维方式和解决问题的方法论。在日常开发中,灵活运用创建型设计模式,可以帮助我们更加优雅地解决各种复杂的设计和实现问题。

相关文章:

【设计模式】(万字总结)深入理解Java中的创建型设计模式

1. 前言 在软件开发的世界里&#xff0c;设计模式是一种被广泛接受并应用的解决方案。它们不仅仅是代码的设计&#xff0c;更是对问题的思考和解决的方法论。在Java开发中&#xff0c;特别是在面向对象的编程中&#xff0c;设计模式尤为重要。创建型设计模式&#xff0c;作为设…...

【全面讲解下Docker in Docker的原理与实践】

🌈个人主页: 程序员不想敲代码啊 🏆CSDN优质创作者,CSDN实力新星,CSDN博客专家 👍点赞⭐评论⭐收藏 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步! 👉目录 👉前言👉原理👉实践👉安全和最佳实践👉前言 🦛…...

Android Settings增加多击事件,增加开发者模式打开难度

软件平台&#xff1a;Android11 硬件平台&#xff1a;QCS6125 需求来源&#xff1a;用户通过系统异常崩溃&#xff0c;进入原生Settings页面&#xff0c;通过默认的多击版本号方式打开开发者模式&#xff0c;继而打开adb调试开关&#xff0c;安装三方apk。 对付这种需求本来有…...

【相机与图像】1. 相机模型的介绍:内参、外参、畸变参数

想着整理下相机模型&#xff08;内容上参考 slam十四讲&#xff09;、相机的内外参标定。方便自己的使用和回顾。 不过&#xff0c;内外参标定啥时候记录随缘 -_- 概述 【构建相机模型】 相机将三位世界中的坐标点&#xff08;单位为米&#xff09;映射到二维图像平面&#xff…...

Linux内核netlink机制 - 用户空间和内核空间数据传输

简介&#xff1a; Netlink socket 是一种Linux特有的socket&#xff0c;用于实现用户空间与内核空间通信的一种特殊的进程间通信方式(IPC) &#xff0c;也是网络应用程序与内核通信的最常用的接口。 Netlink 是一种在内核和用户应用间进行双向数据传输的非常好的方式&a…...

Node.js自动化处理TOML文件

在软件开发过程中&#xff0c;自动化处理配置文件是一种常见的需求。TOML&#xff08;Tom’s Obvious, Minimal Language&#xff09;是一种用于配置文件的简单易读的格式。本文将展示如何使用Node.js和一些流行的库来自动化读取、修改并写入TOML文件。 1. 准备工作 在开始之前…...

Spring boot 后端向前端发送日期时间发现少了8小时

问题 数据库 后端的控制台输出 前端控制台输出 可以发现少了8小时 问题 springboot 向前端响应数据是默认 Json 格式&#xff0c;所以会有类型转换&#xff0c;springboot 就通过 Jackson 来对 data 类型数据进行转换&#xff0c;但是Jackson 类型的时区是 GMT&#xff0c;与…...

MySQL数据库(基础篇)

&#x1f30f;个人博客主页&#xff1a;心.c 前言&#xff1a;今天讲解的是MySQL的详细知识点的&#xff0c;希望大家可以收货满满&#xff0c;话不多说&#xff0c;直接开始搞&#xff01; &#x1f525;&#x1f525;&#x1f525;文章专题&#xff1a;MySQL &#x1f63d;感…...

ffmpeg ffplay.c 源码分析二:数据读取线程

本章主要是分析 数据读取线程read_thread 中的工作。如上图红色框框的部分 从ffplay框架分析我们可以看到&#xff0c;ffplay有专⻔的线程read_thread()读取数据&#xff0c; 且在调⽤av_read_frame 读取数据包之前需要做&#xff1a; 1.例如打开⽂件&#xff0c; 2.查找配置解…...

国科大作业考试资料《人工智能原理与算法》2024新编-第十三次作业整理

1、假设我们从决策树生成了一个训练集&#xff0c;然后将决策树学习应用于该训练集。当训练集的大小趋于无穷时&#xff0c;学习算法将最终返回正确的决策树吗&#xff1f;为什么是或不是&#xff1f; 本次有两个参考&#xff1a; 参考一&#xff1a; 当训练集的大小趋于无穷…...

Netdevops入门之Telnetlib语法案例

1、Telnetlib模块&#xff1a; 支持telnet/ssh远程访问的模块很多&#xff0c;常见的有telnetlib、ciscolib、paramiko、netmiko、pexpect,其中telnetlib和ciscolib对应telnet协议&#xff0c;后面3个对应SSH协议。 ①-通过ENSP环境搭建实验环境 ②-基础语法-telnetlib案例1&…...

永辉“爆改”续命

2024年&#xff0c;在线下零售一片哀嚎之下&#xff0c;胖东来似乎活成了国内零售业的密码&#xff0c;同时也变身成为各大零售企业的咨询公司&#xff0c;四处帮助“友商”救火&#xff0c;就连一直名声在外的永辉超市&#xff0c;也成了救火对象。 作为曾经国内生鲜超市的“…...

IEEE双一区Top“饱受诟病”!曾上医院黑名单,国人占比高达82.405%,目测即将拉下神坛?

本周投稿推荐 SCI&EI • 1区计算机类&#xff0c;3.5-4.0&#xff08;1个月录用&#xff09; • CCF推荐&#xff0c;1区-Top&#xff08;3天初审&#xff09; EI • 各领域沾边均可&#xff08;2天录用&#xff09; 知网&#xff08;CNKI&#xff09;、谷歌学术 •…...

Hive环境搭建(Mysql数据库)

【实验目的】 1) 了解hive的作用 2) 熟练hive的配置过程&#xff08;Mysql数据库&#xff09; 【实验原理】 Hive工具中默认使用的是derby数据库&#xff0c;该数据库使用简单&#xff0c;操作灵活&#xff0c;但是存在一定的局限性&#xff0c;hive支持使用第三方数据库&…...

【ESP32 IDF SPI硬件驱动W25Q64】

目录 SPISPI介绍idf配置初始化配置通信 驱动代码 SPI SPI介绍 详细SPI介绍内容参考我之前写的内容【ESP32 IDF 软件模拟SPI驱动 W25Q64存储与读取数组】 idf配置 初始化配置 spi_bus_initialize() 参数1 &#xff1a;spi几&#xff0c;例如spi2,spi3 参数2&#xff1a;…...

太原高校大学智能制造实验室数字孪生可视化系统平台建设项目验收

随着科技的不断进步&#xff0c;智能制造已经成为推动制造业转型升级的重要力量。太原高校大学智能制造实验室紧跟时代步伐&#xff0c;积极推进数字孪生可视化系统平台的建设&#xff0c;并于近日圆满完成了项目的验收工作。这一里程碑式的成果&#xff0c;不仅标志着实验室在…...

Kafka消息队列

目录 什么是消息队列 高可用性 高扩展性 高可用性 持久化和过期策略 consumer group 分组消费 ZooKeeper 什么是消息队列 普通版消息队列 说白了就是一个队列,生产者生产多少,放在消息队列中存储,而消费者想要多少拿多少,按序列号消费 缓存信息 生产者与消费者解耦…...

@Transactional注解及其事务管理

1. 事务问题概述 事务问题主要来源于数据库&#xff0c;与数据库事务紧密相关。事务的四大特性&#xff08;ACID&#xff09;&#xff1a; 原子性&#xff08;Atomicity&#xff09;&#xff1a;事务要么完全执行&#xff0c;要么完全不执行。一致性&#xff08;Consistency&a…...

ROS2入门到精通—— 3-1 ROS2实战:CasADi —— 优化计算的利器

0 前言 CasADi是一个强大的开源软件库,它提供了一种灵活且高效的方式来构建和解决复杂的非线性模型。通过其直观的API,开发者可以轻松地定义数学表达式并自动求解微分方程、优化问题以及符号计算等任务。 CasADi基于Python编写,但提供了C++和MATLAB接口,使得不同背景的开发…...

日拱一卒 | JVM

文章目录 什么是JVM&#xff1f;JVM的组成JVM的大致工作流程JVM的内存模型 什么是JVM&#xff1f; 我们知道Java面试&#xff0c;只要你的简历上写了了解JVM&#xff0c;那么你就必然会被问到以下问题&#xff1a; 什么是JVM&#xff1f;简单说一下JVM的内存模型&#xff1f;…...

乐尚代驾六订单执行一

加载当前订单 需求 无论是司机端&#xff0c;还是乘客端&#xff0c;遇到页面切换&#xff0c;重新登录小程序等&#xff0c;只要回到首页面&#xff0c;查看当前是否有正在执行订单&#xff0c;如果有跳转到当前订单执行页面 之前这个接口已经开发&#xff0c;为了测试&…...

SciPy 与 MATLAB 数组

SciPy 与 MATLAB 数组 SciPy 是一个开源的 Python 库,广泛用于科学和工程计算。它构建在 NumPy 数组的基础之上,提供了许多高级科学计算功能。MATLAB 是一个高性能的数值计算环境,它也使用数组作为其基础数据结构。在这篇文章中,我们将探讨 SciPy 和 MATLAB 在数组操作上的…...

基于vue-grid-layout插件(vue版本)实现增删改查/拖拽自动排序等功能(已验证、可正常运行)

前端时间有个需求&#xff0c;需要对33&#xff08;不一定&#xff0c;也可能多行&#xff09;的卡片布局&#xff0c;进行拖拽&#xff0c;拖拽过程中自动排序&#xff0c;以下代码是基于vue2&#xff0c;可直接运行&#xff0c;报错可评论滴我 部分代码优化来自于GPT4o和Clau…...

DBoW3相关优化脉络

1 DBow3 GitHub - rmsalinas/DBow3: Improved version of DBow2 2 优化后得到fbow GitHub - rmsalinas/fbow: FBOW (Fast Bag of Words) is an extremmely optimized version of the DBow2/DBow3 libraries. 其中fbow是ucoslam的一部分&#xff1b; ucoslam GitHub - la…...

qt 如何制作动态库插件

首先 首先第一点要确定我们的接口是固定的&#xff0c;也就是要确定 #ifndef RTSPPLUGIN_H #define RTSPPLUGIN_H #include "rtspplugin_global.h" typedef void (*func_callback)(uint8_t* data,int len,uint32_t ssrc,uint32_t ts,const char* ipfrom,uint16_t f…...

一种docker start放回Error response from daemon: task xxx错误的解决方式

1. 问题描述 执行systemctl daemon-reload与systemctl restart docker命令后&#xff0c;发现docker中有的应用无法启动&#xff0c;并显示出Exit(255)的错误提示。 重新执行docker start 容器id后发现返回&#xff0c;Error response from daemon: task xxx的错误。 2. 问题…...

规控面试常见问题

一、项目中遇到的困难或者挑战是什么? 二、A*算法原理(伪代码) 输入:代价地图、start 、 goal(Node结构,包含x、y、g、h、id、pid信息) 首先初始化:创建一个优先级队列openlist,它是一个最小堆,根据节点的f值排序 ( priority_queue<Node, std::vector<Node…...

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙 文章目录 代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙17.太平洋大西洋水流问题一、DFS二、BFS三、本题总结 82…...

【全网最全】CSDN博客的文字颜色、字体和字号设置

文章目录 一、字体颜色二、字体大小三、字体类型四、字体背景色 在这篇博客中&#xff0c;我们将深入探讨如何在Markdown编辑器中设置文字颜色、大小、字体与背景色。Markdown本身并不直接支持这些功能&#xff0c;但通过结合HTML标签和CSS样式&#xff0c;我们可以实现这些视觉…...

C#实现数据采集系统-Mqtt实现采集数据转发

在数据采集系统中,通过ModbusTcp采集到数据之后,再通过MQTT转发到其他应用 MQTT操作 安装MQTT mqtt介绍和环境安装 使用MQTT 在C#/Net中使用Mqtt MQTT类封装 MQTT配置类 public class MqttConfig{public string Ip {get; set;...

common-intellisense:助力TinyVue 组件书写体验更丝滑

本文由体验技术团队Kagol原创~ 前两天&#xff0c;common-intellisense 开源项目的作者 Simon-He95 在 VueConf 2024 群里发了一个重磅消息&#xff1a; common-intellisense 支持 TinyVue 组件库啦&#xff01; common-intellisense 插件能够提供超级强大的智能提示功能&…...

图片在线压缩有效方法详解,分享7款最佳图片压缩工具免费(全新)

当您的系统中图片数量不断增加&#xff0c;却无法删除时&#xff0c;那么就需要通过图片压缩来解决您的问题。随着图片文件的增大&#xff0c;高分辨率图片占据了大量存储空间。而此时系统中的存储空间也开始变得不够用&#xff0c;无法跟上高质量图片的增长。因此&#xff0c;…...

electron安装及快速创建

electron安装及快速创建 electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 详细内容见官网&#xff1a;https://www.electronjs.org/zh/docs/latest/。 今天来记录下练习中的安装过程和hello world的创建。 创建项目文件夹&#xff0c;并执行npm 初始化命…...

需要消化的知识点

需要消化 消灭清单 如何自定义一个Interceptor拦截器&#xff1f; 后端开发可以用上的前端技巧 10个堪称神器的 Java 学习网站. 【前端胖头鱼】11 chrome高级调试技巧&#xff0c;学会效率直接提升666% 【前端胖头鱼】10个我经常逛的“小网站” 【前端劝退师lv-6】Chrome D…...

2024年7月25日(Git gitlab以及分支管理 )

分布式版本控制系统 一、Git概述 Git 是一种分布式版本控制系统,用于跟踪和管理代码的变更。它是由Linus Torvalds创建的,最 初被设计用于Linux内核的开发。Git允许开发人员跟踪和管理代码的版本,并且可以在不同的开 发人员之间进行协作。 Github 用的就是Git系统来管理它们的…...

pdf格式过大怎么样变小 pdf文件过大如何缩小上传 超实用的简单方法

面对体积庞大的 PDF 文件&#xff0c;我们常常需要寻找有效的方法来缩减其大小。这不仅能够优化存储空间&#xff0c;还能提升文件的传输和打开速度。PDF文件以其稳定性和跨平台兼容性成为工作和学习中的重要文件格式。然而&#xff0c;当我们需要通过邮件发送或上传大文件时&a…...

前端文件下载word乱码问题

记录一次word下载乱码问题&#xff1a; 用的请求是axios库&#xff0c;然后用Blob去接收二进制文件 思路&#xff1a;现在的解决办法有以下几种&#xff0c;看看是对应哪种&#xff0c;可以尝试解决 1.将响应类型设为blob&#xff0c;这也是最重要的&#xff0c;如果没有解决…...

repo中的default.xml文件project name为什么一样?

文章目录 default.xml文件介绍为什么 name 是一样的&#xff0c;path 不一样&#xff1f;总结 default.xml文件介绍 在 repo 工具的 default.xml 文件中&#xff0c;定义了多个 project 元素&#xff0c;每个元素都代表一个 Git 仓库。 XML 定义了多个不同的 project 元素&…...

<section id=“nice“ data-tool=“mdnice编辑器“ data-webs

大模型日报 2024-07-24 大模型资讯 Meta发布最大Llama 3 AI模型&#xff0c;语言和数学能力提升 摘要: Meta公司发布了其迄今为止最大的Llama 3人工智能模型。该模型主要免费提供&#xff0c;具备多语言处理能力&#xff0c;并在语言和数学方面表现出显著提升。 Meta发布最强AI…...

作业7.26~28

全双工&#xff1a; 通信双方 既可以发送&#xff0c;也可以接收数据 1. 利用多线程 或者 多进程&#xff0c; 实现TCP服务器 和 客户端的全双工通信 思路&#xff1a; 服务器和客户端&#xff0c; 在建立通信以后&#xff0c;可以创建线程&#xff0c;在线程编写另一个功能代…...

自定义webIpad证件相机(webRTC)

该技术方案可用于各浏览器自定义相机开发 相机UI&#xff08;index.html&#xff09; <!DOCTYPE html> <html lang"zh" prew"-1"><head><meta charset"UTF-8"><meta name"viewport"content"user-sc…...

GO发票真伪批量查验方法、数电票查验接口

“教”给机器标注数据的正确率就决定了人工智能判断的正确率。翔云人工智能开放平台的OCR产品经过我们的开发人员精心调“教”&#xff0c;识别率高、识别速度快。 发票&#xff0c;是发生的成本、费用或收入的原始凭证。于公司来说&#xff0c;发票主要是公司做账的依据&…...

【Go系列】Go的UI框架Fyne

前言 总有人说Go语言是一门后端编程语言。 Go虽然能够很好地处理后端开发&#xff0c;但是者不代表它没有UI库&#xff0c;不能做GUI&#xff0c;我们一起来看看Go怎么来画UI吧。 正文 Go语言由于其简洁的语法、高效的性能和跨平台的编译能力&#xff0c;非常适合用于开发GUI…...

.NET MAUI:跨平台开发的未来

常用资源 &#xff08;1&#xff09;.NET MAUI8构建应用文档。 Build your first .NET MAUI app - .NET MAUI | Microsoft Learn 一、什么是 .NET MAUI&#xff1f; .NET Multi-platform App UI (.NET MAUI) 是微软推出的一款跨平台开发框架。作为 Xamarin.Forms 的下一代产…...

VSCode切换默认终端

我的VSCode默认终端为PowerShell&#xff0c;每次新建都会自动打开PowerShell。但是我想让每次都变为cmd&#xff0c;也就是Command Prompt 更改默认终端的操作方法如下&#xff1a; 键盘调出命令面板&#xff08;CtrlShiftP&#xff09;中,输入Terminal: Select Default Prof…...

卫星观测叶绿素的相反信号

Contrasted Trends in Chlorophyll-a Satellite Products 运用卫星产品研究Chl的长时间序列变化时需要注意 Introduction &#xff08;1&#xff09;研究叶绿素的长期变化&#xff0c;需要至少40年的长时间序列&#xff1b; &#xff08;2&#xff09;Tian and Zhang 2023报告…...

2024年最新NVIDIA T4价格表及行业趋势!

英伟达&#xff08;NVIDIA&#xff09;作为目前全球T0级别的GPU制造商&#xff0c;其T4系列显卡以其卓越的计算性能和能效比&#xff0c;在数据中心、云计算及AI领域占据重要地位。 一、NVIDIA T4价格表概览 在探讨NVIDIA T4显卡的价格时&#xff0c;我们需要从直接购买和租赁…...

HTML + CSS编程规范

编程规范 HTML CSS 命名规范 HTML CSS 命名规范 1. 命名需要是具备语义性的单词&#xff0c;不能用 数字 拼音 数字,符号开头正确示范 &#xff1a; wrap description title content错误示范 : aaaa a1 $we 4tdds 2. 命名需要多个单词连接的情况下, 标记语言中可以使用 …...

机器学习之人脸识别-使用 scikit-learn 和人工神经网络进行高效人脸识别

文章摘要 本文将介绍如何使用 Python 的 scikit-learn 库和人工神经网络&#xff08;ANN&#xff09;来识别人脸。我们将使用 LFW 数据集&#xff08;Labeled Faces in the Wild&#xff09;&#xff0c;这是一个广泛用于人脸识别基准测试的大型人脸数据库。我们将展示如何准备…...

【虚拟化】KVM概念和架构

目录 一、什么是KVM&#xff1f; 二、KVM的功能 2.1 主要的功能 2.2 其它功能 三、KVM核心组件及作用 四、KVM与VMware的优势 五、KVM架构 六、qemu介绍 七、创建虚拟机流程 一、什么是KVM&#xff1f; Kernel-based Virtual Machine的简称&#xff0c;KVM 是基于虚拟…...