工厂方法:鸡肋方法?

2024-01-25 10:08
文章标签 方法 工厂 鸡肋

本文主要是介绍工厂方法:鸡肋方法?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

合理的使用工厂设计模式,不要使其成为鸡肋方法。《设计模式》一书中描述的工厂方法,实在是叫人母语,鸡肋的很。

通俗的描述一下,工厂,就是生产产品的地方,既然是工厂,那么就不能生产一个产品,而是生产一系列产品,不然就没有必要了。投入原料,经过加工,产出产品。new方法本是最原始的单一产品制造工厂了,为了保证产品被确实制造出来,构造函数不出异常,有些实现中用了二段构造,先new再construct。


下文是转载的一篇比较实用的工厂方法的应用。

简介

工厂方法,通常又被称作虚构造函数,给一个ID,就可以产出一个对象。
了解设计模式的人都知道这样一份臭名昭著的实现:

// ---------------------------------------------------------------------------------
// Shape.h

enum ShapeType { ShapeType_Line, ShapeType_Triangle, };
class Shape {
public:
    virtual ~Shape() {}
    virtual void Draw() = 0;
    static Shape* Create( ShapeType shapeType );
};

// ---------------------------------------------------------------------------------
// Shape.cpp

#include "Line.h"
#include "Triangle.h"
Shape* Shape::Create( ShapeType shapeType ) {
    switch ( shapeType ) {
        case ShapeType_Line: return new Line;
        case ShapeType_Triangle: return new Triangle;
        default: return NULL;
    }
}

缺点

一个显著的缺点是新类型难于扩展:
如果你新增了一个类型Rectangle,就要改动
1. Shape.h - 在enum里加一个类型ID
2. Shape.cpp - 增加一个头文件#include,增加一个switch case分支

对于Shape.h的改动的影响面可能超乎你的想象:
1. 所有派生于Shape的类实现,因为包含到Shape.h,将不得不重新编译
2. 所有Shape类的用户同样也得重新编译

另外这种做法无法封装成库,因为我们不可能因为用户新增了类型而去修改lib文件。
所以要具备可扩展性,用户新增的类型就不能影响Shape.h和Shape.cpp。要怎做呢?Loki库给了我们很好的展示,基本做法像下面这样:

解决方案


// ---------------------------------------------------------------------------------
// Shape.h

typedef const char* ShapeType;
typedef Shape* (*Creator)();

class Shape {
public:
    virtual ~Shape() {}
    virtual void Draw() = 0;
};

class ShapeFactory {
public:
    static ShapeFactory& Instance() {
        static ShapeFactory instance;
        return instance;
    }

    Shape* Create( ShapeType shapeType );
    bool RegisterShape( ShapeType shapeType, Creator creator );

private:
    ShapeFactory() {}
    Map< ShapeType, Creator > shapeCreators;
};

// ---------------------------------------------------------------------------------
// Shape.cpp

Shape* ShapeFactory::Create( ShapeType shapeType ) {
    Creator creator = shapeCreators.Find( shapeType );
    if ( creator == NULL ) { return NULL; }
    return creator();
}

bool RegisterShape( ShapeType shapeType, Creator creator ) {
    Creator creator = shapeCreators.Find( shapeType );
    if ( creator == NULL ) {
        shapeCreators.Insert( shapeType, creator );
        return true;
    }
    return false;
}

// ---------------------------------------------------------------------------------
// Line.cpp

namespace {
    Shape* Create() { return new Line; }
    const bool RegisterShape__ = ShapeFactory::Instance().RegisterShape( "Line", Create );
}
    
// ---------------------------------------------------------------------------------
// Triangle.cpp
namespace {
    Shape* Create() { return new Triangle; }
    const bool RegisterShape__ = ShapeFactory::Instance().RegisterShape( "Triangle", Create );
}

如果增加新类,只需在新类里增加类似上面这段就行了。可以把这断代码做成宏放在Shape.h里面。

#define REGISTER_SHAPE( className )                                         \
namespace {                                                                 \
    Shape* Create() { return new className; }                               \
    const bool RegisterShape__ = ShapeFactory::Instance().RegisterShape( #className, Create );\
}

细节讨论

1. 关于ID
a)可以采用任何你喜欢的类型做为ID,但必须保证其唯一性。字符串通常是个不错的选择(如果你用整数,那么最好用GUID生成器产生,因为你并不知道其他子类的ID是什么)。
b)采用const char*作为ID,有一定的风险:如果你的类打算从文件中序列化产生,那么序列化出来的字符串并不位于全局静态存储区,直接对其做 == 操作会产生错误。解决方案是使用string替代const char*。

2. ShapeFactory是个单件,关于单件的详细讨论,可以参考:单件和仿单件的6种做法

3. 创建函数typedef Shape* (*Creator)();
此乃上述做法之关键:首先将ID映射为创建函数,再由此函数产生对象。将函数保存起来并于稍后调用,这也是Command模式之核心观念。(《C++设计新思维》p100)
一些替代的方案可供考虑:
a)传统方案,用函数对象代替函数指针:

struct Creator {
    Shape* operator() {
        return new Line;
    }
}
 
b)使用模板以避免创建子类(详见《设计模式》p74-75):  

struct Creator {
    virtual Shape* Create() = 0;
}

template <class T>
struct StdCreator : public Creator {
    virtual Shape* Create() { return T; }
}

与Loki同行

如果你的工程使用Loki,那么一切都简单了,像下面这样:
// ----------------------------------------------------------------------------------------------------------------------------------
// Shape.h

