C++代码结构化设计:解耦、接口契约与模块化实战

1. 项目概述:为什么“结构化”是C++重用的基石

刚入行那会儿,我总觉得C++代码重用就是把一段写好的函数或类,复制粘贴到新项目里。直到在一个中型游戏引擎项目里踩了坑:为了改一个底层数学库的向量计算方式,我不得不手动修改了十几个分散在不同模块里的相似代码文件,整个过程耗时耗力且极易出错。那次经历让我彻底明白,代码重用绝非简单的“复制粘贴”,而是一场关于“设计”的硬仗。标题里的“为最优化重用结构化你的代码”,正是解开这场硬仗胜利之门的钥匙。

所谓“结构化”,在这里远不止是代码格式整齐、缩进美观。它指的是一种系统性的设计方法,旨在将你的代码库组织成一系列清晰、独立、职责分明的“乐高积木块”。这些积木块(模块、类、函数)通过定义良好的接口进行连接,而非通过混乱的内部依赖纠缠在一起。这样做的核心目标,就是为了实现“最优化重用”——让你的代码不仅能被轻松地“拿起来就用”,还能在需求变更时,以最小的成本进行修改、扩展和替换,而不会引发“牵一发而动全身”的灾难。

这适合所有希望从C++“能用”进阶到“好用”、“耐用”的开发者。无论你是在校学生尝试管理自己的课程项目,还是职场新人接手第一个C++模块,或是资深工程师在重构遗留系统,理解并实践代码的结构化设计,都能让你的开发效率、代码质量和长期维护体验提升一个数量级。简单说,它让你今天的辛勤编码,成为明天乃至更久未来的可靠资产,而不是需要不断偿还的“技术债”。

2. 重用设计的核心思想:解耦与接口契约

2.1 从“高耦合”的泥潭中挣脱出来

在深入结构化之前,我们必须先认清敌人:过度耦合。耦合度衡量的是代码单元之间相互依赖的紧密程度。想象一下,如果你的Car类内部直接#include “Engine.h”并创建了Engine对象,同时Engine类又包含了PistonCylinder等具体部件的实现细节,那么它们就是高度耦合的。这意味着:

  1. 无法单独测试:你想测试Car的启动逻辑,就必须把整个引擎及其所有部件都编译链接进来。
  2. 替换成本极高:想把燃油引擎换成电动引擎?你需要修改Car类的源代码,因为它的实现绑定了特定的Engine类型。
  3. 理解困难:任何阅读Car代码的人,都必须同时理解Engine乃至其内部部件的所有细节。

这种“你中有我,我中有你”的状态,是代码重用的头号杀手。结构化设计的第一步,就是通过各种手段降低耦合度,让每个模块尽可能独立。

2.2 建立清晰的“接口契约”

降低耦合的关键武器是接口。接口定义了一个模块对外提供的、承诺不变的服务集合,同时隐藏了其内部如何实现的细节。这就像电器的电源插头(接口)和墙上的插座(调用者)之间的关系。插座不关心插头后面连着的是电风扇、电脑还是充电器,它只认“两脚或三脚、220V电压”这个契约。只要符合这个契约,任何电器都能即插即用。

在C++中,实现接口契约的主要方式有:

  • 抽象基类(Pure Virtual Class):这是最经典、最强大的方式。通过定义纯虚函数,强制派生类实现特定行为,调用者只需通过基类指针或引用操作。
    // 定义一个“渲染设备”的接口契约 class IRenderDevice { public: virtual ~IRenderDevice() = default; virtual void ClearScreen(const Color& clearColor) = 0; virtual void DrawMesh(const Mesh& mesh, const Matrix4x4& transform) = 0; virtual void Present() = 0; // ... 其他渲染操作 };
    这样,你的游戏主循环可以只依赖IRenderDevice*。今天可以在Windows上用DirectX实现它,明天可以轻松替换成用OpenGL或Vulkan的实现,而游戏主循环的代码一行都不用改。
  • 函数指针与std::function:对于更小粒度的、单一的操作,可以使用函数对象。这在实现回调、策略模式时非常有用,能将算法与具体策略解耦。
  • 模板(泛型编程):通过模板,你可以定义一种操作或算法的“形状”,而具体类型在编译时确定。这提供了另一种形式的契约:“类型T必须支持++操作符”或“必须拥有size()方法”。STL容器和算法就是此中典范。

