设计者:Anke.wang

一、命名与可读性(最高优先级)

在 C++ 编程中,代码的可读性比性能、灵活性甚至短期实现速度都更重要。好的命名相当于内置文档,它能让人不用翻阅注释就理解代码逻辑。以下是具体规范:


1. 命名即文档

核心思想
  • 每个名字都应该清楚表达其用途和行为。
  • 变量、函数、类、结构体的名字本身就是最重要的文档。
示例
❌// 不推荐
double calc1(double a, double b);

✅// 推荐
double calculateAxisVelocity(double currentPosition, double targetPosition);
  • calc1 完全无法告诉读者函数的功能。
  • calculateAxisVelocity 清楚地说明这是计算轴速度的函数,并且参数名也明确表示输入含义。
实践建议
  1. 函数名尽量使用动词开头:get, set, calculate, update, reset
  2. 变量名描述性:避免 x, y,使用 axisPosition, errorValue
  3. 类名表示实体或概念:如 AxisController, MotionPlanner
  4. 宁可名字长,也不要简化到让人猜测的程度。
  5. 参数和局部变量同样需要自解释。

2. 统一命名风格

核心思想
  • 项目中所有命名必须遵循一致规则,便于团队协作和代码维护。
  • 不一致的命名会导致阅读者“读不懂”或“猜意思”,增加认知负担。
C++ 常见规范组合
类型 推荐命名风格 示例
PascalCase AxisController
函数/变量 camelCase 或 snake_case(团队统一) calculateVelocity()calculate_velocity()
常量/宏 UPPER_SNAKE MAX_AXIS_COUNT
成员变量 末尾加 _ domainPd_

[!TIP]

实践建议
  1. 团队确定一种风格,并在整个项目中严格执行。
  2. 避免混合风格。例如,类用 PascalCase,但不要有的类用 snake_case。
  3. 对外 API 和内部函数可略区分风格(如 API 用 camelCase,内部用 snake_case),但要文档明确。
  4. 变量命名规则要与成员变量、全局变量、常量保持一致。

3. 避免缩写与魔法数字

核心思想
  • 缩写和魔法数字会让代码含义模糊,增加理解成本。
  • 除非缩写是领域内公认的,否则尽量全写。
示例
魔法数字
❌// 不推荐
if (n < 8) {
    // ...
}

✅// 推荐
const int MAX_AXIS_COUNT = 8;
if (n < MAX_AXIS_COUNT) {
    // ...
}
  • 8 的含义不清楚,而 MAX_AXIS_COUNT 一眼能看懂。
缩写
❌// 不推荐
double calcVel(double pos, double tgt);

✅// 推荐
double calculateVelocity(double currentPosition, double targetPosition);
  • calcVel 可能需要猜测是“计算速度”,postgt 也不够直观。
  • 领域公认缩写可以使用,如 EtherCAT 的 PDOSDORT,团队成员普遍理解。

[!TIP]

实践建议
  1. 定义常量代替数字字面量。
  2. 避免除团队领域外的缩写。
  3. 常量、宏用 UPPER_SNAKE 命名,增加可读性。
  4. 对于常用参数或状态值,尽量用枚举代替硬编码数字。

C++ 命名规范模板表格
类型 命名风格 示例 说明与建议
类 / 结构体 / 枚举类 PascalCase AxisController, MotionPlanner, ErrorCode 名称表示实体或概念。首字母大写,每个单词首字母大写,不使用下划线。
函数 / 方法 camelCase 或 snake_case(团队统一) calculateVelocity(), updatePosition(), resetSystem() 动词开头,描述动作或功能。参数名自解释。
普通变量 / 局部变量 camelCase 或 snake_case currentPosition, targetVelocity, errorValue 描述性名称,不使用单字母或难懂缩写。
常量 / 宏 UPPER_SNAKE MAX_AXIS_COUNT, DEFAULT_TIMEOUT_MS 避免魔法数字,常量名一目了然。
成员变量 camelCase + _ 后缀 domainPd_, axisCount_, errorCode_ 区分成员变量与局部变量,避免命名冲突。
全局变量 camelCase + g_ 前缀 g_systemStatus, g_masterPointer 全局变量尽量少用,用前缀标识。
指针变量 可加 p 前缀(可选) pMaster, pAxisController 指针类型明确,便于阅读。
布尔变量 camelCase isRunning, hasError, enableLogging 带有 is/has/can/enable 等前缀,使布尔语义清晰。
枚举 / 状态值 PascalCase 或 UPPER_SNAKE(团队统一) AxisState::Idle, AxisState::Running, ERROR_CODE_NONE 枚举值表达状态含义,不使用数字。
命名空间 lowercase motion, ethercat, utilities 使用小写单词,避免与类或函数冲突。
模板参数 PascalCase template <typename T, typename U> 简洁明确,常用单字母如 T 可接受,但复杂模板可用完整词。
缩写 领域公认缩写可保留,否则全写 PDO, SDO, RT 避免自造缩写,保持可读性。
数字 / 魔法值 常量代替 const int MAX_AXIS_COUNT = 8; 避免硬编码数字,如 if(n<8)if(n<MAX_AXIS_COUNT)

