C++设计模式创建型模式———生成器模式

文章目录

  • 一、引言
  • 二、生成器/建造者模式
  • 三、总结

一、引言

上一篇文章我们介绍了工厂模式,工厂模式的主要特点是生成对象。当对象较简单时,可以使用简单工厂模式或工厂模式;而当对象相对复杂时,则可以选择使用抽象工厂模式。

工厂用于生产各种对象,这些对象通常是兄弟类,继承自同一个基类。兄弟子类通过实现基类接口,展现不同的行为,并由工厂函数创建。然而,工厂模式在创建对象时并不关注构造细节,处理复杂对象生成时可能显得力不从心。抽象工厂模式专注于生成一系列相关对象,但在对象构造复杂时,其能力也有限。

相较于工厂模式,生成器模式同样用于对象的生成,但更侧重于构造细节,增加了额外的构建流程,以便处理复杂对象的构建需求。

生成器模式也是一种创建型设计模式, 使我们能够分步骤创建复杂对象。 该模式允许使用相同的创建代码生成不同类型和形式的对象。 也就是说,生成器模式就是为了生成一个复杂的对象。

化繁为简,逐个击破。分步骤创建复杂的对象,并且允许使用相同的代码生成不同类型和形式的对象。

在这里插入图片描述

假设有这样一个复杂对象, 在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。 这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中; 甚至还有更糟糕的情况, 那就是这些代码散落在客户端代码的多个位置。

假设我们要建一个房子,房子由许多部分组成,如门和墙壁。生成器模式可以将对象构造的代码从产品类中抽取出来,并放在一个名为生成器的独立对象中。

该模式会将对象构造过程划分为一组步骤, 比如 build­Walls创建墙壁和 build­Door创建房门创建房门等。 每次创建对象时, 都需要通过生成器对象执行一系列步骤。 重点在于我们无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。

当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 例如, 木屋的房门可能需要使用木头制造, 而城堡的房门则必须使用石头制造。

在这种情况下, 可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤。 然后就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。

我们通过写出不同生成器以不同方式执行相同的任务。

假设我们需要一个木门+石墙的房子。也需要一个石头门+钢铁的房子。在调用同一组步骤后, 第一个建造者会给你一栋普通房屋, 第二个会给你一座小城堡。但是, 只有在调用构造步骤的客户端代码可以通过通用接口与建造者进行交互时, 这样的调用才能返回需要的房屋。

然后我们可以进一步的把用于创建一系列生成器的步骤抽取出来,成为一个单独的主管类。主管类去定义创建步骤的执行顺序。而生成器去提供这些步骤的实现。

但是即使没有主管类,我们的用户也可以直接以特定的顺序去调用创建步骤。但是如果有主管类,主管类可以完全隐藏产品的构造细节。客户端只需要将一个生成器与主管类关联,然后使用主管类去构造产品,就能从生成器处获得构造结果了。


二、生成器/建造者模式

这里还是以游戏中的怪物类来讲解。怪物同样分为亡灵类怪物、元素类怪物、机械类怪物。

在创建怪物对象的过程中,有一个创建步骤非常烦琐,把怪物模型创建出来用于显示给玩家。策划规定,任何一种怪物都由头部、躯干(包括颈部、尾巴等)、肢体3个部位组成,在制作怪物模型时,头部、躯干、肢体模型分开制作。每个部位模型都会有一些位置和方向信息,用于挂接在其他部位模型上,比如将头部挂接到躯干部,再将肢体挂接到躯干部就可以构成一个完整的怪物模型。当然,一些在水中的怪物可能不包含四肢,那么将肢体挂接到躯干部这个步骤什么都不做即可。

之所以在制作怪物模型时将头部、躯干、肢体模型分开制作,是便于同类型怪物的3个
组成部位进行互换。试想一下,如果针对亡灵类怪物制作了3个头部、3个躯干以及3个肢体,则最多可以组合出27个外观不同的亡灵类怪物,这既节省了游戏制作成本,又节省了游戏运行时对内存的消耗。

程序需要先把怪物模型载入内存并进行装配以保证正确地显示给玩家看。所以程序需
要进行如下编码步骤:

  • 将怪物的躯干模型信息读人内存并提取其中的位置和方向信息;

  • 将怪物的头部和四肢模型信息读人内存并提取其中的位置和方向信息;

  • 将头部和四肢模型以正确的位置和方向挂接(Mount)到躯干部位,从而装配出完整的怪物模型。