注意:接口设计并非越抽象越好。一个常见的误区是过早抽象或过度设计,为不存在的“未来需求”创建了大量复杂的接口,反而增加了系统复杂度。接口应源于切实的、当前或可预见的重复使用需求。如果只有一个实现,且短期内看不到变化,直接使用具体类可能更简单直接。记住:“You aren‘t gonna need it” (YAGNI) 原则。

2.3 依赖倒置原则:让高层模块不再“卑微”

这是结构化设计中至关重要的一环:高层模块不应依赖低层模块,二者都应依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。

怎么理解?在传统的自上而下设计中,高层业务逻辑(如“订单处理”)会直接#include并实例化低层工具(如“MySQL数据库连接器”)。这就导致了高层模块依赖于低层模块的具体实现。

依赖倒置原则将其翻转过来:

  1. 首先,定义一个抽象的“数据库访问接口”(如IDatabaseConnection)。
  2. 然后,让“订单处理”模块只依赖这个接口。
  3. 最后,创建“MySQL数据库连接器”这个具体类来实现那个接口。

这样一来,“订单处理”(高层)和“MySQL连接器”(低层)都共同依赖于“数据库访问接口”(抽象)。高层模块的代码变得稳定,因为它只关心抽象的契约;低层模块可以自由地被替换(比如换成PostgreSQLConnector),只要它遵守契约。控制权从低层细节转移到了高层策略手中,这是实现灵活重用的架构基础。

3. 结构化代码的实战模式与技巧

理解了思想,我们来看看在C++项目中具体如何落地。设计模式是前人总结的最佳实践“套路”,能直接指导我们如何结构化代码。

3.1 策略模式:封装可互换的算法族

当你有一系列可以相互替换的算法或行为时,策略模式是首选。它将算法定义成独立的类,使得它们可以独立于使用它们的客户端而变化。

场景:一个图像处理程序,需要支持多种滤镜算法(灰度化、高斯模糊、边缘检测)。非结构化做法:在ImageProcessor类里用一堆if-elseswitch-case判断滤镜类型,然后调用对应的函数。结构化做法(策略模式)

// 1. 定义策略接口 class IFilterStrategy { public: virtual ~IFilterStrategy() = default; virtual void Apply(Image& image) = 0; }; // 2. 实现具体策略 class GrayscaleFilter : public IFilterStrategy { public: void Apply(Image& image) override { // 实现灰度化算法 for (auto& pixel : image.pixels) { uint8_t gray = static_cast<uint8_t>(0.299*pixel.r + 0.587*pixel.g + 0.114*pixel.b); pixel.r = pixel.g = pixel.b = gray; } } }; class GaussianBlurFilter : public IFilterStrategy { /* ... */ }; class EdgeDetectionFilter : public IFilterStrategy { /* ... */ }; // 3. 上下文(使用者)持有策略接口指针 class ImageProcessor { private: std::unique_ptr<IFilterStrategy> m_filter; public: void SetFilter(std::unique_ptr<IFilterStrategy> filter) { m_filter = std::move(filter); } void ProcessImage(Image& img) { if (m_filter) { m_filter->Apply(img); } } }; // 4. 客户端使用 int main() { Image myImage; ImageProcessor processor; // 轻松切换不同滤镜,无需修改ImageProcessor processor.SetFilter(std::make_unique<GrayscaleFilter>()); processor.ProcessImage(myImage); processor.SetFilter(std::make_unique<GaussianBlurFilter>()); processor.ProcessImage(myImage); }

重用价值:现在,任何需要滤镜功能的新模块,都可以直接注入IFilterStrategy。要新增一个“怀旧滤镜”,只需创建一个新的NostalgiaFilter类,系统的其他部分完全不受影响。算法库被完美地结构化和重用了。

3.2 工厂模式:封装对象创建的复杂性

当对象创建逻辑复杂(例如需要依赖配置、环境),或者你希望将创建与使用分离时,工厂模式就派上用场了。它提供了一个创建对象的接口,但允许子类决定实例化哪个类。

场景:一个跨平台图形应用,需要根据当前操作系统创建不同的窗口对象。结构化做法(工厂模式)

