C++工厂模式实战:集成轻量级单元测试框架的FactoryTestApp项目解析

📅 2026/7/25 6:33:41 👁️ 阅读次数 📝 编程学习
C++工厂模式实战:集成轻量级单元测试框架的FactoryTestApp项目解析

1. 项目概述与核心价值

最近在整理过往的项目代码,翻到了一个我个人觉得非常有代表性的小项目,我把它叫做FactoryTestApp。这名字听起来可能有点“官方”,但它确实精准地概括了项目的核心:一个用 C++ 演示工厂设计模式,并集成了一套轻量级单元测试框架的示例程序。如果你正在学习 C++ 设计模式,尤其是工厂模式,并且苦于如何为你的 C++ 代码编写有效的、可维护的单元测试,那么这个项目的思路和实现细节,或许能给你带来不少启发。

简单来说,这个项目解决了两个常见痛点:第一,教科书上的工厂模式例子往往过于简单,和实际项目中的复杂度脱节,导致学习者看完还是不知道如何在自己的代码里应用;第二,C++ 的单元测试环境搭建和框架选择对新手来说有点门槛,很多人止步于“知道要测试”,但不知道“怎么开始测”。FactoryTestApp就是把这两件事揉在一起,通过一个具体的、可运行的例子,展示如何用工厂模式创建对象,并围绕这个模式编写一套完整的、自动化的测试用例。它非常适合有一定 C++ 基础,想深入理解设计模式实战和测试驱动开发(TDD)理念的开发者。

2. 项目整体设计与思路拆解

2.1 为什么选择“工厂模式”作为核心?

工厂模式属于创建型设计模式,它的核心价值在于将对象的创建与使用分离。在小型程序或示例中,直接new一个对象似乎没什么问题,但随着项目复杂度提升,如果创建对象的逻辑(比如需要读取配置、依赖外部服务、进行复杂的初始化)散落在代码各处,就会带来维护灾难。工厂模式通过提供一个统一的“创建接口”,将变化封装起来。当需要新增一种产品类型,或者改变创建逻辑时,你只需要修改工厂类,而无需追踪和修改所有调用处的代码。

在这个项目中,我设计了一个经典的场景:一个图形渲染系统,需要创建不同的形状(如圆形、矩形)。如果不使用工厂,主程序里可能会充斥着if-elseswitch-case来判断该创建哪个形状。而使用工厂模式后,主程序只需要告诉工厂“我需要一个‘圆形’”,具体的创建细节完全由工厂负责。这种解耦使得代码更清晰,也更符合“开闭原则”(对扩展开放,对修改封闭)。

2.2 测试框架的选型与集成思路

为 C++ 选择测试框架时,我考虑了几个因素:轻量、易集成、无外部依赖、报告清晰。像 Google Test、Catch2 都是非常优秀且功能全面的框架。但对于这个以演示和教育为目的的项目,引入这些“巨无霸”可能会让初学者把大量时间花在环境配置上,偏离了核心主题。

因此,我决定自己实现一个极简的测试框架。这个框架的目标非常明确:

  1. 能定义测试用例:用简单的宏或函数来包裹测试逻辑。
  2. 能进行断言:检查条件是否满足,比如EXPECT_EQ(a, b)
  3. 能收集并报告结果:运行所有测试后,清晰地输出通过了多少、失败了哪些,以及失败的具体原因。
  4. 与工厂模式示例无缝结合:测试用例本身就是对工厂模式各个组件(工厂类、产品类)功能正确性的验证。

这个自研框架的代码量不大,但完整地展示了测试框架的核心原理。理解了它,你再去看 Google Test 这样的成熟框架,就会觉得豁然开朗,知道它们内部大概是怎么运作的。

2.3 项目结构规划

一个清晰的项目结构是良好可读性和可维护性的基础。FactoryTestApp采用了如下目录结构:

