【设计模式】设计模式

news2024/9/21 2:39:44

书籍推荐

  • 《设计模式-可复⽤⾯向对象软件的基础》
  • 《重构与模式》

设计模式

设计模式是指在软件开发中,经过验证的,⽤于解决在特定环境下,重复出现的,特定问题的解决⽅案;

内存模型

image.png
image.png
image.png

  • 扩展:c语⾔当中的多态
    • redis
    • nginx

模式设计原则

依赖倒置原则

  • ⾼层模块不应该依赖低层模块,⼆者都应该依赖抽象
  • 抽象不应该依赖具体实现,具体实现应该依赖于抽象;

image.png
⾃动驾驶系统公司是⾼层,汽⻋⽣产⼚商为低层,它们不应该互相依赖,⼀⽅变动另⼀⽅也会跟着变动;⽽应该抽象⼀个⾃动驾驶⾏业标准,⾼层和低层都依赖它;这样以来就解耦了两⽅的变动;⾃动驾驶系统、汽⻋⽣产⼚商都是具体实现,它们应该都依赖⾃动驾驶⾏业标准(抽象);

开放封闭原则

⼀个类应该对扩展开放,对修改关闭;

面向接口编程

  • 不将变量类型声明为某个特定的具体类,⽽是声明为某个接⼝。
  • 客户程序⽆需获知对象的具体类型,只需要知道对象所具有的接⼝。
  • 减少系统中各部分的依赖关系,从⽽实现“⾼内聚、松耦合”的类型设计⽅案。

封装变化点

  • 将稳定点和变化点分离,扩展修改变化点;让稳定点与变化点的实现层次分离;

单一职责原则

  • ⼀个类应该仅有⼀个引起它变化的原因;

里氏替换原则

  • 子类型必须能够替换掉它的父类型;主要出现在子类覆盖父类实现,原来使用父类型的程序可能出现错误;覆盖了父类方法却没实现父类方法的职责

接口隔离原则

  • 不应该强迫客户依赖于他们不⽤的⽅法;
  • ⼀般用于处理⼀个类拥有比较多的接口,而这些接口涉及到很多职责;

对象组合优于类继承

  • 继承耦合度⾼,组合耦合度低;

什么情况下使用设计模式?

  • 系统的关键依赖点;
  • 能明确找到变化点;
  • 能明确找到复用方向;
  • 对需求变化方向熟悉;

如何找到设计模式?

  • 从重构中获得;

重构

  • 静态转变为动态;
  • 早绑定转变为晚绑定;
  • 继承转变为组合;
  • 编译时依赖转变为运⾏时依赖;
  • 紧耦合转变为松耦合;

为什么要学习设计模式?

  • 从已有的且证明有效的设计模式中获取灵感,少⾛弯路;
  • 通⽤语言,知道在已有的设计模式扩展代码;
  • 体会模式设计,设计自己的行之有效的设计模式;

学习设计模式的步骤

  • 深刻体会上⾯的原则;
  • 理解设计模式,能知道设计模式的变化点和稳定点;
  • 能在已使⽤的设计模式中,知道如何写扩展;
  • 能在复杂需求中,抽象出已有设计模式;
  • 能在重构中,开发自己的设计模式;

设计模式

模板方法

  • 定义

定义⼀个操作中的算法的⻣架 ,⽽将⼀些步骤延迟到⼦类中。 Template Method使得⼦类可以不改变⼀个算法的结构即可重定义该算法的某些特定步骤。 ——《设计模式》 GoF

  • 背景

某个品牌动物园,有⼀套固定的表演流程,但是其中有若⼲个表演⼦流程受欢迎程度⽐较低,希望将这⼏个表演流程创新,以尝试迭代更新表演流程;

  • 代码
#include <iostream>
using namespace std;

class ZooShow {
    public:
    // 固定流程封装到这里
    void Show() {
        Show0();
        Show1();
        Show2();
        Show3();
    }
    protected:
    // 子流程 使用protected保护起来 不被客户调用 但允许子类扩展
    virtual void Show0(){
        cout << "show0" << endl;
    }
    virtual void Show2(){
        cout << "show2" << endl;
    }
    virtual void Show1() {

    }
    virtual void Show3() {

    }
};
class ZooShowEx : public ZooShow {
    protected:
    virtual void Show1(){
        cout << "show1" << endl;
    }
    virtual void Show3(){
        cout << "show3" << endl;
    }
    virtual void Show4() {
        //
    }
};

class ZooShowEx1 : public ZooShow {
    protected:
    virtual void Show0(){
        cout << "show1" << endl;
    }
    virtual void Show2(){
        cout << "show3" << endl;
    }
};

class ZooShowEx2 : public ZooShow {
    protected:
    virtual void Show1(){
        cout << "show1" << endl;
    }
    virtual void Show2(){
        cout << "show3" << endl;
    }
};
/*
*/
int main () {
    ZooShow *zs = new ZooShowEx;
    // ZooShow *zs1 = new ZooShowEx1;
    // ZooShow *zs2 = new ZooShowEx2;
    zs->Show();
    return 0;
}
  • 要点
    • ⾮常常⽤的设计模式,⼦类可以复写⽗类的⼦流程,使⽗类的⼤流程更丰富;
    • 反向控制流程的典型应⽤;
    • ⽗类 protected 保护⼦类需要复写的⼦流程;这样⼦类的⼦流程只能⽗类来调⽤;
    • 充分体现了依赖倒置原则;
  • 本质
    • 通过固定算法⻣架来约束⼦类的⾏为;
  • 结构图

image.png

观察者模式

  • 定义