// 1. 产品接口 class IWindow { public: virtual ~IWindow() = default; virtual void Show() = 0; virtual void* GetNativeHandle() const = 0; }; // 2. 具体产品 class Win32Window : public IWindow { /* ... Windows API实现 ... */ }; class CocoaWindow : public IWindow { /* ... macOS Cocoa实现 ... */ }; class X11Window : public IWindow { /* ... Linux X11实现 ... */ }; // 3. 工厂接口(简单工厂也可,这里用抽象工厂概念) class IWindowFactory { public: virtual ~IWindowFactory() = default; virtual std::unique_ptr<IWindow> CreateWindow(const std::string& title, int width, int height) = 0; }; // 4. 具体工厂 class Win32WindowFactory : public IWindowFactory { public: std::unique_ptr<IWindow> CreateWindow(const std::string& title, int width, int height) override { return std::make_unique<Win32Window>(title, width, height); } }; // ... 类似的CocoaWindowFactory, X11WindowFactory // 5. 一个全局的或注入的工厂实例决定创建什么 class Application { std::unique_ptr<IWindowFactory> m_windowFactory; std::unique_ptr<IWindow> m_mainWindow; public: Application(std::unique_ptr<IWindowFactory> factory) : m_windowFactory(std::move(factory)) {} bool Initialize() { m_mainWindow = m_windowFactory->CreateWindow("My App", 800, 600); return m_mainWindow != nullptr; } }; // 6. 根据编译条件或运行时配置选择工厂 #ifdef _WIN32 auto factory = std::make_unique<Win32WindowFactory>(); #elif defined(__APPLE__) auto factory = std::make_unique<CocoaWindowFactory>(); #else auto factory = std::make_unique<X11WindowFactory>(); #endif Application app(std::move(factory)); app.Initialize();

重用价值:应用的核心逻辑(Application类)完全与具体的窗口API解耦。当你需要将应用移植到新的平台(如Wayland)时,只需新增一个WaylandWindowWaylandWindowFactory,核心业务代码无需任何改动。创建逻辑被集中管理,便于维护和扩展。

3.3 观察者模式:建立松散的动态通知机制

当一个对象(主题)的状态变化需要通知其他多个对象(观察者),且你不知道具体有多少观察者,或者不希望主题与观察者紧密耦合时,观察者模式是理想选择。

场景:游戏中的成就系统。当玩家完成特定动作(如击杀敌人、收集物品)时,需要通知成就管理器、UI提示系统、数据统计系统等多个模块。结构化做法(观察者模式)

// 1. 观察者接口 class IGameEventListener { public: virtual ~IGameEventListener() = default; virtual void OnEnemyDefeated(const Enemy& enemy) = 0; virtual void OnItemCollected(const Item& item) = 0; }; // 2. 主题(被观察者) class Player { private: std::vector<IGameEventListener*> m_listeners; // 通常用智能指针管理生命周期更好 public: void AddListener(IGameEventListener* listener) { m_listeners.push_back(listener); } void RemoveListener(IGameEventListener* listener) { m_listeners.erase(std::remove(m_listeners.begin(), m_listeners.end(), listener), m_listeners.end()); } void DefeatEnemy(const Enemy& enemy) { // ... 击败敌人的逻辑 ... // 通知所有观察者 for (auto* listener : m_listeners) { listener->OnEnemyDefeated(enemy); } } }; // 3. 具体观察者 class AchievementManager : public IGameEventListener { public: void OnEnemyDefeated(const Enemy& enemy) override { if (enemy.GetType() == EnemyType::DRAGON) { UnlockAchievement("Dragon Slayer"); } } void OnItemCollected(const Item& item) override { /* ... */ } }; class UINotificationSystem : public IGameEventListener { public: void OnEnemyDefeated(const Enemy& enemy) override { ShowFloatingText(enemy.GetPosition(), "+100 Exp"); } };

重用价值Player类变得非常干净,它只负责维护一个观察者列表和触发通知,完全不知道有哪些系统在监听它。你可以随时增加新的监听系统(如音效系统、任务系统),只需让新系统实现IGameEventListener接口并注册即可,无需修改Player类的代码。这种“发布-订阅”机制极大地提高了模块间的解耦程度和系统的可扩展性。