FactoryTestApp/ ├── include/ # 头文件 │ ├── shape.h # 抽象产品接口和具体产品类声明 │ ├── shape_factory.h # 抽象工厂和具体工厂类声明 │ └── test_framework.h # 自制测试框架头文件 ├── src/ # 源文件 │ ├── shape.cpp # 具体产品类实现 │ ├── shape_factory.cpp # 具体工厂类实现 │ ├── test_framework.cpp # 测试框架实现 │ └── main.cpp # 主程序,演示使用并运行测试 ├── tests/ # 测试用例文件 │ └── test_shapes.cpp # 针对形状和工厂的测试用例 └── Makefile (或 CMakeLists.txt) # 构建脚本

这种分离将接口(include)、实现(src)、测试(tests)清晰地划分开,是中型 C++ 项目的常见实践。构建工具上,我选择了CMake,因为它跨平台,且能很好地管理依赖和构建过程,对于学习者来说,熟悉CMake也是一项很有价值的技能。

3. 核心细节解析与实操要点

3.1 工厂模式的具体实现

工厂模式通常有几种变体:简单工厂、工厂方法、抽象工厂。在这个项目中,我重点实现了工厂方法模式,因为它更强调“每个产品对应一个工厂”的灵活性,比简单工厂更符合设计模式的原则,又比抽象工厂更易于理解。

首先,定义抽象产品IShape

// include/shape.h #ifndef SHAPE_H #define SHAPE_H #include <string> // 抽象产品类:形状 class IShape { public: virtual ~IShape() = default; // 虚析构函数,确保正确释放资源 virtual double area() const = 0; // 计算面积 virtual double perimeter() const = 0; // 计算周长 virtual std::string name() const = 0; // 返回形状名称 }; #endif // SHAPE_H

接着,实现两个具体产品:CircleRectangle。这里的关键是,它们都公有继承自IShape,并实现了所有纯虚函数。

// src/shape.cpp #include “../include/shape.h” #include <cmath> class Circle : public IShape { private: double radius_; public: explicit Circle(double radius) : radius_(radius) { if (radius <= 0) { throw std::invalid_argument(“Radius must be positive.”); } } double area() const override { return M_PI * radius_ * radius_; } double perimeter() const override { return 2 * M_PI * radius_; } std::string name() const override { return “Circle”; } }; class Rectangle : public IShape { private: double width_, height_; public: Rectangle(double width, double height) : width_(width), height_(height) { if (width <= 0 || height <= 0) { throw std::invalid_argument(“Width and height must be positive.”); } } double area() const override { return width_ * height_; } double perimeter() const override { return 2 * (width_ + height_); } std::string name() const override { return “Rectangle”; } };

注意:在构造函数中加入参数校验是一个好习惯,它能尽早暴露问题。但这也意味着我们的测试需要覆盖这些异常情况。

然后,定义抽象工厂IShapeFactory

// include/shape_factory.h #ifndef SHAPE_FACTORY_H #define SHAPE_FACTORY_H #include “shape.h” #include <memory> // 抽象工厂类 class IShapeFactory { public: virtual ~IShapeFactory() = default; // 工厂方法,返回一个智能指针管理的形状对象 virtual std::unique_ptr<IShape> createShape() = 0; virtual std::string factoryName() const = 0; }; #endif // SHAPE_FACTORY_H

最后,为每个具体产品实现对应的具体工厂:

// src/shape_factory.cpp #include “../include/shape_factory.h” class CircleFactory : public IShapeFactory { public: std::unique_ptr<IShape> createShape() override { // 这里为了示例,使用固定半径。实际项目中,参数可能来自配置、用户输入等。 return std::make_unique<Circle>(5.0); } std::string factoryName() const override { return “CircleFactory”; } }; class RectangleFactory : public IShapeFactory { public: std::unique_ptr<IShape> createShape() override { // 固定宽高 return std::make_unique<Rectangle>(4.0, 6.0); } std::string factoryName() const override { return “RectangleFactory”; } };

实操心得:使用std::unique_ptr作为工厂方法的返回类型,可以明确表达所有权的转移——工厂创建了对象,并将所有权交给了调用者。这避免了原始指针可能带来的内存泄漏问题,是现代 C++ 推荐的做法。

3.2 自制轻量级测试框架实现

测试框架的核心是“测试用例”的注册、运行和断言。一个最简单的实现思路是:利用静态对象的构造函数在程序启动前自动完成注册。

首先,定义一个TestCase结构体,包含测试名和测试函数:

// include/test_framework.h #ifndef TEST_FRAMEWORK_H #define TEST_FRAMEWORK_H #include <string> #include <vector> #include <functional> struct TestCase { std::string name; std::function<void()> func; }; class TestFramework { private: static std::vector<TestCase>& getRegistry() { static std::vector<TestCase> registry; return registry; } public: static void registerTest(const std::string& name, std::function<void()> func) { getRegistry().push_back({name, std::move(func)}); } static int runAllTests(); }; // 用于注册测试的宏,简化书写 #define TEST(test_name) \ void test_name##_body(); \ namespace { \ struct test_name##_registrar { \ test_name##_registrar() { \ TestFramework::registerTest(#test_name, &test_name##_body); \ } \ } test_name##_instance; \ } \ void test_name##_body() // 断言宏 #define EXPECT_TRUE(cond) \ do { \ if (!(cond)) { \ throw std::runtime_error(std::string(“Assertion failed: “) + #cond + “, in “ + __func__); \ } \ } while(0) #define EXPECT_EQ(a, b) EXPECT_TRUE((a) == (b)) #define EXPECT_NEAR(a, b, epsilon) EXPECT_TRUE(std::abs((a) - (b)) < (epsilon)) #endif // TEST_FRAMEWORK_H

runAllTests的实现就是遍历注册表,执行每个测试函数,并捕获异常(断言失败会抛出异常):

// src/test_framework.cpp #include “../include/test_framework.h” #include <iostream> int TestFramework::runAllTests() { auto& tests = getRegistry(); int passed = 0; int failed = 0; std::cout << “[==========] Running “ << tests.size() << “ test(s).\n”; for (const auto& test : tests) { std::cout << “[ RUN ] “ << test.name << std::endl; try { test.func(); std::cout << “[ OK ] “ << test.name << std::endl; ++passed; } catch (const std::exception& e) { std::cout << “[ FAILED ] “ << test.name << “ - “ << e.what() << std::endl; ++failed; } catch (...) { std::cout << “[ FAILED ] “ << test.name << “ - Unknown exception\n”; ++failed; } } std::cout << “[==========] “ << tests.size() << “ test(s) run.\n”; std::cout << “[ PASSED ] “ << passed << “ test(s).\n”; if (failed > 0) { std::cout << “[ FAILED ] “ << failed << “ test(s).\n”; return 1; // 返回非零值表示有测试失败 } return 0; }

