可维护性 可扩展性 可复用性

📅 2026/8/15 8:46:37
可维护性 可扩展性 可复用性
实际项目中 c针对这三种特性都有哪些常用方法好问题。我来梳理一下实际项目中真正常用的方法而不是教科书上的理论。我会用最基础的 C 语法每行注释。一、可维护性Maintainability—— 实际项目常用方法方法1拆分函数最基础、最常用原则一个函数不超过 50 行一个函数只做一件事// ❌ 坏一个函数 200 行什么都做 void processOrder(const json data) { // 第1-30行解析数据 // 第31-50行校验数据 // 第51-80行计算价格 // 第81-100行检查库存 // 第101-130行生成订单 // 第131-150行发送邮件 // 第151-180行记录日志 // 第181-200行返回结果 } // ✅ 好拆成多个小函数 Order parseOrderData(const json data); // 只负责解析 bool validateOrder(const Order order); // 只负责校验 double calculatePrice(const Order order); // 只负责计算 bool checkStock(const Order order); // 只负责检查 OrderId generateOrder(const Order order); // 只负责生成 void sendEmail(const Order order); // 只负责发送 void logOrder(const Order order); // 只负责记录实际项目中怎么用团队一般约定一个函数不要超过一屏约 50 行如果超过就拆函数名要能说清楚我做了什么方法2常量命名防止魔法数字// ❌ 坏不知道 60、4096、3 是什么意思 if (age 60) { ... } buffer new char[4096]; if (retryCount 3) { ... } // ✅ 好用常量或枚举 const int MAX_RETRY_COUNT 3; const int MAX_NAME_LENGTH 50; const int BUFFER_SIZE 4096; const int RETIREMENT_AGE 60; if (age RETIREMENT_AGE) { ... } buffer new char[BUFFER_SIZE]; if (retryCount MAX_RETRY_COUNT) { ... }实际项目中怎么用文件开头定义常量或者用const全局变量或者用#define但尽量用 const方法3尽早返回减少嵌套// ❌ 坏嵌套太深逻辑绕 void process(const json data) { if (data.contains(order)) { if (data[order].contains(id)) { if (data[order][id].is_string()) { // 这里才真正开始处理 // ... } } } } // ✅ 好尽早返回减少嵌套 void process(const json data) { // 检查条件不满足就返回 if (!data.contains(order)) { return; // 没有 order 字段直接返回 } if (!data[order].contains(id)) { return; // 没有 id 字段直接返回 } if (!data[order][id].is_string()) { return; // id 不是字符串直接返回 } // 到这里所有条件都满足了开始处理 // ... }实际项目中怎么用先检查所有错误条件尽早返回最后才写正常逻辑这样可以减少 if-else 的嵌套层数方法4用枚举代替字符串比较// ❌ 坏字符串比较容易写错 if (type user) { ... } // 如果写成 uesr 就 bug 了 else if (type order) { ... } // ✅ 好用枚举编译器会检查 enum MessageType { TYPE_USER, // 0 TYPE_ORDER, // 1 TYPE_PRODUCT, // 2 TYPE_COUNT // 3用于数组大小 }; // 然后可以用 switch switch (type) { case TYPE_USER: // ... break; case TYPE_ORDER: // ... break; // 编译器会警告如果少了某个 case }实际项目中怎么用如果类型是固定的比如不超过 10 种用枚举如果类型是动态的比如用户自定义用 map二、可扩展性Extensibility—— 实际项目常用方法方法1函数指针/回调最常用// 实际项目中最常用的扩展方式 // 1. 定义一个函数指针类型 typedef int (*Callback)(int data); // 2. 写一个处理函数接受回调 void processData(int* array, int size, Callback callback) { for (int i 0; i size; i) { array[i] callback(array[i]); // 调用回调 } } // 3. 不同的调用方提供不同的回调 int doubleValue(int x) { return x * 2; } int squareValue(int x) { return x * x; } int addTen(int x) { return x 10; } // 4. 使用 int data[] {1, 2, 3, 4, 5}; processData(data, 5, doubleValue); // 翻倍 processData(data, 5, squareValue); // 平方 processData(data, 5, addTen); // 加10 // 不需要改 processData 函数本身实际项目中怎么用排序函数可以传入不同的比较函数遍历函数可以传入不同的处理函数网络库可以传入不同的回调收到数据做什么方法2虚函数 继承面向对象方式// 实际项目中插件系统、UI 框架 // 1. 定义接口 class IPlugin { public: virtual ~IPlugin() {} virtual void init() 0; // 初始化 virtual void execute() 0; // 执行 virtual void cleanup() 0; // 清理 virtual const char* getName() 0; // 插件名 }; // 2. 不同的插件 class LogPlugin : public IPlugin { public: void init() { /* 初始化日志系统 */ } void execute() { /* 写日志 */ } void cleanup() { /* 关闭日志文件 */ } const char* getName() { return LogPlugin; } }; class MonitorPlugin : public IPlugin { public: void init() { /* 初始化监控 */ } void execute() { /* 收集性能数据 */ } void cleanup() { /* 停止监控 */ } const char* getName() { return MonitorPlugin; } }; // 3. 管理器不用改支持任意插件 class PluginManager { private: IPlugin* plugins[10]; // 最多支持10个插件 int count; public: PluginManager() : count(0) {} // 注册插件扩展点 void registerPlugin(IPlugin* plugin) { if (count 10) { plugins[count] plugin; plugin-init(); // 自动初始化 } } // 执行所有插件 void executeAll() { for (int i 0; i count; i) { plugins[i]-execute(); } } }; // 4. 使用 int main() { PluginManager manager; // 注册插件加新插件只需要 new 一个对象 注册 manager.registerPlugin(new LogPlugin()); manager.registerPlugin(new MonitorPlugin()); // 执行 manager.executeAll(); }实际项目中怎么用游戏引擎不同的角色继承自同一个基类UI 框架不同的控件继承自同一个基类插件系统所有插件实现同一个接口方法3模板编译期扩展// 实际项目中容器、算法库 // 1. 模板函数可以处理任意类型 template typename T T max(T a, T b) { return (a b) ? a : b; } // 2. 使用 int a max(3, 5); // T int double b max(3.14, 2.56); // T double std::string c max(std::string(abc), std::string(xyz)); // T string // 不需要为每种类型重写实际项目中怎么用STL 容器std::vectorint、std::vectorstring算法std::sort、std::find不需要为每种类型重写同样的逻辑方法4策略模式把变化的部分封装起来// 实际项目中压缩、加密、序列化 // 1. 定义策略接口 class ICompressionStrategy { public: virtual ~ICompressionStrategy() {} virtual std::string compress(const std::string data) 0; virtual std::string decompress(const std::string data) 0; }; // 2. 不同的策略 class NoCompression : public ICompressionStrategy { public: std::string compress(const std::string data) { return data; } std::string decompress(const std::string data) { return data; } }; class GzipCompression : public ICompressionStrategy { public: std::string compress(const std::string data) { /* gzip 压缩 */ } std::string decompress(const std::string data) { /* gzip 解压 */ } }; class ZipCompression : public ICompressionStrategy { public: std::string compress(const std::string data) { /* zip 压缩 */ } std::string decompress(const std::string data) { /* zip 解压 */ } }; // 3. 使用策略的类 class FileSaver { private: ICompressionStrategy* compressor_; // 策略对象 public: // 可以在运行时切换策略 void setCompression(ICompressionStrategy* compressor) { compressor_ compressor; } void save(const std::string filename, const std::string data) { std::string compressed compressor_-compress(data); // 保存到文件... } }; // 4. 使用 int main() { FileSaver saver; // 使用不同的压缩策略不需要改 FileSaver 的代码 saver.setCompression(new NoCompression()); saver.save(test.txt, hello); saver.setCompression(new GzipCompression()); saver.save(test.gz, hello); }实际项目中怎么用压缩算法可以切换 zip、gzip、7z加密算法可以切换 AES、DES、RSA序列化格式可以切换 JSON、XML、Protobuf三、可复用性Reusability—— 实际项目常用方法方法1封装成独立模块最常用// 实际项目把通用功能封装成类 // 1. 封装一个日志模块 // 这个模块可以放在任何项目里使用 class Logger { private: FILE* file_; bool enabled_; public: Logger(const char* filename) { file_ fopen(filename, a); enabled_ true; } ~Logger() { if (file_) fclose(file_); } void log(const char* level, const char* message) { if (!enabled_ || !file_) return; // 获取当前时间 time_t now time(NULL); char* timeStr ctime(now); // 去掉换行符 timeStr[strlen(timeStr) - 1] \0; fprintf(file_, [%s] [%s] %s\n, timeStr, level, message); fflush(file_); } void info(const char* msg) { log(INFO, msg); } void warn(const char* msg) { log(WARN, msg); } void error(const char* msg) { log(ERROR, msg); } void setEnabled(bool enabled) { enabled_ enabled; } }; // 2. 任何项目都可以直接使用 // 项目A Logger logger(app.log); logger.info(项目A 启动成功); // 项目B同一个 Logger 类 Logger logger(server.log); logger.info(项目B 开始处理请求);实际项目中怎么用公司内部一般有公共库放这些通用模块日志、配置、网络、文件操作等都封装成独立模块新项目直接引用不用重复造轮子方法2用参数化代替硬编码// ❌ 坏硬编码不能复用 void processFile() { const char* filename C:\\data\\input.txt; int bufferSize 1024; const char* encoding UTF-8; // ... } // ✅ 好参数化可以复用 // 这个函数可以处理任何文件 void processFile( const char* filename, // 文件名可配置 int bufferSize, // 缓冲区大小可配置 const char* encoding // 编码可配置 ) { // ... }实际项目中怎么用不要硬编码路径、大小、数量用参数传进来或者用配置文件读取方法3模板 类型参数// 实际项目通用容器、通用算法 // 1. 写一个通用的缓存类 template typename T class Cache { private: T* data_; int capacity_; int size_; public: Cache(int capacity) : capacity_(capacity), size_(0) { data_ new T[capacity]; } ~Cache() { delete[] data_; } void add(const T item) { if (size_ capacity_) { data_[size_] item; } } T get(int index) { if (index 0 index size_) { return data_[index]; } return T(); // 返回默认值 } int size() { return size_; } }; // 2. 可以缓存任何类型 Cacheint intCache(100); // 缓存 int Cachestd::string strCache(50); // 缓存 string CacheOrder orderCache(20); // 缓存 Order 对象实际项目中怎么用STL 容器都是模板vector、map、set自己写通用类时也尽量用模板方法4组合优于继承// 实际项目通过组合复用功能 // 1. 先写一些小的、可复用的功能类 class Logger { public: void log(const char* msg) { /* ... */ } }; class Database { public: void save(const char* data) { /* ... */ } }; class EmailSender { public: void send(const char* to, const char* content) { /* ... */ } }; // 2. 通过组合使用它们而不是继承 class OrderProcessor { private: // 组合把需要的功能组合进来 Logger logger_; Database db_; EmailSender email_; public: void process(const Order order) { logger_.log(开始处理订单); // 复用日志功能 db_.save(order data...); // 复用数据库功能 email_.send(usertest.com, 订单已处理); // 复用邮件功能 logger_.log(订单处理完成); // 复用日志功能 } };实际项目中怎么用不要为了复用而继承比如订单处理器继承自日志器是不对的把功能封装成小类然后通过组合使用四、总结实际项目中最常用的方法速查表表格目标最常用方法适用场景复杂度可维护性拆分函数所有场景⭐ 低可维护性尽早返回减少嵌套⭐ 低可维护性用枚举代替魔法数字固定类型⭐ 低可扩展性函数指针/回调变化的行为⭐⭐ 中可扩展性虚函数继承插件系统、UI⭐⭐ 中可扩展性模板通用算法⭐⭐⭐ 高可复用性封装独立模块日志、网络、文件⭐⭐ 中可复用性参数化避免硬编码⭐ 低可复用性组合功能复用⭐⭐ 中五、一个完整的实际项目示例假设你要写一个数据导入工具支持从不同格式CSV、JSON、XML导入数据。// 完整的示例展示三者的结合 // 1. 可复用性封装成独立模块日志模块 class Logger { public: void info(const char* msg) { printf([INFO] %s\n, msg); } void error(const char* msg) { printf([ERROR] %s\n, msg); } }; // 2. 可扩展性定义接口支持不同格式 class IDataReader { public: virtual ~IDataReader() {} virtual bool open(const char* filename) 0; virtual bool readRow(std::string* columns, int colCount) 0; virtual void close() 0; }; // CSV 读取器 class CsvReader : public IDataReader { private: FILE* file_; public: bool open(const char* filename) { file_ fopen(filename, r); return file_ ! NULL; } bool readRow(std::string* columns, int colCount) { // 读取一行按逗号分割 // ... } void close() { if (file_) fclose(file_); } }; // JSON 读取器 class JsonReader : public IDataReader { // 类似的实现... }; // 3. 可维护性主函数简洁明了 class DataImporter { private: Logger logger_; // 日志可复用 IDataReader* reader_; // 读取器可扩展 public: void setReader(IDataReader* reader) { reader_ reader; // 可以在运行时切换 } bool import(const char* filename) { logger_.info(开始导入数据); // 尽早返回可维护性 if (!reader_-open(filename)) { logger_.error(无法打开文件); return false; } // 读取数据 std::string columns[10]; int rowCount 0; while (reader_-readRow(columns, 10)) { // 处理每一行... rowCount; } reader_-close(); logger_.info(导入完成共处理); // 用 char 数组拼接数字 char countMsg[50]; sprintf(countMsg, 导入完成共处理 %d 行, rowCount); logger_.info(countMsg); return true; } };这个例子同时体现了表格特性体现在哪里可维护性函数简短、尽早返回、日志清晰可扩展性可以加 XML、Excel 等新格式只需要实现IDataReader接口可复用性Logger模块可以放在任何项目里使用最后实际项目中的黄金法则先写能跑的代码不要一开始就想太多重构时再考虑这些看到代码重复了就抽函数看到 if-else 多了就用 map不要过度设计如果只有 2-3 种类型用 if-else 完全没问题当类型超过 5 个时再考虑用多态或 map记住好的代码是改出来的不是写出来的。