实操心得:在实现观察者模式时,要特别注意生命周期管理。如果观察者对象先于主题被销毁,主题持有的指针就会变成“悬垂指针”,调用时将导致未定义行为。实践中,我强烈建议使用std::weak_ptr来持有观察者,或者在主题的析构函数中自动移除所有观察者。另一种更现代、更安全的方式是使用信号槽库(如boost::signals2),它们内置了连接管理和线程安全机制。

4. 模块化与物理设计:头文件与源文件的艺术

C++的结构化不仅体现在逻辑设计上,也体现在物理设计上——即如何组织.h.cpp文件。糟糕的物理设计会导致编译时间爆炸、循环依赖和难以理解的代码结构。

4.1 头文件:稳定接口的宣言

头文件(.h.hpp)是你的模块对外的承诺书。它应该尽可能稳定、简洁、完整。

  • 只声明,不定义(实现):头文件里应该主要是类声明、函数声明、类型别名、常量定义和模板。除非是简单的inline函数或类模板的实现,否则函数实现、静态变量初始化等都应放在源文件(.cpp)中。
  • 使用前向声明减少依赖:如果类A仅用到类B的指针或引用,那么在A.h中不要#include “B.h”,而是使用class B;进行前向声明。这能显著减少编译依赖,当一个头文件被修改时,依赖它的文件需要重新编译的范围会小很多。
    // A.h - 好的做法 class B; // 前向声明 class A { public: void DoSomething(B* b); // 只用到指针 private: B* m_bPtr; }; // A.h - 坏的做法(增加了不必要的编译依赖) #include “B.h” // 如果B.h很大或者经常变,编译A.cpp的成本就高了 class A { public: void DoSomething(B* b); private: B* m_bPtr; };
  • 使用Pimpl(指针指向实现)惯用法隐藏实现细节:这是降低编译依赖和实现二进制兼容性的利器。将类的所有私有数据成员和实现细节放到一个单独的Impl类中,在公有接口类中仅保留一个指向该Impl的(智能)指针。
    // Widget.h - 对外公开的稳定接口 #include <memory> class Widget { public: Widget(); ~Widget(); // 需要显式定义,因为std::unique_ptr需要看到Impl的完整定义来析构 void PublicApi(); private: class Impl; // 前向声明内部实现类 std::unique_ptr<Impl> pImpl; // 指向实现的指针 }; // Widget.cpp #include “Widget.h” // 这里可以包含Widget实现所需的所有私有头文件,对外不可见 #include “PrivateHelper.h” #include <vector> #include <complex> class Widget::Impl { // 所有私有数据和方法放在这里 std::vector<int> m_data; ComplexType m_privateObject; void PrivateMethod() { /* ... */ } }; Widget::Widget() : pImpl(std::make_unique<Impl>()) {} Widget::~Widget() = default; // 必须在Impl定义之后,否则会编译错误 void Widget::PublicApi() { pImpl->PrivateMethod(); // 通过pImpl调用私有方法 // ... }
    好处Widget.h变得极其简洁,对PrivateHelper.hComplexType的依赖被完全隐藏。修改Widget的私有实现,只需要重新编译Widget.cpp,所有包含Widget.h的客户端代码都无需重新编译。这对于大型项目和库的版本升级至关重要。

4.2 源文件:实现细节的归宿

源文件(.cpp)是实现接口契约的地方。这里可以自由地包含任何必要的头文件,实现复杂的逻辑。

  • 确保头文件自给自足:每个.cpp文件应首先包含其对应的.h文件,这可以检查该头文件是否缺失了必要的依赖。对应的头文件应该包含它自身编译所需的所有头文件。
  • 合理划分编译单元:不要把所有实现都塞进一个巨大的.cpp文件。按照逻辑功能将实现拆分到不同的.cpp文件中。这有利于并行编译(加快构建速度),也便于代码阅读和单元测试。

4.3 目录结构与命名空间规划