定义对象间的⼀种⼀对多(变化)的依赖关系,以便当⼀个对象(Subject)的状态发⽣改变时,所有依赖于它的对象都得到通知并⾃动更新。 ——《设计模式》 GoF

  • 背景

⽓象站发布⽓象资料给数据中⼼,数据中⼼经过处理,将⽓象信息更新到两个不同的显示终端(A和B);

  • 代码
#include <vector>

class IDisplay {
public:
    virtual void Show(float temperature) = 0;
    virtual ~IDisplay() {}
};

class DisplayA : public IDisplay {
public:
    virtual void Show(float temperature);
};

class DisplayB : public IDisplay{
public:
    virtual void Show(float temperature);
};

class WeatherData {
};

class DataCenter {
public:
    void Attach(IDisplay * ob);
    void Detach(IDisplay * ob);
    void Notify() {
        float temper = CalcTemperature();
        for (auto iter = obs.begin(); iter != obs.end(); iter++) {
            (*iter)->Show(temper);
        }
    }

private:
    virtual WeatherData * GetWeatherData();

    virtual float CalcTemperature() {
        WeatherData * data = GetWeatherData();
        // ...
        float temper/* = */;
        return temper;
    }
    std::vector<IDisplay*> obs;
};

int main() {
    DataCenter *center = new DataCenter;
    IDisplay *da = new DisplayA();
    IDisplay *db = new DisplayB();
    center->Attach(da);
    center->Attach(db);
    center->Notify();
    
    //-----
    center->Detach(db);
    center->Notify();
    return 0;
}
  • 要点
  • 观察者模式使得我们可以独⽴地改变⽬标与观察者,从⽽使⼆者之间的关系松耦合;
  • 观察者⾃⼰决定是否订阅通知,⽬标对象并不关注谁订阅了;
  • 观察者不要依赖通知顺序,⽬标对象也不知道通知顺序;
  • 常使⽤在基于事件的ui框架中,也是MVC的组成部分;
  • 常使⽤在分布式系统中,actor框架中;
  • 本质
    • 触发联动;
  • 结构图

image.png

单例模式

  • 定义

保证⼀个类仅有⼀个实例,并提供⼀个该实例的全局访问点。 ——《设计模式》GoF

  • 代码
    • 版本1
// 内存栈区
// 内存堆区
// 常数区
// 静态区 系统释放
// ⼆进制代码区
class Singleton {
public:
	static Singleton * GetInstance() {
	if (_instance == nullptr) {
		_instance = new Singleton();
	}
	return _instance;
}
private:
	Singleton(){}//构造
	Singleton(const Singleton &clone){} //拷⻉构造
	Singleton& operator=(const Singleton&) {}
	static Singleton * _instance;
}
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化

存在问题:

  1. 存在内存泄漏,new Singleton() 在堆内存申请的内存结束时没有释放

  2. 存在线程安全问题,多线程调用GetInstance() 有可能申请多个Singleton对象

    • 版本2
class Singleton {
public:
	static Singleton * GetInstance() {
		if (_instance == nullptr) {
			_instance = new Singleton();
 			atexit(Destructor);
 		}
 		return _instance;
 	}
 	~Singleton() {}
private:
	static void Destructor() {
		if (nullptr != _instance) {
 			delete _instance;
 			_instance = nullptr;
 		}
 	}
 	Singleton();//构造
 	Singleton(const Singleton &cpy); //拷贝构造
 	Singleton& operator=(const Singleton&) {}
 	static Singleton * _instance;
}
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
// 还可以使⽤ 内部类,智能指针来解决; 此时还有线程安全问题

解决问题:

  1. 解决内存泄露问题

存在问题:

  1. 存在线程安全问题,多线程调用GetInstance() 有可能申请多个Singleton对象

    • 版本3
#include <mutex>
class Singleton { // 懒汉模式 lazy load
public:
	static Singleton * GetInstance() {
		//std::lock_guard<std::mutex> lock(_mutex); // 3.1 如果在这里放锁,锁的粒度太大,每次调GetInstance()都会导致切换线程,导致性能不佳
 		if (_instance == nullptr) {
 			std::lock_guard<std::mutex> lock(_mutex); // 3.2 double check。这里放锁,锁的粒度变小,保证线程安全的同时,提高了效率,因为除了第一次new会进来这里,后面new好了后在上面的判断就不会来这里了
 			if (_instance == nullptr) {
 				_instance = new Singleton();
 				atexit(Destructor);
 			}
 		}
 	return _instance;
}
private:
 	static void Destructor() {
 		if (nullptr != _instance) {
 			delete _instance;
		 	_instance = nullptr;
 		}
 	}
 	Singleton(){} //构造
 	Singleton(const Singleton &cpy){} //拷⻉构造
 	Singleton& operator=(const Singleton&) {}
 	static Singleton * _instance;
 	static std::mutex _mutex;
}
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化

解决问题:

  1. 解决内存泄露问题
  2. 解决线程安全问题

存在问题:

  1. 编译器reorder问题。_instance = new Singleton(); 这个语句分为3个CPU指令:a.分配内存 b.调用构造函数 c.赋值操作;正常情况调用顺序是abc。编译器优化的时候可能把abc的顺序优化成顺序调换了,比如acb,如果是先ac最后b,那么此时可能发生的情况是atexit(Destructor); 判断在ac步骤已经存在,直接返回 _instance,导致没有进行b步骤,即调用构造函数。

    • 版本4