[!IMPORTANT]

总结

  • 命名即文档:每个名字都要自解释,不让人猜。
  • 统一风格:团队统一风格,降低认知负担。
  • 避免缩写和魔法数字:明确、直观、可维护。

二、结构与职责

软件结构设计和职责划分决定了代码的可维护性、可扩展性和团队协作效率。以下规范帮助你保持代码清晰、模块化,并降低耦合。


1. 单一职责(SRP, Single Responsibility Principle)

核心思想
  • 每个函数只做一件事。
  • 每个类只管理一个变化方向或职责。
  • 一个类的变更原因尽量只有一个。
示例
❌// 不推荐:一个类同时负责数据和序列化
class AxisManager {
public:
    void loadFromFile(const std::string& filename);
    void saveToFile(const std::string& filename);
    void updatePosition(double pos);
private:
    double currentPosition_;
};

✅// 推荐:职责分离
class AxisDataModel {
public:
    void updatePosition(double pos);
    double getPosition() const;
private:
    double currentPosition_;
};

class AxisConfigSerializer {
public:
    void save(const AxisDataModel& model, const std::string& filename);
    void load(AxisDataModel& model, const std::string& filename);
};

[!TIP]

实践建议
  1. 函数职责:一函数完成一个具体动作或计算。
  2. 类职责
    • 数据管理 → xxxDataModel
    • 序列化 → xxxSerializer
    • 控制逻辑 → xxxController
  3. 好处
    • 便于测试(单元测试针对单一功能)。
    • 便于复用和扩展。
    • 修改某功能只影响相关类。

2. 限制函数与文件长度

核心思想
  • 函数过长,阅读困难且隐藏逻辑问题。
  • 文件过大,维护难度高,编译依赖增加。
建议数值
  • 函数:≤ 500 行
  • 文件:≤ 1500 行

[!TIP]

实践建议
  1. 函数拆分:如果函数逻辑过多,按子任务拆成小函数。
  2. 文件拆分:按功能或模块拆文件,例如:
    • AxisDataModel.h/cpp → 数据模型
    • AxisConfigSerializer.h/cpp → 序列化
    • AxisController.h/cpp → 控制逻辑
  3. 嗅觉信号
    • 一个函数里有多层嵌套循环或 if/else,提示可能过长。
    • 文件里类太多,说明功能过杂,需要拆分。

3. 明确层次边界

核心思想
  • 项目按层划分:UI 层 → 业务逻辑层 → 硬件抽象层(HAL)
  • 依赖只能从上往下:底层不依赖上层,防止耦合和循环依赖。
示例
// UI 层(Qt 相关)
class AxisControlWidget : public QWidget {
    Q_OBJECT
public:
    void onMoveButtonClicked();
private:
    AxisController* controller_; // 调用业务逻辑层
};

// 业务逻辑层
class AxisController {
public:
    void moveAxis(double position);
private:
    AxisDataModel model_;
    AxisHAL* hal_; // 调用硬件抽象层
};

// 硬件抽象层
class AxisHAL {
public:
    void writeMotorCommand(double pos);
};

[!TIP]

实践建议
  1. UI 层 不直接访问 HAL
  2. HAL 层 不依赖 Qt 或业务逻辑
  3. 层与层之间用接口或事件/信号解耦。

4. 接口与实现分离

核心思想
  • 头文件只暴露接口,隐藏实现细节。
  • 降低编译依赖和耦合。
  • PIMPL 或纯虚接口可有效隔离实现。
PIMPL 示例
// AxisController.h
class AxisController {
public:
    AxisController();
    ~AxisController();

    void moveAxis(double pos);

private:
    class Impl;
    Impl* pImpl_;
};

// AxisController.cpp
class AxisController::Impl {
public:
    void moveAxisImpl(double pos) {
        // 真正实现逻辑
    }
};

AxisController::AxisController() : pImpl_(new Impl) {}
AxisController::~AxisController() { delete pImpl_; }
void AxisController::moveAxis(double pos) { pImpl_->moveAxisImpl(pos); }
好处
  • 修改实现时只改 cpp 文件,不触发头文件依赖重新编译。
  • 接口更稳定,API 对外清晰。

[!IMPORTANT]

总结

  1. SRP → 每个函数/类只做一件事。

  2. 函数/文件长度 → 避免过长,拆分模块化。

  3. 层次边界 → UI → 业务逻辑 → HAL,依赖单向。

  4. 接口与实现分离 → 用 PIMPL 或纯虚接口降低耦合。

三、错误处理与健壮性

在高可靠性系统中(尤其是实时控制、EtherCAT/算法类系统),代码的健壮性直接关系到系统安全与稳定。错误处理和资源管理必须从设计阶段就严格考虑。


1. 错误必须显式处理

核心思想
  • 不允许静默吞掉错误。每一个可能失败的操作都要有明确处理或向上传递。
  • 简单 return false 或空操作不算处理,必须至少记录日志或触发异常。
示例
❌// 不推荐
bool readConfigFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        return false;  // 静默失败
    }
    // ...
    return true;
}

✅// 推荐
bool readConfigFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        logError("Failed to open config file: " + filename);
        return false;
    }
    // ...
    return true;
}

[!TIP]

实践建议
  1. 日志记录:所有错误至少打印日志或触发事件。
  2. 返回值 vs 异常
    • 对于实时系统,可使用返回值并配合日志。
    • 对于非实时系统,可使用异常抛出。
  3. 上层感知:调用者必须能知道错误发生,并作出处理或安全回退。

2. 边界条件优先考虑

核心思想
  • 编写主逻辑之前就思考所有可能的边界条件,提前防御。
  • 特别在实时系统中,一次边界条件遗漏可能导致现场停机。
常见边界条件
  1. 空指针

    if (!ptr) {
        logError("Null pointer detected!");
        return;
    }
    
  2. 零长度 / 空容器

    if (vec.empty()) return;
    
  3. 整数溢出 / 下标越界

    if (index >= vec.size()) {
        logError("Index out of bounds: " + std::to_string(index));
        return;
    }
    
  4. 并发竞争 / 锁

    std::lock_guard<std::mutex> lock(mutex_);
    // 防止多线程同时访问共享资源
    

[!TIP]

实践建议
  • 每次处理数据前先检查输入合法性。
  • 对所有 API、系统调用、硬件操作做防护。
  • 高风险操作(IO、网络、共享内存、实时总线)必须考虑异常场景。

3. 资源管理 RAII 化

核心思想
  • 内存、锁、文件句柄、socket 等资源都用 RAII(Resource Acquisition Is Initialization)管理。
  • 底线std::unique_ptrstd::lock_guard。不允许裸 new/delete 或手动 unlock/close。
示例
内存管理
❌// 不推荐
AxisDataModel* model = new AxisDataModel();
// 使用完成后可能忘记 delete
delete model;

// 推荐
auto model = std::make_unique<AxisDataModel>();
// 自动释放,无需手动 delete
锁管理
❌// 不推荐
mutex.lock();
doSomething();
mutex.unlock();  // 异常时可能漏 unlock

// 推荐
std::lock_guard<std::mutex> lock(mutex);
doSomething();  // 离开作用域自动 unlock
文件 / 句柄管理
std::ifstream file(filename); // 构造时打开,析构时关闭

[!TIP]

实践建议
  1. 所有资源都尽量用智能指针、RAII 包装类。
  2. 异常安全:RAII 可保证异常抛出时资源自动释放。
  3. 统一管理接口,减少裸资源操作,降低内存泄漏、死锁风险。

[!IMPORTANT]

总结

  1. 错误显式处理:不吞掉错误,记录日志或抛出。
  2. 边界条件防护:空指针、零长度、溢出、并发,全覆盖。
  3. RAII 管理资源:智能指针、lock_guard、文件流等,保证安全释放。

小技巧:写函数前先列出“可能出错的点 + 边界条件”,再写实现,这能大幅降低现场错误风险。

四、并发与实时性(工业领域关键)

在工业控制和 EtherCAT/IGH 等实时系统中,线程管理和实时性直接影响系统安全和响应速度。错误的线程访问或阻塞操作可能导致现场停机或运动控制异常。


1. 明确线程归属

核心思想
  • 每个数据结构必须明确哪个线程可读、可写。
  • 不同线程间的数据流必须有清晰契约,避免竞态条件。
示例
明确归属
struct AxisData {
    double position; // 仅 RT 线程写
    double velocity; // RT 线程写,Worker 线程可读
};
线程安全访问
  • 双缓冲(DoubleBuffer)
    • RT 线程写缓冲 A
    • Worker/UI 线程读缓冲 B
    • 写完成后交换缓冲
template<typename T>
class DoubleBuffer {
public:
    void write(const T& data) {
        writeBuffer_ = data; // RT 写
        std::swap(writeBuffer_, readBuffer_); // 原子交换
    }

    T read() const {
        return readBuffer_; // 非阻塞读取
    }

private:
    T writeBuffer_;
    T readBuffer_;
};
  • 原子类型(std::atomic)
std::atomic<int> rtCounter{0};
rtCounter++; // RT 线程写
int val = rtCounter.load(); // Worker/ UI 线程读
  • 事件队列(EventQueue)
rtEventQueue.push(event); // RT 线程写
workerThread.process(rtEventQueue.pop()); // Worker 线程处理

[!TIP]

实践建议
  1. 明确每个变量归属的线程:RT、Worker、UI。
  2. 不同线程访问的数据必须用 线程安全结构缓冲机制
  3. 注释中标明“谁写,谁读”,形成契约文档。
  4. 尽量减少跨线程写操作,保证 RT 线程安全。

2. 实时路径零分配、零阻塞

核心思想
  • RT(实时)线程内 禁止内存分配、阻塞等待、系统调用
  • 允许操作必须是 确定性、快速、可预期的
  • 这是硬规则,不是建议,否则 RT 性能和 determinism 会受到破坏。
禁止操作示例
  • malloc, new → 内存分配可能触发页换入/锁页,非确定性。
  • std::mutex::lock → 可能阻塞,RT 不允许。
  • sleep, usleep, std::this_thread::sleep_for → 阻塞。
  • 文件操作、打印日志、网络 I/O → 系统调用可能阻塞。
允许操作
  • 原子操作:std::atomic
  • 已预分配内存的容器操作(循环使用、DoubleBuffer)
  • 固定大小数组读写
  • 内联计算、寄存器/变量访问
  • 快速事件通知:轻量环形队列、lock-free queue
示例
// RT 线程
void rtControlLoop() {
    // 允许:原子写、数组读写
    positionBuffer_[index] = measuredPosition;

    // 不允许:动态分配、锁等待
    // std::lock_guard<std::mutex> lock(mutex_); // ❌
    // auto temp = new AxisData(); // ❌
}

[!TIP]

实践建议
  1. 所有 RT 数据结构在初始化阶段完成内存分配,运行时不再 malloc/new。
  2. 锁使用仅限非 RT 线程。RT 线程使用 lock-free 或原子操作。
  3. 函数调用:RT 线程函数尽量内联,减少调用开销和栈深度。
  4. 测试:严格测 RT 循环周期,确保不超时。

[!IMPORTANT]

总结

  1. 线程归属明确:每个变量谁读谁写必须清晰,跨线程访问需线程安全。
  2. RT 线程零分配、零阻塞:禁止 malloc/new、锁等待和 syscalls。
  3. 工业实时系统硬规则
    • RT 线程永远不被阻塞
    • 所有资源在启动时预分配
    • 线程间通信使用原子或 lock-free 结构
  4. 契约清晰:注释和文档标明线程归属、访问权限。

五、可测试性(Testability)

在工业控制、机器人、EtherCAT/IGH 等系统中,硬件依赖和全局状态容易让测试复杂、不可控。可测试性设计不仅提高代码质量,还降低现场故障风险。


1. 依赖可注入(Dependency Injection)

核心思想
  • 核心逻辑不依赖具体硬件或全局状态。
  • 通过抽象接口注入具体实现,使单元测试和仿真成为可能。
  • 实际系统运行时可以注入硬件 HAL,测试时注入模拟对象。
示例
定义接口
class IEcHal {
public:
    virtual ~IEcHal() = default;
    virtual double readAxisPosition(int axisId) = 0;
    virtual void writeAxisCommand(int axisId, double value) = 0;
};
核心逻辑类
class AxisController {
public:
    AxisController(IEcHal* hal) : hal_(hal) {}

    void moveAxis(int axisId, double target) {
        double pos = hal_->readAxisPosition(axisId);
        double cmd = computeCommand(pos, target);
        hal_->writeAxisCommand(axisId, cmd);
    }

private:
    double computeCommand(double current, double target) {
        return target - current; // 简化计算
    }

    IEcHal* hal_;
};
单元测试示例
class MockHal : public IEcHal {
public:
    double readAxisPosition(int axisId) override { return 1.0; }
    void writeAxisCommand(int axisId, double value) override { lastCommand_ = value; }
    double lastCommand_;
};

void testAxisController() {
    MockHal mock;
    AxisController ctrl(&mock);
    ctrl.moveAxis(0, 5.0);
    assert(mock.lastCommand_ == 4.0);
}

[!TIP]

实践建议
  1. 核心逻辑不依赖硬件,只操作接口。
  2. 接口定义要稳定,实现可替换。
  3. 测试覆盖率:通过 Mock / Stub 模拟不同场景(零位、极限值、异常)。
  4. 仿真优先:工业控制系统可先在仿真环境验证逻辑,再接入硬件。

2. 纯函数优先

核心思想
  • 能写成无副作用的就写成纯函数。
  • 纯函数优点:
    1. 好测试:输入输出可预测,无需 Mock 硬件。
    2. 易推理:不依赖全局状态。
    3. 好并发:没有共享状态冲突。
示例
有副作用的函数
void updatePosition(AxisDataModel& model, IEcHal* hal) {
    double pos = hal->readAxisPosition(0);
    model.setPosition(pos);
}
  • 不容易测试,需要 Mock IEcHalAxisDataModel
纯函数实现
double computeNextPosition(double current, double target) {
    return target - current; // 纯计算,无副作用
}

// 测试
assert(computeNextPosition(2.0, 5.0) == 3.0);

[!TIP]

实践建议
  1. 核心计算逻辑尽量纯函数化。
  2. I/O 或硬件操作通过接口注入,纯逻辑独立测试。
  3. 单元测试覆盖计算函数,提高逻辑可靠性。

[!IMPORTANT]

总结

  1. 依赖可注入:用接口隔离硬件,单元测试可仿真。
  2. 纯函数优先:输入输出可预测,无副作用,更容易测试、并发和推理。
  3. 实践技巧
    • 逻辑与 I/O 分离
    • 核心计算纯函数化
    • Mock/Stub 模拟外部依赖

工业控制系统中,可测试性不仅提高代码质量,还能在仿真阶段发现极限边界和异常,降低现场风险。

六、注释与文档

在工业控制系统中,注释和文档不仅是辅助工具,而是 安全和效率保障。错误、遗漏或误解文档可能导致现场停机或控制事故。


1. 注释解释“为什么”而非“是什么”

核心思想
  • 代码已经表达了“做什么”,不需要重复。
  • 注释的价值在于解释为什么这么做
    • 为什么选择这个算法?
    • 为什么要规避某个 bug?
    • 为什么违反直觉做了特殊处理?
示例
不推荐
int n = MAX_AXIS_COUNT; // 设置轴数量
  • 注释只是重复代码,不提供信息。
推荐
int n = MAX_AXIS_COUNT; 
// MAX_AXIS_COUNT 受硬件限制,超过 8 会导致 EtherCAT PDO 映射异常
  • 注释解释了“为什么最大是 8”,提供决策背景。
规避坑示例
// 注意:EtherCAT SDO 写入时,必须先置位 PREOP,否则从站可能拒绝写操作
writeSdo(index, subIndex, value);
  • 告诉后人为什么这行代码必须这么做。

[!TIP]

实践建议
  1. “什么”交给代码:函数、变量、类名。
  2. “为什么”交给注释
    • 特殊处理
    • 规避已知问题
    • 算法选择理由
  3. 注释要简洁、精准,避免长篇废话。

2. 公共 API 必须有文档注释

核心思想
  • 所有对外接口必须清楚说明:
    • 参数含义
    • 单位(度、弧度、米、秒……)
    • 取值范围
    • 线程安全性
    • 错误返回 / 异常情况
示例(Doxygen 风格)
/**
 * @brief Move an axis to the target position
 * @param axisId Index of the axis (0..MAX_AXIS_COUNT-1)
 * @param position Target position in radians
 * @param speed Maximum speed in radians/s
 * @return true if move command accepted, false otherwise
 * @note Thread safe: can be called from Worker thread, not from RT thread
 * @warning Ensure RT control loop is running, otherwise command may be ignored
 */
bool moveAxis(int axisId, double position, double speed);
重点解释
  1. 单位清楚:弧度 vs 度,米 vs mm。单位错一天可能毁掉整套实验。
  2. 取值范围明确:避免超界。
  3. 线程安全说明:告诉调用者是否可以跨线程调用。
  4. 错误返回说明:返回值或异常含义清楚。

[!TIP]

实践建议
  1. 公共函数、类、模块全部写文档注释。
  2. API 文档应包含示例和注意事项。
  3. 工业系统中,单位和线程安全尤为关键。
  4. 任何违反直觉的设计必须用注释标明。

[!IMPORTANT]

总结

  1. 注释价值
    • 不解释“做什么”,解释“为什么这么做”。
    • 特殊处理、规避坑、算法选择理由必须标明。
  2. 公共 API 文档
    • 参数含义、单位、范围
    • 错误返回、异常
    • 线程安全性、调用约束
  3. 工业实践
    • 单位错误、线程误用或边界不清可能导致停机事故。
    • 注释和文档是安全保障,不是负担。

七、版本控制与协作

在工业控制系统开发中,代码质量不仅靠个人能力,更靠 团队流程与规范。版本控制和代码评审是保证可维护性、可追踪性和知识共享的核心机制。


1. 小步提交、信息清晰

核心思想
  • 每次 commit 只解决一个逻辑问题。
  • 提交信息要说明 “做了什么 + 为什么”,而不是模糊的“update”、“fix”。
  • 小步提交便于追踪问题和回退,避免大改动带来风险。
示例
不推荐
commit: update
- 改了 AxisController, AxisDataModel, AxisConfigSerializer 等文件
  • 信息模糊,不知道为什么修改。
  • 涉及多个功能,回退困难。
推荐
commit: fix AxisController moveAxis bug when target > MAX_AXIS_COUNT
- 修复 moveAxis 函数在超出最大轴数时崩溃
- 添加边界检查并日志记录
  • 清楚说明做了什么(修复 bug)和为什么(边界检查)。
  • 修改范围明确,便于回溯和测试。

[!TIP]

实践建议
  1. 一次 commit 对应 一个功能或修复
  2. 提交信息格式:
    • 标题:简明说明做了什么
    • 正文:必要时解释为什么这么做、可能影响
  3. 避免大杂烩式提交,尤其是工业系统关键模块。

2. 代码评审强制化

核心思想
  • 再小的项目也要 review,哪怕是自己回头看自己的代码。
  • 评审目的:
    1. 查 bug
    2. 传播团队规范和知识
    3. 提高代码可读性、可维护性
    4. 确保设计与架构符合规范

[!TIP]

实践建议
  1. 小而频繁的 PR / Merge Request
    • 小提交更容易 review
    • 代码逻辑清晰
  2. 检查重点
    • 命名是否清晰
    • 函数/类职责是否单一
    • 错误处理是否完整
    • 并发安全、实时路径是否合规
    • 注释与 API 文档是否完整
  3. 复审与自审
    • 自己先检查一遍,修正明显问题
    • 再让团队成员 review,分享知识
  4. 工具辅助
    • 使用 Git + GitLab/GitHub/Gitee 等代码评审功能
    • 强制每个 merge request 至少一人 review

[!IMPORTANT]

总结

  1. 小步提交 + 清晰信息
    • 每次 commit 对应一个逻辑改动
    • 信息写明“做了什么 + 为什么”
  2. 代码评审强制化
    • 再小的改动也 review
    • Review 目的不仅是 bug,也传播规范和知识
  3. 工业实践重点
    • 版本控制和评审是现场安全保障的一部分
    • 大改动或未 review 的代码容易导致系统不可预测行为

八、工程规约(Engineering Guidelines)

工业系统不仅代码要可靠,工程实践也必须规范。硬编码和不可观测系统是现场停机和调试困难的根源。


1. 配置外置、环境隔离

核心思想
  • 禁止硬编码路径、IP、端口、参数。
  • 配置使用 外部文件(TOML/JSON)或环境变量
  • 不同环境(开发 / 仿真 / 生产)配置必须可切换,互不干扰。
示例
外置配置文件(JSON)
{
    "network": {
        "masterIp": "192.168.0.100",
        "port": 502
    },
    "axis": {
        "maxCount": 8,
        "defaultSpeed": 1.5
    }
}
C++ 读取示例
#include <nlohmann/json.hpp>
#include <fstream>

nlohmann::json loadConfig(const std::string& path) {
    std::ifstream file(path);
    nlohmann::json config;
    file >> config;
    return config;
}

auto config = loadConfig("config/dev_config.json");
std::string ip = config["network"]["masterIp"];
double speed = config["axis"]["defaultSpeed"];
环境切换
export APP_ENV=dev
./myApp --config config/${APP_ENV}_config.json
实践建议
  1. 硬编码禁止:路径、IP、参数、算法系数都应外置。
  2. 环境隔离
    • Dev / Sim / Prod 配置独立
    • 切换方便,无风险覆盖
  3. 统一配置格式:JSON/TOML/YAML,团队一致,便于解析和维护。

2. 可观测性内建

核心思想
  • 系统内部必须可观测:
    • 日志分级(DEBUG / INFO / WARN / ERROR)
    • 关键状态可导出
    • 性能指标可采集
  • 遇到问题靠日志复现,而不是“重启试试”。
示例
日志分级
enum class LogLevel { DEBUG, INFO, WARN, ERROR };

void log(LogLevel level, const std::string& msg) {
    // 简化示例,可扩展为文件、网络或实时终端输出
    std::cout << "[" << static_cast<int>(level) << "] " << msg << std::endl;
}

// 使用
log(LogLevel::INFO, "AxisController initialized");
log(LogLevel::ERROR, "Axis 3 communication timeout");
关键状态导出
struct AxisStatus {
    double position;
    double velocity;
    bool isRunning;
};

AxisStatus status = getAxisStatus();
exportToMonitoringSystem(status); // 可采集到监控界面
性能指标采集
  • 循环时间统计、CPU占用、内存占用、EtherCAT周期延迟
  • 通过日志或监控接口输出
auto start = std::chrono::steady_clock::now();
// RT loop...
auto duration = std::chrono::steady_clock::now() - start;
log(LogLevel::DEBUG, "RT loop duration: " + std::to_string(duration.count()) + " ns");

[!TIP]

实践建议
  1. 日志分级:DEBUG/INFO/WARN/ERROR,便于不同场景调试。
  2. 关键状态可导出
    • Axis位置、速度、报警状态
    • EtherCAT主站状态
    • 控制器运行模式
  3. 性能指标:RT循环时间、I/O延迟、资源占用
  4. 不可靠重启:问题必须可追溯和复现。

[!IMPORTANT]

总结

  1. 配置外置、环境隔离
    • 禁止硬编码
    • 支持 Dev/Sim/Prod 切换
  2. 可观测性内建
    • 日志分级
    • 关键状态导出
    • 性能指标采集
  3. 工业实践意义
    • 提高调试效率
    • 降低现场风险
    • 遇到问题可复现,而非盲目操作

更多推荐