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

设计模式之享元模式

参考资料

  • 曾探《JavaScript设计模式与开发实践》;
  • 「设计模式 JavaScript 描述」享元模式
  • 设计模式之享元模式
  • Javascript 设计模式 - 享元模式

定义

享元模式的英文叫:Flyweight Design Pattern。享元设计模式是用于性能优化的模式,这种设计模式的核心在于可以共享技术并支持对大量细分过后的对象进行调整,如果系统中因为创建大量类似的对象而导致内存占用过高,享元设计模式在其中就会起到非常重要的作用,因为它可以使其减少重复创建相同类似的实例对象。在JavaScript中浏览器特别是移动端的浏览器部分所能够使用的内存并不是很多,所以在其中节省内存就变得至关重要。

享元模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。

就是分享之意,指一物被众人共享,而这也正是该模式的终旨所在,意为单元,蝇量级的个体,该模式的核心就是使用共享技术来有效的支持大量的细粒度对象。

使用场景:

  • 数据库连接池;
  • 线程池;
  • String常量池;

内部状态与外部状态

享元模式要求将对象的属性划分为内部状态外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,那么如何划分内部状态和外部状态呢?

  • 内部状态存储于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态 可以从对象身上剥离出来,并储存在外部。

剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整 的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系 统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。

使用享元模式的关键是如何区别内部状态外部状态

举例说明-下象棋

我们还是通过网络上开房间下象棋的经典案例来说明享元设计模式。一个象棋游戏平台肯定有很多的房间,每个房间都有一盘棋,每盘棋上有32颗棋子,就有32个对象,但是每个房间里的【将】的属性有很多都是相同的,比如名字、颜色等,而且它们在各个房间的棋盘中都是不会发生变化的,唯一不同的就是所在的位置不一样。所以相同的属性就可以抽出来作为共享单元。

看例子:

享元类,即棋子公共的属性

public class ShareChessAttr {private Integer id;private String name;private String color;public ShareChessAttr(Integer id, String name, String color) {this.id = id;this.name = name;this.color = color;}
}

定义一个工厂去获取对应棋子的享元

public class ShareChessAttrFactory {public static final HashMap<Integer, ShareChessAttr> shareChessAttrMap = new HashMap();static {shareChessAttrMap.put(1, new ShareChessAttr(1, "将", "红"));shareChessAttrMap.put(2, new ShareChessAttr(2, "帅", "黑"));shareChessAttrMap.put(3, new ShareChessAttr(3, "车", "红"));shareChessAttrMap.put(4, new ShareChessAttr(4, "车", "黑"));}public static ShareChessAttr getShareChessAttr(Integer i) {return shareChessAttrMap.get(i);}
}

定义棋子

public class Chess {private ShareChessAttr shareChessAttr;private Integer positionX;private Integer positionY;public Chess(ShareChessAttr shareChessAttr, Integer positionX, Integer positionY) {this.shareChessAttr = shareChessAttr;this.positionX = positionX;this.positionY = positionY;}
}

定义棋盘

public class ChessBoard {private HashMap<Integer, Chess> chessOnBoard = new HashMap<Integer, Chess>();public ChessBoard() {chessOnBoard.put(1, new Chess(ShareChessAttrFactory.getShareChessAttr(1), 5, 0));chessOnBoard.put(2, new Chess(ShareChessAttrFactory.getShareChessAttr(2), 5, 0));}
}

如此每个棋盘中的棋子的id,名字和颜色都指向了同一个shareChessAttr。实现享元。

举例说明- HashMap 对象池

代码关键点:用 HashMap 对象池存储这些对象。

// 享元模式,对象池缓存对象
class colorFactory {constructor(name) {this.colors = {};}create(name) {let color = this.colors[name];if (color) return color;this.colors[name] = new Color(name);return this.colors[name];}
};

举例说明- 文件上传

在云文件上传模块的开发中,我们可以借助享元模式提升了程序的性能。下面我们就讲述这个例子。

对象爆炸

在云文件上传模块的开发中,可能会出现对象爆炸的问题。云文件的文件上传功能虽然可以选择依照队列,一个一个地排队上传,但也支持同时选择 2000 个文件。每一个文件都对应着一个 JavaScript 上传对象的创建,可是往程序里同时 new 了 2000 个 upload 对象,结 果可想而知,Chrome 中还勉强能够支撑,IE 下直接进入假死状态。

云文件支持好几种上传方式,比如浏览器插件、Flash 和表单上传等,为了简化例子,我们先假设只有插件和 Flash 这两种。不论是插件上传,还是 Flash 上传,原理都是一样的,当用户选择了文件之后,插件和 Flash 都会通知调用 Window 下的一个全局 JavaScript 函数,它的名字是 startUpload,用户选择的文件列表被组合成一个数组 files 塞进该函数的参数列表里,代码如下:

let id = 0;window.startUpload = function (uploadType, files) { // uploadType 区分是控件还是 flash for (let i = 0; i < files.length; i++) {const uploadObj = new Upload(uploadType, files[i].fileName, files[i].fileSize);uploadObj.init(id++); // 给 upload 对象设置一个唯一的 id }
};

当用户选择完文件之后,startUpload 函数会遍历 files 数组来创建对应的 upload 对象。接下来定义 Upload 构造函数,它接受 3 个参数,分别是插件类型文件名文件大小。这些信息都已经被插件组装在 files 数组里返回,代码如下:

const Upload = function (uploadType, fileName, fileSize) {this.uploadType = uploadType;this.fileName = fileName;this.fileSize = fileSize;this.dom = null;
};Upload.prototype.init = function (id) {const that = this;this.id = id;this.dom = document.createElement('div');this.dom.innerHTML ='<span>文件名称:' + this.fileName + ', 文件大小: ' + this.fileSize + '</span>' +'<button class="delFile">删除</button>';this.dom.querySelector('.delFile').onclick = function () {that.delFile();}document.body.appendChild(this.dom);
};

同样为了简化示例,我们暂且去掉了 upload 对象的其他功能,只保留删除文件的功能,对应 的方法是 Upload.prototype.delFile。该方法中有一个逻辑:当被删除的文件小于 3000 KB 时,该文件将被直接删除。否则页面中会弹出一个提示框,提示用户是否确认要删除该文件,代码如下:

Upload.prototype.delFile = function () {if (this.fileSize < 3000) {return this.dom.parentNode.removeChild(this.dom);}if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {return this.dom.parentNode.removeChild(this.dom);}
};

接下来分别创建 3 个插件上传对象和 3 个 Flash 上传对象:

startUpload('plugin', [{fileName: '1.txt',fileSize: 1000},{fileName: '2.html',fileSize: 3000},{fileName: '3.txt',fileSize: 5000}
]);startUpload('flash', [{fileName: '4.txt',fileSize: 1000},{fileName: '5.html',fileSize: 3000},{fileName: '6.txt',fileSize: 5000}
]);

当点击删除最后一个文件时,可以看到弹出了是否确认删除的提示,如下图所示。

享元模式重构文件上传

上一节的代码是第一版的文件上传,在这段代码里有多少个需要上传的文件,就一共创建了多少个 upload 对象,接下来我们用享元模式重构它。

首先,我们需要确认插件类型 uploadType 是内部状态,那为什么单单 uploadType 是内部状态呢?前面讲过,划分内部状态和外部状态的关键主要有以下几点。

  • 内部状态储存于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

在文件上传的例子里,upload 对象必须依赖 uploadType 属性才能工作,这是因为插件上传、Flash 上传、表单上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,必须在对象创建之初就明确它是什么类型的插件,才可以在程序的运行过程中,让它们分别调用各自的 startpausecanceldel 等方法。

实际上在云文件的真实代码中,虽然插件和 Flash 上传对象最终创建自一个大的工厂类,但它们实际上根据 uploadType 值的不同,分别是来自于两个不同类的对象。(在目前的例子中,为了 简化代码,我们把插件和 Flash 的构造函数合并成了一个。)

一旦明确了 uploadType,无论我们使用什么方式上传,这个上传对象都是可以被任何文件共 用的。而 fileNamefileSize 是根据场景而变化的,每个文件的 fileNamefileSize 都不一样,fileNamefileSize 没有办法被共享,它们只能被划分为外部状态。

剥离外部状态

明确了 uploadType 作为内部状态之后,我们再把其他的外部状态从构造函数中抽离出来,Upload 构造函数中只保留 uploadType 参数:

const Upload = function (uploadType) {this.uploadType = uploadType;
};

Upload.prototype.init 函数也不再需要,因为 upload 对象初始化的工作被放在了 uploadManager.add 函数里面,接下来只需要定义 Upload.prototype.del 函数即可:

Upload.prototype.delFile = function (id) {uploadManager.setExternalState(id, this);  // (1) if (this.fileSize < 3000) {return this.dom.parentNode.removeChild(this.dom);}if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {return this.dom.parentNode.removeChild(this.dom);}
};

在开始删除文件之前,需要读取文件的实际大小,而文件的实际大小被储存在外部管理器 uploadManager 中,所以在这里需要通过 uploadManager.setExternalState 方法给共享对象设置正确的 fileSize,上段代码中的(1)处表示把当前 id 对应的对象的外部状态都组装到共享对象中。

工厂进行对象实例化

接下来定义一个工厂来创建 upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象:

const UploadFactory = (function () {const createdFlyWeightObjs = {};return {create: function (uploadType) {if (createdFlyWeightObjs[uploadType]) {return createdFlyWeightObjs[uploadType];}return createdFlyWeightObjs[uploadType] = new Upload(uploadType);}}
})();

管理器封装外部状态

现在我们来完善前面提到的 uploadManager 对象,它负责向 UploadFactory 提交创建对象的请求,并用一个 uploadDatabase 对象保存所有 upload 对象的外部状态,以便在程序运行过程中给 upload 共享对象设置外部状态,代码如下:

const uploadManager = (function () {const uploadDatabase = {};return {add: function (id, uploadType, fileName, fileSize) {const flyWeightObj = UploadFactory.create(uploadType);const dom = document.createElement('div');dom.innerHTML ='<span>文件名称:' + fileName + ', 文件大小: ' + fileSize + '</span>' +'<button class="delFile">删除</button>';dom.querySelector('.delFile').onclick = function () {flyWeightObj.delFile(id);}document.body.appendChild(dom);uploadDatabase[id] = {fileName: fileName,fileSize: fileSize,dom: dom};return flyWeightObj;},setExternalState: function (id, flyWeightObj) {const uploadData = uploadDatabase[id];for (const i in uploadData) {flyWeightObj[i] = uploadData[i];}}}
})();

然后是开始触发上传动作的 startUpload 函数:

let id = 0;
window.startUpload = function (uploadType, files) {for (let i = 0; i < files.length; i++) {const uploadObj = uploadManager.add(++id, uploadType, files[i].fileName, files[i].fileSize);}
};

最后是测试时间,运行下面的代码后,可以发现运行结果跟用享元模式重构之前一致:

startUpload('plugin', [{fileName: '1.txt',fileSize: 1000},{fileName: '2.html',fileSize: 3000},{fileName: '3.txt',fileSize: 5000}
]);startUpload('flash', [{fileName: '4.txt',fileSize: 1000},{fileName: '5.html',fileSize: 3000},{fileName: '6.txt',fileSize: 5000}
]);

享元模式重构之前的代码里一共创建了 6个 upload 对象,而通过享元模式重构之后,对象的数量减少为 2,更幸运的是, 就算现在同时上传 2000个文件,需要创建的 upload 对象数量依然是 2。

享元模式的适用性

享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题,从前面两组代码的比较可以看到,使用了享元模式之后,我们需要分别多维护一个 factory 对象和一个 manager 对 象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。

享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式。

  • 一个程序中使用了大量的相似对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 对象的大多数状态都可以变为外部状态。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。

可以看到,文件上传的例子完全符合这四点。

再谈内部状态和外部状态

如果顺利的话,通过前面的例子我们已经了解了内部状态和外部状态的概念以及享元模式的工作原理。我们知道,实现享元模式的关键是把内部状态和外部状态分离开来。有多少种内部状态的组合,系统中便最多存在多少个共享对象,而外部状态储存在共享对象的外部,在必要时被传入共享对象来组装成一个完整的对象。现在来考虑两种极端的情况,即对象没有外部状态和没有内部状态的时候。

没有内部状态的享元

在文件上传的例子中,我们分别进行过插件调用和 Flash 调用,即 startUpload('plugin', [])startUpload('flash', []),导致程序中创建了内部状态不同的两个共享对象。也许你会奇怪,在文件上传程序里,一般都会提前通过特性检测来选择一种上传方式,如果浏览器支持插件就用插件上传,如果不支持插件,就用 Flash 上传。那么,什么情况下既需要插件上传又需要 Flash 上传呢?

实际上这个需求是存在的,很多网盘都提供了极速上传(控件)与普通上传(Flash)两种模式,如果极速上传不好使(可能是没有安装控件或者控件损坏),用户还可以随时切换到普通上传模式,所以这里确实是需要同时存在两个不同的 upload 共享对象。