#include <mutex>
#include <atomic>
class Singleton {
public:
static Singleton * GetInstance() {
 	Singleton* tmp = _instance.load(std::memory_order_relaxed);
 	std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障
 	if (tmp == nullptr) {
 		std::lock_guard<std::mutex> lock(_mutex);
 		tmp = _instance.load(std::memory_order_relaxed);
 		if (tmp == nullptr) {
 			tmp = new Singleton;
 			std::atomic_thread_fence(std::memory_order_release);//释放内存屏障
 			_instance.store(tmp, std::memory_order_relaxed);
 			atexit(Destructor);
 		}
 	}
 	return tmp;
}
private:
static void Destructor() {
 	Singleton* tmp = _instance.load(std::memory_order_relaxed);
 	if (nullptr != tmp) {
 		delete tmp;
 	}
}
Singleton(){}
Singleton(const Singleton&) {}
Singleton& operator=(const Singleton&) {}
static std::atomic<Singleton*> _instance;
static std::mutex _mutex;
};
std::atomic<Singleton*> Singleton::_instance;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化
// g++ Singleton.cpp -o singleton -std=c++11

解决问题:

  1. 解决内存泄露问题
  2. 解决线程安全问题
  3. 解决编译器reorder问题

存在问题:

  1. 操作太多

    • 版本5
// c++11 magic static 特性:如果当变量在初始化的时候,并发同时进⼊声明语句,并发
线程将会阻塞等待初始化结束。
class Singleton
{
public:
 ~Singleton(){}
 static Singleton& GetInstance() {
 	static Singleton instance; //静态局部变量 懒汉模式 系统自动释放 c++11 static自带线程安全
 	return instance;
 }
private:
 Singleton(){} //私有化构造函数,因此该类不能继承
 Singleton(const Singleton&) {}
 Singleton& operator=(const Singleton&) {}
};
// 继承 Singleton
// g++ Singleton.cpp -o singleton -std=c++11
/*该版本具备 版本5 所有优点:
1. 利⽤静态局部变量特性,延迟加载;
2. 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函数;
3. 静态局部变量初始化时,没有 new 操作带来的cpu指令reorder操作;
4. c++11 静态局部变量初始化时,具备线程安全;
*/

版本5优点:
1. 利用静态局部变量特性,延迟加载;
2. 利用静态局部变量特性,系统自动回收内存,自动调用析构函数;
3. 静态局部变量初始化时,没有 new 操作带来的cpu指令reorder操作;
4. c++11 静态局部变量初始化时,具备线程安全;
存在问题:

  1. 无法被继承

    • 版本6
template<typename T>
class Singleton {
public:
static T& GetInstance() {
 	static T instance; // 这⾥要初始化DesignPattern,需要调⽤DesignPattern 构造函数,同时会调⽤⽗类的构造函数。
 	return instance;
}
protected:
virtual ~Singleton() {} 
Singleton() {} // protected修饰构造函数,才能让别⼈继承
Singleton(const Singleton&) {}
Singleton& operator =(const Singleton&) {}
};

class DesignPattern : public Singleton<DesignPattern> {
friend class Singleton<DesignPattern>; // friend 能让 Singleton<T> 访问到 DesignPattern构造函数
private:
 DesignPattern(){}
 DesignPattern(const DesignPattern&) {}
 DesignPattern& operator=(const DesignPattern&) {}
}
  • 要点
  • 结构图

image.png

策略模式

  • 定义

定义⼀系列算法,把它们⼀个个封装起来,并且使它们可互相替换。该模式使得算法可独⽴于使⽤它的客户程序⽽变化。 ——《设计模式》 GoF

  • 背景

某商场节假⽇有固定促销活动,为了加⼤促销⼒度,现提升国庆节促销活动规格;

  • 代码

不使用策略模式:

enum VacationEnum {
	VAC_Spring,
    VAC_QiXi,
	VAC_Wuyi,
	VAC_GuoQing,
    //VAC_ShengDan,
};

// 这个类if-else职责太多,负责的节目全耦合在这个类。不符合单一职责原则
class Promotion {
    VacationEnum vac;
public:
    double CalcPromotion(){
        if (vac == VAC_Spring){
            // 春节
        }
        else if (vac == VAC_QiXi){
            // 七夕
        }
        else if (vac == VAC_Wuyi){
            // 五一
        }
		else if (vac == VAC_GuoQing){
			// 国庆
		}
     }
    
};

使用策略模式:

class Context {

};

class ProStategy {
public:
    virtual double CalcPro(const Context &ctx) = 0;
    virtual ~ProStategy();
};
// cpp
class VAC_Spring : public ProStategy {
public:
    virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_QiXi : public ProStategy {
public:
    virtual double CalcPro(const Context &ctx){}
};
class VAC_QiXi1  : public VAC_QiXi {
public:
    virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_Wuyi : public ProStategy {
public:
    virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_GuoQing : public ProStategy {
public:
    virtual double CalcPro(const Context &ctx){}
};

class VAC_Shengdan : public ProStategy {
public:
    virtual double CalcPro(const Context &ctx){}
};

// 稳定的  封装变化的
class Promotion {
public:
    Promotion(ProStategy *sss) : s(sss){}
    ~Promotion(){}
    double CalcPromotion(const Context &ctx){
        return s->CalcPro(ctx);
    }
private:
    ProStategy *s;
};

int main () {
    Context ctx;
    ProStategy *s = new VAC_QiXi1();
    Promotion *p = new Promotion(s);
    p->CalcPromotion(ctx);
    return 0;
}
  • 要点
    • 策略模式提供了⼀系列可重⽤的算法,从⽽可以使得类型在运⾏时⽅便地根据需要在各个算法之间进⾏切换;
    • 策略模式消除了条件判断语句;就是在解耦合;
    • 充分体现了开闭原则;单⼀职责;
  • 本质
    • 分离算法,选择实现;
  • 结构图

image.png

责任链模式

  • 定义

使多个对象都有机会处理请求,从⽽避免请求的发送者和接收者之间的耦合关系。将这些对象连成⼀条链,并沿着这条链传递请求,直到有⼀个对象处理它为⽌。 ——《设计模式》GoF

  • 背景

请假流程,1天内需要主程序批准,3天内需要项⽬经理批准,3天以上需要⽼板批准;

  • 代码

不使用责任链:

#include <string>

class Context {
public:
    std::string name;
    int day;
};

class LeaveRequest {
public:
    // 随着判断的增加,LeaveRequest类变得不稳定
    bool HandleRequest(const Context &ctx) {
        if (ctx.day <= 3)
            HandleByMainProgram(ctx);
        else if (ctx.day <= 10)
            HandleByProjMgr(ctx);
        else
            HandleByBoss(ctx);
    }

private:
    bool HandleByMainProgram(const Context &ctx) {

    }
    bool HandleByProjMgr(const Context &ctx) {

    }
    bool HandleByBoss(const Context &ctx) {

    }
};

使用责任链:

#include <string>

class Context {
    public:
    std::string name;
    int day;
};


class IHandler {
public:
    virtual ~IHandler() {}
    void SetNextHandler(IHandler *next) {
        next = next;
    }
    bool Handle(const Context &ctx) {
        if (CanHandle(ctx)) {
            return HandleRequest(ctx);
        } else if (GetNextHandler()) {
            return GetNextHandler()->HandleRequest(ctx);
        } else {
            // err
        }
        return false;
    }
protected:
    virtual bool HandleRequest(const Context &ctx) = 0;
    virtual bool CanHandle(const Context &ctx) =0;
    IHandler * GetNextHandler() {
        return next;
    }
private:
    IHandler *next;
};

class HandleByMainProgram : public IHandler {
    protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        return true;
    }
};

class HandleByProjMgr : public IHandler {
    protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        return true;
    }
};
class HandleByBoss : public IHandler {
    protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        return true;
    }
};

class HandleByBeauty : public IHandler {
    protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        return true;
    }
};

int main() {
    // IHandler * h1 = new HandleByMainProgram();
    // IHandler * h2 = new HandleByProjMgr();
    // IHandler * h3 = new HandleByBoss();
    // h1->SetNextHandler(h2);
    // h2->SetNextHandler(h3);

    IHandler * h0 = new HandleByBeauty();
    IHandler * h1 = new HandleByMainProgram();
    IHandler * h2 = new HandleByProjMgr();
    IHandler * h3 = new HandleByBoss();
    h0->SetNextHandler(h1);
    h1->SetNextHandler(h2);
    h2->SetNextHandler(h3);
    // 设置下一指针 
    Context ctx;
    h0->Handle(ctx);
    return 0;
}
  • 要点
    • 解耦请求⽅和处理⽅,请求⽅不知道请求是如何被处理,处理⽅的组成是由相互独⽴的⼦处理构成,⼦处理流程通过链表的⽅式连接,⼦处理请求可以按任意顺序组合;
    • 责任链请求强调请求最终由⼀个⼦处理流程处理;通过了各个⼦处理条件判断;
    • 责任链扩展就是功能链,功能链强调的是,⼀个请求依次经由功能链中的⼦处理流程处理;
    • 充分体现了单⼀职责原则;将职责以及职责顺序运⾏进⾏抽象,那么职责变化可以任意扩展,同时职责顺序也可以任意扩展;
  • 本质

分离职责,动态组合;

  • 结构图

image.png

装饰器模式

  • 定义

动态地给⼀个对象增加⼀些额外的职责。就增加功能⽽⾔,装饰器模式⽐⽣成⼦类更为灵活。 —— 《设计模式》GoF

  • 背景

普通员⼯有销售奖⾦,累计奖⾦,部⻔经理除此之外还有团队奖⾦;后⾯可能会添加环⽐增⻓奖⾦,同时可能针对不同的职位产⽣不同的奖⾦组合;
![image.png](https://img-blog.csdnimg.cn/img_convert/edd01ecfd68e75e9c5fd9e5b06926895.png#averageHue=#f1e7e7&clientId=u38543159-e800-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=327&id=u7962834e&margin=[object Object]&name=image.png&originHeight=409&originWidth=2165&originalType=binary&ratio=1&rotation=0&showTitle=false&size=89589&status=done&style=none&taskId=u138e781a-4dbc-40b7-ab0c-71aad2ffe46&title=&width=1732)

  • 代码

不使用装饰器

// 普通员工有销售奖金,累计奖金,部门经理除此之外还有团队奖金;后面可能会添加环比增长奖金,同时可能产生不同的奖金组合;
// 销售奖金 = 当月销售额 * 4%
// 累计奖金 = 总的回款额 * 0.2%
// 部门奖金 = 团队销售额 * 1%
// 环比奖金 = (当月销售额-上月销售额) * 1%
// 销售后面的参数可能会调整
class Context {
public:
    bool isMgr;
    // User user;
    // double groupsale;
};

class Bonus {
public:
    double CalcBonus(Context &ctx) {
        double bonus = 0.0;
        bonus += CalcMonthBonus(ctx);
        bonus += CalcSumBonus(ctx);
        if (ctx.isMgr) {
            bonus += CalcGroupBonus(ctx);
        }
        return bonus;
    }
private:
    double CalcMonthBonus(Context &ctx) {
        double bonus/* = */;
        return bonus;
    }
    double CalcSumBonus(Context &ctx) {
        double bonus/* = */;
        return bonus;
    }
    double CalcGroupBonus(Context &ctx) {
        double bonus/* = */;
        return bonus;
    }
};

int main() {
    Context ctx;
    // 设置 ctx
    Bonus *bonus = new Bonus;
    bonus->CalcBonus(ctx);
}

使用装饰器

#include <iostream>
// 普通员工有销售奖金,累计奖金,部门经理除此之外还有团队奖金;后面可能会添加环比增长奖金,同时可能产生不同的奖金组合;
// 销售奖金 = 当月销售额 * 4%
// 累计奖金 = 总的回款额 * 0.2%
// 部门奖金 = 团队销售额 * 1%
// 环比奖金 = (当月销售额-上月销售额) * 1%
// 销售后面的参数可能会调整
using namespace std;
class Context {
public:
    bool isMgr;
    // User user;
    // double groupsale;
};

// 试着从职责出发,将职责抽象出来
class CalcBonus {    
public:
    CalcBonus(CalcBonus * c = nullptr) : cc(c) {}
    virtual double Calc(Context &ctx) {
        return 0.0; // 基本工资
    }
    virtual ~CalcBonus() {}

protected:
    CalcBonus* cc;
};

class CalcMonthBonus : public CalcBonus {
public:
    CalcMonthBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double mbonus /*= 计算流程忽略*/; 
        return mbonus + cc->Calc(ctx);
    }
};

class CalcSumBonus : public CalcBonus {
public:
    CalcSumBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double sbonus /*= 计算流程忽略*/; 
        return sbonus + cc->Calc(ctx);
    }
};

class CalcGroupBonus : public CalcBonus {
public:
    CalcGroupBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double gbnonus /*= 计算流程忽略*/; 
        return gbnonus + cc->Calc(ctx);
    }
};

class CalcCycleBonus : public CalcBonus {
public:
    CalcCycleBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double gbnonus /*= 计算流程忽略*/; 
        return gbnonus + cc->Calc(ctx);
    }
};

int main() {
    // 1. 普通员工
    Context ctx1;
    CalcBonus *base = new CalcBonus();
    CalcBonus *cb1 = new CalcSumBonus(base);
    CalcBonus *cb2 = new CalcMonthBonus(cb1);
    cb2->Calc(ctx1);
    // 2. 部门经理
    Context ctx2;
    CalcBonus *cb3 = new CalcGroupBonus(cb2);
    cb3->Calc(ctx2);
}

// 能不能用 功能链的写法 来实现 装饰器功能?
// 可以
  • 要点
    • 通过采⽤组合⽽⾮继承的⼿法, 装饰器模式实现了在运⾏时动态扩展对象功能的能⼒,⽽且可以根据需要扩展多个功能。 避免了使⽤继承带来的“灵活性差”和“多⼦类衍⽣问题”。
    • 不是解决“多⼦类衍⽣的多继承”问题,⽽是解决“⽗类在多个⽅向上的扩展功能”问题;
    • 装饰器模式把⼀系列复杂的功能分散到每个装饰器当中,⼀般⼀个装饰器只实现⼀个功能,实现复⽤装饰器的功能;
  • 本质
    • 动态组合
  • 结构图

image.png

工厂方法模式

  • 定义

定义⼀个⽤于创建对象的接⼝,让⼦类决定实例化哪⼀个类。Factory Method使得⼀个类的实例化延迟到⼦类。 ——《设计模式》GoF

  • 背景

实现⼀个导出数据的接⼝,让客户选择数据的导出⽅式;

  • 代码
    • 版本1
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:
    virtual bool Export(const std::string &data) = 0;
    virtual ~IExport(){}
};

class ExportXml : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportJson : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportTxt : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

// =====1
int main() {
    std::string choose/* = */;
    if (choose == "txt") {
        IExport *e = new ExportTxt();
        e->Export("hello world");
    } else if (choose == "json") {
        IExport *e = new ExportJson();
        e->Export("hello world");
    } else if (choose == "xml") {
        IExport *e = new ExportXml();
        e->Export("hello world");
    }
}

存在问题:让客户端通过if-else选择创建不同对象,然后导出

  • 版本2
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:
    virtual bool Export(const std::string &data) = 0;
    virtual ~IExport(){}
};

class ExportXml : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportJson : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportTxt : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class IExportFactory {
public:
    virtual IExport * NewExport(/* ... */) = 0;
};

class ExportXmlFactory : public IExportFactory {
public:
    IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportXml;
        // 可能之后有什么操作
        return temp;
    }
};

class ExportJsonFactory : public IExportFactory {
public:
    IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportJson;
        // 可能之后有什么操作
        return temp;
    }
};

class ExportTxtFactory : public IExportFactory {
public:
    IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportTxt;
        // 可能之后有什么操作
        return temp;
    }
};

class ExportData {
public:
    ExportData(IExportFactory *factory) : _factory(factory) {}
    ~ExportData() {
        if (_factory) {
            delete _factory;
            _factory = nullptr;
        }
    }
    bool Export(const std::string &data) {
        IExport * e = _factory->NewExport();
        e->Export(data);
    }
private:
    IExportFactory *_factory;
};

int main() {
    ExportData ed(new ExportTxtFactory);
    ed.Export("hello world");
    return 0;
} 

解决问题:解决让客户端通过if-else选择创建不同的对象
存在问题:export方法是通用的,可以封装到factory基类上

  • 版本3
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:
    virtual bool Export(const std::string &data) = 0;
    virtual ~IExport(){}
};

class ExportXml : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportJson : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportTxt : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportCSV : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class IExportFactory {
public:
    IExportFactory() {
        _export = nullptr;
    }
    virtual ~IExportFactory() {
        if (_export) {
            delete _export;
            _export = nullptr;
        }
    }
    bool Export(const std::string &data) {
        if (_export == nullptr) {
            _export = NewExport();
        }
        return _export->Export(data);
    }
protected:
    virtual IExport * NewExport(/* ... */) = 0;
private:
    IExport* _export;
};

class ExportXmlFactory : public IExportFactory {
protected:
    virtual IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportXml();
        // 可能之后有什么操作
        return temp;
    }
};
class ExportJsonFactory : public IExportFactory {
protected:
    virtual IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportJson;
        // 可能之后有什么操作
        return temp;
    }
};
class ExportTxtFactory : public IExportFactory {
protected:
    IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportTxt;
        // 可能之后有什么操作
        return temp;
    }
};

class ExportCSVFactory : public IExportFactory {
protected:
    virtual IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportCSV;
        // 可能之后有什么操作
        return temp;
    }
};

int main () {
    IExportFactory *factory = new ExportTxtFactory();
    factory->Export("hello world");
    return 0;
}
  • 要点
    • 解决创建过程⽐较复杂,希望对外隐藏这些细节;
      • ⽐如连接池,线程池;
      • 隐藏对象真实类型;
      • 对象创建会有很多参数来决定如何创建;
      • 创建对象有复杂的依赖关系;
  • 本质
    • 延迟到⼦类来选择实现;
  • 结构图

image.png

抽象工厂模式

  • 定义

提供⼀个接⼝,让该接⼝负责创建⼀系列“相关或者相互依赖的对象”,⽆需指定它们具体的类。 ——《设计模式》GoF

  • 背景

实现⼀个拥有导出导⼊数据的接⼝,让客户选择数据的导出导⼊⽅式;

  • 代码
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:
    virtual bool Export(const std::string &data) = 0;
    virtual ~IExport(){}
};

class ExportXml : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportJson : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportTxt : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class ExportCSV : public IExport {
public:
    virtual bool Export(const std::string &data) {
        return true;
    }
};

class IImport {
public:
    virtual bool Import(const std::string &data) = 0;
    virtual ~IImport(){}
};

class ImportXml : public IImport {
public:
    virtual bool Import(const std::string &data) {
        return true;
    }
};

class ImportJson : public IImport {
public:
    virtual bool Import(const std::string &data) {
        return true;
    }
};

class ImportTxt : public IImport {
public:
    virtual bool Import(const std::string &data) {
        return true;
    }
};

class ImportCSV : public IImport {
public:
    virtual bool Import(const std::string &data) {
        // ....
        return true;
    }
};

class IDataApiFactory {
public:
    IDataApiFactory() {
        _export = nullptr;
        _import = nullptr;
    }
    virtual ~IDataApiFactory() {
        if (_export) {
            delete _export;
            _export = nullptr;
        }
        if (_import) {
            delete _import;
            _import = nullptr;
        }
    }
    bool Export(const std::string &data) {
        if (_export == nullptr) {
            _export = NewExport();
        }
        return _export->Export(data);
    }
    bool Import(const std::string &data) {
        if (_import == nullptr) {
            _import = NewImport();
        }
        return _import->Import(data);
    }
protected:
    virtual IExport * NewExport(/* ... */) = 0;
    virtual IImport * NewImport(/* ... */) = 0;
private:
    IExport *_export;
    IImport *_import;
};

class XmlApiFactory : public IDataApiFactory {
protected:
    virtual IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportXml;
        // 可能之后有什么操作
        return temp;
    }
    virtual IImport * NewImport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IImport * temp = new ImportXml;
        // 可能之后有什么操作
        return temp;
    }
};

class JsonApiFactory : public IDataApiFactory {
protected:
    virtual IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportJson;
        // 可能之后有什么操作
        return temp;
    }
    virtual IImport * NewImport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IImport * temp = new ImportJson;
        // 可能之后有什么操作
        return temp;
    }
};
class TxtApiFactory : public IDataApiFactory {
protected:
    virtual IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportTxt;
        // 可能之后有什么操作
        return temp;
    }
    virtual IImport * NewImport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IImport * temp = new ImportTxt;
        // 可能之后有什么操作
        return temp;
    }
};

class CSVApiFactory : public IDataApiFactory {
protected:
    virtual IExport * NewExport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IExport * temp = new ExportCSV;
        // 可能之后有什么操作
        return temp;
    }
    virtual IImport * NewImport(/* ... */) {
        // 可能有其它操作,或者许多参数
        IImport * temp = new ImportCSV;
        // 可能之后有什么操作
        return temp;
    }
};

int main () {
    IDataApiFactory *factory = new CSVApiFactory();
    factory->Import("hello world");
    factory->Export("hello world");
    return 0;
}
  • 要点
  • 本质
  • 结构图

image.png

适配器模式

  • 定义

将⼀个类的接⼝转换成客户希望的另⼀个接⼝。Adapter模式使得原本由于接⼝不兼容⽽不能⼀起⼯作的那些类可以⼀起⼯作。 ——《设计模式》GoF

  • 背景

⽇志系统,原来是通过写磁盘的⽅式进⾏存储,后来因为查询不便,需要额外添加往数据库写⽇志的功能(写⽂件和数据库并存);

  • 代码
#include <string>
#include <vector>
using namespace std;

class LogSys {
public:
    LogSys() {}
    void WriteLog(const vector<string> &) {
        // ... 日志id 时间戳 服务器id 具体日志内容 roleid
    }
    vector<string>& ReadLog() {
        // ...
        vector<string> data /* = ...*/;
        return data;
    }
};

class DB;  // 面向接口编程,应该使用非具体类指针(例如后期可改成mysql mongo等其他数据库类型),而不是具体类(强依赖、耦合性高)  

class LogSysEx : public LogSys {
public:
    LogSysEx(DB *db) : _db(db) {}

    void AddLog(const vector<string> &data) {
        LogSys::WriteLog(data);
        /*
            这里调用 _db 的方法将 data 数据存储到数据库
        */
    }

    void DelLog(const int logid) {
        vector<string>& data = LogSys::ReadLog();
        // 从 vector<string> 中删除 logid的日志
        LogSys::WriteLog(data);
        // 调用 _db 的方法将 logid的日志删除
    }

    void UpdateLog(const int logid, const string &udt) {
        vector<string>& data = LogSys::ReadLog();
        // 从 vector<string> 中更新 logid的日志 udt
        LogSys::WriteLog(data);
        // 调用 _db 的方法将 logid的日志更改
    }

    string& LocateLog(const int logid) {
        vector<string>& data = LogSys::ReadLog();
        string log1 /* = from log file*/;
        string log2 /* = from db */;
        string temp = log1 + ";" + log2;
        return temp;
    }
    
private:
    DB* _db;    // 以组合方式扩展功能
};
  • 要点
    • 原来的接⼝是稳定的,新的外来的需求是变化的,那么可以通过继承原来的接⼝,让原来的接⼝继续保持稳定,在⼦类通过组合的⽅式来扩展功能;
  • 本质
    • 转换匹配,复⽤功能;
  • 结构图

image.png

代理模式

  • **定义 **

为其他对象提供⼀种代理以控制对这对象的访问。 ——《设计模式》GoF

  • 背景

在有些系统中,为了某些对象的纯粹性,只进⾏了功能相关封装(稳定点),后期添加了其他功能需要对该对象进⾏额外操作(变化点),为了隔离变化点(也就是不直接在稳定点进⾏修改,这样会让稳定点也变得不稳定),可以抽象⼀层代理层;

  • 代码

class ISubject {
public:
    virtual void Handle() = 0;
    virtual ~ISubject() {}
};

// 该类在当前进程,也可能在其他进程当中
class RealSubject : public ISubject {
public:
    virtual void Handle() {
        // 只完成功能相关的操作,不做其他模块的判断
    }
};

// 在当前进程当中  只会在某个模块中使用
class Proxy1 : public ISubject {
public:
    Proxy1(ISubject *subject) : _subject(subject) {}
    virtual void Handle() {
        // 在访问 RealSubject 之前做一些处理
        //if (不满足条件)
        //    return;
        _subject->Handle();
        count++;
        // 在访问 RealSubject 之后做一些处理
    }
private:
    ISubject* _subject;
    static int count;
};
int Proxy1::count = 0;

// 在分布式系统当中  skynet actor
class Proxy2 : public ISubject {
public:
    virtual void Handle() {
        // 在访问 RealSubject 之前做一些处理
        
        // 发送到数据到远端  网络处理  同步非阻塞 ntyco c协程
        //IResult * val = rpc->call("RealSubject", "Handle");

        // 在访问 RealSubject 之后做一些处理
    }
private:
    /*void callback(IResult * val) {
        // 在访问 RealSubject 之后做一些处理
    }*/
};
  • 要点
    • 远程代理(隐藏⼀个对象存在不同的地址空间的事实),虚代理(延迟加载lazyload),保护代理(在代理前后做额外操作,权限管理,引⽤计数等);
    • 在分布式系统中,actor模型(skynet)等常⽤的设计模式;
  • 本质
    • 控制对象访问;
  • 结构图

image.png

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

年产2万吨山楂酒工厂的设计-装瓶工段及车间的设计(lunwen+任务书+开题+选题表+cad图纸)

目 录 目 录 I 第一章 绪论 1 1.1设计背景及目的 1 1.1.1山楂酒的介绍及功效 1 1.1.2目标人群 1 1.1.3发展前景 2 1.2设计依据 2 1.3设计内容 2 1.4原材料的选择 3 第二章 工艺设计 4 2.1 生产时间安排 4 2.2 工艺流程图 4 2.3 工艺要点 4 2.3.1山楂 5 2.3.3压榨 5 2.3.4分离取…

【pen200-lab】10.11.1.8

pen200-lab 学习笔记 【pen200-lab】10.11.1.5 &#x1f525;系列专栏&#xff1a;pen200-lab &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年12月4日&#x1f334; &#x1f36d;作者…

Java自动装箱与自动拆箱

1、Java基本数据类型及其对应的包装器类类型 Java中共用8种基本数据类型&#xff0c;并为这8种基本数据类型中的每一种都提供了一个包装器类&#xff0c;例如int类型对应的包装器类是Integer。具体类型如下表&#xff1a; 2、自动装箱和自动拆箱 自动装箱&#xff1a;就是指…

【大道模式】状态模式 - State Pattern(审核状态流转)

最近在实现一个简单的审核系统&#xff0c;包含多个审核状态和多个审核动作&#xff0c;将业务逻辑映射到代码中感觉会有很多繁杂的逻辑&#xff0c;因此&#xff0c;会思考用一个比较通用、易扩展、易维护、易测试的模式来应对这种场景&#xff0c;了解了一下&#xff0c;就是…

Java数据结构与Java算法学习Day03---线性表(简略笔记记录)