#ifndef Shape_H_INCLUDED_
#define Shape_H_INCLUDED_

#include <string>
#include <loki/Singleton.h>
#include <loki/Factory.h>

class Shape {
public:
    virtual ~Shape() {}
    virtual void Draw() = 0;
};

typedef Loki::SingletonHolder< Loki::Factory< Shape, std::string > > ShapeFactory;

#define REGISTER_SHAPE( className )                                                         \
namespace {                                                                                 \
    Shape* Create() { return new className; }                                               \
    const bool RegisterShape__ = ShapeFactory::Instance().Register( #className, Create );   \
}

#endif // Shape_H_INCLUDED_

// ----------------------------------------------------------------------------------------------------------------------------------
// Line.h

#ifndef Line_H_INCLUDED_
#define Line_H_INCLUDED_

#include "Shape.h"

class Line : public Shape {
public:
    virtual void Draw();
};

#endif // Line_H_INCLUDED_

// ----------------------------------------------------------------------------------------------------------------------------------
// Line.cpp

#include <iostream>
#include "Line.h"

REGISTER_SHAPE( Line )

void Line::Draw() {
    std::cout << "Line::Draw()" << std::endl;
}

// ----------------------------------------------------------------------------------------------------------------------------------
// main.cpp

#include "Shape.h"

int main() {
    Shape* line = ShapeFactory::Instance().CreateObject( "Line" );
    line->Draw();    
    delete line;
    return 0;
}

参考资料

《设计模式》chapter3.3
《C++设计新思维》chapter8

这篇关于工厂方法:鸡肋方法?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/642938

相关文章

Python字符串处理方法超全攻略

《Python字符串处理方法超全攻略》字符串可以看作多个字符的按照先后顺序组合,相当于就是序列结构,意味着可以对它进行遍历、切片,:本文主要介绍Python字符串处理方法的相关资料,文中通过代码介... 目录一、基础知识:字符串的“不可变”特性与创建方式二、常用操作:80%场景的“万能工具箱”三、格式化方法

springboot+redis实现订单过期(超时取消)功能的方法详解

《springboot+redis实现订单过期(超时取消)功能的方法详解》在SpringBoot中使用Redis实现订单过期(超时取消)功能,有多种成熟方案,本文为大家整理了几个详细方法,文中的示例代... 目录一、Redis键过期回调方案(推荐)1. 配置Redis监听器2. 监听键过期事件3. Redi

基于SpringBoot实现分布式锁的三种方法

《基于SpringBoot实现分布式锁的三种方法》这篇文章主要为大家详细介绍了基于SpringBoot实现分布式锁的三种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、基于Redis原生命令实现分布式锁1. 基础版Redis分布式锁2. 可重入锁实现二、使用Redisso

自定义注解SpringBoot防重复提交AOP方法详解

《自定义注解SpringBoot防重复提交AOP方法详解》该文章描述了一个防止重复提交的流程,通过HttpServletRequest对象获取请求信息,生成唯一标识,使用Redis分布式锁判断请求是否... 目录防重复提交流程引入依赖properties配置自定义注解切面Redis工具类controller

Java调用DeepSeek API的8个高频坑与解决方法

《Java调用DeepSeekAPI的8个高频坑与解决方法》现在大模型开发特别火,DeepSeek因为中文理解好、反应快、还便宜,不少Java开发者都用它,本文整理了最常踩的8个坑,希望对... 目录引言一、坑 1:Token 过期未处理,鉴权异常引发服务中断问题本质典型错误代码解决方案:实现 Token

Nginx 访问控制的多种方法

《Nginx访问控制的多种方法》本文系统介绍了Nginx实现Web访问控制的多种方法,包括IP黑白名单、路径/方法/参数控制、HTTP基本认证、防盗链机制、客户端证书校验、限速限流、地理位置控制等基... 目录一、IP 白名单与黑名单1. 允许/拒绝指定IP2. 全局黑名单二、基于路径、方法、参数的访问控制

Python中Request的安装以及简单的使用方法图文教程

《Python中Request的安装以及简单的使用方法图文教程》python里的request库经常被用于进行网络爬虫,想要学习网络爬虫的同学必须得安装request这个第三方库,:本文主要介绍P... 目录1.Requests 安装cmd 窗口安装为pycharm安装在pycharm设置中为项目安装req

nginx跨域访问配置的几种方法实现

《nginx跨域访问配置的几种方法实现》本文详细介绍了Nginx跨域配置方法,包括基本配置、只允许指定域名、携带Cookie的跨域、动态设置允许的Origin、支持不同路径的跨域控制、静态资源跨域以及... 目录一、基本跨域配置二、只允许指定域名跨域三、完整示例四、配置后重载 nginx五、注意事项六、支持

MySQL查看表的历史SQL的几种实现方法

《MySQL查看表的历史SQL的几种实现方法》:本文主要介绍多种查看MySQL表历史SQL的方法,包括通用查询日志、慢查询日志、performance_schema、binlog、第三方工具等,并... 目录mysql 查看某张表的历史SQL1.查看MySQL通用查询日志(需提前开启)2.查看慢查询日志3.

MySQL底层文件的查看和修改方法

《MySQL底层文件的查看和修改方法》MySQL底层文件分为文本类(可安全查看/修改)和二进制类(禁止手动操作),以下按「查看方法、修改方法、风险管控三部分详细说明,所有操作均以Linux环境为例,需... 目录引言一、mysql 底层文件的查看方法1. 先定位核心文件路径(基础前提)2. 文本类文件(可直