但不是每个网站都必须做得如此复杂,很多小一些的网站就只支持单一的上传方式。假设我们是这个网站的开发者,不需要考虑极速上传与普通上传之间的切换,这意味着在之前的代码中作为内部状态的 uploadType 属性是可以删除掉的。 在继续使用享元模式的前提下,构造函数 Upload 就变成了无参数的形式:

const Upload = function(){}; 

其他属性如 fileNamefileSizedom 依然可以作为外部状态保存在共享对象外部。在 uploadType 作为内部状态的时候,它可能为控件,也可能为 Flash,所以当时最多可以组合出两个共享对象。而现在已经没有了内部状态,这意味着只需要唯一的一个共享对象。现在我们要改写创建享元对象的工厂,代码如下:

const UploadFactory = (function () {let uploadObj;return {create: function () {if (uploadObj) {return uploadObj;}return uploadObj = new Upload();}}
})();

管理器部分的代码不需要改动,还是负责剥离和组装外部状态。可以看到,当对象没有内部状态的时候,生产共享对象的工厂实际上变成了一个单例工厂。虽然这时候的共享对象没有内部状态的区分,但还是有剥离外部状态的过程,我们依然倾向于称之为享元模式。

没有外部状态的享元

网上许多资料中,经常把 Java 或者 C#的字符串看成享元,这种说法是否正确呢?我们看看下面这段 Java 代码,来分析一下:

// Java 代码
public class Test {public static void main(String args[]) {String a1 = new String("a").intern();String a2 = new String("a").intern();System.out.println(a1 == a2); // true }
}

在这段 Java 代码里,分别 new 了两个字符串对象 a1a2intern 是一种对象池技术, new String("a").intern()的含义如下。

  • 如果值为 a 的字符串对象已经存在于对象池中,则返回这个对象的引用。
  • 反之,将字符串 a 的对象添加进对象池,并返回这个对象的引用。

所以 a1 == a2 的结果是 true,但这并不是使用了享元模式的结果,享元模式的关键是区别内部状态和外部状态。享元模式的过程是剥离外部状态,并把外部状态保存在其他地方,在合适的时刻再把外部状态组装进共享对象。这里并没有剥离外部状态的过程,a1a2 指向的完全就是同一个对象,所以如果没有外部状态的分离,即使这里使用了共享的技术,但并不是一个纯粹的享元模式。

优缺点

优点:

  • 大大减少对象的创建,降低系统的内存,使效率提高。

缺点:

