通用抽象工厂框架的实现

白癜风禁忌什么食物 http://m.39.net/pf/a_6985661.html
概述

工厂模式是最为常用的设计模式之一,在许多中大型的代码项目中都有应用,它提供了一种创建对象的最佳方式,解决了接口选择的问题。大致上来讲,该模式具备如下优点:

?调用者只要提供产品的名称,就能创建出所需的对象;?扩展性高,符合“开闭原则”,如果想增加一个产品,只要扩展一个工厂类就可以;?屏蔽了产品的具体实现(创建逻辑),调用者只关心创建接口;

在实际应用中,通过加入模板技术、反射机制、配置文件等元素,使得工厂模式的运用更具美感。

本文将从实现最简单的工厂模式出发,展现该模式带来的便利性;然后依据需求的变化,介绍更为复杂的抽象工厂模式,文章的最后,作者将给出大疆RoboRTS项目中的一个通用抽象工厂框架[1]。

相机驱动问题

在应用一个模式到你的代码中前,应当先明白这样做的意义,这也是学习设计模式的首要原则。

以我在战队中的工作为例,因为我们实验室很有钱,所以囤了不同厂家的工业相机,比如海康、大恒、迈德威视等等。恰巧我们造了一只机器人,需要配备至少两台相机。用过工业相机的朋友们都知道:第一,每个厂家会提供自家的相机驱动;第二,世界上没有两台参数(内参和畸变参数)相同的相机。

那么,问题来了,如何为这个新机器人设计相机模块的代码?注意你必须考虑到以下几个现实:

?两台相机必须以多线程方式并发工作;?相机参数应当在配置文件中设置;?由于战队内资源协调问题,不能保证这两台相机不会被暂时更换为其他厂家相机;?考虑到今后的功能升级,机器人可能会搭载三台或者更多相机;

遵循面对对象的设计原则,一般的想法是把每种相机驱动封装成单独的类,然后在某个地方创建对象和对应线程,如下所示(下列代码仅用于说明,无法编译运行)。

