日志系统第二弹:设计模式介绍
设计模式
就是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结,是很多大佬在编程过程中总结下来的经验,类似于"武林秘籍"
我们的项目当中需要用到几个常见的设计模式:
- 单例模式
- 工厂模式
- 建造者模式
- 代理模式
其中,单例模式广为人知,我们就不赘述了
我们这篇博客要介绍工厂模式、建造者模式和代理模式
但是,在介绍设计模式之前,我们要先介绍程序设计的六大原则
一、六大原则
1、单一职责原则
定义:一个类应该只负责一项职责
目的:使责任划分清晰明了,便于责任归咎和整改
实现:当发现一个类承担了多项职责时,应该考虑将其分为多个类,每个类只负责一项职责
2.开闭原则
定义:软件实体(类、模块、函数等)对扩展开放,对修改关闭
即:软件实体在扩展新功能时不应该修改原有代码
目的:提高代码的灵活性和可维护性,降低因修改代码而引入的风险
实现:通过多态,实现在不修改原有代码的基础上扩展新的类
3.里氏替换原则
定义:子类对象能够安全地替换父类对象被调用,且不会影响程序的正确性
目的:要求子类继承父类时,遵循父类的规定,提高代码的稳定性和可维护性
4.依赖倒置原则
定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖具体,而具体应该依赖抽象
实现:每个类都应该派生于抽象类,而不应该派生于具体类
5.接口隔离原则
定义:类之间的依赖的关系应该建立在最少的接口上,不提供没有必要的接口
应用:在定义接口时,应该根据客户端具体需求来定义接口,不提供没有必要的接口
6.迪米特法则(最少知道原则)
定义:一个对象应该对其他对象保持最少程度了解,即尽量减少对象之间的交互和依赖关系
目的:尽量减少对象和对象之间的交互,从而减少类之间的耦合
二、工厂模式
1.介绍
工厂模式是一种创建型设计模式,提供一种创建对象的最佳方式。
在工厂模式当中,我们创建对象时,不会对上层暴露创建逻辑,而是通过一个共同结构来指向新创建的对象,以此实现创建和使用的分离
2.为何要有工厂模式
很多时候,我们要用一个对象,不过我们需要先构造该对象。
这么做没有什么毛病,但是一旦对象修改了导致我们构造的地方也需要变动,那就需要该很多地方,非常不优雅
因此,大佬们决定在中间加一层,就形成了简单工厂模式:
这样的话具体细节被工厂类屏蔽了,改动代码时只需要改动工厂类即可
3.简单工厂模式
1.代码
Talk is cheap,Show me the code
cpp
class Fruit
{
public:
// 父类指针指向new出来的子类对象,父类的析构函数必须要加virtual
// 否则delete时就会调用父类的析构函数,极大可能会导致 内存泄漏或其他潜在风险
virtual ~Fruit() {}
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
virtual void name()
{
cout << "我是苹果\n";
}
};
class Banana : public Fruit
{
public:
virtual void name()
{
cout << "我是香蕉\n";
}
};
enum FruitType
{
APPLE,
BANANA
};
namespace ns_easy
{
class FruitFactory
{
public:
static shared_ptr<Fruit> create(FruitType type)
{
if (type == APPLE)
return make_shared<Apple>();
else if (type == BANANA)
return make_shared<Banana>();
else
return shared_ptr<Fruit>(); // 空的智能指针
}
};
}
int main()
{
auto sp = ns_easy::FruitFactory::create(APPLE);
sp->name();
sp = ns_easy::FruitFactory::create(BANANA);
sp->name();
return 0;
}
有了简单工厂模式之后,构造苹果和香蕉就方便多了,但是一旦再次引入新水果(比如橙子),那就需要修改工厂类当中的create函数了
2.优缺点
特点:通过参数控制,便可以生产任何产品
优点:简单粗暴,直观易懂,使用一个工厂便可以生产一类产品
缺点:
- 违背了开闭原则,想要新增产品就需要修改工厂源代码
因此为了遵循开闭原则,大佬们搞出了工厂设计模式
4.工厂设计模式
1.介绍
既然不能修改原工厂,那么我们就仿照水果和(苹果,香蕉。。)之间的关系,将原工厂改为抽象类,一个类型的水果就对应一个工厂
2.代码
Talk is cheap,Show me the code
cpp
class Fruit
{
public:
// 父类指针指向new出来的子类对象,父类的析构函数必须要加virtual
// 否则delete时就会调用父类的析构函数,极大可能会导致 内存泄漏或其他潜在风险
virtual ~Fruit() {}
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
virtual void name()
{
cout << "我是苹果\n";
}
};
class Banana : public Fruit
{
public:
virtual void name()
{
cout << "我是香蕉\n";
}
};
class Orange : public Fruit
{
public:
virtual void name()
{
cout << "我是橙子\n";
}
};
namespace ns_method
{
class FruitFactory
{
public:
virtual ~FruitFactory(){}
virtual shared_ptr<Fruit> create() = 0;
};
class AppleFactory : public FruitFactory
{
public:
virtual shared_ptr<Fruit> create()
{
return make_shared<Apple>();
}
};
class BananaFactory : public FruitFactory
{
public:
virtual shared_ptr<Fruit> create()
{
return make_shared<Banana>();
}
};
class OrangeFactory : public FruitFactory
{
public:
virtual shared_ptr<Fruit> create()
{
return make_shared<Orange>();
}
};
}
int main()
{
std::shared_ptr<ns_method::FruitFactory> factory = std::make_shared<ns_method::AppleFactory>();
auto fruit = factory->create();
fruit->name();
factory = std::make_shared<ns_method::BananaFactory>();
fruit = factory->create();
fruit->name();
factory = std::make_shared<ns_method::OrangeFactory>();
fruit = factory->create();
fruit->name();
return 0;
}
3.优缺点
特点:一个产品,一个工厂
优点:遵循了开闭原则
缺点:当产品特别多的时候,会造成代码臃肿
因此为了应对产品特别多的时候,大佬们发明了抽象工厂模式:
5.抽象工厂模式
1.介绍
依然是一个产品抽象类一个工厂,只不过这种方式更适用于多产品模式
2.代码
cpp
class Animal
{
public:
virtual ~Animal() {}
virtual void name() = 0;
};
class Dog : public Animal
{
public:
virtual void name()
{
cout << "我是狗\n";
}
};
class Cat : public Animal
{
public:
virtual void name()
{
cout << "我是猫\n";
}
};
enum AnimalType
{
DOG,
CAT
};
namespace ns_abstract
{
class Factory
{
public:
virtual shared_ptr<Fruit> createFruit(FruitType type) = 0;
virtual shared_ptr<Animal> createAnimal(AnimalType type) = 0;
};
class FruitFactory : public Factory
{
public:
virtual shared_ptr<Fruit> createFruit(FruitType type)
{
if (type == APPLE)
return make_shared<Apple>();
if (type == BANANA)
return make_shared<Banana>();
if (type == ORANGE)
return make_shared<Orange>();
return shared_ptr<Fruit>();
}
virtual shared_ptr<Animal> createAnimal(AnimalType type)
{
return shared_ptr<Animal>();
}
};
class AnimalFactory : public Factory
{
public:
virtual shared_ptr<Fruit> createFruit(FruitType type)
{
return shared_ptr<Fruit>();
}
virtual shared_ptr<Animal> createAnimal(AnimalType type)
{
if (type == DOG)
return make_shared<Dog>();
if (type == CAT)
return make_shared<Cat>();
return shared_ptr<Animal>();
}
};
}
int main()
{
std::shared_ptr<ns_abstract::Factory> factory = make_shared<ns_abstract::FruitFactory>();
auto fruit = factory->createFruit(APPLE);
fruit->name();
fruit = factory->createFruit(BANANA);
fruit->name();
fruit = factory->createFruit(ORANGE);
fruit->name();
factory = make_shared<ns_abstract::AnimalFactory>();
auto animal = factory->createAnimal(DOG);
animal->name();
animal = factory->createAnimal(CAT);
animal->name();
return 0;
}
3.优缺点
优点:适用于多产品,代码冗余度相对较小
缺点:违背开闭原则,增加新的产品类需要修改代码
三、建造者模式
1.介绍
建造者模式是一种创建型设计模式,主要用于解决对象的构建过于复杂的问题
使用一个个简单的对象一步一步构建成一个复杂的对象
2.为何要有建造者模式
在项目开发当中,有些类是由一堆零部件组装而成的,因此构造对象时过于复杂,不够优雅
比如:电脑的组装:
建筑者分为:装Windows电脑的,装Linux,装Mac OS电脑的人
他们都继承于同一个抽象建筑者类
然后搞一个指示者,负责按照正确的顺序调用建筑者的相应接口来完成特定任务
最后电脑就装好了
(如果零部件之间没有特定顺序要求的话,那么实际上是不需要这个指示者的,直接让建筑者提供一个总的build接口即可)
cpp
因此建造者模式需要:
1. 抽象产品类
2. 具体产品类
3. 抽象Builder类:创建所有零部件的抽象接口
4. 具体Builder类:实现抽象接口,构建对应负责的零部件
5. 指挥者Director类:统一组件过程,通过指挥者来构造产品
3.代码
cpp
// 每个电脑都必须要有一个OS,因此我们把OS定为纯虚函数,强制子类重写
class Computer
{
public:
virtual ~Computer() {}
void setBoard(const string &board)
{
_board = board;
}
void setDisplay(const string &display)
{
_display = display;
}
virtual void setOS() = 0;
void show()
{
cout << "board:\t" << _board << "\n";
cout << "display:\t" << _display << "\n";
cout << "os:\t" << _os << "\n";
}
protected:
string _board;
string _display;
string _os;
};
class Winbook : public Computer
{
public:
virtual void setOS()
{
_os = "Win 11";
}
};
class Builder
{
public:
virtual ~Builder() {}
virtual void setBoard(const string &board) = 0;
virtual void setDisplay(const string &display) = 0;
virtual void setOS() = 0;
virtual shared_ptr<Computer> get() = 0;
};
class WinbookBuilder : public Builder
{
public:
WinbookBuilder() : _book(std::make_shared<Winbook>()) {}
virtual void setBoard(const string &board)
{
_book->setBoard(board);
}
virtual void setDisplay(const string &display)
{
_book->setDisplay(display);
}
virtual void setOS()
{
_book->setOS();
}
virtual shared_ptr<Computer> get()
{
return _book;
}
private:
shared_ptr<Winbook> _book;
};
class Director
{
public:
Director(const shared_ptr<Builder> &builder)
: _builder(builder) {}
void build(const string &board, const string &display)
{
_builder->setBoard(board);
_builder->setDisplay(display);
_builder->setOS();
}
shared_ptr<Builder> get()
{
return _builder;
}
private:
shared_ptr<Builder> _builder;
};
int main()
{
Director director(make_shared<WinbookBuilder>());
shared_ptr<Builder> builder = director.get();
director.build("XXX主板", "XXX显示器");
auto sp = builder->get();
sp->show();
return 0;
}
四、代理模式
1.介绍
代理模式是一种结构型设计模式,它为其他对象提供一种代理,以控制对该对象的访问
代理模式的主要目的是在客户端和目标对象之间增加一个中间层,起到增加功能、控制访问、减少系统间的耦合度的作用
cpp
代理模式的结构包括:
一个目标对象和一个代理对象,目标对象和代理对象实现同一个接口
先访问代理对象,在通过代理对象来访问目标对象
代理模式分为静态代理和动态代理:
静态代理:在编译时就已经确定了代理类和被代理类之间的关系
动态代理:在运行时才能确定代理类和被代理类之间的关系
2.代码
以租房为例,房东在租房的时候需要发布招租启示,带人看房,负责维修等等,而房东为了图方便,可以把自己的房子委托给中介进行租赁
cpp
class RentHouse
{
public:
virtual void rent()=0;
};
class Landlord:public RentHouse
{
public:
virtual void rent()
{
cout<<"把房子租出去\n";
}
};
class Intermediary:public RentHouse
{
public:
virtual void rent()
{
cout<<"发布招租启示\n";
cout<<"带人开房\n";
_landlord.rent();
cout<<"负责维修\n";
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rent();
return 0;
}
五、C不定参宏函数的介绍
我们的项目不仅仅会用到那四个设计模式,还会用到C和C++的不定参函数与C的不定参宏函数
因此,我们把他们介绍一下:
在学习C的时候,我们见过printf的函数原型:int printf(const char *format, ...);
这...
是可变参数列表,支持传入任意多个参数,首先我们先介绍它在C的宏函数当中的使用
我们就实现一个printf的宏函数吧
cpp
__VA_ARGS__是一个宏,用来展开...当中的所有参数
因此,我们就可以实现一个printf
cpp
#define PRINT(format,...) printf(format,__VA_ARGS__)
int main()
{
PRINT("hello %s - %d\n","world",1);
return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args
hello world - 1
打印日志时,我们通常把打印该日志的地方标记到日志当中,通常是以文件名:行号的方式来进行记录
需要用到__FILE__和__LINE__这两个宏,而C当中字符串是可以默认拼接的:
cpp
#define PRINT(format, ...) printf("%s:%d " format "\n", __FILE__, __LINE__, __VA_ARGS__)
int main()
{
PRINT("hello %s - %d", "world", 1);
return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args
macro_args.c:7 hello world - 1
如果用户调用时,没有给...传入任何参数,那么宏替换之后,就会多出一个逗号,函数调用就会有问题
我们来看一下,宏替换之后会变成什么样子
cpp
gcc -E macro_args.c > macro_args.i
针对这种情况,编译器提供了##__VA_ARGS__
,这个##的作用就是当...为空时,他会删除前面的那个逗号
因此,正确的版本是这样的:
cpp
#define PRINT(format, ...) printf("%s:%d " format "\n", __FILE__, __LINE__, ##__VA_ARGS__)
然后我们重新gcc -E一下
六、C不定参函数的介绍
C的不定参函数的函数签名是这样的:
cpp
至少需要一个固定参数,最后用...来接受任意数量的参数
函数内部需要根据固定参数来解析可变参数列表当中的所有参数
例如:
void func(int num,...);
void printf(const char* format,...);
1.C可变参数列表的介绍
在<stdarg.h>当中,定义了几个宏用来解决该问题
cpp
va_list:这是一个类型,用于声明一个变量,该变量将用于访问可变参数列表
va_start(ap, last):宏,用于初始化 va_list 类型的变量 ap,以访问 last 参数之后的参数。
last 是可变参数列表之前的最后一个固定参数。
va_arg(ap, type):宏,用于返回参数列表中下一个参数的值,并将其类型转换为 type。
每次调用 va_arg 都会使 ap 指向下一个参数。
va_end(ap):宏,用于清理 va_list 变量 ap。
这是使用可变参数列表后应该进行的清理工作。
2.vsprintf系列函数的介绍
cpp
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
int vasprintf(char **strp, const char *fmt, va_list ap);
根据format格式化字符串和ap可变参数列表,将对应参数提取出来存放到str当中
vsprintf和vsnprintf的区别是:
vsnprint允许指定最大字符数目,以防止缓冲区溢出
而vsprintf则没有这样的限制,容易发生安全问题
vasprintf是允许我们传入一级指针的地址,他在函数内部会给我们malloc一段空间,将数据放到其中
需要我们自行free掉这个strp,是这三个当中最好用的
若失败,则返回-1
3.使用可变参数列表
先给大家演示一下可变参数列表的使用:
cpp
void func(int num, ...)
{
va_list ap;
va_start(ap, num);
for (int i = 0; i < num; i++)
{
// 指针偏移
int val = va_arg(ap, int);
printf("%d ",val);
}
printf("\n");
va_end(ap);
}
int main()
{
func(3,1,2,3);
func(0);
func(1,1);
return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args
1 2 3
1
4.实现myprintf
想要使用vasprintf函数,需要 #define _GNU_SOURCE
cpp
void myprintf(const char *format, ...)
{
va_list ap;
va_start(ap, format);
char* buf=NULL;
int ret=vasprintf(&buf,format,ap);
if(ret!=-1)
{
printf("%s\n",buf);
free(buf);
}
va_end(ap);
}
int main()
{
myprintf("hello %s - %d","world",1);
myprintf("hello %s","world");
myprintf("hello");
return 0;
}
cpp
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args
hello world - 1
hello world
hello
七、C++不定参函数的介绍
C++不定参函数是通过可变模板参数包来实现的,是通过递归的方法来进行解包的
注意:因为可变模板参数包需要在编译时完成递归,而if语句在编译时,两条路都要走
因此如果args模板包为空,递归调用自己的路还是要编译的
而myprintf不支持传入0个参数,因此我们需要给myprintf加一个接受0个参数的函数重载,来解决编译时报错的问题
cpp
void myprintf()
{
cout << "\n";
}
template <class T, class... Args>
void myprintf(const T &val, Args &&...args)
{
// 先打印val
cout << val;
// 如果args模板包当中参数个数大于0,则递归分用
if ((sizeof...(args)) > 0)
{
myprintf(forward<Args>(args)...);
}
else
{
myprintf();
}
}
int main()
{
myprintf();
myprintf("hello");
myprintf("hello ", "world");
myprintf("hello ", "world-", 11);
return 0;
}
否则就会:
cpp
cpp_args.cc: In function 'void myprintf(const T&, Args&& ...)':
cpp_args.cc:21:18: error: no matching function for call to 'myprintf()'
21 | myprintf();
| ^
cpp_args.cc:10:6: note: candidate: 'template<class T, class ... Args> void myprintf(const T&, Args&& ...)'
10 | void myprintf(const T &val, Args &&...args)
| ^~~~~~~~
cpp_args.cc:10:6: note: template argument deduction/substitution failed:
cpp_args.cc:21:18: note: candidate expects at least 1 argument, 0 provided
21 | myprintf();
candidate expects at least 1 argument, 0 provided:
我期待至少一个参数,结果你给我0个...
八、项目工具类
我们的项目还需要几个小工具类:
1.获取系统当前时间
cpp
class DateHelper
{
public:
static time_t now()
{
return time(nullptr);
}
};
2.判断文件是否存在
cpp
class FileHelper
{
public:
static bool exists(const std::string &pathname)
{
struct stat st;
// stat遵循POSIX标准(Portable Operating System Interface for UNIX 可移植操作系统接口)
// 具有良好的跨平台移植性
return stat(pathname.c_str(), &st) >= 0; // 第二个参数不能给nullptr,因为它是一个输出型参数
}
3.获取文件父级路径
find_last_of是从后往前找第一个符合条件的字符,只要该字符位于传入的字符集合即可
cpp
static std::string getPath(const std::string &pathname)
{
// ./a/b/c/d
// 从后往前找第一个'/' 然后返回./a/b/c
// 如果没有,则返回.
// find_last_of
size_t pos = pathname.find_last_of(R"(/\)"); // 支持(/)Linux、Mac OS和Win(\)
if (pos == std::string::npos)
{
return ".";
}
return pathname.substr(0, pos);
}
因为\是转义字符,想要表示单纯的\的话,需要二次转义\\
不够优雅,所以我们用了一下C++11的 字符串字面量R"(/\)"
4.创建目录
./a/b/c
从前往后依次创建 ./a ./a/b ./a/b/c即可,就是简单的string的find和substr还有mkdir的使用而已
cpp
static bool createDir(const std::string &pathname)
{
// ./a/b/c
// 从前往后依次创建 ./a ./a/b ./a/b/c
size_t pos = pathname.find_first_of(R"(/\)");
while (pos != std::string::npos)
{
umask(0);
std::string substr = pathname.substr(0, pos);
int ret = mkdir(substr.c_str(), 0775);
if (ret == -1 && errno != EEXIST)
{
std::cout << "目录创建失,总目录:" << pathname << ",当前目录:" << substr << "\n";
return false;
}
pos = pathname.find_first_of(R"(/\)", pos + 1);
}
// 创建整个目录
return (mkdir(pathname.c_str(), 0775) != -1 || errno == EEXIST);
}
以上就是日志系统第二弹:设计模式介绍,C和C++不定参函数的介绍的全部内容