物理结构的顶层设计同样重要。

  • 按模块/组件划分目录:例如,一个游戏引擎可能有/Core(数学库、内存管理)、/Graphics(渲染)、/Audio/Physics/UI等目录。每个目录下有自己的include(公共头文件)和src(私有源文件)子目录。
  • 使用命名空间反映逻辑结构:命名空间是防止名称冲突、表达代码归属的逻辑工具。目录结构应与命名空间层次大致对应。
    // 文件路径: MyEngine/Graphics/Renderer/DirectX11Renderer.h namespace MyEngine { namespace Graphics { namespace Renderer { class DirectX11Renderer { /* ... */ }; } // namespace Renderer } // namespace Graphics } // namespace MyEngine
  • 区分公共API与内部实现:通常将需要暴露给外部用户使用的头文件放在一个单独的include/ProjectName目录下,而项目内部的私有头文件和源文件放在src目录下。这清晰地定义了项目的边界。

5. 构建系统与依赖管理:结构化的保障

再好的代码结构,也需要一个可靠的构建系统来组织和编译。现代C++项目强烈推荐使用CMake

5.1 使用CMake定义模块化构建

CMake允许你以声明式的方式描述项目的模块、它们的依赖关系以及构建目标(可执行文件、静态库、动态库)。

# CMakeLists.txt (根目录) cmake_minimum_required(VERSION 3.15) project(MyAwesomeGame LANGUAGES CXX) # 设置C++标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加子目录,每个子目录是一个模块 add_subdirectory(Core) # 核心基础库 add_subdirectory(Graphics) # 图形模块 add_subdirectory(Audio) # 音频模块 add_subdirectory(Game) # 游戏逻辑,依赖以上所有模块 # Game/CMakeLists.txt add_executable(MyGame main.cpp) # 声明依赖:Game可执行文件依赖于Core、Graphics、Audio这三个库 target_link_libraries(MyGame PRIVATE CoreLib GraphicsLib AudioLib ) # 添加包含目录,让Game能找到依赖模块的头文件 target_include_directories(MyGame PRIVATE ${CMAKE_SOURCE_DIR}/Core/include ${CMAKE_SOURCE_DIR}/Graphics/include ${CMAKE_SOURCE_DIR}/Audio/include ) # Graphics/CMakeLists.txt # 将Graphics模块编译成一个静态库 add_library(GraphicsLib STATIC src/RenderDevice.cpp src/Shader.cpp src/Mesh.cpp ) # 设置这个库的公共头文件目录,这样链接它的目标(如Game)会自动获得这个包含路径 target_include_directories(GraphicsLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) # Graphics模块可能依赖于第三方库,如OpenGL find_package(OpenGL REQUIRED) target_link_libraries(GraphicsLib PRIVATE OpenGL::GL)

通过CMake这样定义,模块间的依赖关系变得清晰且自动化。修改Core模块后,CMake能自动识别出需要重新编译GraphicsLib和最终的MyGame,而不会去重新编译Audio模块。

5.2 管理第三方依赖

对于第三方库(如spdlog用于日志,glm用于数学,fmt用于格式化),手动下载、编译、配置包含路径和库路径非常繁琐且容易出错。现代C++项目应该使用包管理器,如vcpkgConan

以vcpkg为例:

  1. 在项目中创建一个vcpkg.json清单文件,声明依赖。
    { "name": "my-awesome-game", "version": "1.0.0", "dependencies": [ "spdlog", "glm", "fmt" ] }
  2. 使用CMake集成vcpkg。在配置CMake时,通过-DCMAKE_TOOLCHAIN_FILE指定vcpkg的工具链文件。
  3. CMake在配置阶段会自动通过vcpkg下载、编译(或获取预编译包)这些依赖,并为你设置好正确的find_package路径。

这种方式将依赖管理结构化、自动化,确保了团队每个成员、每个构建环境(开发机、CI服务器)都能获得完全一致的依赖版本和配置,是项目可重现性和稳定性的重要保障。

6. 常见陷阱与进阶优化策略

6.1 循环依赖:结构化设计的“死锁”

循环依赖是模块化设计中最常见也最棘手的问题之一。当模块A依赖模块B,同时模块B又依赖模块A时,就形成了循环依赖。这会导致编译失败(“未定义的符号”)或逻辑上的纠缠不清。