这个框架虽然简单,但具备了核心功能。TEST宏利用了一个静态对象的构造函数(test_name##_instance)在main函数执行前,自动将测试函数注册到全局列表中。runAllTests则负责执行并报告。

4. 实操过程与核心环节实现

4.1 编写测试用例

有了工厂和测试框架,接下来就是为它们编写测试。测试用例应该覆盖正常功能和异常情况。

// tests/test_shapes.cpp #include “../include/shape.h” #include “../include/shape_factory.h” #include “../include/test_framework.h” #include <cmath> TEST(CircleAreaAndPerimeter) { Circle c(2.0); EXPECT_NEAR(c.area(), M_PI * 4.0, 1e-9); EXPECT_NEAR(c.perimeter(), M_PI * 4.0, 1e-9); EXPECT_EQ(c.name(), “Circle”); } TEST(RectangleAreaAndPerimeter) { Rectangle r(3.0, 4.0); EXPECT_NEAR(r.area(), 12.0, 1e-9); EXPECT_NEAR(r.perimeter(), 14.0, 1e-9); EXPECT_EQ(r.name(), “Rectangle”); } TEST(CircleFactoryCreatesCircle) { CircleFactory cf; auto shape = cf.createShape(); // 应返回一个半径为5的Circle EXPECT_EQ(shape->name(), “Circle”); EXPECT_NEAR(shape->area(), M_PI * 25.0, 1e-9); } TEST(RectangleFactoryCreatesRectangle) { RectangleFactory rf; auto shape = rf.createShape(); // 应返回一个4x6的Rectangle EXPECT_EQ(shape->name(), “Rectangle”); EXPECT_NEAR(shape->area(), 24.0, 1e-9); } TEST(InvalidCircleRadiusThrows) { try { Circle c(0.0); // 半径非法 EXPECT_TRUE(false); // 如果执行到这里,说明没抛出异常,测试失败 } catch (const std::invalid_argument&) { EXPECT_TRUE(true); // 捕获到预期异常,测试通过 } catch (...) { EXPECT_TRUE(false); // 捕获到其他异常,测试失败 } }

实操心得:测试异常时,使用try-catch块是标准做法。注意,在try块中如果创建对象成功,我们需要一个断言(如EXPECT_TRUE(false))来明确标记“这里不应该成功”,否则测试可能会静默通过,掩盖了 bug。

4.2 主程序集成与演示

主程序main.cpp有两个作用:一是演示工厂模式的使用,二是启动测试框架。

// src/main.cpp #include “../include/shape.h” #include “../include/shape_factory.h” #include “../include/test_framework.h” #include <iostream> #include <vector> #include <memory> void demonstrateFactoryPattern() { std::cout << “\n=== Demonstrating Factory Pattern ===\n”; std::vector<std::unique_ptr<IShapeFactory>> factories; factories.push_back(std::make_unique<CircleFactory>()); factories.push_back(std::make_unique<RectangleFactory>()); for (const auto& factory : factories) { std::cout << “Using factory: “ << factory->factoryName() << std::endl; auto shape = factory->createShape(); std::cout << “ Created shape: “ << shape->name() << std::endl; std::cout << “ Area: “ << shape->area() << std::endl; std::cout << “ Perimeter: “ << shape->perimeter() << “\n” << std::endl; } } int main() { // 第一部分:演示工厂模式 demonstrateFactoryPattern(); // 第二部分:运行所有单元测试 std::cout << “\n=== Running Unit Tests ===\n”; int testResult = TestFramework::runAllTests(); return testResult; // 程序退出码反映测试是否全部通过 }

4.3 使用 CMake 构建项目

为了让项目易于编译和跨平台,我编写了一个CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.10) project(FactoryTestApp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 将头文件目录加入包含路径 include_directories(${PROJECT_SOURCE_DIR}/include) # 添加主程序可执行文件 add_executable(FactoryTestApp src/main.cpp src/shape.cpp src/shape_factory.cpp src/test_framework.cpp tests/test_shapes.cpp ) # 在 Windows 上可能需要定义 _USE_MATH_DEFINES 来使用 M_PI if(WIN32) target_compile_definitions(FactoryTestApp PRIVATE _USE_MATH_DEFINES) endif()

在项目根目录下,执行以下命令即可构建并运行:

mkdir build && cd build cmake .. cmake --build . # 或 make,取决于生成器 ./FactoryTestApp # 在 Windows 上是 .\FactoryTestApp.exe

运行后,你会先看到工厂模式的演示输出,然后是所有测试用例的运行报告。

5. 常见问题与排查技巧实录

在实际编写和运行这个项目的过程中,你可能会遇到一些典型问题。这里我记录了几个踩过的坑和解决方法。

5.1 链接错误:未定义的引用

这是 C++ 新手最常见的错误之一。

问题描述:编译通过,但链接时失败,报错信息类似undefined reference toCircle::area() const‘`。

原因分析:这通常意味着头文件(.h)中声明了函数,但在源文件(.cpp)中没有提供定义(实现),或者定义了但没有被编译进最终的可执行文件。

排查步骤

  1. 检查实现文件:首先确认shape.cpp文件是否存在,并且确实包含了Circle::area()等方法的实现。
  2. 检查编译命令:确认你的构建脚本(如CMakeLists.txtMakefile)将shape.cpp添加到了源文件列表中。在上面的CMakeLists.txt中,src/shape.cpp被明确添加到了add_executable命令里。
  3. 检查命名空间和类名:确保实现文件中的类名和方法签名与头文件中的声明完全一致,包括const修饰符。

实操心得:养成“修改头文件后,同步检查相关源文件”的习惯。使用 IDE(如 VSCode、CLion)可以大大降低这类错误的发生率,因为它们能提供更好的代码导航和重构支持。

5.2 测试未运行或报告为空

问题描述:程序运行了,但只看到工厂模式的演示输出,没有看到测试报告,或者报告显示运行了 0 个测试。

原因分析:测试用例没有成功注册到TestFramework的注册表中。最可能的原因是TEST宏展开的静态对象没有被正确初始化。

排查步骤

  1. 检查宏定义:确保TEST宏正确定义,并且测试用例文件(test_shapes.cpp)包含了test_framework.h
  2. 检查链接:确保test_shapes.cpp被编译并链接到了最终的可执行文件中。在CMakeLists.txt里,它被添加到了add_executable的参数里。
  3. 检查静态初始化顺序:在极少数情况下,跨编译单元的静态变量初始化顺序可能有问题。一个简单的验证方法是,在main函数最开始加一句打印,在TestFramework::registerTest中也加一句打印,看看注册是否发生。

解决方案:对于自制的简单框架,确保测试用例文件被正确编译和链接通常就能解决问题。也可以将测试注册表从static局部变量改为全局变量,但要注意避免“静态初始化顺序问题”。

5.3 浮点数比较断言失败

问题描述:测试Circle面积时,使用EXPECT_EQ(M_PI * 4.0, c.area())失败,尽管计算结果看起来一样。

原因分析:这是浮点数计算的经典问题。由于二进制表示和精度限制,浮点数的计算结果可能存在微小的误差,直接使用==比较几乎总会失败。

解决方案:使用“近似相等”断言。这就是为什么我在测试框架中提供了EXPECT_NEAR(a, b, epsilon)宏。它检查两个数的绝对值差是否小于一个很小的阈值epsilon(例如1e-9)。在比较浮点数结果时,务必使用EXPECT_NEAR而不是EXPECT_EQ

5.4 工厂创建的对象类型不对

问题描述CircleFactory::createShape()返回的对象,调用name()方法返回的不是“Circle”

原因分析

  1. 工厂内部return语句写错了,比如return std::make_unique<Rectangle>(...);
  2. 具体产品类(如Circle)的name()方法实现有误,返回了错误的字符串。
  3. 存在多态问题,比如返回的基类指针被错误地向下转型了(在这个例子里,我们不需要向下转型)。

排查步骤

  1. 仔细检查工厂类createShape方法的实现。
  2. 在调试器中运行测试,查看shape指针的动态类型。
  3. IShape添加一个typeid或自定义的type字段来辅助调试。

实操心得:为工厂类编写单元测试是至关重要的。TEST(CircleFactoryCreatesCircle)这个用例就是专门用来验证工厂返回的对象类型和基本属性是否正确。这是保证工厂模式正确性的第一道防线。

5.5 内存泄漏问题

问题描述:虽然在示例中使用了std::unique_ptr,但如果在某些分支路径上忘记返回,或者错误地使用了原始指针,仍可能导致内存泄漏。

原因分析:手动管理内存(new/delete)是 C++ 内存泄漏的主要根源。

解决方案

  1. 坚持使用智能指针:工厂方法统一返回std::unique_ptr<IShape>。这几乎可以完全避免显式的内存管理。
  2. 使用 RAII 管理其他资源:将文件句柄、网络连接等资源也封装在对象中,利用析构函数自动释放。
  3. 利用工具检测:在 Linux/macOS 下可以使用valgrind,在 Windows 下可以使用 Visual Studio 的内存诊断工具,来运行你的程序,检查是否有内存泄漏。

提示:即使在这个小项目中,我也坚持让工厂返回unique_ptr。这是一个值得培养的好习惯,它能将你从繁琐且易错的内存管理中解放出来,让代码更安全、更清晰。当你需要共享所有权时,再考虑std::shared_ptr,但默认情况下优先使用unique_ptr