目录 一、线性表 38 二、顺序表 39 三、链表 45 3.1单向链表 46、47、48 3.2双向链表 49 3.3链表反转 &#xff08;面试中高频题目&#xff09; 56 3.4快慢指针 57 3.4.1中间值问题 57 3.4.2单向链表是否有环问题 58 3.4.3有环链表入口问题 60 3.5循环链表 61 3.6…

(Java)Mybatis学习笔记(三)

前言 继续学习MyBatis各种查询功能章节&#xff0c;争取早日学完mybatis MyBatis中各种查询功能 ❄️若查询出的 数据只有一条&#xff0c;可以通过实体类对象或集合接收 ❄️若查询出的 数据有多条&#xff0c;可以通过集合接收&#xff0c;一定不能通过实体类对象接收&am…

社区系统项目复盘-8

文章目录任务执行和调度热帖排行生成长图优化网站的性能使用Quartz执行定时任务&#xff0c;实现热帖排行功能时&#xff0c;通过定时任务定时计算帖子分数&#xff0c;降低计算的数据量。使用wkhtmltopdf生成长图。通过多级缓存对热帖功能进行优化&#xff0c;提升网站性能。 …

回归分析-书后习题回顾总结

4-1 题目 理论基础 经典多元线性回归模型 参数β\betaβ的最小二乘估计 设rank(C)m1≤nrank(C)m1≤nrank(C)m1≤n&#xff0c;则β^b(CTC)−1CTY\widehat{\beta}b(C^{T}C)^{-1}C^{T}Yβ​b(CTC)−1CTY是β\betaβ的最小二乘估计 具体解题

[附源码]计算机毕业设计网文论坛管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

本田125摩托安装版成型模具设计及仿真(任务书+说明书+工艺卡片+cad图纸+sw三维图)

目录 1 绪论 1 2 模塑工艺规程的编制 3 2.1塑件的工艺性分析 3 2.1.1塑件的原材料分析 3 2.2塑件的结构和尺寸精度及表面质量分析 4 2.2.1结构分析 4 2.2.2尺寸精度分析 4 2.2.3表面质量分析 4 2&#xff0e;3计算塑件的体积和质量 4 2&#xff0e;4塑件成型工艺参数的确定 5 2…

34. 在排序数组中查找元素的第一个和最后一个位置

&#x1f308;&#x1f308;&#x1f604;&#x1f604; 欢迎来到茶色岛独家岛屿&#xff0c;本期将为大家揭晓34. 在排序数组中查找元素的第一个和最后一个位置&#xff0c;做好准备了么&#xff0c;那么开始吧。 &#x1f332;&#x1f332;&#x1f434;&#x1f434; 一、题…

[激光原理与应用-36]:《光电检测技术-3》- 光学测量基础 - 光电效应与光电探测器的基本原理

目录 一、概述 二、光电检测的理论基础&#xff1a;光电效应 三、分类 3.1 光子效应 3.2 热效应 四、光电检测器的参数 五、常见的光电探测器 5.1 光电倍增管&#xff1a;微弱光信号转换成电信号 5.2 光电导器件&#xff1a;电阻或电流随着光强的变化而变化 5.3 光伏…

idea 启动报错 Command line is too long

idea 运行启动类报错 Command line is too long 启动报错信息&#xff1a;Error running ‘Application‘: Command line is too long. 翻译过来就是&#xff1a;启动命令过长&#xff01; 解决方案 1、点开项目启动配置项目&#xff1b; 2、shorten command line 选项选择 J…

(五)进程管理:进程的状态与控制

文章目录一、进程的状态二、进程控制1. 进程控制的原语2. 挂起与激活一、进程的状态 进程的生命周期&#xff1a;从创建到终止的过程 进程的三种基本状态 就绪&#xff08;Ready&#xff09; 可运行而未运行的状态&#xff0c;进程已经分配到除了处理机外的所有资源&#xf…

[附源码]计算机毕业设计校园订餐系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

python数据分析及可视化(十六)金融量化(金融工具、金融分析、Tushare安装使用、双均线分析)

金融介绍 金融就是对现有资源进行重新整合之后&#xff0c;实现价值和利润的等效流通。 比如小明想把手里的资金投资给小李&#xff0c;而小李有好的增值项目但是缺少资金&#xff0c;如果小李的项目创业成功&#xff0c;小明的资金就会增长。 金融工具 在金融市场中可交易的…

时序数据库基本概念学习

目录1、时序数据1.1 定义1.2 数学模型1.3 数据特点2、存储优化3、存储原理4、时序数据模型4.1 基于标签&#xff08;tag-value&#xff09;4.2 基于树形&#xff08;tree schema&#xff09;1、时序数据 1.1 定义 时序数据就是一串按时间维度索引的数据&#xff0c;这类数据描…

C++11闭包函数的几种实现方法

什么是闭包函数 函数就是对传入的一组参数进行运算的行为&#xff0c;闭包函数就是有状态的函数&#xff0c;在参与运算时&#xff0c;除了传入的参数外&#xff0c;还可以对上下文的状态进行运算。类函数运行时就是典型的闭包函数&#xff0c;类函数运行起来后其对象就是状态…

Spring - @PostConstruct 源码解析

文章目录Prejavax.annotation.PostConstruct 注解源码解析扩展示例Pre Spring Boot - 扩展接口一览 javax.annotation.PostConstruct 注解 Documented Retention (RUNTIME) Target(METHOD) public interface PostConstruct { }严格意义上来说这个并不算一个扩展点&#xff0c…

[附源码]计算机毕业设计校园订餐管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…