解决方案

  1. 提取公共部分:分析A和B,看是否存在可以提取到第三个模块C中的公共逻辑或接口。让A和B都依赖C,从而打破循环。
  2. 使用前向声明和指针/引用:如果依赖关系主要是为了使用对方的类型(而非成员),尝试将#include替换为前向声明,并将成员变量改为指针或引用。这通常能解决头文件层面的循环包含。
  3. 依赖接口而非实现:这是最根本的解决方法。如果A需要调用B的功能,而B也需要回调A,那么可以定义一个A的接口IA,让B依赖IA。A实现IA,并依赖B。这样,B只依赖于抽象的IA,而不依赖于具体的A,从物理上解除了循环。
    // IA.h (独立模块或放在公共位置) class IA { public: virtual void OnEvent() = 0; }; // B.h #include “IA.h” class B { public: void SetListener(IA* listener) { m_listener = listener; } void DoWork() { /* ... */ if (m_listener) m_listener->OnEvent(); } private: IA* m_listener; }; // A.h #include “IA.h” #include “B.h” // A现在可以安全地包含B.h了 class A : public IA { public: A() { m_b.SetListener(this); } void OnEvent() override { /* ... */ } private: B m_b; };

6.2 过度设计:为不存在的灵活性买单

初学者(包括曾经的我)在学习了设计模式后,容易陷入“模式狂热”,恨不得在每个地方都用上模式,导致代码变得过度抽象和复杂。记住,所有的结构化和抽象都是有成本的(理解成本、维护成本、运行时开销)。

如何避免

  • 遵循“三次法则”:当你在不同的地方写了第三遍相似的代码时,再考虑将其抽象出来。第一次是写功能,第二次是复用,第三次才证明它值得被设计成一个可重用的组件。
  • 从具体开始:先写出能工作的、具体的代码。然后审视它,如果发现重复、或者预见到明确的变化点(比如“我们明年肯定要支持数据库B”),再着手进行重构和抽象。
  • 保持接口精简:接口应该只提供客户端真正需要的方法。不要为了“将来可能有用”而添加方法。一个庞大臃肿的接口比一个紧密耦合的具体类更难维护和重用。

6.3 性能考量:零成本抽象与权衡

C++哲学强调“零成本抽象”,即你使用的抽象机制(如虚函数、智能指针)不应带来不必要的运行时开销。结构化设计有时会引入间接层,可能对性能有细微影响。

  • 虚函数调用:相比直接函数调用,虚调用有一次额外的指针解引用(查找虚表)。在绝大多数应用场景下,这可以忽略不计。但在最核心的、每秒调用上亿次的循环(如物理模拟、粒子更新)中,可能需要考虑是否可以用模板、策略对象(非多态)或编译期多态(CRTP)来替代运行时多态。
  • 动态内存分配:很多模式(如Pimpl、工厂返回对象)会涉及动态内存分配(new)。在性能敏感处,可以考虑使用内存池、栈上分配(std::aligned_storage)或自定义分配器来管理这些对象的生命周期。
  • 内联优化:过度使用接口和单独编译可能会阻碍编译器的内联优化。对于非常小的、热点的函数,有时将其定义在头文件中(作为inline或模板)是值得的,即使这会牺牲一些编译依赖的隔离性。

核心原则是:先保证设计清晰正确,再在性能分析(Profiling)工具的指导下进行有针对性的优化。不要为了臆想中的性能损失而牺牲代码的可读性和可维护性。99%的性能问题都出现在那1%的代码中。

6.4 测试驱动开发:结构化设计的试金石

良好的结构化设计会自然而然地让你的代码更容易测试。因为高内聚、低耦合的模块可以很容易被独立出来进行单元测试。

  • 利用接口进行模拟:你可以为依赖项(如数据库、网络、文件系统)创建“模拟对象”,在测试时注入进去,从而隔离被测模块,实现快速、稳定的单元测试。
  • 测试即文档:一套完整的单元测试,实际上就是你的代码接口如何使用、在各种边界条件下应如何工作的活文档。它迫使你从调用者的角度思考接口设计是否合理。
  • 重构的安全网:当你需要调整内部结构以优化重用性时,完善的测试套件能给你信心,确保你的修改没有破坏现有功能。

我个人习惯在实现一个具有明确接口的类之后,立刻为其编写单元测试。这不仅能验证功能,更能从使用者的角度审视接口设计是否直观、方便。很多时候,编写测试的过程会暴露出接口设计的缺陷,促使我回头去改进设计,从而形成一个“设计-实现-测试-重构”的良性循环。