  • 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

总结

享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在 大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。

享元模式的思想与单例比较类似,但是单例模式强调的是全局唯一,而享元模式则强调的是内存共享。

相关文章:

设计模式之享元模式

参考资料 曾探《JavaScript设计模式与开发实践》&#xff1b;「设计模式 JavaScript 描述」享元模式设计模式之享元模式Javascript 设计模式 - 享元模式 定义 享元模式的英文叫&#xff1a;Flyweight Design Pattern。享元设计模式是用于性能优化的模式&#xff0c;这种设计…...

【GAMES101】05 Rasterization(Triangles)

光栅化过程&#xff1a;将一系列变换后的三角形转换为像素的过程。 三角形在图形学中得到很多的应用。 最基础的多边形&#xff08;边数最少&#xff09;。任何多边形都可以拆成三角形。性质&#xff1a;三角形内部一定是平面的。三角形内外部定义非常清楚。定义三个顶点后&a…...

13. Pod 从入门到深入理解(二)

本章讲解知识点 Pod 容器共享 VolumeConfigMapSecretDownward APIEmptyDir VolumeHostPath Volume1. Pod 容器共享 Volume 1.1. Volume 的背景及需要解决的问题 存储是必不可少的,对于服务运行产生的日志、数据,必须有一个地方进行保存,但是我们的容器每一次重启都是“恢复…...

ORBBEC(奥比中光)AstraPro相机在ROS2下的标定与D2C(标定与配准)

文章目录 1.rgb、depth相机标定矫正1.1.标定rgb相机1.2.标定depth相机1.3.rgb、depth相机一起标定&#xff08;效果重复了&#xff0c;但是推荐使用&#xff09;1.4.取得标定结果1.4.1.得到的标定结果的意义 1.5.IR、RGB相机分别应用标定结果1.5.1.openCV应用标定结果1.5.2.ros…...

常量与变量:编程中重要的两种数据类型

常量与变量 在编程中&#xff0c;我们常常需要存储一些数据。这些数据有些是恒定不变的&#xff0c;有些却是可以随时变化的。对于恒定不变的数据&#xff0c;我们称之为常量&#xff1b;对于可以变化的数据&#xff0c;我们则称之为变量。这两种数据类型在程序中非常重要&…...

( 数组和矩阵) 287. 寻找重复数 ——【Leetcode每日一题】

❓287. 寻找重复数 难度&#xff1a;中等 给定一个包含 n 1 个整数的数组 nums &#xff0c;其数字都在 [1, n] 范围内&#xff08;包括 1 和 n&#xff09;&#xff0c;可知至少存在一个重复的整数。 假设 nums 只有 一个重复的整数 &#xff0c;返回 这个重复的数 。 你…...

【学习笔记】「JOISC 2022 Day2」复制粘贴 3

看了正解。我觉得很厉害。虽然用减枝水过去了。 区间 d p dp dp。但是这个转移怎么看都不是 O ( 1 ) O(1) O(1)的。 border \text{border} border 那么 trick \text{trick} trick应该都能看出来。能进行剪切操作当且仅当 s [ l , p ] s [ q , r ] s_{[l,p]}s_{[q,r]} s[l,p]​…...

武忠祥老师每日一题||定积分基础训练(三)

常用的基本不等式&#xff1a; sin ⁡ x < x < t a n x , x ∈ ( 0 , π 2 ) \sin x<x<\ tan x,x\in(0,\frac{\pi}{2}) sinx<x< tanx,x∈(0,2π​) e x ≥ 1 x , x ∈ ( − ∞ , ∞ ) e^x\ge1x,x\in(-\infty,\infty) ex≥1x,x∈(−∞,∞) x 1 x ≤ ln …...

Docker安装常用软件-Apollo(有问题)

零&#xff1a;apollo概念介绍 官网网站&#xff1a;GitHub - apolloconfig/apollo: Apollo is a reliable configuration management system suitable for microservice configuration management scenarios. gitee网址&#xff1a;mirrors / ctripcorp / apollo GitCode …...

f(x)与|f(x)|,f ‘ (x),F(x)常见关系。

1.f(x)与|f(x)|关系。 1.连续关系。(f(x)在"[a,b]上连续" > |f(x)|在"[a,b]连续") ①如果f(x)在[a,b]上连续。则|f(x)|在[a,b]上连续. &#xff08;因为f(x)在x0的连续点>x0必为|f(x)|的连续点&#xff09; 注&#xff1a;”[a,b]连续“包括&#…...

今天面了一个来字节要求月薪23K,明显感觉他背了很多面试题...

最近有朋友去字节面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…...

如何使用二元三次回归分析建立预测模型?(分析、原理、代码示例)

二元三次回归是一种用于建立两个自变量与一个因变量之间关系的回归模型&#xff0c;常用于数据分析和预测。下面我会更详细地解释一下二元三次回归的原理、分析和示例代码。 1、原理 二元三次回归分析用多项式回归建立预测模型&#xff0c;其中包括两个自变量&#xff08;通常…...

面向万物智联的应用框架的思考和探索(上)

原文&#xff1a;面向万物智联的应用框架的思考和探索&#xff08;上&#xff09;&#xff0c;点击链接查看更多技术内容。 应用框架&#xff0c;是操作系统连接开发者生态&#xff0c;实现用户体验的关键基础设施。其中&#xff0c;开发效率和运行体验是永恒的诉求&#xff0c…...

《Python机器学习基础教程》第1章学习笔记

目录 第1章 引言 1.1 为何选择机器学习 1.1.1 机器学习能够解决的问题 第1章 引言 机器学习又称为预测分析或统计学习&#xff0c;是一个交叉学科&#xff0c;是从数据中提取知识。 1.1 为何选择机器学习 智能应用早期&#xff0c;使用专家设计的规则体系来设计。 缺点&…...

ClickHouse 内存管理是如何实现的

概述 本文介绍Clickhouse内存管理的实现原理。通过本文的分析&#xff0c;可以对Clickhouse的内存管理有一个概要的理解。 Clickouse内存管理组成 ClickHouse 使用内存管理系统来控制内存资源的分配和释放。内存管理系统的主要组成部分是&#xff1a; 内存池&#xff1a;Cl…...

docker容器技术

什么是docker Docker 使用 Google 公司推出的 Go 语言 进行开发实现&#xff0c;基于 Linux 内核的 cgroup&#xff0c;namespace&#xff0c;以及 OverlayFS 类的 Union FS 等技术&#xff0c;对进程进行封装隔离&#xff0c;属于 操作系统层面的虚拟化技术。由于隔离的进程独…...

设计模式七大设计原则

文章目录 1、什么是设计模式2、单一职责原则3、开闭原则4、接口隔离原则5、依赖倒置原则6、迪米特法则&#xff08;最少知道原则&#xff09;7、里式替换原则8、组合优于继承 设计模式主要是为了满足一个字 变&#xff0c;这个字&#xff0c;可能是需求变更、可能是场景变更&a…...

【Hello Network】TCP协议相关理解

作者&#xff1a;小萌新 专栏&#xff1a;网络 作者简介&#xff1a;大二学生 希望能和大家一起进步 本篇博客简介&#xff1a;补充下对于TCP协议的各种理解 TCP协议相关实验 TCP相关试验理解CLOSE_WAIT状态理解TIME_WAIT状态解决TIME_WAIT状态引起的bind失败的方法理解listen的…...

实施CRM目标有哪几步?如何制定CRM目标?

在当今竞争激烈的商业环境中&#xff0c;与客户建立持久的关系是企业重要的工作。CRM客户管理系统能有效帮助企业管理优化流程、管理客户&#xff0c;提高销售成功率&#xff0c;推动收入增长。那么您了解如何实施CRM吗&#xff1f;下面说说实施CRM目标是什么&#xff0c;如何设…...

船舶建造概论(船舶建造工艺任务与现代造船模式)

船舶建造概论 1 船舶建造概论1.1 船舶建造工艺主要任务1.2 船舶建造流程&#xff08;1&#xff09;钢材料预处理&#xff08;2&#xff09; 钢材料加工&#xff08;3&#xff09;分段制作&#xff08;4&#xff09;总段制作&#xff08;5&#xff09;船台合拢&#xff08;6&…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

QT: `long long` 类型转换为 `QString` 2025.6.5

在 Qt 中&#xff0c;将 long long 类型转换为 QString 可以通过以下两种常用方法实现&#xff1a; 方法 1&#xff1a;使用 QString::number() 直接调用 QString 的静态方法 number()&#xff0c;将数值转换为字符串&#xff1a; long long value 1234567890123456789LL; …...

基于Java+MySQL实现(GUI)客户管理系统

客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息&#xff0c;对客户进行统一管理&#xff0c;可以把所有客户信息录入系统&#xff0c;进行维护和统计功能。可通过文件的方式保存相关录入数据&#xff0c;对…...

Linux nano命令的基本使用

参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时&#xff0c;显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...

Golang——9、反射和文件操作

反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一&#xff1a;使用Read()读取文件2.3、方式二&#xff1a;bufio读取文件2.4、方式三&#xff1a;os.ReadFile读取2.5、写…...

解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用

在工业制造领域&#xff0c;无损检测&#xff08;NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统&#xff0c;以非接触式光学麦克风技术为核心&#xff0c;打破传统检测瓶颈&#xff0c;为半导体、航空航天、汽车制造等行业提供了高灵敏…...

掌握 HTTP 请求:理解 cURL GET 语法

cURL 是一个强大的命令行工具&#xff0c;用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中&#xff0c;cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...

android RelativeLayout布局

<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...

上位机开发过程中的设计模式体会(1):工厂方法模式、单例模式和生成器模式

简介 在我的 QT/C 开发工作中&#xff0c;合理运用设计模式极大地提高了代码的可维护性和可扩展性。本文将分享我在实际项目中应用的三种创造型模式&#xff1a;工厂方法模式、单例模式和生成器模式。 1. 工厂模式 (Factory Pattern) 应用场景 在我的 QT 项目中曾经有一个需…...

2025年- H71-Lc179--39.组合总和(回溯,组合)--Java版

1.题目描述 2.思路 当前的元素可以重复使用。 &#xff08;1&#xff09;确定回溯算法函数的参数和返回值&#xff08;一般是void类型&#xff09; &#xff08;2&#xff09;因为是用递归实现的&#xff0c;所以我们要确定终止条件 &#xff08;3&#xff09;单层搜索逻辑 二…...