我们实现一个Monster父类。

class Monster
{
public:Monster(int life, int magic, int attack):m_life(life), m_magic(magic), m_attack(attack){}// 创建怪物的纯虚函数,具体实现将在子类中进行void Assemble(string strmodelno)//参数:模型编号,形如“1253679201245”等,每种位的组合都有一些特别的含义,这里无须深究{LoadTrunkModel(strmodelno.substr(4, 3));//载人躯干模型,截取某部分字符串以表示躯干模型的编号LoadHeadkModel(strmodelno.substr(7, 3));//载人头部模型并挂接到躯干模型上LoadLimbsModel(strmodelno.substr(10, 3)); //载人四肢模型并挂接到躯干模型上}virtual void LoadTrunkModel(const string& strno) = 0;virtual void LoadHeadkModel(const string& strno) = 0;virtual void LoadLimbsModel(const string& strno) = 0;virtual~Monster() {}
protected:						int m_life; int m_magic;int m_attack;
};

在上述代码中做了很多简化,只是大致的实现代码,在Assemble成员函数中实现了载人一个怪物模型的固定流程一分别载入了躯干、头部、四肢模型并将它们装配到一起,游戏中所有怪物的载入都遵循该流程(其中的代码是稳定的,不发生变化。

因为亡灵类怪物、元素类怪物、机械类怪物的外观差别巨大,所以虽然这3类怪物的载人流程相同,但不同种类怪物的细节载人差别很大,所以,将LoadTrunkModelLoadHeadModelLoadLimbsModel(构建模型的子步骤)成员函数写为虚函数以方便在Monster的子类中重新实现。

// 亡灵怪物类
class M_Undead : public Monster {
public:M_Undead(int life, int magic, int attack) : Monster(life, magic, attack) {cout << "一只亡灵类怪物来到了这个世界" << endl;}void LoadTrunkModel(const string& strno) override {cout << "载入亡灵躯干模型:" << strno << endl;}void LoadHeadkModel(const string& strno) override {cout << "载入亡灵头部模型:" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入亡灵四肢模型:" << strno << endl;}
};
// 元素类怪物
class M_Element : public Monster {
public:M_Element(int life, int magic, int attack) : Monster(life, magic, attack) {cout << "一只元素类怪物来到了这个世界" << endl;}void LoadTrunkModel(const string& strno) override {cout << "载入元素躯干模型:" << strno << endl;}void LoadHeadkModel(const string& strno) override {cout << "载入元素头部模型:" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入元素四肢模型:" << strno << endl;}
};// 机械类怪物
class M_Mechanic : public Monster {
public:M_Mechanic(int life, int magic, int attack) : Monster(life, magic, attack) {cout << "一只机械类怪物来到了这个世界" << endl;}void LoadTrunkModel(const string& strno) override {cout << "载入机械躯干模型:" << strno << endl;}void LoadHeadkModel(const string& strno) override {cout << "载入机械头部模型:" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入机械四肢模型:" << strno << endl;}
};

这时在main函数中加入如下代码,创建一个怪物对象并对齐进行生成:

unique_ptr<Monster> undead = make_unique<M_Undead>(100, 50, 20);
undead->Assemble("1253679201245"); // 传入模型编号进行组装
/*
我们会看到如下输入结结果:
一只亡灵类怪物来到了这个世界
载入亡灵躯干模型:679
载入亡灵头部模型:201
载入亡灵四肢模型:245
*/

上述这些代码用于创建怪物对象以显示给玩家看,但怪物的创建比较复杂,严格地说,应该是怪物模型的载入过程比较复杂,需要按顺序分别载入躯干、头部、四肢模型并实现不同部位模型之间的挂接。但是,目前的代码并不能称为生成器模式。通过对过程进一步拆分还可以进一步提高灵活性。

这里将AssembleLoadTrunkModelLoadHeadModelLoadLimbsModel这些与模型载人与挂接步骤相关的成员函数称为构建过程相关函数。

考虑到Monster类中要实现的逻辑功能可能较多,如果把构建过程相关函数提取出来(分离)放到一个单独的类中,不但可以减少Monster类中的代码量,还可以增加构建过程相关代码的独立性,日后游戏中任何由头部、躯干、肢体3个部位组成并需要将头部挂接到躯干部,再将肢体挂接到躯干部的生物,都可以通过这个单独的类实现模型的构建。

// 怪物基类
class Monster {
public:virtual ~Monster() {}
};// 亡灵怪物类
class M_Undead : public Monster {
public:M_Undead() {cout << "一只亡灵类怪物来到了这个世界" << endl;}
};// 元素怪物类
class M_Element : public Monster {
public:M_Element() {cout << "一只元素类怪物来到了这个世界" << endl;}
};// 机械怪物类
class M_Mechanic : public Monster {
public:M_Mechanic() {cout << "一只机械类怪物来到了这个世界" << endl;}
};// 怪物构建器基类
class MonsterBuilder {
public:virtual ~MonsterBuilder() {}// 载入不同部分模型的纯虚函数virtual void LoadTrunkModel(const string& strno) = 0;virtual void LoadHeadkModel(const string& strno) = 0;virtual void LoadLimbsModel(const string& strno) = 0;// 组装模型void Assemble(const string& strmodelno) {LoadTrunkModel(strmodelno.substr(4, 3)); // 躯干模型LoadHeadkModel(strmodelno.substr(7, 3));  // 头部模型LoadLimbsModel(strmodelno.substr(10, 3)); // 四肢模型}// 获取构建结果unique_ptr<Monster> GetResult() { return move(m_pMonster); }protected:unique_ptr<Monster> m_pMonster; // 使用智能指针管理怪物对象
};// 亡灵怪物构建器
class UndeadBuilder : public MonsterBuilder {
public:UndeadBuilder() {m_pMonster = make_unique<M_Undead>(); // 创建亡灵怪物}void LoadTrunkModel(const string& strno) override {cout << "载入亡灵类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现}void LoadHeadkModel(const string& strno) override {cout << "载入亡灵类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout <<"载入亡灵类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......"  << strno << endl;}
};// 元素怪物构建器
class ElementBuilder : public MonsterBuilder {
public:ElementBuilder() {m_pMonster = make_unique<M_Element>(); // 创建元素怪物}void LoadTrunkModel(const string& strno) override {cout << "载入元素类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现}void LoadHeadkModel(const string& strno) override {cout << "载入元素类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入元素类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}
};// 机械怪物构建器
class MechanicBuilder : public MonsterBuilder {
public:MechanicBuilder() {m_pMonster = make_unique<M_Mechanic>(); // 创建机械怪物}void LoadTrunkModel(const string& strno) override {cout << "载入机械类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现}void LoadHeadkModel(const string& strno) override {cout << "载入机械类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入机械类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}
};

在上述代码中,可以注意到,在MonsterBuilder类中放置了一个指向Monster类的成员变量智能指针m_pMonster,同时引人GetResult成员函数用于返回这个m_pMonster指针,也就是说,当一个复杂的对象通过构建器构建完成后,可以通过GetResult返回。

重点观察MonsterBuilder类中的Assemble成员函数,前面曾经提过,该成员函数中的代码是稳定的,不会发生变化。所以可以继续把Assemble成员函数的功能拆出到一个新类中(这步拆分也不是必需的)。创建新类MonsterDirector(扮演一个指挥者角色),将
MonsterBuilder类中的Assemble成员函数整个迁移到MonsterDirector类中并按照惯例重新命名为Construct,同时,在MonsterDirector类中放置一个指向MonsterBuilder类的成员变量指针m_pMonsterBuilder,同时对Construct成员函数的代码进行调整(注意也增加了返回值)。完整的MonsterDirector类代码如下:

//指挥者类
class MonsterDirector {
public:// 使用 unique_ptr 作为构造函数的参数MonsterDirector(std::unique_ptr<MonsterBuilder> ptmpBuilder): m_pMonsterBuilder(std::move(ptmpBuilder)) {}// 使用构建器创建怪物unique_ptr<Monster> ConstructMonster(const string& modelNumber) {m_pMonsterBuilder->LoadTrunkModel(modelNumber.substr(0, 3)); // 组装躯干m_pMonsterBuilder->LoadHeadkModel(modelNumber.substr(3, 3)); // 组装头部m_pMonsterBuilder->LoadLimbsModel(modelNumber.substr(6, 3)); // 组装四肢return m_pMonsterBuilder->GetResult();}void SetBuilder() {//指定新的生成器}
private:unique_ptr<MonsterBuilder> m_pMonsterBuilder; // 指向生成器类的父类
};

我们可以这样使用:

string modelNumber = "1253679201245"; // 模型编号// 创建指挥者,并传入智能指针
auto undeadBuilder = std::make_unique<UndeadBuilder>();
MonsterDirector director(std::move(undeadBuilder));// 构造怪物
director.ConstructMonster(modelNumber);

引入生成器模型的定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

在上述范例中,MonsterBuilder类是对象的构建,而Monster类是对象的表示,这两个类是相互分离的。构建过程是指MonsterDirector类中的Construct成员函数所代表的怪物模型的载人和装配(挂接)过程,该过程稳定不会发生变化(稳定的算法),所以只要传递给MonsterDirector不同的构建器子类(M_UndeadBuilderM_ElementBuilderM_MechanicBuilder),就会构建出不同的怪物,可以随时调用MonsterDirector类的SetBuilder成员函数为MonsterDirector(指挥者)指定一个新的构建器以创建不同种类的怪物对象。

在这里插入图片描述

我们可以发现生成器模式包含四种角色。

  • 抽象构建器Builder):为创建一个产品对象的各个部件指定抽象接口
    LoadTrunkModelLoadHeadModelLoadLimbsModel),同时,也会指定一个接口(GetResult)用于返回所创建的复杂对象。这里指MonsterBuilder类。
  • 具体构建器Concrete Builder):实现了Builder接口以创建(构造和装配)该产品的各个部件,定义并明确其所创建的复杂对象,有时也可以提供一个方法用于返回创建好的复杂对象。这里指M_UndeadBuilderM_ElementBuilderM_MechanicBuilder类。
  • 产品Product):指的是被构建的复杂对象,其包含多个部件,由具体构建器创建该产品的内部表示并定义它的装配过程。这里指M_UndeadM_ElementM_Mechanic类。
  • 指挥者Director):又称主管类,这里指MonsterDirector类。该类有一个指向抽象构建器的指针(m_pMonsterBuilder),利用该指针可以在Construct成员函数中调用构建器对象中“构建和装配产品部件”的方法来完成复杂对象的构建,只要指定不同的具体构建器,用相同的构建过程就会构建出不同的产品。同时,Construct成员函数还控制复杂对象的构建次序(例如,在Construct成员函数中对LoadTrunkModelLoadHeadModelLoadLimbsModel的调用是有先后次序的)。在使用这段内容时,只需要生成一个具体的构建器对象,并利用该构建器对象创建指挥者对象并调用指挥者类的Construct成员函数,就可以构建一个复杂的对象。

前面已经说过,从MonsterBuilder分拆出MonsterDirector这步不是必需的,不做分拆可以看作生成器模式的一种退化情形,当然,此时客户端就需要直接针对构建器进行编码了。一般的建议是:如果MonsterBuilder类本身非常庞大、非常复杂,则进行分拆,否则可以不进行分拆,总之,复杂的东西就考虑做拆解,简单的东西就考虑做合并。

生成器模式结构

在这里插入图片描述

生成器模式的核心组成部分如下:

  1. 生成器接口(Builder:声明通用的产品构造步骤。
  2. 具体生成器(Concrete Builders:实现生成器接口,提供不同的构造过程,能够生成不遵循同一接口的产品。
  3. 产品(Products:最终生成的对象,不同生成器构造的产品可以不属于同一类层次结构。
  4. 主管(Director:定义构造步骤的调用顺序,用于创建和复用特定的产品配置。
  5. 客户端(Client:将生成器对象与主管类关联,通过主管类调用生成器来构建产品。可以在不同的构建过程中使用不同的生成器。

三、总结

通过上述案例,我们不难看出生成器模式主要用于分布建立一个复杂的对象,其中的构建步骤是一个稳定的算法,而复杂对象各个部分的创建会有不同的变化。

生成器模式的核心要点在于将构建算法和具体的构建算法分离,这样构建算法就可以被重用,通过编写不同的代码又可以很方便地对构建实现进行功能扩展。引入指挥者类后,只要使用不同的生成器,利用相同的构建过程就可以构建出不同的产品。

构建器接口定义的是如何构建各个部件,也就是说,当需要创建具体部件的时候,交给构建器来做。而指挥者有两个作用:

  • 负责通过部件以指定的顺序来构建整个产品(控制了构建的过程)。
  • 指挥者通过提供Construct接口隔离了客户端(指main主函数中的代码)与具体构建过程必须要调用的类的成员函数之间的关联

对于客户端,只需要知道各种具体的构建器以及指挥者的Construct接口即可,并不需要知道如何构建具体的产品。想象一个项目开发小组,如果main中构建产品的代码由普通组员编写,这项工作自然比较轻松,但是,支撑代码编写所运用的设计模式及实现一般是由组长来完成,显然这项工作要复杂得多。

工厂方法模式与生成器模式也有类似之处,但生成器模式侧重于一步步构建一个复杂的产品对象,构建完成后返回所构建的产品,工厂方法模式侧重于多个产品对象(且对象所属的类继承自同一个父类)的构建而无论产品本身是否复杂。

  • 生成器模式可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。

  • 生成不同形式的产品时, 可以复用相同的制造代码。将一个复杂对象的创建过程封装起来。用同一个构建算法可以构建出表现上完全不同的产品,实现产品构建和产品表现(表示)上的分离。建造者模式也正是通过把产品构建过程独立出来,从而才使构建算法可以被复用。这样的程序结构更容易扩展和复用。

  • 单一职责原则。 可以将复杂构造代码从产品的业务逻辑中分离出来。

  • 产品的实现可以随时被替换(将不同的构建器提供给指挥者)。而且向客户端隐藏了产品内部的表现。

但是有如下缺点:

  • 要求所创建的产品有比较多的共同点,创建步骤(组成部分)要大致相同,如果产品很不相同,创建步骤差异极大,则不适合使用建造者模式,这是该模式使用范围受限的地方。
  • 生成器模式涉及很多的类,例如需要组合指挥者和构建器对象,然后才能开始对象的构建工作。

生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。你可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/459798.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

创作三周年:在忙碌中寻找灵感与快乐

目录 机缘 收获 技能的提升 粉丝的积累 正向的反馈 同行的伙伴 日常 运动 旅行 生活 憧憬 结语 机缘 不知不觉已经成为创作者3年了&#xff0c;这一路走来&#xff0c;有过高峰和低谷&#xff0c;但始终让我坚持的&#xff0c;是最初那份简单的初心&#xff1a;我…

C#从零开始学习(用户界面)(unity Lab4)

这是书本中第四个unity Lab 在这次实验中,将学习如何搭建一个开始界面 分数系统 点击球,会增加分数 public void ClickOnBall(){Score;}在OneBallBehaviour类添加下列方法 void OnMouseDown(){GameController controller Camera.main.GetComponent<GameController>();…

分布式搜索引擎elasticsearch操作文档操作介绍

1.DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。 1.1.DSL查询分类 Elasticsearch提供了基于JSON的DSL&#xff08;Domain Specific Language&#xff09;来定义查询。常见的查询类型包括&#xff1a; 查询所有&#xff1a;查询出所有数据&#xff0c;…

软件系统安全保证措施,质量保证措施方案(Word原件套用)

系统安全保证措施是构建稳固防御体系的核心&#xff0c;旨在全方位保障信息系统的安全性。以下是对这七项措施的简要概述&#xff1a; 一、身份鉴别&#xff1a;采用多种认证方式&#xff0c;如密码、生物识别等&#xff0c;确保用户身份的准确无误&#xff0c;防止非法入侵。 …

玩转Docker | 使用Docker部署捕鱼网页小游戏

玩转Docker | 使用Docker部署捕鱼网页小游戏 一、项目介绍项目简介项目预览 二、系统要求环境要求环境检查Docker版本检查检查操作系统版本 三、部署捕鱼网页小游戏下载镜像创建容器检查容器状态下载项目内容查看服务监听端口安全设置 四、访问捕鱼网页小游戏五、总结 一、项目…

局域网 docker pull 使用代理拉取镜像

局域网 docker pull 使用代理拉取镜像 1、需求&#xff1a; 我有win主机&#xff0c;上面装有代理可连接dockerhub&#xff1b;我另有linux主机&#xff0c;直接pull因墙失败&#xff0c;想走win的代理访问dockerhub拉镜像&#xff1b;两台主机在同一个局域网中&#xff1b; …

c语言中结构体传参和实现位段

结构体传参 有两种方法: #include<stdio.h> struct S {int data[1000];int num; }; //结构体传参 void print1(struct S s) {printf("%d\n",s.num); } //结构体地址传参 void print2(struct S *ps) {printf("%d\n",ps->num); }int main() {pr…

2024年10月HarmonyOS应用开发者基础认证全新题库

注意事项&#xff1a;切记在考试之外的设备上打开题库进行搜索&#xff0c;防止切屏三次考试自动结束&#xff0c;题目是乱序&#xff0c;每次考试&#xff0c;选项的顺序都不同 这是基础认证题库&#xff0c;不是高级认证题库注意看清楚标题 高级认证题库地址&#xff1a;20…

HTML3D旋转相册

文章目录 序号目录1HTML满屏跳动的爱心(可写字)2HTML五彩缤纷的爱心3HTML满屏漂浮爱心4HTML情人节快乐

Depcheck——专门用于检测 JavaScript 和 Node.js 项目中未使用依赖项的工具

文章目录 Depcheck 是什麽核心功能&#x1f4da;检测未使用的依赖&#x1f41b;检测缺失的依赖✨支持多种文件类型&#x1f30d;可扩展性 安装与使用1. 安装 Depcheck2. 使用 Depcheck Depcheck 的应用总结项目源码&#xff1a; Depcheck 是什麽 来看一个常见错误场景&#x1…

Chrome和Firefox哪款浏览器的密码管理更安全

在当今数字化时代&#xff0c;浏览器已成为我们日常生活中不可或缺的工具。其中&#xff0c;谷歌Chrome和Mozilla Firefox是两款广受欢迎的浏览器。除了浏览网页外&#xff0c;它们还提供了密码管理功能&#xff0c;帮助用户保存和管理登录凭证。然而&#xff0c;关于哪款浏览器…

Camp4-L0:Linux 前置基础

书生浦语大模型实战营Camp4-L0:Linux前置基础 教程地址&#xff1a;https://github.com/InternLM/Tutorial/tree/camp4/docs/L0/linux任务地址&#xff1a;https://github.com/InternLM/Tutorial/blob/camp4/docs/L0/linux/task.md 任务描述完成所需时间闯关任务完成SSH连接与…

C++之多态的深度剖析

目录 前言 1.多态的概念 2.多态的定义及实现 2.1多态的构成条件 2.1.1重要条件 2.1.2 虚函数 2.1.3 虚函数的重写/覆盖 2.1.4 选择题 2.1.5 虚函数其他知识 协变&#xff08;了解&#xff09; 析构函数的重写 override 和 final关键字 3. 重载&#xff0c;重写&…

如何从iconfont中获取字体图标并应用到微信小程序中去?

下面我们一一个微信小程序的登录界面的制作为例来说明&#xff0c;如何从iconfont中获取字体图标是如何应用到微信小程序中去的。首先我们看效果。 这里所有的图标&#xff0c;都是从iconfont中以字体的形式来加载的&#xff0c;也就是说&#xff0c;我们自始至终没有使用一张…

Linux shell编程学习笔记87:blkid命令——获取块设备信息

0 引言 在进行系统安全检测时&#xff0c;我们需要收集块设备的信息&#xff0c;这些可以通过blkid命令来获取。 1 blkid命令的安装 blkid命令是基于libblkid库的命令行工具&#xff0c;可以在大多数Linux发行版中使用。 如果你的Linux系统中没有安装blkid命令&#xff0c;…

RuoYi-Vue 使用开发 人员管理-查询功能

说明&#xff1a;这里仅仅开发列表显示 与 查询功能&#xff0c;剩下的添加、修改等可能会遇到报错&#xff0c;后面有机会&#xff0c;会单独写一篇文章教学处理 1.了解开发需求 作为示例的二级开发&#xff0c;这里的人员管理&#xff0c;管理的是 部门信息&#xff0c;员工…

Tomcat 11 下载/安装 与基本使用

为什么要使用Tomcat&#xff1f; 使用Apache Tomcat的原因有很多&#xff0c;以下是一些主要的优点和特点&#xff1a; 1. 开源与免费 Tomcat是一个完全开源的项目&#xff0c;任何人都可以免费使用。它由Apache软件基金会维护&#xff0c;拥有一个活跃的社区&#xff0c;这…

Django入门教程——用户管理实现

第六章 用户管理实现 教学目的 复习数据的增删改查的实现。了解数据MD5加密算法以及实现模型表单中&#xff0c;自定义控件的使用中间件的原理和使用 需求分析 系统问题 员工档案涉及到员工的秘密&#xff0c;不能让任何人都可以看到&#xff0c;主要是人事部门进行数据的…

[ 问题解决篇 ] 解决远程桌面安全登录框的问题

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

微信小程序时间弹窗——年月日时分

需求 1、默认当前时间2、选择时间弹窗限制最大值、最小值3、每次弹起更新最大值为当前时间&#xff0c;默认值为上次选中时间4、 minDate: new Date(2023, 10, 1).getTime(),也可以传入时间字符串new Date(2023-10-1 12:22).getTime() html <view class"flex bb ptb…