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

发布时间:2026/7/25 6:24:24
C++工厂模式实战:集成轻量级单元测试框架的FactoryTestApp项目解析 1. 项目概述与核心价值最近在整理过往的项目代码翻到了一个我个人觉得非常有代表性的小项目我把它叫做FactoryTestApp。这名字听起来可能有点“官方”但它确实精准地概括了项目的核心一个用 C 演示工厂设计模式并集成了一套轻量级单元测试框架的示例程序。如果你正在学习 C 设计模式尤其是工厂模式并且苦于如何为你的 C 代码编写有效的、可维护的单元测试那么这个项目的思路和实现细节或许能给你带来不少启发。简单来说这个项目解决了两个常见痛点第一教科书上的工厂模式例子往往过于简单和实际项目中的复杂度脱节导致学习者看完还是不知道如何在自己的代码里应用第二C 的单元测试环境搭建和框架选择对新手来说有点门槛很多人止步于“知道要测试”但不知道“怎么开始测”。FactoryTestApp就是把这两件事揉在一起通过一个具体的、可运行的例子展示如何用工厂模式创建对象并围绕这个模式编写一套完整的、自动化的测试用例。它非常适合有一定 C 基础想深入理解设计模式实战和测试驱动开发TDD理念的开发者。2. 项目整体设计与思路拆解2.1 为什么选择“工厂模式”作为核心工厂模式属于创建型设计模式它的核心价值在于将对象的创建与使用分离。在小型程序或示例中直接new一个对象似乎没什么问题但随着项目复杂度提升如果创建对象的逻辑比如需要读取配置、依赖外部服务、进行复杂的初始化散落在代码各处就会带来维护灾难。工厂模式通过提供一个统一的“创建接口”将变化封装起来。当需要新增一种产品类型或者改变创建逻辑时你只需要修改工厂类而无需追踪和修改所有调用处的代码。在这个项目中我设计了一个经典的场景一个图形渲染系统需要创建不同的形状如圆形、矩形。如果不使用工厂主程序里可能会充斥着if-else或switch-case来判断该创建哪个形状。而使用工厂模式后主程序只需要告诉工厂“我需要一个‘圆形’”具体的创建细节完全由工厂负责。这种解耦使得代码更清晰也更符合“开闭原则”对扩展开放对修改封闭。2.2 测试框架的选型与集成思路为 C 选择测试框架时我考虑了几个因素轻量、易集成、无外部依赖、报告清晰。像 Google Test、Catch2 都是非常优秀且功能全面的框架。但对于这个以演示和教育为目的的项目引入这些“巨无霸”可能会让初学者把大量时间花在环境配置上偏离了核心主题。因此我决定自己实现一个极简的测试框架。这个框架的目标非常明确能定义测试用例用简单的宏或函数来包裹测试逻辑。能进行断言检查条件是否满足比如EXPECT_EQ(a, b)。能收集并报告结果运行所有测试后清晰地输出通过了多少、失败了哪些以及失败的具体原因。与工厂模式示例无缝结合测试用例本身就是对工厂模式各个组件工厂类、产品类功能正确性的验证。这个自研框架的代码量不大但完整地展示了测试框架的核心原理。理解了它你再去看 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接着实现两个具体产品Circle和Rectangle。这里的关键是它们都公有继承自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_ptrIShape 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_ptrIShape createShape() override { // 这里为了示例使用固定半径。实际项目中参数可能来自配置、用户输入等。 return std::make_uniqueCircle(5.0); } std::string factoryName() const override { return “CircleFactory”; } }; class RectangleFactory : public IShapeFactory { public: std::unique_ptrIShape createShape() override { // 固定宽高 return std::make_uniqueRectangle(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::functionvoid() func; }; class TestFramework { private: static std::vectorTestCase getRegistry() { static std::vectorTestCase registry; return registry; } public: static void registerTest(const std::string name, std::functionvoid() 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_HrunAllTests的实现就是遍历注册表执行每个测试函数并捕获异常断言失败会抛出异常// 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::vectorstd::unique_ptrIShapeFactory factories; factories.push_back(std::make_uniqueCircleFactory()); factories.push_back(std::make_uniqueRectangleFactory()); 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中没有提供定义实现或者定义了但没有被编译进最终的可执行文件。排查步骤检查实现文件首先确认shape.cpp文件是否存在并且确实包含了Circle::area()等方法的实现。检查编译命令确认你的构建脚本如CMakeLists.txt或Makefile将shape.cpp添加到了源文件列表中。在上面的CMakeLists.txt中src/shape.cpp被明确添加到了add_executable命令里。检查命名空间和类名确保实现文件中的类名和方法签名与头文件中的声明完全一致包括const修饰符。实操心得养成“修改头文件后同步检查相关源文件”的习惯。使用 IDE如 VSCode、CLion可以大大降低这类错误的发生率因为它们能提供更好的代码导航和重构支持。5.2 测试未运行或报告为空问题描述程序运行了但只看到工厂模式的演示输出没有看到测试报告或者报告显示运行了 0 个测试。原因分析测试用例没有成功注册到TestFramework的注册表中。最可能的原因是TEST宏展开的静态对象没有被正确初始化。排查步骤检查宏定义确保TEST宏正确定义并且测试用例文件test_shapes.cpp包含了test_framework.h。检查链接确保test_shapes.cpp被编译并链接到了最终的可执行文件中。在CMakeLists.txt里它被添加到了add_executable的参数里。检查静态初始化顺序在极少数情况下跨编译单元的静态变量初始化顺序可能有问题。一个简单的验证方法是在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”。原因分析工厂内部return语句写错了比如return std::make_uniqueRectangle(...);。具体产品类如Circle的name()方法实现有误返回了错误的字符串。存在多态问题比如返回的基类指针被错误地向下转型了在这个例子里我们不需要向下转型。排查步骤仔细检查工厂类createShape方法的实现。在调试器中运行测试查看shape指针的动态类型。为IShape添加一个typeid或自定义的type字段来辅助调试。实操心得为工厂类编写单元测试是至关重要的。TEST(CircleFactoryCreatesCircle)这个用例就是专门用来验证工厂返回的对象类型和基本属性是否正确。这是保证工厂模式正确性的第一道防线。5.5 内存泄漏问题问题描述虽然在示例中使用了std::unique_ptr但如果在某些分支路径上忘记返回或者错误地使用了原始指针仍可能导致内存泄漏。原因分析手动管理内存new/delete是 C 内存泄漏的主要根源。解决方案坚持使用智能指针工厂方法统一返回std::unique_ptrIShape。这几乎可以完全避免显式的内存管理。使用 RAII 管理其他资源将文件句柄、网络连接等资源也封装在对象中利用析构函数自动释放。利用工具检测在 Linux/macOS 下可以使用valgrind在 Windows 下可以使用 Visual Studio 的内存诊断工具来运行你的程序检查是否有内存泄漏。提示即使在这个小项目中我也坚持让工厂返回unique_ptr。这是一个值得培养的好习惯它能将你从繁琐且易错的内存管理中解放出来让代码更安全、更清晰。当你需要共享所有权时再考虑std::shared_ptr但默认情况下优先使用unique_ptr。