structCameraInfo{std::stringcamera_name;//相机名称cv::Matcamera_matrix;//内参矩阵cv::Matcamera_distortion;//畸变参数矩阵};classMINDVisionDriver{public:MINDVisionDriver(CameraInfocamera_info):camera_info_(camera_info){MINDVisionInit();//构造函数内对相机进行初始化};voidMINDReadCamera(cv::Matimg);//从相机读取一帧图片private:voidMINDVisionInit();CameraInfocamera_info_;};classHIKVisionDriver{public:HIKVisionDriver(CameraInfocamera_info):camera_info_(camera_info){HIKVisionInit();};voidHIKReadCamera(cv::Matimg);private:voidHIKVisionInit();CameraInfocamera_info_;};classCameraNode{public:CameraNode(){//使用Google开源的ProtoBuf读取参数文件CameraParamFromProtobufcamera_param(./camera_config.prototxt);CameraInfomind_camera_info=camera_param.GetCameraParam("mind_vision");CameraInfohik_camera_info=camera_param.GetCameraParam("hik_vision");//利用读取的参数创建相机对象mind_camera_=std::make_sharedMINDVisionDriver(mind_camera_info);hik_camera_=std::make_sharedHIKVisionDriver(hik_camera_info);}~CameraNode(){//等待线程结束mind_thread_.join();hik_thread_.join();}//创建相机线程voidStartThreads(){mind_thread_=std::thread(CameraNode::UpdateMind,this);hik_thread_=std::thread(CameraNode::UpdateHik,this);}private://Mind相机线程循环voidUpdateMind(){while(1){cv::Matframe;mind_camera_-MINDReadCamera(frame);img_buffer_.push(frame);}}//Hik相机线程循环voidUpdateHik(){while(1){cv::Matframe;hik_camera_-HIKReadCamera(frame);img_buffer_.push(frame);}}std::shared_ptrMINDVisionDrivermind_camera_;std::shared_ptrHIKVisionDriverhik_camera_;std::threadmind_thread_,hik_thread_;LockedImageBufferimg_buffer_;//多线程读写安全的图片缓冲区,被图像处理线程访问};intmain(){CameraNodecamera_node;camera_node.StartThreads();/*图像处理逻辑...*/}

好多代码!事实上,我几乎快将项目中图像生产的整个逻辑都写出来了。总体上说,我们为海康相机和迈德威视相机都分别建立了一个类,你可以发现它们是多么的相像,这大概就是封装带来的好处吧!不过,请不要忘了MINDVisionInit和HIKVisionInit、MINDReadCamera和HIKReadCamera内部实现的逻辑是不同的。

CameraNode类用于管理多个相机,负责参数的读取、相机对象的创建、线程管理、线程循环逻辑等等。试想一下,如果现在要增加一台相机或者更换相机的类型,将要对CameraNode类做多少的修改工作——是的,太繁重了,正所谓“改需求是程序员的噩梦”。

此外,理解配置文件如何工作也同样重要:在工程目录下会有一个配置文件camera_config.prototxt,其中罗列了海康相机和迈德威视相机的具体参数,CameraParamFromProtobuf类负责解析这个配置文件,我们只要调用GetCameraParam并指定相机的名字camera_name就能得到相机完整的参数对象mind_camera_info和hik_camera_info;当然,我们现在并不关心CameraParamFromProtobuf的实现,但要明白,通过配置文件,我们能减少程序的许多改动(修改程序变量需要重新编译代码,修改配置文件则不用)。这里有必要给出一个配置文件的demo以帮助理解,读者可以在脑海中想象解析该文件的过程。

camera:{camera_name:"mind_vision"camera_matrix{data:.data:0.0data:.data:0.0data:.data:.data:0.0data:0.0data:1.0}camera_distortion{data:0.083data:0.data:0.data:-0.data:0.0}}camera:{camera_name:"hik_vision"camera_matrix{data:.data:0.0data:.data:0.0data:.data:.data:0.0data:0.0data:1.0}camera_distortion{data:0.0data:0.0data:0.0data:0.0data:0.0}}使用多态

现在,不妨先想想改进这份代码(准确来说是改写CameraNode)的方法吧!

首先,用数组容纳相机驱动对象是个很不错的主意,即使哪一天机器人要搭载上千数量的相机,我们也可以通过迭代轻而易举地解决。唯一的麻烦在于数组要求元素类型相同,然而我们要装入的驱动对象属于不同的类。为了搞定这个麻烦,我们可以使用多态——定义一个驱动抽象类,让海康相机驱动类和迈德威视相机驱动类继承它,然后用基类指针指向不同派生对象,从而做到类型统一。改进后的第二版本代码如下:

classCameraDriver{public:CameraDriver(CameraInfocamera_info):camera_info_(camera_info){}virtualvoidReadCamera(cv::Matimg)=0;virtualvoidCameraInit()=0;protected:CameraInfocamera_info_;};classMINDVisionDriver:publicCameraDriver{public:MINDVisionDriver(CameraInfocamera_info):CameraDriver(camera_info){}voidReadCamera(cv::Matimg){}voidCameraInit(){}};classHIKVisionDriver:publicCameraDriver{public:HIKVisionDriver(CameraInfocamera_info):CameraDriver(camera_info){}voidReadCamera(cv::Matimg){}voidCameraInit(){}};classCameraNode{public:CameraNode(){CameraParamFromProtobufcamera_param(./camera_config.prototxt);CameraInfomind_camera_info=camera_param.GetCameraParam("mind_vision");CameraInfohik_camera_info=camera_param.GetCameraParam("hik_vision");camera_drivers_.push_back(std::make_sharedMINDVisionDriver(mind_camera_info));camera_drivers_.push_back(std::make_sharedHIKVisionDriver(hik_camera_info));}~CameraNode(){for(autoth:camera_threads_){th.join();}}voidStartThreads(){for(inti=0;icamera_drivers_.size();i++){camera_threads_.push_back(std::thread(CameraNode::Update,this,i));}}private:voidUpdate(intindex){camera_drivers_[index]-CameraInit();while(1){cv::Matframe;camera_drivers_[index]-ReadCamera(frame);img_buffer.push(frame);}}std::vectorstd::shared_ptrCameraDrivercamera_drivers_;std::vectorstd::threadcamera_threads_;LockedImageBufferimg_buffer;};工厂模式

加入多态特性后,代码明显精炼了不少,唯一的缺陷就是当相机类型或者数量变动时,我们还需要修改CameraNode中的构造函数。事实上,如果我们的机器人一年半载才换一次相机的话,这点不足根本不算什么,大不了就是到时候改几句代码的事情。正因如此,是否应用工厂模式也得看实际的需求,一味得使用设计模式并不是一个明智的做法。

这里,我们使用工厂模式+反射+配置文件的方案来完善第二版代码,改善的结果就是即使相机类型或数量变动,我们的代码也不需要修改和重新编译,仅仅只要改动配置文件即可。

首先,需要编写驱动工厂类。工厂类具有以下特点:

?使用map结构存储驱动信息(驱动名+驱动类构造函数),通过开放注册接口Register来向map注入信息;?采用单例模式,隐藏构造函数,提供实例获取接口getInstance;?提供对象创建接口,根据驱动名返回对应驱动对象指针;

classCameraFactory{public:typedefstd::functionstd::unique_ptrCameraDriver(CameraInfo)FUNC;staticstd::shared_ptrCameraFactorygetInstance(){staticstd::shared_ptrCameraFactoryinstance_ptr(newCameraFactory());returninstance_ptr;}voidRegister(conststd::stringname,FUNCgenerator){autoiter=driver_map.find(name);if(iter==driver_map.end()){driver_map[name]=generator;std::coutname"registeredsuccessfully!"std::endl;}else{std::coutname"hasbeenregistered!"std::endl;}}std::unique_ptrCameraDrivercreateDriver(std::stringname,CameraInfocamera_info){autoiter=driver_map.find(name);if(iter!=driver_map.end()){returniter-second(camera_info);}else{std::cout"Cantcreatalgorithm"name",becauseyouhaventregisterit!"std::endl;returnnullptr;}}private:CameraFactory()=default;CameraFactory(constCameraFactory);CameraFactoryoperator=(constCameraFactory);std::mapstd::string,FUNCdriver_map;};

工厂类只负责管理驱动信息,实际的注册工作由注册执行类CameraRegister完成。其注册是通过实例化执行类对象完成的,每注册一次,就调用一次执行类的构造函数,构造函数又会调用工厂类的注册接口。宏定义CAMERA_REGISTER的工作就是构建Lambda函数并创建对象,于是,我们就可以在每个派生类定义的头文件里,添加这个宏定义来进行注册,其中的驱动名应该和配置文件中使用的camera_name相同。

classCameraRegister{public:CameraRegister(conststd::stringname,CameraFactory::FUNCgenerator){CameraFactory::getInstance()-Register(name,generator);}};#defineCAMERA_REGISTER(Driver,DriverName)\CameraRegisterregister_##DriverName_##Driver(DriverName,[](CameraInfocamera_info)-std::unique_ptrDriver\{\returnstd::make_uniqueDriver(camera_info);\});CAMERA_REGISTER(MINDVisionDriver,"mind_vision");CAMERA_REGISTER(HIKVisionDriver,"hik_vision");

最后,我们来看看采用工厂模式后CameraNode的写法。CameraParamFromProtobuf需要为我们解析出配置文件中列出的所有相机实例名,然后前文的工厂类会创建出对应的所有相机驱动。

classCameraNode{public:CameraNode(){CameraParamFromProtobufcamera_param(./camera_config.prototxt);std::vectorstd::stringcamera_names=camera_param.GetCameraNames();for(autoname:camera_names){CameraInfocamera_info=camera_param.GetCameraParam(name);camera_drivers_.push_back(CameraFactory::getInstance()-createDriver(name,camera_info));}}~CameraNode(){for(autoth:camera_threads_){th.join();}}voidStartThreads(){for(inti=0;icamera_drivers_.size();i++){camera_threads_.push_back(std::thread(CameraNode::Update,this,i));}}private:voidUpdate(intindex){camera_drivers_[index]-CameraInit();while(1){cv::Matframe;camera_drivers_[index]-ReadCamera(frame);img_buffer.push(frame);}}std::vectorstd::shared_ptrCameraDrivercamera_drivers_;std::vectorstd::threadcamera_threads_;LockedImageBufferimg_buffer;};

好了!工厂模式到这里就完美实现了,尽管在一些细节上仍有待优化,但我觉得无伤大雅。实践证明,采用该模式在实际测试过程了极大地便利了我,也许你会觉得这些代码写起来有些费神费力,但是一旦有了这样一个demo,在其他的项目里也就只剩下了CV操作,难道不是吗?

如果你对上面的实现仍然不甚满意,抱怨着有没有更为通用性的框架可用,亦或者你的项目需求似乎超出了工厂模式的服务上限,那么接下来对于抽象工厂模式的介绍一定会合你的胃口!

通用框架

对于一个机器人系统来说,不仅仅只有相机传感器,对于其他传感器而言,如果也有不同驱动选择的需要,同样也可以应用工厂模式;此外,对于一个算法模块而言,可能有多种备选的算法方案,那么也可以应用工厂模式。这些工厂模式可以交给一个更高级的工厂来管理,即抽象工厂模式。令人唯一厌烦的在于我们要为不同的种类或方案创建不同的工厂,在一个大型项目中,这样的工作量是巨大的,这也是抽象工厂一般写法存在的主要缺点。

究其原因,在于每个具体工厂类需要知道自己生产的产品类型,比如CameraFactory内部依赖CameraDriver类型;为了消除这个问题,我们可以使用模板来编写抽象工厂模式。

下图给出了抽象工厂模板框架的原理图。位于最顶层的是模板类AlgorithmFactory,和之前的工厂类的作用是类似的,不同的在于模板本身的代码在编译时并不会加到可执行程序中,它相当于建造工厂的蓝图,通过指定AlgorithmBase和Args特例化出各种具体工厂(代码)。工厂类CameraFactory等价于AlgorithmFactoryCameraDriver,CameraInfo(CameraFactory这个类名不真实存在,只是为了区别于模板),负责相机驱动的管理;而工厂类DetectFactory等价于AlgorithmFactoryDetectBase,DetectParams,ObjectPose3d,负责检测算法方案的管理。

不难发现,原来抽象工厂模式中新增一个产品类,就需要编写相应的工厂类,现在非常方便了——只要给定产品基类和构造函数参数类型就能特例化出工厂的代码。当产品类只有一种时,这套框架等价于前文实现的工厂模式。

下面给出了这个通用框架的完整代码,整体的结构和之前的工厂类似,有几点需要注意的地方:

?所有成员函数都声明为静态类型,通过类作用域符号访问;?AlgorithmFactory不生成实例对象,故不采用单例模式,直接定义静态的AlgorithmHash;?原先宏定义中的Lambda函数因为依赖于具体产品,所以框架中将其定义为执行模板类的成员函数create;

templatetypenameAlgorithmBase,typename...ArgsclassAlgorithmFactory{public:usingAlgorithmConstructor=std::functionstd::unique_ptrAlgorithmBase(Args...);usingAlgorithmHash=std::unordered_mapstd::string,AlgorithmConstructor;staticAlgorithmHashGetAlgorithmHash(){staticAlgorithmHashalgorithm_hash;returnalgorithm_hash;}staticboolRegister(conststd::stringname,AlgorithmConstructorconstructor){AlgorithmHashalgorithm_hash=GetAlgorithmHash();autoiter=algorithm_hash.find(name);if(iter==algorithm_hash.end()){algorithm_hash[name]=constructor;std::coutname"registeredsuccessfully!"std::endl;returntrue;}else{std::coutname"hasbeenregistered!"std::endl;returnfalse;}}staticstd::unique_ptrAlgorithmBaseCreateAlgorithm(std::stringname,Args...args){AlgorithmHashalgorithm_hash=GetAlgorithmHash();autoiter=algorithm_hash.find(name);if(iter!=algorithm_hash.end()){returniter-second(args...);}else{std::cout"Cantcreatalgorithm"name",becauseyouhaventregisterit!"std::endl;returnnullptr;}}private:AlgorithmFactory(){};};templatetypenameAlgorithmBase,typenameAlgorithm,typename...ArgsclassAlgorithmRegister{public:AlgorithmRegister(conststd::stringname){AlgorithmFactoryAlgorithmBase,Args...::Register(name,AlgorithmRegisterAlgorithmBase,Algorithm,Args...::create);}staticstd::unique_ptrAlgorithmBasecreate(Args...args){returnstd::make_uniqueAlgorithm(args...);}};#defineNAME(name)register_##name##_algorithm#defineREGISTER_ALGORITHM(AlgorithmBase,AlgorithmName,Algorithm,...)\AlgorithmRegisterAlgorithmBase,Algorithm,##__VA_ARGS__NAME(Algorithm)(AlgorithmName)

最后,我们再提一下该框架的使用,以相机驱动为例,我们在MINDVisionDriver和HIKVisionDriver分别定义的头文件中进行注册,

REGISTER_ALGORITHM(CameraDriver,"hik_vision",HIKVisionDriver,CameraInfo);REGISTER_ALGORITHM(CameraDriver,"mind_vision",MINDVisionDriver,CameraInfo);

然后在需要驱动的地方从工厂获取对象。

std::shared_ptrCameraDriverhik_driver=AlgorithmFactoryCameraDriver,CameraInfo::CreateAlgorithm("hik_vision",hik_camera_info);std::shared_ptrCameraDrivermind_driver=AlgorithmFactoryCameraDriver,CameraInfo::CreateAlgorithm("mind_vision",mind_camera_info);References

[1]通用抽象工厂框架:(

转载请注明:http://www.sonphie.com/jbzl/14318.html

网站简介| 发布优势| 服务条款| 隐私保护| 广告合作| 网站地图| 版权申明

当前时间: