CPP-Summit-2025 学习:C++语言在Xiaomi Vela中的应用 2
"毛线团"是一个形象的比喻,描述的是软件工程中的高耦合、低内聚状态:毛线团=高耦合+低内聚+隐式依赖+循环引用\text{毛线团} = \text{高耦合} + \text{低内聚} + \text{隐式依赖} + \text{循环引用}毛线团=高耦合+低内聚+隐式依赖+循环引用理想模块=低耦合+高内聚+显式依赖+单向依赖\text{理想模块} = \text{低耦合} + \text{高内聚}
C++ 改进模块化设计:避免代码变成混乱的毛线团
一、什么是"毛线团"
"毛线团"是一个形象的比喻,描述的是软件工程中的高耦合、低内聚状态:
毛线团=高耦合+低内聚+隐式依赖+循环引用\text{毛线团} = \text{高耦合} + \text{低内聚} + \text{隐式依赖} + \text{循环引用}毛线团=高耦合+低内聚+隐式依赖+循环引用
理想模块=低耦合+高内聚+显式依赖+单向依赖\text{理想模块} = \text{低耦合} + \text{高内聚} + \text{显式依赖} + \text{单向依赖}理想模块=低耦合+高内聚+显式依赖+单向依赖
两种状态的复杂度增长规律截然不同。设系统有 nnn 个模块:
毛线团:连接数=O(n2)(任意模块互连)\text{毛线团:连接数} = O(n^2) \quad \text{(任意模块互连)}毛线团:连接数=O(n2)(任意模块互连)
良好设计:连接数=O(n)(每个模块只依赖少数邻居)\text{良好设计:连接数} = O(n) \quad \text{(每个模块只依赖少数邻居)}良好设计:连接数=O(n)(每个模块只依赖少数邻居)
二、常见误区
误区1:大力出奇迹——God Class(上帝类)
"刚猛风格"的典型表现:一个类承担所有职责,能解决问题就行。
// ✗ 典型的"上帝类":什么都做,什么都知道
// 这种类在 IoT 项目中极为常见,往往从"临时方案"演变而来
class SystemManager {
public:
// ---- 音频相关 ----
void initAudio();
void playSound(const char* file);
void setVolume(int vol);
int getVolume();
void stopAudio();
// ---- 网络相关 ----
void connectWifi(const char* ssid, const char* pwd);
void disconnectWifi();
bool isWifiConnected();
int sendHttpRequest(const char* url, const char* body);
// ---- 显示相关 ----
void initDisplay();
void drawText(int x, int y, const char* text);
void drawBitmap(int x, int y, const uint8_t* bmp);
void refreshScreen();
// ---- 传感器相关 ----
float readTemperature();
float readHumidity();
int readADC(uint8_t channel);
// ---- 存储相关 ----
bool saveConfig(const char* key, const char* value);
std::string loadConfig(const char* key);
bool deleteFile(const char* path);
// ---- 蓝牙相关 ----
void startBLEScan();
void stopBLEScan();
bool connectBLE(const char* addr);
// ---- 业务逻辑相关 ----
void handleUserInput(int keyCode);
void updateUI();
void syncToCloud();
void runAlgorithm();
// 内部状态:所有模块的数据混在一起
private:
int volume_ = 50;
bool wifiConn_ = false;
bool bleConn_ = false;
float temperature_ = 0.0f;
uint8_t* screenBuf_ = nullptr;
// ... 几十个成员变量
};
// 问题量化:
// 假设这个类有 50 个方法,修改任何一个都可能影响其他 49 个
// 每次改动的影响面 = O(N),N = 方法数
// 测试难度:无法单独测试某个功能,必须初始化整个 SystemManager
误区2:飞线——全局变量和跨层直接访问
// ✗ 典型的"飞线":模块之间不通过接口,直接访问对方内部
// audio.cpp 中定义的全局变量
int g_audioVolume = 50;
bool g_audioPlaying = false;
uint8_t* g_audioBuffer = nullptr;
// network.cpp 直接飞线访问 audio 的内部状态(毫无关系的两个模块!)
extern int g_audioVolume; // 飞线!
extern bool g_audioPlaying; // 飞线!
void NetworkManager::onDataReceived(const uint8_t* data, size_t len) {
// 网络模块直接修改音频状态——这是一种严重的耦合
if (isCloudCommand(data, len)) {
g_audioVolume = parseVolumeCommand(data); // 网络直接改音量
g_audioPlaying = true; // 网络直接控制播放
}
}
// display.cpp 也来飞线
extern uint8_t* g_audioBuffer; // 飞线!显示模块读音频缓冲区?
void DisplayManager::renderWaveform() {
// 显示模块直接读音频内部缓冲区——极度危险
// 线程安全?生命周期?完全不考虑
drawWave(g_audioBuffer, 1024);
}
飞线的危害:
飞线数∝耦合度⇒改动一处,崩溃四方\text{飞线数} \propto \text{耦合度} \Rightarrow \text{改动一处,崩溃四方}飞线数∝耦合度⇒改动一处,崩溃四方
误区3:循环依赖——死结
// ✗ 循环依赖:A 依赖 B,B 又依赖 A
// 这是"毛线团"最难解开的结
// audio_player.h
#include "ui_controller.h" // AudioPlayer 依赖 UIController(更新进度条)
class AudioPlayer {
public:
void play(const char* file);
void onPlaybackProgress(float progress) {
ui_.updateProgressBar(progress); // 调用 UI
}
private:
UIController& ui_; // 直接持有 UI 引用
};
// ui_controller.h
#include "audio_player.h" // UIController 依赖 AudioPlayer(响应按钮)
class UIController {
public:
void onPlayButtonClicked() {
player_.play("music.mp3"); // 调用 AudioPlayer
}
private:
AudioPlayer& player_; // 直接持有 AudioPlayer 引用
};
// 编译结果:循环 include,两个类互相不完整,编译失败
// 即使用前向声明绕过编译,逻辑上仍是死结:
// AudioPlayer 必须在 UIController 之前初始化
// UIController 必须在 AudioPlayer 之前初始化
// → 无解
误区4:日复一日的累积——补丁叠补丁
// ✗ 需求不断增加,每次都在原有代码上"打补丁"
// 最终形成无法理解的"考古代码"
class BLEManager {
public:
// 第1个月:基础扫描
void startScan();
void stopScan();
// 第2个月:需要过滤,加参数(破坏接口)
void startScan(const char* filterName); // 重载,还好
// 第3个月:需要超时,继续加(接口越来越臃肿)
void startScan(const char* filterName, int timeoutMs);
// 第4个月:需要 RSSI 过滤
void startScan(const char* filterName, int timeoutMs, int minRssi);
// 第5个月:有人需要不过滤名字但过滤 RSSI,于是:
void startScanByRssi(int minRssi); // 新函数!接口爆炸
// 第6个月:需要回调而不是轮询
void startScanWithCallback(ScanCallback cb, int timeoutMs, int minRssi);
// 第7个月:需要连续扫描
void startContinuousScan(ScanCallback cb, int intervalMs);
// ... 一年后:18个 startScan 变体,没有人知道该用哪个
// 每次修改都怕影响其他变体,所以继续"安全地"新增
// 业务逻辑也混进来了(第3个月的"临时方案")
void connectIfFound(const char* targetName) {
// 扫描 + 连接 + 重试 + UI更新,全在这个方法里
// 200行,无人敢改
}
private:
// 标志位炼狱:每个 flag 对应某个历史需求
bool scanning_ = false;
bool continuousScan_ = false;
bool hasNameFilter_ = false;
bool hasRssiFilter_ = false;
bool callbackMode_ = false;
bool autoConnect_ = false;
// ... 又是十几个 bool
};
三、解决方案:模块化设计的核心原则
3.1 单一职责原则(SRP)——拆开上帝类
// ✓ 将上帝类按职责拆分为独立模块
// 每个类只做一件事,只有一个"变化的理由"
// ---- 音频模块(只管音频)----
class AudioPlayer {
public:
enum class State { IDLE, PLAYING, PAUSED };
bool open(const char* filePath);
void play();
void pause();
void stop();
void seekTo(int64_t posMs);
void setVolume(int vol); // 0~100
int getVolume() const;
int64_t getDurationMs() const;
int64_t getPositionMs() const;
State getState() const;
private:
// 只有音频相关的状态
int volume_ = 50;
State state_ = State::IDLE;
int64_t posMs_ = 0;
int64_t durationMs_ = 0;
// 解码器、硬件句柄等音频专属资源
};
// ---- 网络模块(只管网络)----
class WifiManager {
public:
struct Config {
char ssid[64];
char password[64];
bool autoReconnect = true;
};
bool connect(const Config& cfg);
void disconnect();
bool isConnected() const;
int getRssi() const;
uint32_t getLocalIP() const;
private:
bool connected_ = false;
int rssi_ = -100;
uint32_t ip_ = 0;
};
// ---- 显示模块(只管显示)----
class DisplayDriver {
public:
void clear(uint16_t color = 0x0000);
void drawText(int x, int y, const char* text, uint16_t color);
void drawRect(int x, int y, int w, int h, uint16_t color);
void flush(); // 将缓冲区刷新到屏幕
private:
uint16_t frameBuf_[240 * 240] = {}; // 只有显示相关数据
};
// 量化收益:
// 拆分前:1个上帝类,50个方法,改动影响面 O(50)
// 拆分后:5个模块,每个约10个方法,改动影响面 O(10)
// 影响面缩减 = 1 - 1/5 = 80%
3.2 依赖倒置原则(DIP)——用接口斩断飞线
核心思想:高层模块不依赖低层模块的具体实现,双方都依赖抽象接口。
// ✓ 用纯虚接口替代直接依赖
// ---- 定义抽象接口(稳定,不经常变动)----
// 音频控制接口(抽象)
class IAudioController {
public:
virtual ~IAudioController() = default;
virtual void setVolume(int vol) = 0;
virtual int getVolume() const = 0;
virtual void play(const char* url) = 0;
virtual void stop() = 0;
};
// UI 通知接口(抽象)
class IUINotifier {
public:
virtual ~IUINotifier() = default;
virtual void onPlaybackProgress(float progress) = 0; // 0.0~1.0
virtual void onPlaybackState(bool isPlaying) = 0;
virtual void onVolumeChanged(int vol) = 0;
};
// ---- 具体实现依赖接口,而非具体类 ----
// AudioPlayer 依赖 IUINotifier 接口,不知道 UIController 的存在
class AudioPlayer : public IAudioController {
public:
// 通过接口注入 UI 通知器(依赖注入,见下文)
explicit AudioPlayer(IUINotifier* notifier)
: notifier_(notifier) {}
void setVolume(int vol) override {
volume_ = std::clamp(vol, 0, 100);
if (notifier_) notifier_->onVolumeChanged(volume_); // 通过接口通知
}
int getVolume() const override { return volume_; }
void play(const char* url) override;
void stop() override;
private:
IUINotifier* notifier_; // 持有抽象接口,不持有具体类
int volume_ = 50;
};
// UIController 依赖 IAudioController 接口,不知道 AudioPlayer 的存在
class UIController : public IUINotifier {
public:
explicit UIController(IAudioController* audio)
: audio_(audio) {}
// 实现 IUINotifier(被 AudioPlayer 回调)
void onPlaybackProgress(float p) override {
display_.drawProgressBar(p); // 只更新 UI,不碰音频内部
}
void onPlaybackState(bool playing) override {
display_.setPlayIcon(playing);
}
void onVolumeChanged(int vol) override {
display_.drawVolumeBar(vol);
}
// UI 事件处理
void onPlayButtonClicked() {
audio_->play("current_track.mp3"); // 通过接口调用,不知道具体实现
}
void onVolumeUpClicked() {
audio_->setVolume(audio_->getVolume() + 5);
}
private:
IAudioController* audio_; // 持有接口,不持有具体类
DisplayDriver display_;
};
// ---- 组装:只在最顶层(main/App)知道具体类型 ----
void appInit() {
// 1. 创建 UIController(还没有 AudioPlayer,先传 nullptr)
UIController ui(nullptr);
// 2. 创建 AudioPlayer,注入 UI 通知接口
AudioPlayer audio(&ui);
// 3. 将 AudioPlayer 注入 UIController
// (实际项目中用依赖注入容器或 App 类统一管理)
ui.setAudioController(&audio);
// 此时:
// AudioPlayer 只知道 IUINotifier,不知道 UIController
// UIController 只知道 IAudioController,不知道 AudioPlayer
// 循环依赖被彻底消除!
}
依赖关系的变化:
之前(循环):AudioPlayer⇆UIController\text{之前(循环):} \text{AudioPlayer} \leftrightarrows \text{UIController}之前(循环):AudioPlayer⇆UIController
之后(有向无环):AudioPlayer→IUINotifier←UIController\text{之后(有向无环):} \text{AudioPlayer} \rightarrow \text{IUINotifier} \leftarrow \text{UIController}之后(有向无环):AudioPlayer→IUINotifier←UIController
AudioPlayer←IAudioController→UIController\text{AudioPlayer} \leftarrow \text{IAudioController} \rightarrow \text{UIController}AudioPlayer←IAudioController→UIController
3.3 依赖注入(DI)——显式声明依赖
// ✓ 依赖注入:把依赖关系从隐式变为显式
// ✗ 反面:在内部偷偷创建依赖(隐式依赖,难以测试)
class AudioPlayer {
public:
AudioPlayer() {
// 内部偷偷创建 Logger 和 HardwareCodec——无法替换!
logger_ = new Logger("/data/audio.log"); // 写死路径
codec_ = new HardwareCodec(); // 只能用硬件编解码
}
private:
Logger* logger_;
HardwareCodec* codec_;
};
// ✓ 构造函数注入:依赖从外部传入,可替换、可测试
class AudioPlayer {
public:
// 依赖通过构造函数显式传入
// 调用者一看构造函数就知道 AudioPlayer 需要什么
AudioPlayer(ILogger* logger,
ICodec* codec,
IHardware* hw)
: logger_(logger)
, codec_(codec)
, hw_(hw) {}
void play(const char* url) {
logger_->log(LOG_INFO, "playing: %s", url); // 通过接口使用
auto frame = codec_->decode(url); // 解码
hw_->output(frame); // 输出到硬件
}
private:
ILogger* logger_; // 接口,不是具体类
ICodec* codec_;
IHardware* hw_;
};
// 生产环境:注入真实实现
AudioPlayer prodPlayer(
new FileLogger("/data/audio.log"),
new HardwareH264Codec(),
new DACHardware()
);
// 单元测试:注入 Mock 实现(无需真实硬件!)
class MockCodec : public ICodec {
public:
AudioFrame decode(const char* url) override {
// 返回固定的测试音频帧,不读真实文件
return AudioFrame::silence(1024);
}
int lastDecodeCount = 0; // 可以记录调用次数,用于断言
};
class MockHardware : public IHardware {
public:
void output(const AudioFrame& f) override {
outputFrames.push_back(f); // 存下来供断言使用
}
std::vector<AudioFrame> outputFrames;
};
void testAudioPlayer() {
MockCodec mockCodec;
MockHardware mockHW;
NullLogger nullLogger; // 测试时不写日志
AudioPlayer player(&nullLogger, &mockCodec, &mockHW);
player.play("test.mp3");
// 断言:play 后应该有一帧输出
assert(mockHW.outputFrames.size() == 1);
}
3.4 观察者模式——解耦事件通知
替代"飞线通知"的标准方案:
// ✓ 观察者模式:事件发布方不知道谁在监听
// 通用事件总线(轻量级,适合 IoT)
template<typename EventType>
class EventBus {
public:
using Listener = std::function<void(const EventType&)>;
// 订阅事件(返回 token,用于取消订阅)
int subscribe(Listener listener) {
int token = nextToken_++;
listeners_[token] = std::move(listener);
return token;
}
// 取消订阅
void unsubscribe(int token) {
listeners_.erase(token);
}
// 发布事件(通知所有订阅者)
void publish(const EventType& event) {
// 拷贝一份,防止回调中修改 listeners_
auto snapshot = listeners_;
for (auto& [token, listener] : snapshot) {
listener(event);
}
}
private:
std::unordered_map<int, Listener> listeners_;
int nextToken_ = 0;
};
// 定义事件类型(简单的数据结构)
struct VolumeChangedEvent {
int oldVolume;
int newVolume;
};
struct PlaybackStateEvent {
enum class State { PLAYING, PAUSED, STOPPED };
State state;
float progress; // 0.0~1.0
};
struct NetworkStateEvent {
bool connected;
int rssi;
};
// ---- 使用事件总线解耦模块 ----
// 全局事件总线(或通过 IoC 容器注入)
EventBus<VolumeChangedEvent> g_volumeBus;
EventBus<PlaybackStateEvent> g_playbackBus;
EventBus<NetworkStateEvent> g_networkBus;
// AudioPlayer:只负责发布事件,不知道谁在听
class AudioPlayer {
public:
void setVolume(int vol) {
int old = volume_;
volume_ = std::clamp(vol, 0, 100);
// 发布事件,不知道谁会响应
g_volumeBus.publish({old, volume_});
hw_setVolume(volume_);
}
void onPlayProgress(float progress) {
g_playbackBus.publish({PlaybackStateEvent::State::PLAYING, progress});
}
private:
int volume_ = 50;
};
// UIController:订阅感兴趣的事件
class UIController {
public:
UIController() {
// 订阅音量变化事件
volumeToken_ = g_volumeBus.subscribe([this](const VolumeChangedEvent& e) {
display_.drawVolumeBar(e.newVolume);
VELA_LOGI("UI", "volume: %d→%d", e.oldVolume, e.newVolume);
});
// 订阅播放状态事件
playbackToken_ = g_playbackBus.subscribe([this](const PlaybackStateEvent& e) {
display_.updateProgressBar(e.progress);
display_.setPlayIcon(e.state == PlaybackStateEvent::State::PLAYING);
});
}
~UIController() {
// 析构时取消订阅,防止悬空回调
g_volumeBus.unsubscribe(volumeToken_);
g_playbackBus.unsubscribe(playbackToken_);
}
private:
int volumeToken_;
int playbackToken_;
DisplayDriver display_;
};
// 网络模块也可以订阅事件(例如:网络恢复时自动继续播放)
class CloudSyncService {
public:
CloudSyncService() {
networkToken_ = g_networkBus.subscribe([this](const NetworkStateEvent& e) {
if (e.connected) {
startSync(); // 网络恢复,自动同步
}
});
}
~CloudSyncService() { g_networkBus.unsubscribe(networkToken_); }
private:
void startSync();
int networkToken_;
};
// 模块间关系:
// AudioPlayer ──publish──→ g_volumeBus ←──subscribe── UIController
// ←──subscribe── VolumeLogger
// ←──subscribe── 任意新模块(开放扩展)
//
// 添加新订阅者,无需修改 AudioPlayer!满足开闭原则(OCP)
3.5 参数对象模式——解决接口爆炸
针对上文 BLEManager 的 18 个 startScan 变体问题:
// ✓ 用参数对象(Parameter Object)统一接口
// 扫描参数对象:集中所有可配置项,用默认值表达"可选"
struct BLEScanConfig {
// 过滤条件(默认不过滤)
const char* nameFilter = nullptr; // nullptr = 不过滤名称
int minRssi = -127; // -127 = 不过滤信号强度
const char* uuidFilter = nullptr; // nullptr = 不过滤 UUID
// 扫描行为
int timeoutMs = 10000; // 默认 10 秒超时
bool continuous = false; // 默认单次扫描
int intervalMs = 1000; // continuous=true 时的间隔
// 结果处理
std::function<void(const BLEDevice&)> onFound; // 发现设备回调
std::function<void()> onTimeout; // 超时回调
};
// 接口统一为一个,通过参数对象表达所有变体
class BLEManager {
public:
// 只有一个 startScan!
bool startScan(const BLEScanConfig& config = {});
void stopScan();
bool isScanning() const;
// 连接也同理
struct ConnectConfig {
const char* address;
int timeoutMs = 5000;
int retryCount = 3;
bool autoReconn = true;
std::function<void(bool success)> onResult;
};
bool connect(const ConnectConfig& config);
};
// 使用:非常灵活,但接口只有一个
void scanExample() {
BLEManager ble;
// 场景1:最简单的扫描(全用默认值)
ble.startScan();
// 场景2:过滤名称
ble.startScan({
.nameFilter = "Xiaomi_Watch",
.timeoutMs = 5000,
.onFound = [](const BLEDevice& dev) {
VELA_LOGI("BLE", "found: %s", dev.name);
}
});
// 场景3:持续扫描 + RSSI 过滤(原来需要一个新函数,现在直接配置)
ble.startScan({
.minRssi = -70, // 只要强信号
.continuous = true,
.intervalMs = 500,
.onFound = [](const BLEDevice& dev) {
connectIfGoodDevice(dev);
}
});
}
3.6 层次化架构——斩断跨层飞线
// ✓ 明确分层,每层只能调用下层接口,绝不跨层调用
// 层次定义(从上到下):
// ┌─────────────────────────────────┐
// │ Layer 4: 应用层(业务逻辑) │ 只依赖 Layer 3
// ├─────────────────────────────────┤
// │ Layer 3: 服务层(跨模块协作) │ 只依赖 Layer 2
// ├─────────────────────────────────┤
// │ Layer 2: 功能层(独立功能模块) │ 只依赖 Layer 1
// ├─────────────────────────────────┤
// │ Layer 1: 基础层(硬件抽象/OS) │ 只依赖硬件接口
// └─────────────────────────────────┘
// Layer 1:基础层(HAL,硬件无关接口)
namespace hal {
class IGPIODriver {
public:
virtual void setOutput(int pin, bool high) = 0;
virtual bool getInput(int pin) const = 0;
virtual void setInterrupt(int pin,
std::function<void()> cb) = 0;
};
class II2CDriver {
public:
virtual bool write(uint8_t addr, const uint8_t* data, size_t len) = 0;
virtual bool read (uint8_t addr, uint8_t* buf, size_t len) = 0;
};
} // namespace hal
// Layer 2:功能层(独立功能模块,只依赖 HAL 接口)
namespace feature {
class TemperatureSensor {
public:
explicit TemperatureSensor(hal::II2CDriver& i2c) : i2c_(i2c) {}
float readCelsius() {
uint8_t buf[2];
i2c_.read(SENSOR_ADDR, buf, 2); // 只依赖 HAL,不知道具体硬件
return ((buf[0] << 8 | buf[1]) * 0.0625f);
}
private:
hal::II2CDriver& i2c_;
static constexpr uint8_t SENSOR_ADDR = 0x48;
};
class LEDIndicator {
public:
explicit LEDIndicator(hal::IGPIODriver& gpio, int pin)
: gpio_(gpio), pin_(pin) {}
void setColor(bool r, bool g, bool b) {
gpio_.setOutput(pin_ + 0, r);
gpio_.setOutput(pin_ + 1, g);
gpio_.setOutput(pin_ + 2, b);
}
private:
hal::IGPIODriver& gpio_;
int pin_;
};
} // namespace feature
// Layer 3:服务层(协调多个 feature 模块,实现业务规则)
namespace service {
class EnvironmentMonitor {
public:
EnvironmentMonitor(feature::TemperatureSensor& temp,
feature::LEDIndicator& led)
: temp_(temp), led_(led) {}
// 业务规则:温度超过阈值时亮红灯
void update() {
float t = temp_.readCelsius();
if (t > HIGH_TEMP_THRESHOLD) {
led_.setColor(true, false, false); // 红灯警告
publishAlert(t);
} else {
led_.setColor(false, true, false); // 绿灯正常
}
}
private:
void publishAlert(float temp);
feature::TemperatureSensor& temp_;
feature::LEDIndicator& led_;
static constexpr float HIGH_TEMP_THRESHOLD = 40.0f;
};
} // namespace service
// Layer 4:应用层(最顶层,组装所有模块,处理用户交互)
namespace app {
class SmartHomeApp {
public:
SmartHomeApp()
: i2c_(createI2CDriver()) // 创建 HAL 实现
, gpio_(createGPIODriver())
, tempSensor_(*i2c_) // 注入 HAL
, led_(*gpio_, LED_PIN)
, envMonitor_(tempSensor_, led_) // 注入 feature
{}
void run() {
while (true) {
envMonitor_.update(); // 只调用服务层
sleepMs(1000);
}
}
private:
std::unique_ptr<hal::II2CDriver> i2c_;
std::unique_ptr<hal::IGPIODriver> gpio_;
feature::TemperatureSensor tempSensor_;
feature::LEDIndicator led_;
service::EnvironmentMonitor envMonitor_;
static constexpr int LED_PIN = 10;
};
} // namespace app
分层架构的依赖方向严格为单向:
app→service→feature→hal→hardware\text{app} \rightarrow \text{service} \rightarrow \text{feature} \rightarrow \text{hal} \rightarrow \text{hardware}app→service→feature→hal→hardware
任何反向依赖(如 feature 依赖 app)或跨层依赖(如 app 直接依赖 hal)都是飞线,必须通过接口或事件消除。
四、量化收益:好设计 vs 毛线团
设系统有 nnn 个模块,毛线团与良好设计的对比:
毛线团的修改代价:Cbad=O(n)(改一处,影响大部分模块)\text{毛线团的修改代价:} C_{\text{bad}} = O(n) \quad \text{(改一处,影响大部分模块)}毛线团的修改代价:Cbad=O(n)(改一处,影响大部分模块)
良好设计的修改代价:Cgood=O(1)(改一处,只影响该模块)\text{良好设计的修改代价:} C_{\text{good}} = O(1) \quad \text{(改一处,只影响该模块)}良好设计的修改代价:Cgood=O(1)(改一处,只影响该模块)
随着项目规模增长,两种代价的差距呈线性扩大:
ΔC=Cbad−Cgood=O(n)−O(1)≈O(n)\Delta C = C_{\text{bad}} - C_{\text{good}} = O(n) - O(1) \approx O(n)ΔC=Cbad−Cgood=O(n)−O(1)≈O(n)
当 n=50n = 50n=50(一个中型 IoT 项目的模块数),修改代价相差约 50 倍。这就是为什么"大力出奇迹"在项目初期看起来很快,但随着代码累积,速度会越来越慢,最终陷入"改哪里都怕崩,加功能比重写还难"的困境。
五、总结:模块化设计的核心准则
| 问题 | 根因 | 解决方案 | 关键原则 |
|---|---|---|---|
| 上帝类 | 职责未拆分 | 按职责拆分小类 | 单一职责(SRP) |
| 飞线依赖 | 直接访问内部状态 | 接口 + 依赖注入 | 依赖倒置(DIP) |
| 循环依赖 | 双向引用 | 接口 + 事件总线 | 依赖有向无环 |
| 接口爆炸 | 补丁式新增 | 参数对象 | 开闭原则(OCP) |
| 跨层飞线 | 无层次意识 | 严格分层 | 最小知识原则 |
优雅的代码=高内聚×低耦合×显式依赖×单向分层\text{优雅的代码} = \text{高内聚} \times \text{低耦合} \times \text{显式依赖} \times \text{单向分层}优雅的代码=高内聚×低耦合×显式依赖×单向分层
C++ 模块化设计常见误区深度解析
一、误区全景与危害量化
三种误区本质上是同一个问题的不同表现层次:
设计腐化=模块交叉引用⏟宏观:模块间+接口设计不合理⏟中观:模块边界+类内无关内容多⏟微观:类内部\text{设计腐化} = \underbrace{\text{模块交叉引用}}_{\text{宏观:模块间}} + \underbrace{\text{接口设计不合理}}_{\text{中观:模块边界}} + \underbrace{\text{类内无关内容多}}_{\text{微观:类内部}}设计腐化=宏观:模块间
模块交叉引用+中观:模块边界
接口设计不合理+微观:类内部
类内无关内容多
设系统有 nnn 个模块,每个模块平均有 kkk 个类,每个类平均有 mmm 个方法:
理想依赖数=O(n)实际交叉依赖数=O(n2)腐化倍数=n2n=n\text{理想依赖数} = O(n) \quad \text{实际交叉依赖数} = O(n^2) \quad \text{腐化倍数} = \frac{n^2}{n} = n理想依赖数=O(n)实际交叉依赖数=O(n2)腐化倍数=nn2=n
当 n=20n = 20n=20 时,交叉引用使依赖复杂度膨胀 20 倍。
二、误区一:模块交叉引用
2.1 什么是交叉引用
A 引用 B 的内容,B 同时引用 A 的内容,形成有向环:
A→B且B→A⇒环形依赖,无法解耦A \rightarrow B \quad \text{且} \quad B \rightarrow A \quad \Rightarrow \quad \text{环形依赖,无法解耦}A→B且B→A⇒环形依赖,无法解耦
2.2 典型场景:音频与UI的死结
// ✗ 最经典的交叉引用:两个模块互相持有对方的头文件和指针
// ---- audio_player.h ----
#include "ui_controller.h" // A 包含 B 的头文件
class AudioPlayer {
public:
explicit AudioPlayer(UIController* ui) : ui_(ui) {}
void play(const char* url) {
// 播放时直接调用 UI 更新进度条
ui_->updateProgressBar(0.0f);
ui_->setPlayButtonState(true);
doPlay(url);
}
void onProgress(float p) {
ui_->updateProgressBar(p); // 直接调用 UI 具体方法
}
void onPlayComplete() {
ui_->setPlayButtonState(false);
ui_->showCompletionToast(); // 甚至知道 UI 有 Toast!
}
private:
UIController* ui_; // 持有 UI 具体类型
void doPlay(const char* url);
};
// ---- ui_controller.h ----
#include "audio_player.h" // B 包含 A 的头文件(死结!)
class UIController {
public:
explicit UIController(AudioPlayer* player) : player_(player) {}
void onPlayButtonClicked() {
player_->play("track.mp3"); // UI 直接调用 AudioPlayer 具体方法
}
void onVolumeSliderChanged(int val) {
player_->setVolume(val); // UI 直接控制播放器
}
void updateProgressBar(float p) { /* 更新进度条 UI */ }
void setPlayButtonState(bool playing) { /* 更新按钮状态 */ }
void showCompletionToast() { /* 弹出完成提示 */ }
private:
AudioPlayer* player_; // 持有 AudioPlayer 具体类型
};
// 编译问题:
// audio_player.h 包含 ui_controller.h
// ui_controller.h 包含 audio_player.h
// → 循环 include,编译器报错:类型不完整
//
// 即使用前向声明临时绕过编译,逻辑死结仍在:
// AudioPlayer 必须在 UIController 之前构造(因为 UI 要传给 Audio)
// UIController 必须在 AudioPlayer 之前构造(因为 Audio 要传给 UI)
// → 鸡生蛋蛋生鸡,无解
2.3 更隐蔽的交叉引用:通过全局变量
// ✗ 更常见、更难发现的交叉引用:通过全局状态间接相互依赖
// network_manager.cpp
namespace GlobalState {
bool isNetworkConnected = false;
int networkRssi = -100;
char lastError[128] = {};
}
// audio_player.cpp
// "只是读一个全局变量而已"——但这是隐式依赖!
extern bool GlobalState::isNetworkConnected;
class AudioPlayer {
void play(const char* url) {
// 音频播放器隐式依赖网络状态
if (strncmp(url, "http", 4) == 0) {
if (!GlobalState::isNetworkConnected) { // 读网络全局状态
showError("网络未连接");
return;
}
}
doPlay(url);
}
};
// network_manager.cpp 反过来又读音频状态
extern bool GlobalState::isAudioBuffering; // 音频定义的全局变量
class NetworkManager {
void onDataReceived() {
// 网络管理器隐式依赖音频状态
if (GlobalState::isAudioBuffering) { // 读音频全局状态
prioritizeAudioStream(); // 音频在缓冲时优先处理音频流
}
}
};
// 这种交叉依赖的危害:
// 1. 编译时完全看不出来(没有 include 关系)
// 2. 运行时随机崩溃(多线程下的竞争条件)
// 3. 测试时必须同时初始化两个模块才能工作
// 4. 删除任何一个全局变量,另一个模块悄悄失效
2.4 解决方案:接口隔离 + 事件总线
// ✓ 解法核心:引入抽象层,让 A 和 B 都只依赖抽象,互相不知道对方
// ---- 第一步:定义双向通信的抽象接口 ----
// AudioPlayer 需要通知外界的事件(抽象接口,A 定义,不依赖任何模块)
class IAudioEventListener {
public:
virtual ~IAudioEventListener() = default;
virtual void onPlaybackStarted() = 0;
virtual void onPlaybackProgress(float ratio) = 0; // 0.0~1.0
virtual void onPlaybackCompleted() = 0;
virtual void onPlaybackError(const char* msg)= 0;
};
// UIController 需要控制音频的接口(抽象接口,B 定义,不依赖任何模块)
class IAudioControl {
public:
virtual ~IAudioControl() = default;
virtual void play(const char* url) = 0;
virtual void pause() = 0;
virtual void stop() = 0;
virtual void setVolume(int vol) = 0;
virtual int getVolume() const = 0;
};
// ---- 第二步:AudioPlayer 只依赖 IAudioEventListener(不依赖 UIController)----
// audio_player.h 不再 include ui_controller.h!
class AudioPlayer : public IAudioControl {
public:
// 注入监听器接口(不是具体的 UIController)
void setEventListener(IAudioEventListener* listener) {
listener_ = listener;
}
void play(const char* url) override {
doPlay(url);
if (listener_) listener_->onPlaybackStarted(); // 通过接口通知
}
void pause() override { doPause(); }
void stop() override { doStop(); }
void setVolume(int vol) override {
volume_ = std::clamp(vol, 0, 100);
}
int getVolume() const override { return volume_; }
private:
void doPlay(const char* url);
void doPause();
void doStop();
// 进度更新(由解码线程回调)
void onDecodeProgress(float p) {
if (listener_) listener_->onPlaybackProgress(p);
}
IAudioEventListener* listener_ = nullptr; // 只持有接口,不知道 UIController
int volume_ = 50;
};
// ---- 第三步:UIController 只依赖 IAudioControl(不依赖 AudioPlayer)----
// ui_controller.h 不再 include audio_player.h!
class UIController : public IAudioEventListener {
public:
// 注入音频控制接口(不是具体的 AudioPlayer)
explicit UIController(IAudioControl& audio) : audio_(audio) {}
// 实现 IAudioEventListener(被 AudioPlayer 回调)
void onPlaybackStarted() override {
playBtn_.setIcon(ICON_PAUSE); // 播放开始,按钮变暂停图标
progressBar_.setProgress(0.0f);
}
void onPlaybackProgress(float ratio) override {
progressBar_.setProgress(ratio); // 更新进度条
timeLabel_.setText(formatTime(ratio));
}
void onPlaybackCompleted() override {
playBtn_.setIcon(ICON_PLAY); // 播放完成,按钮变播放图标
toast_.show("播放完成");
}
void onPlaybackError(const char* msg) override {
errorDialog_.show(msg);
}
// UI 事件处理(通过接口控制音频)
void onPlayButtonClicked() {
audio_.play("current_track.mp3"); // 通过接口,不知道是哪个实现
}
void onVolumeChanged(int val) {
audio_.setVolume(val);
}
private:
IAudioControl& audio_; // 只持有接口,不知道 AudioPlayer
PlayButton playBtn_;
ProgressBar progressBar_;
TimeLabel timeLabel_;
Toast toast_;
ErrorDialog errorDialog_;
std::string formatTime(float ratio);
};
// ---- 第四步:在顶层(App)组装,只有这里知道具体类型 ----
class MusicApp {
public:
MusicApp()
: player_()
, ui_(player_) // UIController 得到 IAudioControl 接口
{
// AudioPlayer 得到 IAudioEventListener 接口
player_.setEventListener(&ui_);
// 此处是唯一知道二者具体类型的地方
}
private:
AudioPlayer player_; // 具体类型
UIController ui_; // 具体类型
};
// 依赖图变化:
// 之前(有环):AudioPlayer ←→ UIController
//
// 之后(无环):
// AudioPlayer ──implements──→ IAudioControl ←──uses── UIController
// AudioPlayer ──uses──→ IAudioEventListener ←──implements── UIController
// 两个接口由各自的使用方定义,没有任何环
三、误区二:接口设计不合理
3.1 因需求/实现设置接口——接口暴露内部细节
// ✗ 典型反例:接口设计完全跟着实现走,把内部细节暴露出去
class DataSyncService {
public:
// 以下接口都是"因实现设接口"的典型反面教材:
// 问题1:暴露内部数据结构(调用方被迫了解 sqlite3)
sqlite3* getDatabaseHandle(); // 直接把数据库句柄给出去!
// 调用方拿到 sqlite3*,可以随意执行 SQL,完全绕过服务层
// 问题2:暴露内部分步实现(强迫调用方按特定顺序调用)
bool openConnection(const char* host, int port);
bool authenticate(const char* token);
bool selectDatabase(const char* dbName);
bool beginTransaction();
void syncTableA(); // 必须在 beginTransaction 之后调用!
void syncTableB(); // 必须在 syncTableA 之后调用!
void syncTableC();
bool commitTransaction(); // 必须最后调用!
// 调用方必须记住这个顺序:
// open → auth → select → begin → A → B → C → commit
// 任何一步忘了或顺序错了,结果未定义
// 问题3:因某个特殊需求临时加的接口,逻辑不自洽
void syncOnlyIfWifiAndBatteryAbove50Percent();
// 为什么是 50%?为什么只有 WiFi?这是业务逻辑,不是服务接口!
// 问题4:返回类型暴露实现细节
std::vector<std::pair<std::string,
std::vector<uint8_t>>> getRawSyncQueue();
// 为什么是 vector<pair<string, vector<uint8_t>>>?
// 这是内部存储格式,不是外部关心的数据模型
// 问题5:布尔参数地狱(调用者无法理解参数含义)
void sync(bool force, bool async, bool retry, bool notify, bool log);
// 调用处:sync(true, false, true, false, true)
// 这是什么意思???
};
3.2 接口不合理的类型分类
// 类型一:接口粒度过细(调用方需要组合多步才能完成一件事)
// ✗ 过细
class FileManager {
public:
FILE* openFile(const char* path, const char* mode);
size_t readBytes(FILE* fp, void* buf, size_t n);
bool seekFile(FILE* fp, long offset, int whence);
void closeFile(FILE* fp);
// 读一个配置文件需要:open → read → seek → close,4次调用
// 调用方还得处理 FILE* 的生命周期,极易出错
};
// 类型二:接口粒度过粗(一个接口做太多事)
// ✗ 过粗
class AppManager {
public:
// 这个接口做了:校验权限 + 准备资源 + 启动进程 + 注册服务 + 更新UI
// 失败时不知道是哪一步出错
bool initializeAndStartAppWithPermissionCheckAndRegisterServices(
const char* appId, const char* userId, bool checkBattery);
};
// 类型三:接口语义模糊(名字和行为不一致)
// ✗ 语义模糊
class NetworkClient {
public:
// "send" 听起来是发送,实际上还会阻塞等待响应、更新内部缓存
// 调用方看名字以为是纯发送,被隐藏的行为坑到
bool send(const uint8_t* data, size_t len);
// "get" 听起来是纯读取,实际上会触发网络请求(有副作用)
std::string getDeviceStatus();
};
// 类型四:接口依赖上下文顺序(隐式状态机)
// ✗ 隐式状态机
class BLEConnection {
public:
void scan(); // 必须第一步
void connect(); // 必须第二步(scan 之后)
void discover(); // 必须第三步(connect 之后)
void subscribe(); // 必须第四步(discover 之后)
// 接口没有任何机制阻止调用方乱序调用
// 乱序时行为完全不确定,没有错误提示
};
3.3 解决方案:以使用者为中心设计接口
// ✓ 接口设计核心原则:
// "接口是给调用方用的,不是给实现方用的"
// 从调用方的角度出发,问:我想做什么?而不是:我怎么实现?
// ---- 原则1:最小惊讶原则(函数名即文档)----
class DataSyncService {
public:
// ✓ 接口语义清晰,一个接口做一件明确的事
// 调用方只需知道:我想同步数据
struct SyncOptions {
bool forceSync = false; // 是否强制同步(忽略增量检查)
bool notifyOnDone = true; // 完成后是否发通知
// 注意:没有 wifi/battery 这类业务逻辑,那是调用方的责任
};
// 同步结果:明确告知调用方发生了什么
struct SyncResult {
enum class Status { SUCCESS, NETWORK_ERROR, AUTH_FAILED, PARTIAL };
Status status;
int syncedCount; // 成功同步的条目数
int failedCount; // 失败的条目数
char errorMsg[128]; // 失败原因(status != SUCCESS 时有效)
};
// ✓ 高层接口:调用方只需一次调用,内部处理所有细节
SyncResult syncAll(const SyncOptions& opts = {});
// ✓ 如果确实需要部分同步,用枚举表达,而不是多个方法
enum class SyncTarget { ALL, USER_DATA, APP_CONFIG, MEDIA };
SyncResult syncTarget(SyncTarget target, const SyncOptions& opts = {});
// ✓ 异步版本:调用方不需要知道内部是线程还是协程
using SyncCallback = std::function<void(SyncResult)>;
void syncAllAsync(SyncCallback cb, const SyncOptions& opts = {});
};
// ---- 原则2:用强类型替代布尔参数地狱 ----
// ✗ 之前:sync(true, false, true, false, true) // 完全看不懂
// ✓ 之后:
struct SyncFlags {
bool force : 1; // 是否强制同步
bool async : 1; // 是否异步
bool retry : 1; // 是否重试
bool notify : 1; // 是否通知
bool logging : 1; // 是否记录日志
};
// 调用处清晰可读:
service.sync({.force=true, .retry=true, .logging=true});
// ---- 原则3:用状态机显式化隐式顺序 ----
// ✓ 让接口自身约束调用顺序,而不是靠文档和调用方自觉
// BLE 连接的正确设计:状态机 + 工厂方法
class BLEScanner {
public:
struct ScanResult {
std::string address;
std::string name;
int rssi;
};
using ScanCallback = std::function<void(std::vector<ScanResult>)>;
// 扫描返回扫描结果,只有从结果才能发起连接
void scan(int timeoutMs, ScanCallback cb);
};
// BLEConnector 只能从 ScanResult 构造,强制要求先扫描
class BLEConnector {
public:
// 构造函数需要 ScanResult,没有扫描结果就无法创建连接对象
// 这在类型系统层面强制了"先扫描再连接"的顺序
explicit BLEConnector(const BLEScanner::ScanResult& device);
struct ConnectionHandle {
// 只有 ConnectionHandle 才能 discover,强制了连接后才能发现
void discoverServices(std::function<void(ServiceList)> cb);
};
// connect 成功后返回 ConnectionHandle,失败返回 nullopt
// 没有 ConnectionHandle 就无法 discover,类型系统保证顺序
std::optional<ConnectionHandle> connect(int timeoutMs);
};
// 使用:顺序错误直接编译失败,而不是运行时崩溃
void connectBLE() {
BLEScanner scanner;
scanner.scan(5000, [](auto results) {
if (results.empty()) return;
BLEConnector connector(results[0]); // 必须有 ScanResult
auto handle = connector.connect(3000);
if (!handle) return; // 连接失败,handle 为空
handle->discoverServices([](auto services) { // 必须有 handle
// 处理服务发现结果
});
// 不可能在没有 connect 的情况下调用 discoverServices
// 因为 discoverServices 在 ConnectionHandle 上,
// 而 ConnectionHandle 只能通过成功的 connect() 获得
});
}
// ---- 原则4:RAII 管理资源,不暴露底层句柄 ----
// ✗ 暴露 sqlite3*(内部实现细节)
sqlite3* getDatabaseHandle();
// ✓ 用 RAII 对象封装,调用方看不到 sqlite3
class DatabaseQuery {
public:
// 使用方只需要知道:如何执行查询、如何读取结果
bool execute(const char* sql);
bool next(); // 移动到下一行
std::string getString(int col); // 读取字符串列
int getInt (int col); // 读取整数列
// 析构时自动释放 sqlite3_stmt*,调用方完全不知道 sqlite3 的存在
private:
sqlite3_stmt* stmt_ = nullptr; // 内部实现细节,对外不可见
};
class DataRepository {
public:
// 返回查询对象,而不是数据库句柄
DatabaseQuery query(const char* sql);
bool execute(const char* sql); // 无返回结果的语句
private:
sqlite3* db_ = nullptr; // 完全隐藏,外部无法触及
};
四、误区三:类内无关内容过多
4.1 一个类里塞入太多无关内容的表现
// ✗ 一个真实项目中常见的"大杂烩"类
// 这个类随时间增长,每次"快速添加"一个功能就塞进来
class SmartWatchApp {
public:
// ---- 健康监测(第1个月加的)----
void startHeartRateMonitor();
void stopHeartRateMonitor();
float getHeartRate();
void startSleepTracking();
void stopSleepTracking();
SleepReport getSleepReport();
// ---- 消息通知(第2个月加的)----
void showNotification(const char* title, const char* body);
void dismissNotification(int notifId);
void setNotificationVolume(int vol);
std::vector<Notification> getPendingNotifications();
// ---- 运动追踪(第3个月加的)----
void startWorkout(WorkoutType type);
void stopWorkout();
void pauseWorkout();
float getCaloriesBurned();
float getDistanceKm();
int getStepCount();
// ---- 支付功能(第4个月加的,完全不相关!)----
bool loadPaymentCard(const char* cardData);
bool triggerNFCPayment();
std::string getCardLastFourDigits();
bool isPaymentReady();
// ---- 表盘管理(第5个月加的)----
void setWatchFace(const char* faceId);
void updateWatchFaceData();
std::vector<std::string> getAvailableFaces();
// ---- 固件升级(第6个月加的)----
bool checkForUpdate();
void downloadUpdate(const char* url);
void applyUpdate();
float getUpdateProgress();
// ---- 调试工具(临时加的,忘了删)----
void dumpMemoryStats();
void simulateHeartRate(float bpm); // 测试用!!不该在生产代码
void injectFakeSteps(int n); // 测试用!!
// ---- 数据存储(每个功能都往这里加)----
void saveToFlash(const char* key, const void* data, size_t len);
void loadFromFlash(const char* key, void* buf, size_t len);
void clearAllData();
private:
// 一锅乱炖的成员变量:
float heartRate_ = 0.0f;
bool hrMonitoring_ = false;
SleepData sleepData_;
int notifVolume_ = 50;
std::vector<Notification> notifications_;
WorkoutData workoutData_;
bool workoutActive_ = false;
char paymentCardData_[256] = {};
bool nfcReady_ = false;
std::string currentFaceId_;
uint8_t* updateBuffer_ = nullptr;
float updateProgress_ = 0.0f;
// ... 还有30多个成员变量
};
内聚度量化。LCOM(缺乏内聚度量):
LCOM=1−∑f∣M(f)∣∣M∣×∣F∣\text{LCOM} = 1 - \frac{\sum_{f} |M(f)|}{|M| \times |F|}LCOM=1−∣M∣×∣F∣∑f∣M(f)∣
其中 ∣M∣|M|∣M∣ 是方法数,∣F∣|F|∣F∣ 是字段数,∣M(f)∣|M(f)|∣M(f)∣ 是访问字段 fff 的方法数。
LCOM→0⇒高内聚(好)LCOM→1⇒低内聚(坏)\text{LCOM} \rightarrow 0 \Rightarrow \text{高内聚(好)} \quad \text{LCOM} \rightarrow 1 \Rightarrow \text{低内聚(坏)}LCOM→0⇒高内聚(好)LCOM→1⇒低内聚(坏)
上面那个大杂烩类的 LCOM 接近 111,因为大量方法只访问自己那部分字段,彼此毫无关联。
4.2 解决方案:按凝聚度拆分类
// ✓ 按单一职责原则拆分:每个类只负责一个关注点
// ---- 1. 健康监测模块(只管健康数据)----
class HealthMonitor {
public:
// 心率相关(内聚:都访问 hrData_)
void startHeartRate();
void stopHeartRate();
float getHeartRate() const;
bool isHeartRateMonitoring() const;
// 睡眠相关(内聚:都访问 sleepData_)
void startSleepTracking();
void stopSleepTracking();
SleepReport getSleepReport() const;
// 注册数据变化回调(不依赖 UI 具体实现)
using HeartRateCallback = std::function<void(float bpm)>;
void setHeartRateCallback(HeartRateCallback cb);
private:
struct HeartRateData {
float currentBpm = 0.0f;
bool monitoring = false;
int64_t lastSampleUs = 0;
} hrData_;
SleepData sleepData_;
HeartRateCallback hrCallback_;
};
// ---- 2. 运动追踪模块(只管运动数据)----
class WorkoutTracker {
public:
enum class Type { RUNNING, WALKING, CYCLING, SWIMMING };
void start(Type type);
void stop();
void pause();
void resume();
bool isActive() const;
float getCalories() const;
float getDistanceKm() const;
int getStepCount() const;
int64_t getElapsedMs() const;
// 步数更新(由计步器硬件驱动调用)
void onStepDetected();
private:
struct WorkoutSession {
Type type = Type::WALKING;
bool active = false;
bool paused = false;
int steps = 0;
float distanceKm = 0.0f;
int64_t startTimeMs = 0;
int64_t pausedMs = 0;
} session_;
float calculateCalories() const;
};
// ---- 3. 通知管理模块(只管通知)----
class NotificationManager {
public:
struct Notification {
int id;
std::string title;
std::string body;
int64_t timestampMs;
bool dismissed = false;
};
int show(const char* title, const char* body); // 返回通知 ID
void dismiss(int notifId);
void dismissAll();
void setVolume(int vol);
int getVolume() const;
const std::vector<Notification>& getPending() const;
private:
std::vector<Notification> notifications_;
int volume_ = 50;
int nextId_ = 1;
};
// ---- 4. 支付模块(只管支付,且有独立安全边界)----
class PaymentManager {
public:
// 支付相关接口内聚性强(都和支付卡/NFC有关)
bool loadCard(const char* encryptedCardData); // 加密数据
bool triggerNFCPayment();
bool isReady() const;
std::string getMaskedNumber() const; // 返回 **** **** **** 1234
// 支付安全:只暴露必要信息,不暴露原始卡数据
bool isCardLoaded() const;
private:
// 支付数据应在安全存储中,这里只存句柄
uint32_t secureCardHandle_ = 0; // TEE 中的卡数据句柄
bool nfcReady_ = false;
// 明确不提供:getCardRawData(),任何暴露原始卡数据的接口
};
// ---- 5. 固件升级模块(只管OTA)----
class OTAManager {
public:
enum class State { IDLE, CHECKING, DOWNLOADING, READY, APPLYING, FAILED };
// 状态变化通知
using StateCallback = std::function<void(State, float progress)>;
void setStateCallback(StateCallback cb);
void checkForUpdate(); // 异步检查
void startDownload(); // 异步下载
void applyUpdate(); // 重启并应用
void cancelDownload();
State getState() const;
float getProgress() const; // 0.0~1.0
std::string getVersion() const; // 可用版本号
private:
State state_ = State::IDLE;
float progress_ = 0.0f;
std::string newVersion_;
uint8_t* downloadBuf_ = nullptr;
StateCallback stateCallback_;
};
// ---- 6. 表盘管理模块(只管表盘)----
class WatchFaceManager {
public:
void setFace(const char* faceId);
void updateData(); // 刷新表盘显示的数据(时间、步数等)
std::vector<std::string> getAvailable() const;
const char* getCurrent() const;
private:
std::string currentFaceId_;
std::vector<std::string> availableFaces_;
};
// ---- 7. 数据存储模块(统一存储,避免每个模块自己搞)----
class PersistentStorage {
public:
bool write(const char* key, const void* data, size_t len);
bool read (const char* key, void* buf, size_t bufLen, size_t* outLen);
bool remove(const char* key);
bool exists(const char* key) const;
void clearAll(); // 恢复出厂设置时调用
private:
// 内部用 LittleFS 或 KV 数据库实现
};
// ---- 8. 顶层 App:组装所有模块(只做组装,不加业务逻辑)----
class SmartWatchApp {
public:
SmartWatchApp()
: storage_()
, health_()
, workout_()
, notifications_()
, payment_()
, ota_()
, watchFace_()
{
// 连接模块间的必要依赖(通过回调或事件总线)
health_.setHeartRateCallback([this](float bpm) {
// 心率数据更新时,刷新表盘
watchFace_.updateData();
});
workout_.setStepCallback([this](int totalSteps) {
watchFace_.updateData();
});
}
// 顶层只暴露高层操作,不暴露子模块的细节
HealthMonitor& health() { return health_; }
WorkoutTracker& workout() { return workout_; }
NotificationManager& notifications(){ return notifications_; }
PaymentManager& payment() { return payment_; }
OTAManager& ota() { return ota_; }
WatchFaceManager& watchFace() { return watchFace_; }
private:
PersistentStorage storage_;
HealthMonitor health_;
WorkoutTracker workout_;
NotificationManager notifications_;
PaymentManager payment_;
OTAManager ota_;
WatchFaceManager watchFace_;
};
4.3 测试用代码混入生产代码的专项处理
// ✗ 测试用代码混在生产类里(上文 SmartWatchApp 中的反例)
class HealthMonitor {
public:
void simulateHeartRate(float bpm); // 测试用,不该在生产代码!
void injectFakeSteps(int n); // 测试用!
};
// ✓ 解法:用接口隔离,测试实现和生产实现完全分开
// 生产代码中:只有真实接口
class IHeartRateSensor {
public:
virtual ~IHeartRateSensor() = default;
virtual float readBpm() = 0;
virtual bool isOnline() = 0;
};
// 生产实现(在 hardware/ 目录)
class HardwareHeartRateSensor : public IHeartRateSensor {
public:
float readBpm() override {
return i2c_.readRegFloat(HR_SENSOR_ADDR, REG_BPM);
}
bool isOnline() override { return i2c_.probe(HR_SENSOR_ADDR); }
private:
I2CDriver& i2c_;
// ...
};
// 测试实现(在 test/ 目录,不进入生产构建)
// #if defined(VELA_TEST_BUILD) 或 CMake 中只在测试目标包含此文件
class MockHeartRateSensor : public IHeartRateSensor {
public:
// 测试专用:设置模拟数据
void setSimulatedBpm(float bpm) { simulatedBpm_ = bpm; }
void setOffline(bool offline) { offline_ = offline; }
// 记录调用次数,供 assert 使用
int readCount = 0;
float readBpm() override { ++readCount; return simulatedBpm_; }
bool isOnline() override { return !offline_; }
private:
float simulatedBpm_ = 75.0f;
bool offline_ = false;
};
// HealthMonitor:依赖接口,不知道是真实硬件还是 Mock
class HealthMonitor {
public:
explicit HealthMonitor(IHeartRateSensor& sensor)
: sensor_(sensor) {}
float getHeartRate() { return sensor_.readBpm(); } // 通过接口
private:
IHeartRateSensor& sensor_; // 接口,不是具体类
// 生产代码中没有任何 simulate 或 inject 方法!
};
// 测试代码:
void testHealthMonitor() {
MockHeartRateSensor mockSensor;
mockSensor.setSimulatedBpm(120.0f); // 模拟运动心率
HealthMonitor monitor(mockSensor);
float bpm = monitor.getHeartRate();
assert(bpm == 120.0f);
assert(mockSensor.readCount == 1); // 确认只读了一次
// 测试完全不依赖真实硬件!
}
五、三大误区的内在联系
三种误区并非独立存在,往往相互促进,形成恶性循环:
类内无关内容多
│
│ 大类变成"什么都能做"的工具箱
▼
其他模块直接依赖这个大类
│
│ 多个模块都依赖同一个大类
▼
模块交叉引用(A依赖大类,大类又依赖A)
│
│ 为了解决交叉引用,临时在接口上加方法
▼
接口设计不合理(接口随实现走,越来越乱)
│
│ 接口乱了,类的职责更模糊
▼
类内无关内容更多(循环加剧)
打破这个循环的切入点只有一个:从接口设计开始。
好的设计顺序:接口先行⇒明确边界⇒类内聚⇒模块无环\text{好的设计顺序:接口先行} \Rightarrow \text{明确边界} \Rightarrow \text{类内聚} \Rightarrow \text{模块无环}好的设计顺序:接口先行⇒明确边界⇒类内聚⇒模块无环
坏的设计顺序:实现先行⇒边界模糊⇒类膨胀⇒模块交叉\text{坏的设计顺序:实现先行} \Rightarrow \text{边界模糊} \Rightarrow \text{类膨胀} \Rightarrow \text{模块交叉}坏的设计顺序:实现先行⇒边界模糊⇒类膨胀⇒模块交叉
先想清楚"模块对外提供什么能力、需要什么依赖",用接口把边界固化,再去写实现。这就是所谓接口先于实现(Interface-First Design),也是从"大力出奇迹"走向"优雅精巧"的根本路径。
示例1:交叉引用——依赖外溢问题深度解析
一、从图片还原原始代码
根据图片内容,原始代码大致如下:
// ✗ 原始问题代码:WidgetCanvasContextModule 的构造函数
// 引入了两个"非核心"的外部模块依赖
class WidgetCanvasContextModule : public ModuleBase {
public:
// 构造函数接收了三个参数:
// jse_context_ref ctx → JS 引擎上下文(外部模块)
// WidgetCanvasContext2D* context → Canvas 2D 上下文(核心依赖,合理)
// DomEntity* canvas → DOM 实体对象(外部模块)
WidgetCanvasContextModule(
jse_context_ref ctx, // ← 外部模块:JS引擎
WidgetCanvasContext2D* context, // ← 核心依赖(合理)
DomEntity* canvas // ← 外部模块:DOM实体
)
: _ctx(ctx)
, _context(context)
, _canvas(canvas)
{
// 构造函数体:初始化操作
// 向外通知消息时使用 _ctx 和 _canvas
}
// ... 其他方法
void notifyCanvasUpdate() {
// 用 jse_context_ref 向 JS 引擎发消息
jse_context_call_function(_ctx, ...);
// 用 DomEntity 触发 DOM 事件
_canvas->dispatchEvent("update", ...);
}
private:
jse_context_ref _ctx; // JS引擎上下文
WidgetCanvasContext2D* _context; // Canvas核心对象
DomEntity* _canvas; // DOM实体
};
二、什么是"依赖外溢"
2.1 核心依赖 vs 非核心依赖
WidgetCanvasContextModule 的职责是什么?
→ 管理 Canvas 的 2D 绘图上下文
为此,它真正需要的核心依赖是什么?
→ WidgetCanvasContext2D*(绘图上下文本身)
那 jse_context_ref 和 DomEntity* 是什么?
→ 仅仅是为了"向外通知消息"而引入的
→ 是非核心需求带来的依赖
→ 这就是"依赖外溢"
依赖外溢的定义:
依赖外溢=模块实际依赖数−模块核心职责所需依赖数>0\text{依赖外溢} = \text{模块实际依赖数} - \text{模块核心职责所需依赖数} > 0依赖外溢=模块实际依赖数−模块核心职责所需依赖数>0
外溢量=∣{jse_context_ref, DomEntity}∣=2(本例)\text{外溢量} = |\{jse\_context\_ref,\ DomEntity\}| = 2 \quad \text{(本例)}外溢量=∣{jse_context_ref, DomEntity}∣=2(本例)
2.2 为什么两个依赖会变成网状依赖
直觉上看:只是多了两个参数,有什么大不了?
实际情况:每个外部依赖背后都有一棵依赖树
jse_context_ref
├── JSEngine
│ ├── GarbageCollector
│ ├── BytecodeCompiler
│ └── NativeBindingRegistry
│ ├── PlatformAPI
│ └── MemoryAllocator
└── JSRuntime
├── EventLoop
└── ModuleLoader
DomEntity
├── EventDispatcher
│ ├── EventQueue
│ └── ListenerRegistry
├── StyleManager
│ ├── CSSParser
│ └── LayoutEngine
└── RenderTree
├── CompositeLayer
└── DisplayList
WidgetCanvasContextModule 原本只需要:
WidgetCanvasContext2D
└── RenderBuffer(合理,这是 Canvas 核心)
引入两个"小依赖"后,实际传递依赖变成:
WidgetCanvasContextModule
├── WidgetCanvasContext2D(核心,合理)
├── JSEngine + JSRuntime + GC + Compiler + ...(外溢!)
└── EventDispatcher + StyleManager + RenderTree + ...(外溢!)
传递依赖的数量爆炸:
D传递=D直接+∑d∈D直接D传递(d)D_{\text{传递}} = D_{\text{直接}} + \sum_{d \in D_{\text{直接}}} D_{\text{传递}}(d)D传递=D直接+d∈D直接∑D传递(d)
D传递(Canvas)≈1+8+6=15(粗略估计)D_{\text{传递}}(\text{Canvas}) \approx 1 + 8 + 6 = 15 \quad \text{(粗略估计)}D传递(Canvas)≈1+8+6=15(粗略估计)
原本只有 D=1D = 1D=1 的干净依赖,因为两个非核心引入,变成了 D=15D = 15D=15 的网状依赖,复杂度膨胀 15 倍。
三、依赖外溢的完整危害展示
// 展示依赖外溢如何逐步把代码变成毛线团
// 第1个月:WidgetCanvasContextModule 正常工作
// 初始依赖链:Canvas → WidgetCanvasContext2D
// 第2个月:需要向 JS 层回调,工程师"快速"加入 jse_context_ref
// 依赖链:Canvas → WidgetCanvasContext2D
// → jse_context_ref → JSEngine → ...
// 第3个月:需要触发 DOM 事件,工程师"快速"加入 DomEntity
// 依赖链:Canvas → WidgetCanvasContext2D
// → jse_context_ref → JSEngine → ...
// → DomEntity → EventDispatcher → ...
// 第4个月:Canvas 需要响应屏幕旋转,工程师加入 ScreenManager
// Canvas 的头文件现在 include 了:
// canvas_context_2d.h
// jse_context.h ← JS引擎头文件(1200行)
// dom_entity.h ← DOM头文件(800行)
// screen_manager.h ← 屏幕管理(600行)
// 第5个月:其他模块需要使用 Canvas
// NetworkImageLoader 需要把网络图片绘制到 Canvas
class NetworkImageLoader {
// 为了创建 WidgetCanvasContextModule,
// NetworkImageLoader 被迫也依赖:
// jse_context_ref(它根本不需要!)
// DomEntity(它也不需要!)
void drawToCanvas(WidgetCanvasContextModule* canvas,
jse_context_ref ctx, // ← 被迫传入,毫无意义
DomEntity* dom) { // ← 被迫传入,毫无意义
// ...
}
};
// 第6个月:单元测试 Canvas
// 为了测试 Canvas 的绘图功能,必须:
// 1. 初始化完整的 JS 引擎(jse_context_ref)
// 2. 构造一个 DomEntity(需要完整的 DOM 树)
// 3. 才能创建 WidgetCanvasContextModule
// 原本只需要一个 RenderBuffer,现在需要整个前端运行时!
void testCanvasDrawRect() {
// ✗ 为了测试一个 drawRect,要初始化这些:
JSEngine jsEngine; // 几百毫秒启动时间
jse_context_ref ctx = jsEngine.createContext();
DOMTree domTree;
DomEntity* entity = domTree.createElement("canvas");
WidgetCanvasContext2D context;
// 终于可以创建被测对象了...
WidgetCanvasContextModule module(ctx, &context, entity);
module.drawRect(0, 0, 100, 100); // 测这一行,前面铺垫了10行
// 测试 Canvas 的绘图,却要依赖 JS 引擎,荒谬!
}
四、问题根源分析
// 追问:为什么工程师会这样设计?
// 通常是这样的思维过程:
// "Canvas 需要在绘图完成后通知 JS 层"
// → "通知 JS 层需要 jse_context_ref"
// → "那就把 jse_context_ref 传进来吧,简单直接"
// "Canvas 需要触发 DOM 的 onchange 事件"
// → "触发事件需要 DomEntity"
// → "那就把 DomEntity 也传进来吧"
// 这种思维模式的问题:
// 把"如何实现通知"(实现细节)混入了"谁负责通知"(职责划分)
// Canvas 的职责是绘图,不是管理JS回调和DOM事件
// "向外通知"这个需求本身没错,错在让 Canvas 自己持有通知所需的全部资源
// 正确的思维应该是:
// "Canvas 需要通知外界"
// → "通知是一种抽象能力,用接口表达"
// → "Canvas 持有通知接口,不持有具体的 JS 引擎或 DOM 对象"
职责边界原则:
Canvas 的职责=管理2D绘图状态+执行绘图命令\text{Canvas 的职责} = \text{管理2D绘图状态} + \text{执行绘图命令}Canvas 的职责=管理2D绘图状态+执行绘图命令
Canvas 不应该的职责=管理JS回调+触发DOM事件\text{Canvas 不应该的职责} = \text{管理JS回调} + \text{触发DOM事件}Canvas 不应该的职责=管理JS回调+触发DOM事件
违反此原则⇒依赖外溢⇒网状依赖\text{违反此原则} \Rightarrow \text{依赖外溢} \Rightarrow \text{网状依赖}违反此原则⇒依赖外溢⇒网状依赖
五、解决方案:三步消除依赖外溢
5.1 第一步:识别"向外通知"的本质
// "向外通知"是一种抽象行为,与通知的具体机制无关
// Canvas 只需要知道:我有事情要告诉外界
// 至于外界是 JS 引擎、DOM 系统、还是其他,Canvas 不关心
// 抽象出通知接口(Canvas 自己定义,不依赖任何外部模块)
class ICanvasEventNotifier {
public:
virtual ~ICanvasEventNotifier() = default;
// Canvas 关心的事件(从 Canvas 的视角定义,而非从 JS/DOM 的视角)
virtual void onCanvasUpdated() = 0;
virtual void onDrawCallCompleted(const char* operation) = 0;
virtual void onCanvasError(int code, const char* msg) = 0;
virtual void onFrameReady(const uint8_t* pixels,
int width, int height) = 0;
};
// 注意:这个接口完全不提 jse_context_ref 或 DomEntity
// Canvas 只说"我更新了",不关心谁在听、怎么处理
5.2 第二步:重构 Canvas 模块,只依赖核心 + 抽象接口
// ✓ 重构后的 WidgetCanvasContextModule
// 依赖关系变成:
// WidgetCanvasContext2D(核心绘图,必须)
// ICanvasEventNotifier(抽象通知,必须)
// 完全消除了 jse_context_ref 和 DomEntity 的直接依赖
class WidgetCanvasContextModule : public ModuleBase {
public:
// 构造函数:只接受核心依赖 + 抽象接口
WidgetCanvasContextModule(
WidgetCanvasContext2D* context, // 核心依赖:绘图上下文
ICanvasEventNotifier* notifier // 抽象接口:通知能力
)
: _context(context)
, _notifier(notifier)
{
// 不再需要 jse_context_ref 和 DomEntity!
}
// ---- 核心绘图 API ----
void clearRect(float x, float y, float w, float h) {
_context->clearRect(x, y, w, h);
// 绘图完成后通过接口通知,不知道通知的具体实现
if (_notifier) _notifier->onDrawCallCompleted("clearRect");
}
void fillRect(float x, float y, float w, float h) {
_context->fillRect(x, y, w, h);
if (_notifier) _notifier->onDrawCallCompleted("fillRect");
}
void drawImage(const ImageData& img, float x, float y) {
_context->drawImage(img, x, y);
if (_notifier) _notifier->onDrawCallCompleted("drawImage");
}
// 提交当前帧(渲染完一帧后通知外界)
void commit() {
auto pixels = _context->getPixelBuffer();
_context->flush();
// 通过接口通知帧就绪,不知道谁在处理这个帧
if (_notifier) {
_notifier->onFrameReady(
pixels.data,
pixels.width,
pixels.height
);
}
}
void setFillColor(uint32_t rgba) { _context->setFillColor(rgba); }
void setStrokeColor(uint32_t rgba){ _context->setStrokeColor(rgba); }
void setLineWidth(float w) { _context->setLineWidth(w); }
private:
WidgetCanvasContext2D* _context; // 只持有核心对象
ICanvasEventNotifier* _notifier; // 只持有抽象接口
// ✗ 以下两个成员彻底消失:
// jse_context_ref _ctx; ← 删除!
// DomEntity* _canvas; ← 删除!
};
5.3 第三步:在外部实现具体通知逻辑
// ✓ JS 引擎侧的通知实现(在 JS 桥接层,而非 Canvas 层)
// 这个类知道 jse_context_ref,但 Canvas 不知道它的存在
class JSCanvasNotifier : public ICanvasEventNotifier {
public:
// 构造时注入 JS 引擎上下文(这里持有 jse_context_ref 是合理的)
explicit JSCanvasNotifier(jse_context_ref ctx,
const char* callbackName)
: _ctx(ctx)
, _callbackName(callbackName)
{}
void onCanvasUpdated() override {
// 在这里才使用 jse_context_ref 调用 JS 回调
jse_value_t callback = jse_get_named_property(
_ctx, jse_get_global(_ctx), _callbackName);
if (jse_value_is_function(_ctx, callback)) {
jse_call_function(_ctx, callback,
jse_make_undefined(_ctx), 0, nullptr);
}
jse_free_value(_ctx, callback);
}
void onDrawCallCompleted(const char* operation) override {
// 可以记录性能数据,回调 JS 的 onDrawCall 钩子
jse_value_t args[1] = {
jse_create_string(_ctx, operation)
};
// ... 调用 JS 钩子
jse_free_value(_ctx, args[0]);
}
void onCanvasError(int code, const char* msg) override {
// 向 JS 抛出错误事件
jse_value_t errObj = jse_create_object(_ctx);
jse_set_property(_ctx, errObj, "code",
jse_create_number(_ctx, code));
jse_set_property(_ctx, errObj, "message",
jse_create_string(_ctx, msg));
// 触发 JS error 回调
// ...
jse_free_value(_ctx, errObj);
}
void onFrameReady(const uint8_t* pixels,
int width, int height) override {
// 将像素数据传回 JS(通过 ArrayBuffer)
jse_value_t buffer = jse_create_array_buffer(
_ctx, pixels, width * height * 4);
// 调用 JS 的 onframe 回调
// ...
jse_free_value(_ctx, buffer);
}
private:
jse_context_ref _ctx; // JS引擎上下文:在这里持有是合理的
const char* _callbackName;
};
// ✓ DOM 侧的通知实现(在 DOM 桥接层,而非 Canvas 层)
class DOMCanvasNotifier : public ICanvasEventNotifier {
public:
explicit DOMCanvasNotifier(DomEntity* canvasElement)
: _canvasElement(canvasElement)
{}
void onCanvasUpdated() override {
// 在这里才使用 DomEntity 触发 DOM 事件
DomEvent event("canvasupdate");
event.bubbles = true;
event.cancelable = false;
_canvasElement->dispatchEvent(event);
}
void onDrawCallCompleted(const char* operation) override {
// 可以触发 DOM 的 draw 事件
DomEvent event("draw");
event.detail = operation;
_canvasElement->dispatchEvent(event);
}
void onCanvasError(int code, const char* msg) override {
DomEvent errEvent("error");
errEvent.detail = msg;
_canvasElement->dispatchEvent(errEvent);
}
void onFrameReady(const uint8_t* pixels,
int w, int h) override {
// 通知 DOM 更新显示
_canvasElement->markDirty();
_canvasElement->scheduleRender();
}
private:
DomEntity* _canvasElement; // DomEntity:在这里持有是合理的
};
// ✓ 可以同时通知多个目标(组合模式)
class CompositeCanvasNotifier : public ICanvasEventNotifier {
public:
void addNotifier(ICanvasEventNotifier* n) {
_notifiers.push_back(n);
}
void onCanvasUpdated() override {
for (auto* n : _notifiers) n->onCanvasUpdated();
}
void onDrawCallCompleted(const char* op) override {
for (auto* n : _notifiers) n->onDrawCallCompleted(op);
}
void onCanvasError(int code, const char* msg) override {
for (auto* n : _notifiers) n->onCanvasError(code, msg);
}
void onFrameReady(const uint8_t* px, int w, int h) override {
for (auto* n : _notifiers) n->onFrameReady(px, w, h);
}
private:
std::vector<ICanvasEventNotifier*> _notifiers;
};
5.4 第四步:在组装层(顶层)连接所有模块
// ✓ 只有在最顶层,才把所有具体类型组装在一起
// Canvas 模块本身完全不知道 JS 引擎或 DOM 的存在
class CanvasWidget {
public:
CanvasWidget(jse_context_ref jsCtx, DomEntity* domEntity) {
// 创建核心绘图上下文(Canvas 的真正核心)
_context = std::make_unique<WidgetCanvasContext2D>(
width_, height_
);
// 创建具体通知实现(组装层知道具体类型,Canvas 不知道)
_jsNotifier = std::make_unique<JSCanvasNotifier>(
jsCtx, "onCanvasFrame"
);
_domNotifier = std::make_unique<DOMCanvasNotifier>(domEntity);
// 用组合通知器同时通知 JS 和 DOM
_compositeNotifier = std::make_unique<CompositeCanvasNotifier>();
_compositeNotifier->addNotifier(_jsNotifier.get());
_compositeNotifier->addNotifier(_domNotifier.get());
// 创建 Canvas 模块(只传入核心依赖 + 抽象接口)
// WidgetCanvasContextModule 完全不知道 jse_context_ref 的存在!
_canvasModule = std::make_unique<WidgetCanvasContextModule>(
_context.get(),
_compositeNotifier.get() // 只传接口
);
}
WidgetCanvasContextModule* getContext() {
return _canvasModule.get();
}
private:
int width_ = 300;
int height_ = 150;
std::unique_ptr<WidgetCanvasContext2D> _context;
std::unique_ptr<JSCanvasNotifier> _jsNotifier;
std::unique_ptr<DOMCanvasNotifier> _domNotifier;
std::unique_ptr<CompositeCanvasNotifier> _compositeNotifier;
std::unique_ptr<WidgetCanvasContextModule> _canvasModule;
};
六、重构前后的依赖对比
重构前(依赖外溢)
WidgetCanvasContextModule
├── WidgetCanvasContext2D ← 核心,合理
├── jse_context_ref ← 外溢!
│ ├── JSEngine
│ │ ├── GarbageCollector
│ │ ├── BytecodeCompiler
│ │ └── NativeBindingRegistry ── PlatformAPI
│ └── JSRuntime
│ ├── EventLoop
│ └── ModuleLoader
└── DomEntity ← 外溢!
├── EventDispatcher ── EventQueue
│ └── ListenerRegistry
├── StyleManager ── CSSParser
│ └── LayoutEngine
└── RenderTree ── CompositeLayer
直接依赖:3个
传递依赖:约15个
重构后(依赖收敛)
WidgetCanvasContextModule
├── WidgetCanvasContext2D ← 核心,合理
└── ICanvasEventNotifier ← 抽象接口,稳定
JSCanvasNotifier(实现 ICanvasEventNotifier)
└── jse_context_ref ← 在正确的位置持有
DOMCanvasNotifier(实现 ICanvasEventNotifier)
└── DomEntity ← 在正确的位置持有
WidgetCanvasContextModule 的直接依赖:2个
WidgetCanvasContextModule 的传递依赖:1个(WidgetCanvasContext2D 的内部)
依赖复杂度的变化:
D重构前(Canvas)≈15D重构后(Canvas)≈2D_{\text{重构前}}(\text{Canvas}) \approx 15 \quad D_{\text{重构后}}(\text{Canvas}) \approx 2D重构前(Canvas)≈15D重构后(Canvas)≈2
复杂度下降=1−215≈87%\text{复杂度下降} = 1 - \frac{2}{15} \approx 87\%复杂度下降=1−152≈87%
七、单元测试验证重构效果
// ✓ 重构后,Canvas 测试完全不需要 JS 引擎或 DOM
// Mock 通知器:测试专用,零外部依赖
class MockCanvasNotifier : public ICanvasEventNotifier {
public:
// 记录被调用的事件,供断言使用
std::vector<std::string> drawCalls;
int frameReadyCount = 0;
int errorCount = 0;
bool canvasUpdated = false;
void onCanvasUpdated() override {
canvasUpdated = true;
}
void onDrawCallCompleted(const char* op) override {
drawCalls.emplace_back(op);
}
void onCanvasError(int code, const char* msg) override {
++errorCount;
}
void onFrameReady(const uint8_t*, int, int) override {
++frameReadyCount;
}
};
// 测试 Canvas 的核心绘图功能
void testCanvasFillRect() {
// 准备:只需要最小依赖
WidgetCanvasContext2D context(100, 100); // 100x100 的离屏 Canvas
MockCanvasNotifier mockNotifier; // 零依赖的 Mock
// 创建被测对象(不需要 JSEngine!不需要 DomEntity!)
WidgetCanvasContextModule canvas(&context, &mockNotifier);
// 执行测试
canvas.setFillColor(0xFF0000FF); // 红色
canvas.fillRect(10, 10, 50, 50);
// 断言
assert(mockNotifier.drawCalls.size() == 1);
assert(mockNotifier.drawCalls[0] == "fillRect");
// 验证像素(直接从 context 读,不需要通过 JS 或 DOM)
auto pixel = context.getPixel(30, 30); // 矩形中心点
assert(pixel.r == 0xFF);
assert(pixel.g == 0x00);
assert(pixel.b == 0x00);
// 测试通知机制
canvas.commit();
assert(mockNotifier.frameReadyCount == 1);
}
void testCanvasDrawSequence() {
WidgetCanvasContext2D context(200, 200);
MockCanvasNotifier mockNotifier;
WidgetCanvasContextModule canvas(&context, &mockNotifier);
// 执行一系列绘图命令
canvas.clearRect(0, 0, 200, 200);
canvas.setFillColor(0x0000FFFF);
canvas.fillRect(0, 0, 100, 100);
canvas.setFillColor(0x00FF00FF);
canvas.fillRect(100, 100, 100, 100);
// 验证绘图命令序列
assert(mockNotifier.drawCalls.size() == 3);
assert(mockNotifier.drawCalls[0] == "clearRect");
assert(mockNotifier.drawCalls[1] == "fillRect");
assert(mockNotifier.drawCalls[2] == "fillRect");
}
// 整个测试套件:
// - 不需要 JS 引擎(节省几百毫秒启动时间)
// - 不需要 DOM 树(节省大量初始化代码)
// - 可以在 CI 上纯 CPU 环境运行(无需图形硬件)
// - 测试速度:毫秒级 vs 原来的秒级
八、依赖外溢的识别清单
在代码审查中,以下信号提示可能存在依赖外溢:
// 信号1:构造函数参数中出现与类名不相关的类型
WidgetCanvasContextModule(
jse_context_ref ctx, // ← "Canvas" 里出现 "jse"?警惕!
DomEntity* canvas // ← "Canvas" 里出现 "Dom"?警惕!
);
// 信号2:头文件 include 了与类职责无关的头文件
// canvas_context_module.h 中出现:
#include "jse/jse_context.h" // Canvas 为何需要 JS 引擎头文件?
#include "dom/dom_entity.h" // Canvas 为何需要 DOM 头文件?
// 信号3:成员变量中有"为了通知用"的外部对象
class SomeModule {
ExternalObject* obj_; // 注释写着"仅用于向外通知"
// ← 这正是依赖外溢的典型症状!
};
// 信号4:单元测试需要大量 setup 才能创建被测对象
void testSomeSimpleFeature() {
// 初始化了10个对象才能测一个功能 ← 强烈信号!
JSEngine js; DOMTree dom; NetworkStack net; ...
SomeModule module(js, dom, net, ...);
module.doSimpleThing();
}
判断一个依赖是否"外溢"的核心问题只有一个:
如果去掉这个依赖,这个类还能完成它的核心职责吗?\boxed{\text{如果去掉这个依赖,这个类还能完成它的核心职责吗?}}如果去掉这个依赖,这个类还能完成它的核心职责吗?
能⇒这个依赖是外溢的,应该通过抽象接口替代\text{能} \Rightarrow \text{这个依赖是外溢的,应该通过抽象接口替代}能⇒这个依赖是外溢的,应该通过抽象接口替代
不能⇒这个依赖是核心的,直接持有是合理的\text{不能} \Rightarrow \text{这个依赖是核心的,直接持有是合理的}不能⇒这个依赖是核心的,直接持有是合理的
对于本例:去掉 jse_context_ref 和 DomEntity,Canvas 依然能绘图。因此它们是外溢依赖,用 ICanvasEventNotifier 接口替代,是正确的解法。
解决经验:分层设计——上层依赖下层,下层不依赖上层
一、两张图的对比:问题 vs 解决
图一:问题状态——依赖外溢
问题核心:下层的 WidgetCanvasContextModule 反向依赖了上层的 Application 和 DomEntity,以及完全无关层的 jse_context_ref。
图二:解决状态——分层 + 接口隔离
解决核心:引入 WidgetCanvasContextClient 接口,让 Canvas 模块只依赖抽象,外溢依赖转移到专门的 ClientImpl 层。
二、分层设计的核心原则
2.1 层次定义
正确的分层方向(依赖只能向下):
┌─────────────────────────────────────┐ ← 最高层
│ Application │ 知道一切
│ (知道 DomEntity、Widget、Impl) │
└──────────────┬──────────────────────┘
│ 依赖(向下)
┌──────────────▼──────────────────────┐
│ DomEntity │ 知道 Widget
│ (知道 Widget,不知道 Application) │
└──────────────┬──────────────────────┘
│ 依赖(向下)
┌──────────────▼──────────────────────┐
│ Widget │ 只知道接口
│ WidgetCanvasContextModule │
│ (只依赖 WidgetCanvasContextClient)│
└──────────────┬──────────────────────┘
│ 依赖(接口,稳定)
┌──────────────▼──────────────────────┐
│ WidgetCanvasContextClient │ ← 抽象接口层
│ (纯抽象,无任何具体依赖) │ 最稳定
└─────────────────────────────────────┘
分层的数学约束:设层级编号从下到上为 L1<L2<⋯<LnL_1 < L_2 < \cdots < L_nL1<L2<⋯<Ln,合法依赖方向为:
Li→Lj当且仅当i>j(上层依赖下层)L_i \rightarrow L_j \quad \text{当且仅当} \quad i > j \quad \text{(上层依赖下层)}Li→Lj当且仅当i>j(上层依赖下层)
Li→Lj当i<j⇒违规!下层依赖上层L_i \rightarrow L_j \quad \text{当} \quad i < j \quad \Rightarrow \text{违规!下层依赖上层}Li→Lj当i<j⇒违规!下层依赖上层
本例的违规:
LWidget→LApplication⇒下层依赖上层,违规L_{\text{Widget}} \rightarrow L_{\text{Application}} \quad \Rightarrow \quad \text{下层依赖上层,违规}LWidget→LApplication⇒下层依赖上层,违规
LWidget→LDomEntity⇒下层依赖上层,违规L_{\text{Widget}} \rightarrow L_{\text{DomEntity}} \quad \Rightarrow \quad \text{下层依赖上层,违规}LWidget→LDomEntity⇒下层依赖上层,违规
三、解决方案的完整 C++ 实现
3.1 定义抽象接口(最底层,最稳定)
// widget_canvas_context_client.h
// 这个文件是整个解耦方案的核心
// 它处于最底层:不依赖任何具体模块,只定义纯抽象行为
// Canvas 模块需要"向外通知"的所有能力,都在这里定义
#pragma once
// 前向声明(连具体类型都不引入)
struct CanvasPixelBuffer {
const uint8_t* data;
int width;
int height;
int stride; // 每行字节数
};
// WidgetCanvasContextClient:Canvas 模块对外通信的唯一出口
// 命名规范:Client 表示"Canvas 是服务提供方,这是服务消费方的接口"
class WidgetCanvasContextClient {
public:
virtual ~WidgetCanvasContextClient() = default;
// ---- Canvas 状态变化通知 ----
// 画布内容已更新(用于触发重绘)
virtual void onCanvasInvalidated() = 0;
// 一帧绘制完成,像素数据就绪
virtual void onFrameReady(const CanvasPixelBuffer& frame) = 0;
// ---- 错误通知 ----
// Canvas 发生错误(错误码 + 描述)
virtual void onError(int code, const char* message) = 0;
// ---- JS 脚本事件通知 ----
// (Canvas 不知道是 JS 在消费,只知道"有脚本事件需要触发")
// 绘图操作完成,通知脚本层
virtual void onDrawCallDispatched(const char* opName,
int64_t durationUs) = 0;
// Canvas 尺寸变化
virtual void onSizeChanged(int newWidth, int newHeight) = 0;
};
3.2 重构 Canvas 模块(只依赖接口,彻底干净)
// widget_canvas_context_module.h
// Canvas 模块重构后的样子:
// 依赖:WidgetCanvasContext2D(核心绘图)+ WidgetCanvasContextClient(通知接口)
// 完全没有 jse_context_ref、DomEntity、Application 的痕迹
#pragma once
#include "widget_canvas_context_2d.h"
#include "widget_canvas_context_client.h" // 只 include 接口,不 include 任何上层模块
// ✗ 以下 include 全部删除:
// #include "jse/jse_context.h" ← 删除
// #include "dom/dom_entity.h" ← 删除
// #include "app/application.h" ← 删除
class WidgetCanvasContextModule : public ModuleBase {
public:
// 构造函数:只接受核心依赖 + 抽象接口
// 参数列表一眼就能看出:Canvas 需要什么,非常清晰
WidgetCanvasContextModule(
WidgetCanvasContext2D* context, // 核心:2D绘图能力
WidgetCanvasContextClient* client // 抽象:对外通知能力
)
: _context(context)
, _client(client)
{
// 构造函数体简洁,无任何奇怪的初始化
}
// ---- 2D 绘图 API(Canvas 的核心职责)----
void clearRect(float x, float y, float w, float h) {
int64_t t0 = getMonotonicUs();
_context->clearRect(x, y, w, h);
// 通过接口通知,不知道谁在处理
notifyDrawCall("clearRect", getMonotonicUs() - t0);
}
void fillRect(float x, float y, float w, float h) {
int64_t t0 = getMonotonicUs();
_context->fillRect(x, y, w, h);
notifyDrawCall("fillRect", getMonotonicUs() - t0);
}
void drawImage(const ImageData& img, float dx, float dy) {
int64_t t0 = getMonotonicUs();
_context->drawImage(img, dx, dy);
notifyDrawCall("drawImage", getMonotonicUs() - t0);
}
void strokeRect(float x, float y, float w, float h) {
int64_t t0 = getMonotonicUs();
_context->strokeRect(x, y, w, h);
notifyDrawCall("strokeRect", getMonotonicUs() - t0);
}
// 样式设置
void setFillColor(uint32_t rgba) { _context->setFillColor(rgba); }
void setStrokeColor(uint32_t rgba) { _context->setStrokeColor(rgba); }
void setLineWidth(float w) { _context->setLineWidth(w); }
void setGlobalAlpha(float alpha) { _context->setGlobalAlpha(alpha);}
// 提交当前帧到显示
void commit() {
// 获取当前帧像素数据
CanvasPixelBuffer frame = _context->getPixelBuffer();
_context->flush();
// 通过接口通知帧就绪,不知道是谁在接收(JS?DOM?硬件?)
if (_client) _client->onFrameReady(frame);
}
// 尺寸变化(外部调用)
void resize(int newWidth, int newHeight) {
_context->resize(newWidth, newHeight);
if (_client) _client->onSizeChanged(newWidth, newHeight);
}
private:
// 成员变量:只有两个,极度干净
WidgetCanvasContext2D* _context; // 核心绘图对象
WidgetCanvasContextClient* _client; // 抽象通知接口
// 辅助:通知一次绘图调用完成
void notifyDrawCall(const char* opName, int64_t durationUs) {
// 标记画布需要重绘
if (_client) _client->onCanvasInvalidated();
// 通知绘图调用信息(用于性能监控、JS回调等)
if (_client) _client->onDrawCallDispatched(opName, durationUs);
}
int64_t getMonotonicUs() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}
};
3.3 实现具体的 ClientImpl(外溢依赖的正确归宿)
// widget_canvas_context_client_impl.h
// 这个类处于"解耦实现层":
// 它知道 DomEntity 和 Application,这是它的职责
// 它把 Canvas 的通知成 DOM 事件和 JS 回调
#pragma once
#include "widget_canvas_context_client.h" // 实现的接口
#include "dom/dom_entity.h" // 合理:这里持有 DOM 依赖
#include "app/application.h" // 合理:这里持有 App 依赖
#include "jse/jse_context.h" // 合理:这里持有 JS 引擎依赖
class WidgetCanvasContextClientImpl : public WidgetCanvasContextClient {
public:
// 构造函数:在正确的层次持有外部依赖
WidgetCanvasContextClientImpl(
jse_context_ref jsCtx, // JS引擎:在这里持有,合理
DomEntity* canvasDom, // DOM实体:在这里持有,合理
Application* app // 应用层:在这里持有,合理
)
: _jsCtx(jsCtx)
, _canvasDom(canvasDom)
, _app(app)
{}
// ---- 实现 WidgetCanvasContextClient 接口 ----
// Canvas 内容变化 → 触发 DOM 重绘
void onCanvasInvalidated() override {
if (_canvasDom) {
// 告知 DOM 树该节点需要重新渲染
_canvasDom->markDirty();
_canvasDom->scheduleLayout();
}
}
// 新帧就绪 → 传递给 JS 和 DOM
void onFrameReady(const CanvasPixelBuffer& frame) override {
// 1. 更新 DOM 的纹理数据
if (_canvasDom) {
_canvasDom->updateTexture(
frame.data, frame.width, frame.height
);
}
// 2. 触发 JS 的 onframe 回调(如果有注册)
if (_jsCtx) {
triggerJSCallback("__onCanvasFrame__", [&](jse_value_t* args) {
// 将像素数据包装为 JS ArrayBuffer
args[0] = jse_create_array_buffer(
_jsCtx,
frame.data,
frame.width * frame.height * 4
);
args[1] = jse_create_number(_jsCtx, frame.width);
args[2] = jse_create_number(_jsCtx, frame.height);
return 3; // 参数个数
});
}
}
// Canvas 错误 → 通知 Application 和 JS 错误处理
void onError(int code, const char* message) override {
// 1. 通知 Application 层记录日志
if (_app) {
_app->onComponentError("WidgetCanvas", code, message);
}
// 2. 触发 JS 的 onerror 回调
if (_jsCtx) {
triggerJSCallback("__onCanvasError__", [&](jse_value_t* args) {
jse_value_t errObj = jse_create_object(_jsCtx);
jse_set_named_property(
_jsCtx, errObj, "code",
jse_create_number(_jsCtx, code)
);
jse_set_named_property(
_jsCtx, errObj, "message",
jse_create_string(_jsCtx, message)
);
args[0] = errObj;
return 1;
});
}
// 3. 触发 DOM 错误事件
if (_canvasDom) {
DomEvent errEvent("error");
errEvent.detail = message;
_canvasDom->dispatchEvent(errEvent);
}
}
// 绘图调用完成 → 通知 JS 性能监控回调
void onDrawCallDispatched(const char* opName,
int64_t durationUs) override {
if (!_jsCtx) return;
// 触发 JS 的 ondraw 钩子(用于性能分析)
triggerJSCallback("__onDrawCall__", [&](jse_value_t* args) {
args[0] = jse_create_string(_jsCtx, opName);
args[1] = jse_create_number(_jsCtx, (double)durationUs);
return 2;
});
}
// Canvas 尺寸变化 → 更新 DOM 布局
void onSizeChanged(int newWidth, int newHeight) override {
if (_canvasDom) {
_canvasDom->setSize(newWidth, newHeight);
_canvasDom->triggerReflow(); // 触发 DOM 重排
}
if (_jsCtx) {
triggerJSCallback("__onCanvasResize__", [&](jse_value_t* args) {
args[0] = jse_create_number(_jsCtx, newWidth);
args[1] = jse_create_number(_jsCtx, newHeight);
return 2;
});
}
}
private:
// 在正确的层次持有这些依赖——这里持有是完全合理的
jse_context_ref _jsCtx; // JS 引擎上下文
DomEntity* _canvasDom; // Canvas 对应的 DOM 节点
Application* _app; // 应用层引用
// 辅助:统一的 JS 回调触发逻辑
using ArgBuilder = std::function<int(jse_value_t*)>;
void triggerJSCallback(const char* callbackName, ArgBuilder buildArgs) {
// 在全局对象上查找回调函数
jse_value_t global = jse_get_global(_jsCtx);
jse_value_t fn = jse_get_named_property(
_jsCtx, global, callbackName
);
jse_free_value(_jsCtx, global);
if (!jse_value_is_function(_jsCtx, fn)) {
jse_free_value(_jsCtx, fn);
return; // 没有注册回调,直接返回
}
// 构造参数
jse_value_t args[8];
int argCount = buildArgs(args);
// 调用 JS 函数
jse_value_t result = jse_call_function(
_jsCtx, fn,
jse_make_undefined(_jsCtx),
args, argCount
);
// 释放所有 JS 值(RAII 此处用手动释放,避免 JS 引擎的特殊要求)
jse_free_value(_jsCtx, fn);
jse_free_value(_jsCtx, result);
for (int i = 0; i < argCount; ++i) {
jse_free_value(_jsCtx, args[i]);
}
}
};
3.4 顶层组装:Application 负责连接一切
// application.cpp 或 widget_factory.cpp
// 只有这里知道所有具体类型,负责把各层连接起来
class CanvasWidgetFactory {
public:
// 创建一个完整可用的 Canvas Widget
// 参数:JS引擎上下文 + 所属的 DOM 节点
static WidgetCanvasContextModule* create(
jse_context_ref jsCtx,
DomEntity* domNode,
Application* app
) {
// 1. 创建核心绘图上下文(Canvas 最底层)
auto* context = new WidgetCanvasContext2D(
domNode->getWidth(),
domNode->getHeight()
);
// 2. 创建具体的 ClientImpl(解耦实现层)
// 外溢依赖(jsCtx、domNode、app)在这里被正确地封装
auto* clientImpl = new WidgetCanvasContextClientImpl(
jsCtx, // JS引擎:在 ClientImpl 层持有
domNode, // DOM节点:在 ClientImpl 层持有
app // Application:在 ClientImpl 层持有
);
// 3. 创建 Canvas 模块(只传入核心 + 接口)
// WidgetCanvasContextModule 完全不知道 jsCtx、domNode、app 的存在!
auto* canvasModule = new WidgetCanvasContextModule(
context, // 核心绘图对象
clientImpl // 只是一个 WidgetCanvasContextClient*,不知道是 Impl
);
return canvasModule;
// 生命周期管理:实际项目中用 unique_ptr 或自定义 RAII
}
};
// 在 Application 初始化时使用:
void Application::initUI() {
jse_context_ref jsCtx = _jsEngine->getContext();
DomEntity* canvasDom = _domTree->getElementById("main-canvas");
// 一行创建,依赖关系在工厂内部正确组装
_canvas = CanvasWidgetFactory::create(jsCtx, canvasDom, this);
// 使用:完全干净的 API,看不到任何 JS 或 DOM 的痕迹
_canvas->setFillColor(0xFF0000FF);
_canvas->fillRect(0, 0, 100, 100);
_canvas->commit();
}
四、分层设计的核心收益
4.1 可测试性对比
// ✗ 重构前:测试 Canvas 绘图,需要初始化整个前端运行时
void testBeforeRefactor() {
JSEngine jsEngine; // 启动 JS 引擎(耗时 200ms)
jse_context_ref ctx = jsEngine.createContext();
DOMTree domTree; // 构建 DOM 树
DomEntity* entity = domTree.createElement("canvas");
Application app; // 初始化整个应用
WidgetCanvasContext2D context;
// 终于能创建被测对象了...
WidgetCanvasContextModule module(ctx, &context, entity);
module.fillRect(0, 0, 10, 10); // 测这一行
// 总耗时:几百毫秒,依赖真实硬件/环境
}
// ✓ 重构后:测试 Canvas 绘图,只需要 Mock
class MockCanvasClient : public WidgetCanvasContextClient {
public:
int invalidateCount = 0;
std::vector<std::string> drawCalls;
void onCanvasInvalidated() override { ++invalidateCount; }
void onFrameReady(const CanvasPixelBuffer&) override {}
void onError(int, const char*) override {}
void onDrawCallDispatched(const char* op, int64_t) override {
drawCalls.emplace_back(op);
}
void onSizeChanged(int, int) override {}
};
void testAfterRefactor() {
WidgetCanvasContext2D context(100, 100); // 离屏,无硬件依赖
MockCanvasClient mockClient; // 零依赖
WidgetCanvasContextModule module(&context, &mockClient);
module.fillRect(0, 0, 10, 10); // 直接测,无需任何前端运行时
assert(mockClient.drawCalls[0] == "fillRect");
assert(mockClient.invalidateCount == 1);
// 总耗时:<1ms,纯内存操作,可在任何 CI 环境运行
}
4.2 可扩展性:新增通知目标零成本
// 分层设计的开放封闭原则(OCP)体现:
// 需要新增一个"性能监控通知目标"?
// ✓ 只需新增一个 ClientImpl,不修改任何现有代码
class PerformanceMonitorClient : public WidgetCanvasContextClient {
public:
explicit PerformanceMonitorClient(PerformanceDB* db) : _db(db) {}
void onCanvasInvalidated() override {}
void onFrameReady(const CanvasPixelBuffer& f) override {
// 记录帧率数据
_db->recordFrame(f.width * f.height, getCurrentMs());
}
void onError(int code, const char*) override {
_db->recordError(code);
}
void onDrawCallDispatched(const char* op, int64_t us) override {
// 记录每个绘图调用的耗时
_db->recordDrawCall(op, us);
}
void onSizeChanged(int, int) override {}
private:
PerformanceDB* _db;
};
// 同时通知多个 Client(组合模式)
class CompositeCanvasClient : public WidgetCanvasContextClient {
public:
void add(WidgetCanvasContextClient* c) { _clients.push_back(c); }
void onCanvasInvalidated() override {
for (auto* c : _clients) c->onCanvasInvalidated();
}
void onFrameReady(const CanvasPixelBuffer& f) override {
for (auto* c : _clients) c->onFrameReady(f);
}
void onError(int code, const char* msg) override {
for (auto* c : _clients) c->onError(code, msg);
}
void onDrawCallDispatched(const char* op, int64_t us) override {
for (auto* c : _clients) c->onDrawCallDispatched(op, us);
}
void onSizeChanged(int w, int h) override {
for (auto* c : _clients) c->onSizeChanged(w, h);
}
private:
std::vector<WidgetCanvasContextClient*> _clients;
};
// 使用组合:Canvas 模块无需任何修改
void Application::initUI() {
auto* jsClient = new WidgetCanvasContextClientImpl(ctx, dom, this);
auto* perfClient = new PerformanceMonitorClient(&_perfDB);
auto* composite = new CompositeCanvasClient();
composite->add(jsClient); // 原有功能
composite->add(perfClient); // 新增性能监控
// Canvas 模块接收的还是 WidgetCanvasContextClient*,毫不知情
_canvas = new WidgetCanvasContextModule(&_context, composite);
}
五、分层原则总结
分层设计的本质是对信息流向的管控:
信息可以从上层流向下层(上层知道下层存在)\text{信息可以从上层流向下层(上层知道下层存在)}信息可以从上层流向下层(上层知道下层存在)
信息不能从下层流向上层(下层不知道上层存在)\text{信息不能从下层流向上层(下层不知道上层存在)}信息不能从下层流向上层(下层不知道上层存在)
下层需要"向上通知"时⇒用抽象接口反转依赖方向\text{下层需要"向上通知"时} \Rightarrow \text{用抽象接口反转依赖方向}下层需要"向上通知"时⇒用抽象接口反转依赖方向
这三条规则统一起来就是依赖倒置原则(DIP) 在分层架构中的具体应用:
WidgetCanvasContextModule⏟下层→WidgetCanvasContextClient(接口)⏟稳定抽象←WidgetCanvasContextClientImpl⏟上层\underbrace{\text{WidgetCanvasContextModule}}_{\text{下层}} \rightarrow \underbrace{\text{WidgetCanvasContextClient(接口)}}_{\text{稳定抽象}} \leftarrow \underbrace{\text{WidgetCanvasContextClientImpl}}_{\text{上层}}下层
WidgetCanvasContextModule→稳定抽象
WidgetCanvasContextClient(接口)←上层
WidgetCanvasContextClientImpl
下层定义接口,上层实现接口。依赖的箭头全部指向接口,没有任何跨层的直接指向,毛线团彻底解开。
经验2:Delegate/Client 解决反向依赖
一、核心思想
“反向依赖"是指下层模块需要通知上层模块时,若直接持有上层的引用,就违反了分层原则。Delegate 模式是解决这个问题的经典手法:
反向依赖问题:下层→上层⇒违反分层原则\text{反向依赖问题} : \text{下层} \rightarrow \text{上层} \quad \Rightarrow \quad \text{违反分层原则}反向依赖问题:下层→上层⇒违反分层原则
Delegate 解法:下层→Delegate(接口)⏟下层自己定义←上层实现⇒依赖方向正确\text{Delegate 解法} : \text{下层} \rightarrow \underbrace{\text{Delegate(接口)}}_{\text{下层自己定义}} \leftarrow \text{上层实现} \quad \Rightarrow \quad \text{依赖方向正确}Delegate 解法:下层→下层自己定义
Delegate(接口)←上层实现⇒依赖方向正确
关键洞察:接口由下层定义,上层来实现。下层只知道"有人在听我”,不知道是谁。
二、架构图中每个角色的职责
从 Mermaid 图出发,逐一理解每个类:
角色地图:
┌─────────────────────────────────────────────────────────────┐
│ 外部模块(图的两侧) │
│ Server(服务器) Service(具体服务实例) │
└───────────┬──────────────────────────┬──────────────────────┘
│ │
┌───────────▼──────────────────────────▼──────────────────────┐
│ 橙色框:ServiceManager 模块 │
│ │
│ ServiceManager(核心调度器) │
│ │ 1:1 │ 1:N │
│ ▼ ▼ │
│ Server ServiceRecoder │
│ │
│ ServerDelegateImpl ServiceRecorderDelegate │
│ (实现 Server::Delegate) (实现 ServiceRecorder::Delegate) │
└─────────────────────────────────────────────────────────────┘
│ │
┌───────────▼──────────────────────────▼──────────────────────┐
│ 接口层(最稳定) │
│ Server::Delegate ServiceRecorder::Delegate │
└─────────────────────────────────────────────────────────────┘
三、完整 C++ 实现
3.1 底层模块:Server 及其 Delegate 接口
// server.h
// Server 是底层模块,负责网络连接、数据收发
// 它需要向上层通知各种事件,但不能直接依赖上层
// 解法:定义内嵌的 Delegate 接口,上层来实现
#pragma once
#include <cstdint>
#include <cstddef>
class Server {
public:
// ---- 内嵌 Delegate 接口(下层自己定义)----
// Server 只知道"有一个监听者",不知道监听者是谁
class Delegate {
public:
virtual ~Delegate() = default;
// Server 连接建立
virtual void onServerConnected(Server* server) = 0;
// Server 收到数据
virtual void onServerDataReceived(Server* server,
const uint8_t* data,
size_t len) = 0;
// Server 连接断开
virtual void onServerDisconnected(Server* server,
int reason) = 0;
// Server 发生错误
virtual void onServerError(Server* server,
int code,
const char* msg) = 0;
};
// ---- Server 的核心接口 ----
bool start(const char* host, uint16_t port);
void stop();
bool send(const uint8_t* data, size_t len);
bool isConnected() const { return _connected; }
// 注入 Delegate(由上层在构造/初始化时传入)
void setDelegate(Delegate* delegate) {
_delegate = delegate;
}
protected:
// 内部事件触发(网络层回调时调用这些方法)
void notifyConnected() {
if (_delegate) _delegate->onServerConnected(this);
}
void notifyDataReceived(const uint8_t* data, size_t len) {
if (_delegate) _delegate->onServerDataReceived(this, data, len);
}
void notifyDisconnected(int reason) {
if (_delegate) _delegate->onServerDisconnected(this, reason);
}
void notifyError(int code, const char* msg) {
if (_delegate) _delegate->onServerError(this, code, msg);
}
private:
Delegate* _delegate = nullptr; // 持有接口,不持有具体类
bool _connected = false;
};
3.2 底层模块:ServiceRecoder 及其 Delegate 接口
// service_recoder.h
// ServiceRecoder 负责管理单个 Service 的生命周期和状态记录
// 同样需要向上层(ServiceManager)汇报状态变化
#pragma once
#include "service.h"
class ServiceRecoder {
public:
// ---- 内嵌 Delegate 接口 ----
class Delegate {
public:
virtual ~Delegate() = default;
// Service 状态变化通知
virtual void onServiceStarted(ServiceRecoder* recoder,
Service* service) = 0;
virtual void onServiceStopped(ServiceRecoder* recoder,
Service* service,
int exitCode) = 0;
virtual void onServiceCrashed(ServiceRecoder* recoder,
Service* service,
const char* reason) = 0;
// 服务性能指标上报
virtual void onServiceMetrics(ServiceRecoder* recoder,
float cpuUsage,
size_t memBytes) = 0;
};
// ---- ServiceRecoder 核心接口 ----
explicit ServiceRecoder(Service* service)
: _service(service) {}
bool startService();
bool stopService();
bool restartService();
Service* getService() const { return _service; }
bool isRunning() const { return _running; }
float getCpuUsage() const { return _cpuUsage; }
size_t getMemBytes() const { return _memBytes; }
void setDelegate(Delegate* delegate) {
_delegate = delegate;
}
// Service 通过弱引用回调 ServiceRecoder(见 3.5 节)
// 这里提供给 Service 调用的接口
void onServiceHeartbeat(float cpu, size_t mem) {
_cpuUsage = cpu;
_memBytes = mem;
// 向上汇报性能指标
if (_delegate) _delegate->onServiceMetrics(this, cpu, mem);
}
private:
Service* _service = nullptr;
Delegate* _delegate = nullptr; // 持有接口,不持有 ServiceManager
bool _running = false;
float _cpuUsage = 0.0f;
size_t _memBytes = 0;
};
3.3 核心调度层:ServiceManager + 两个 Delegate 实现
// service_manager.h
// ServiceManager 是系统的核心调度模块(橙色框内容)
// 它管理 1个Server 和 N个ServiceRecoder
// 同时它实现了 Server::Delegate 和 ServiceRecoder::Delegate
// 通过这两个实现类接收来自下层的通知
#pragma once
#include "server.h"
#include "service_recoder.h"
#include <vector>
#include <memory>
// 前向声明
class ServiceManager;
// ---- ServerDelegateImpl:实现 Server::Delegate ----
// 职责:接收 Server 的网络事件,转发给 ServiceManager 处理
// 注意:持有 ServiceManager 的弱引用,防止循环引用
class ServerDelegateImpl : public Server::Delegate {
public:
// 弱引用:不影响 ServiceManager 的生命周期
// 使用原始指针 + 生命周期保证,或 wp<ServiceManager>
explicit ServerDelegateImpl(ServiceManager* manager)
: _manager(manager) // 弱引用(不增加引用计数)
{}
void onServerConnected(Server* server) override;
void onServerDataReceived(Server* server,
const uint8_t* data,
size_t len) override;
void onServerDisconnected(Server* server, int reason) override;
void onServerError(Server* server, int code, const char* msg) override;
private:
ServiceManager* _manager; // 弱引用!不是 sp<>,避免循环引用
// 循环引用分析:
// ServiceManager 持有 ServerDelegateImpl(强引用)
// ServerDelegateImpl 持有 ServiceManager(若强引用 → 循环!)
// 解法:ServerDelegateImpl 用弱引用持有 ServiceManager
};
// ---- ServiceRecorderDelegate:实现 ServiceRecoder::Delegate ----
// 职责:接收 ServiceRecoder 的服务状态事件,转发给 ServiceManager
class ServiceRecorderDelegate : public ServiceRecoder::Delegate {
public:
explicit ServiceRecorderDelegate(ServiceManager* manager)
: _manager(manager) // 弱引用
{}
void onServiceStarted(ServiceRecoder* recoder,
Service* service) override;
void onServiceStopped(ServiceRecoder* recoder,
Service* service,
int exitCode) override;
void onServiceCrashed(ServiceRecoder* recoder,
Service* service,
const char* reason) override;
void onServiceMetrics(ServiceRecoder* recoder,
float cpu,
size_t mem) override;
private:
ServiceManager* _manager; // 弱引用!
};
// ---- ServiceManager:系统核心调度器 ----
class ServiceManager {
public:
ServiceManager();
~ServiceManager();
// 初始化:创建 Server 并注入 Delegate
bool init(const char* host, uint16_t port);
// 注册一个新的 Service
bool registerService(Service* service);
// 启动/停止所有服务
bool startAll();
void stopAll();
// 查询
size_t getServiceCount() const { return _recoders.size(); }
bool isServerConnected() const;
// 供 Delegate Impl 调用的内部接口
// (Delegate Impl 通过弱引用调用这些方法)
void handleServerConnected();
void handleServerData(const uint8_t* data, size_t len);
void handleServerDisconnected(int reason);
void handleServiceStarted(Service* service);
void handleServiceStopped(Service* service, int exitCode);
void handleServiceCrashed(Service* service, const char* reason);
void handleServiceMetrics(ServiceRecoder* recoder,
float cpu, size_t mem);
private:
// 1:1 管理一个 Server
std::unique_ptr<Server> _server;
// 1:N 管理多个 ServiceRecoder
std::vector<std::unique_ptr<ServiceRecoder>> _recoders;
// 两个 Delegate 实现(橙色框内的组成部分)
// ServiceManager 强持有这两个实现对象
std::unique_ptr<ServerDelegateImpl> _serverDelegate;
std::unique_ptr<ServiceRecorderDelegate> _serviceRecorderDelegate;
};
3.4 ServiceManager 的实现
// service_manager.cpp
#include "service_manager.h"
#include <cstdio>
// ---- ServerDelegateImpl 实现 ----
void ServerDelegateImpl::onServerConnected(Server* server) {
// 弱引用使用前必须检查有效性
if (_manager) {
_manager->handleServerConnected();
}
}
void ServerDelegateImpl::onServerDataReceived(Server* server,
const uint8_t* data,
size_t len) {
if (_manager) {
_manager->handleServerData(data, len);
}
}
void ServerDelegateImpl::onServerDisconnected(Server* server,
int reason) {
if (_manager) {
_manager->handleServerDisconnected(reason);
}
}
void ServerDelegateImpl::onServerError(Server* server,
int code,
const char* msg) {
// 错误处理:记录日志,但不崩溃
fprintf(stderr, "[ServerDelegate] error %d: %s\n", code, msg);
if (_manager) {
// 可以转发给 Manager 决定是否重连
_manager->handleServerDisconnected(code);
}
}
// ---- ServiceRecorderDelegate 实现 ----
void ServiceRecorderDelegate::onServiceStarted(ServiceRecoder* recoder,
Service* service) {
if (_manager) {
_manager->handleServiceStarted(service);
}
}
void ServiceRecorderDelegate::onServiceStopped(ServiceRecoder* recoder,
Service* service,
int exitCode) {
if (_manager) {
_manager->handleServiceStopped(service, exitCode);
}
}
void ServiceRecorderDelegate::onServiceCrashed(ServiceRecoder* recoder,
Service* service,
const char* reason) {
fprintf(stderr, "[ServiceRecorder] crashed: %s\n", reason);
if (_manager) {
_manager->handleServiceCrashed(service, reason);
}
}
void ServiceRecorderDelegate::onServiceMetrics(ServiceRecoder* recoder,
float cpu,
size_t mem) {
if (_manager) {
_manager->handleServiceMetrics(recoder, cpu, mem);
}
}
// ---- ServiceManager 实现 ----
ServiceManager::ServiceManager() {
// 创建两个 Delegate 实现,传入 this 作为弱引用
// 注意:此时 ServiceManager 已经存在,弱引用安全
_serverDelegate = std::make_unique<ServerDelegateImpl>(this);
_serviceRecorderDelegate = std::make_unique<ServiceRecorderDelegate>(this);
}
ServiceManager::~ServiceManager() {
stopAll();
// _server、_recoders、_serverDelegate、_serviceRecorderDelegate
// 均由 unique_ptr 自动释放,析构顺序:
// 1. _serviceRecorderDelegate 析构(Impl 对象消失)
// 2. _serverDelegate 析构
// 3. _recoders 析构(ServiceRecoder 们消失)
// 4. _server 析构(Server 消失)
// 不会有悬空指针,因为 Impl 先析构,Server 后析构
}
bool ServiceManager::init(const char* host, uint16_t port) {
_server = std::make_unique<Server>();
// 把 ServerDelegateImpl 注入给 Server
// Server 持有 Delegate 接口指针(不知道是 ServerDelegateImpl)
_server->setDelegate(_serverDelegate.get());
return _server->start(host, port);
}
bool ServiceManager::registerService(Service* service) {
auto recoder = std::make_unique<ServiceRecoder>(service);
// 把 ServiceRecorderDelegate 注入给每个 Recoder
recoder->setDelegate(_serviceRecorderDelegate.get());
_recoders.push_back(std::move(recoder));
return true;
}
bool ServiceManager::startAll() {
for (auto& recoder : _recoders) {
if (!recoder->startService()) return false;
}
return true;
}
void ServiceManager::stopAll() {
for (auto& recoder : _recoders) {
recoder->stopService();
}
}
// 处理来自 ServerDelegateImpl 的事件
void ServiceManager::handleServerConnected() {
fprintf(stdout, "[Manager] Server connected, starting services...\n");
startAll(); // 服务器连接后启动所有服务
}
void ServiceManager::handleServerData(const uint8_t* data, size_t len) {
// 解析数据,路由到对应的 ServiceRecoder
// 例如:根据 serviceId 找到对应的 recoder 处理
uint32_t serviceId = parseServiceId(data, len);
for (auto& recoder : _recoders) {
if (recoder->getService()->getId() == serviceId) {
recoder->getService()->handleData(data + 4, len - 4);
return;
}
}
}
void ServiceManager::handleServerDisconnected(int reason) {
fprintf(stdout, "[Manager] Server disconnected(%d), stopping...\n",
reason);
stopAll();
// 可以触发重连逻辑
}
void ServiceManager::handleServiceStarted(Service* service) {
fprintf(stdout, "[Manager] Service %u started\n", service->getId());
}
void ServiceManager::handleServiceStopped(Service* service, int exitCode) {
fprintf(stdout, "[Manager] Service %u stopped, code=%d\n",
service->getId(), exitCode);
}
void ServiceManager::handleServiceCrashed(Service* service,
const char* reason) {
fprintf(stderr, "[Manager] Service %u crashed: %s, restarting...\n",
service->getId(), reason);
// 找到对应 recoder,触发重启
for (auto& recoder : _recoders) {
if (recoder->getService() == service) {
recoder->restartService();
return;
}
}
}
void ServiceManager::handleServiceMetrics(ServiceRecoder* recoder,
float cpu,
size_t mem) {
// 聚合所有服务的资源使用情况
if (cpu > 0.9f) {
fprintf(stderr, "[Manager] Service CPU overload: %.1f%%\n",
cpu * 100);
}
}
3.5 弱引用处理循环引用
图中有两处"弱引用"标注,这是整个设计的关键安全机制:
// 循环引用分析:
// 潜在循环1:
// ServiceManager ──strong──→ ServerDelegateImpl
// ServerDelegateImpl ──strong──→ ServiceManager ← 循环!
//
// 如果用强引用:两者互相持有,永远不会释放(内存泄漏)
//
// 解法:ServerDelegateImpl 用弱引用持有 ServiceManager
// ServiceManager ──strong──→ ServerDelegateImpl
// ServerDelegateImpl ──weak──→ ServiceManager ← 打破循环
// 使用 Vela 的 wp<T>(弱指针)实现弱引用
class ServerDelegateImpl : public Server::Delegate {
public:
// 用 wp<ServiceManager> 而非 ServiceManager*
explicit ServerDelegateImpl(const sp<ServiceManager>& manager)
: _manager(manager) // wp<> 构造:不增加强引用计数
{}
void onServerConnected(Server* server) override {
// 使用弱引用时,必须先 promote 为强引用
// 如果 ServiceManager 已被销毁,promote 返回 nullptr
sp<ServiceManager> manager = _manager.promote();
if (manager == nullptr) {
// ServiceManager 已析构,不再处理
return;
}
// promote 成功:在此作用域内 manager 保证有效
manager->handleServerConnected();
// 离开作用域,manager(临时强引用)自动释放
}
private:
wp<ServiceManager> _manager; // 弱引用:不影响生命周期
};
// 潜在循环2(图底部的弱引用):
// ServiceRecoder ──strong──→ Service(管理关系)
// Service ──weak──→ ServiceRecoder(回调心跳)
//
// Service 需要定期回调 ServiceRecoder 汇报心跳
// 若 Service 强持有 ServiceRecoder:
// ServiceRecoder 强持有 Service(管理)
// Service 强持有 ServiceRecoder(回调)← 循环!
//
// 解法:Service 用弱引用持有 ServiceRecoder
class Service {
public:
// Service 弱引用持有所属的 ServiceRecoder
void setRecoder(const sp<ServiceRecoder>& recoder) {
_recoder = recoder; // 存为弱引用
}
// 定期心跳(由内部定时器调用)
void heartbeat() {
sp<ServiceRecoder> recoder = _recoder.promote();
if (recoder) {
float cpu = measureCpuUsage();
size_t mem = measureMemUsage();
// 通过弱引用升级后安全回调
recoder->onServiceHeartbeat(cpu, mem);
}
// 若 recoder 已析构,promote 失败,心跳静默丢弃
}
private:
wp<ServiceRecoder> _recoder; // 弱引用:不阻止 ServiceRecoder 析构
uint32_t _id = 0;
};
弱引用的生命周期保证:
强引用计数>0⇒对象存活\text{强引用计数} > 0 \Rightarrow \text{对象存活}强引用计数>0⇒对象存活
强引用计数=0⇒对象析构,但控制块保留直到弱引用计数也为0\text{强引用计数} = 0 \Rightarrow \text{对象析构,但控制块保留直到弱引用计数也为0}强引用计数=0⇒对象析构,但控制块保留直到弱引用计数也为0
wp::promote()={sp<T>(有效)若强引用计数>0nullptr若强引用计数=0\text{wp::promote()} = \begin{cases} \text{sp<T>(有效)} & \text{若强引用计数} > 0 \\ \text{nullptr} & \text{若强引用计数} = 0 \end{cases}wp::promote()={sp<T>(有效)nullptr若强引用计数>0若强引用计数=0
3.6 整体组装与使用
// main.cpp 或 app_init.cpp
// 只有最顶层才知道所有具体类型,负责把所有模块连接起来
#include "service_manager.h"
#include "service.h"
int main() {
// 1. 创建 ServiceManager(橙色框)
// 构造时自动创建 ServerDelegateImpl 和 ServiceRecorderDelegate
auto manager = std::make_shared<ServiceManager>();
// 2. 创建具体的 Service 实例
auto serviceA = std::make_shared<Service>(/*id=*/1, "AudioService");
auto serviceB = std::make_shared<Service>(/*id=*/2, "NetworkService");
auto serviceC = std::make_shared<Service>(/*id=*/3, "StorageService");
// 3. 注册 Service(内部创建 ServiceRecoder,注入 Delegate)
manager->registerService(serviceA.get());
manager->registerService(serviceB.get());
manager->registerService(serviceC.get());
// 4. 建立 Service → ServiceRecoder 的弱引用(心跳回调)
// (实际中在 registerService 内部完成)
// 5. 初始化 Server(内部注入 ServerDelegateImpl)
if (!manager->init("127.0.0.1", 8080)) {
fprintf(stderr, "Server init failed\n");
return -1;
}
// 6. 运行主循环
// 此后:
// Server 事件 → ServerDelegateImpl → ServiceManager
// Service 心跳 → ServiceRecorderDelegate → ServiceManager
// ServiceManager 统一调度,不被任何下层直接依赖
runEventLoop();
return 0;
}
四、Delegate 模式的本质总结
4.1 与直接回调的对比
// ✗ 不用 Delegate:下层直接持有上层(反向依赖)
class Server {
ServiceManager* _manager; // Server 知道 ServiceManager 的存在!
void onConnected() {
_manager->handleServerConnected(); // 下层调用上层,违规
}
};
// ✓ 用 Delegate:下层只知道接口
class Server {
Server::Delegate* _delegate; // Server 只知道 Delegate 接口
void onConnected() {
if (_delegate) _delegate->onServerConnected(this); // 通过接口
}
// Server 完全不知道 ServiceManager 的存在!
};
4.2 Delegate 模式的依赖方向
Server⏟下层→定义Server::Delegate⏟接口←实现ServerDelegateImpl⏟上层→弱引用ServiceManager⏟上层核心\underbrace{\text{Server}}_{\text{下层}} \xrightarrow{\text{定义}} \underbrace{\text{Server::Delegate}}_{\text{接口}} \xleftarrow{\text{实现}} \underbrace{\text{ServerDelegateImpl}}_{\text{上层}} \xrightarrow{\text{弱引用}} \underbrace{\text{ServiceManager}}_{\text{上层核心}}下层
Server定义接口
Server::Delegate实现上层
ServerDelegateImpl弱引用上层核心
ServiceManager
所有箭头从上层指向下层,或指向接口。没有任何下层指向上层的箭头,分层原则完全满足。
4.3 三个设计决策的原因
| 决策 | 原因 |
|---|---|
| Delegate 接口定义在下层(Server 内部) | 接口描述的是下层"能报告什么",下层最清楚 |
| Delegate 实现在上层(ServiceManager 模块内) | 上层决定"如何处理"这些事件 |
| Impl 用弱引用持有 ServiceManager | 打破 Manager→Impl→Manager 的循环引用 |
| Service 用弱引用持有 ServiceRecoder | 打破 Recoder→Service→Recoder 的循环引用 |
这四个决策共同构成了一个无环、无泄漏、可测试的模块化设计。
经验3:避免因实现/需求定接口
一、核心问题:接口是郑重承诺
接口(Interface)不是实现细节的镜像,而是模块对外世界的契约:
接口=模块对外的承诺≠模块内部实现的映射\text{接口} = \text{模块对外的承诺} \neq \text{模块内部实现的映射}接口=模块对外的承诺=模块内部实现的映射
好接口的标准:稳定性↑最小化↑必要性↑\text{好接口的标准:} \quad \text{稳定性} \uparrow \quad \text{最小化} \uparrow \quad \text{必要性} \uparrow好接口的标准:稳定性↑最小化↑必要性↑
坏接口的成因:因实现需要加→接口膨胀→所有实现类被迫实现\text{坏接口的成因:} \quad \text{因实现需要加} \rightarrow \text{接口膨胀} \rightarrow \text{所有实现类被迫实现}坏接口的成因:因实现需要加→接口膨胀→所有实现类被迫实现
二、从图片还原问题代码
// ✗ 问题代码还原
// 原始设计:IApplication 是 Application 的接口
// i_application.h
class IApplication {
public:
virtual ~IApplication() = default;
// ---- 原本合理的接口 ----
virtual bool init() = 0;
virtual void start() = 0;
virtual void stop() = 0;
virtual void destroy() = 0;
// ---- 问题接口:因 Application 实现需要加,所以接口也加了 ----
// 第 317 行:
virtual void notifyEventSync(int evt) = 0;
// 注释里写着:
// "因 Application 需要加,所以给 IApplication 也加了"
// 这正是"因实现定接口"的典型症状!
};
// application.h
class Application : public IApplication {
public:
bool init() override;
void start() override;
void stop() override;
void destroy() override;
// Application 的某个功能需要 notifyEventSync
// 于是直接在接口上加了这个方法
void notifyEventSync(int evt) override {
// 具体实现:同步通知某个事件
for (auto& listener : _listeners) {
listener->onEvent(evt); // 同步分发
}
}
private:
std::vector<EventListener*> _listeners;
};
三、为什么"只有一个实现"最容易出问题
通常情况:
IApplication ← Application(只有这一个实现!)
心理误区:
"反正就一个实现,接口和实现差不多是一回事"
"Application 需要这个方法,IApplication 加一下而已"
"以后也不会有第二个实现,无所谓"
实际后果(3个月后):
IApplication 变成了 Application 的"影子接口"
每次 Application 加功能,IApplication 跟着加
接口从 4 个方法膨胀到 40 个方法
新来的工程师:想实现一个 MockApplication 做测试
→ 被迫实现 40 个方法,其中 35 个完全不知道是干什么的
接口污染的增长模型:
∣It∣=∣I0∣+∑i=1tΔi|I_t| = |I_0| + \sum_{i=1}^{t} \Delta_i∣It∣=∣I0∣+i=1∑tΔi
其中 Δi\Delta_iΔi 是第 iii 次迭代中"因实现需要"而添加的方法数。若每个月平均新增 Δ=3\Delta = 3Δ=3 个方法,111 年后:
∣I12∣=4+3×12=40(膨胀 10 倍)|I_{12}| = 4 + 3 \times 12 = 40 \quad \text{(膨胀 10 倍)}∣I12∣=4+3×12=40(膨胀 10 倍)
每个实现类的负担:
被迫实现的无关方法数=∣It∣−∣核心方法∣→∞\text{被迫实现的无关方法数} = |I_t| - |\text{核心方法}| \rightarrow \infty被迫实现的无关方法数=∣It∣−∣核心方法∣→∞
四、问题的三个层次
4.1 第一层:notifyEventSync 根本不该在接口里
// 追问:IApplication 的调用方为什么需要 notifyEventSync?
// 调用方A:ModuleManager(管理应用模块)
class ModuleManager {
void initialize(IApplication* app) {
app->init(); // 合理:ModuleManager 需要初始化 app
app->start(); // 合理:ModuleManager 需要启动 app
// 问题:ModuleManager 需要 notifyEventSync 吗?
// app->notifyEventSync(EVT_MODULE_READY); // 为什么需要这个?
}
};
// 调用方B:TestHarness(测试框架)
class TestHarness {
void runTest(IApplication* app) {
app->init();
app->start();
// 测试框架需要 notifyEventSync 吗?
// 99% 的情况:不需要!
// 但因为接口有这个方法,测试框架被迫实现一个空的版本
}
};
// 结论:notifyEventSync 是 Application 内部实现细节
// 没有任何外部调用方真正需要通过 IApplication 调用它
// 它被放在接口里,纯粹是因为"Application 加了这个方法"
4.2 第二层:接口职责不清晰
// ✗ 职责混乱的接口:把所有东西都塞进去
class IApplication {
public:
// 生命周期(合理)
virtual bool init() = 0;
virtual void start() = 0;
virtual void stop() = 0;
virtual void destroy() = 0;
// 事件通知(不合理:这是内部实现细节)
virtual void notifyEventSync(int evt) = 0;
virtual void notifyEventAsync(int evt) = 0;
virtual void registerEventListener(void* l) = 0;
virtual void unregisterEventListener(void* l) = 0;
// 配置(不合理:调用方不应该通过 IApplication 改配置)
virtual void setDebugMode(bool on) = 0;
virtual void setLogLevel(int level) = 0;
virtual void setMaxMemory(size_t bytes) = 0;
// 状态查询(部分合理,部分不合理)
virtual bool isRunning() const = 0; // 合理
virtual int getInternalThreadCount() const = 0; // 不合理:内部细节
virtual void* getRawContext() = 0; // 不合理:暴露实现细节
};
五、解决方案
5.1 第一招:果断删除接口,只保留必要的核心约定
// ✓ 重新设计:IApplication 只包含调用方真正需要的方法
// 评估标准:如果没有这个方法,调用方还能正常工作吗?
// 能 → 删除;不能 → 保留
// i_application.h(精简后)
class IApplication {
public:
virtual ~IApplication() = default;
// ---- 只保留生命周期管理(这是外部调用方真正需要的)----
// 每个方法都问:调用方为什么需要这个?
virtual bool init() = 0; // 调用方需要初始化:✓ 保留
virtual void start() = 0; // 调用方需要启动:✓ 保留
virtual void stop() = 0; // 调用方需要停止:✓ 保留
virtual void destroy() = 0; // 调用方需要销毁:✓ 保留
// 状态查询(调用方确实需要知道是否在运行)
virtual bool isRunning() const = 0; // ✓ 保留
// ✗ 以下全部删除:
// virtual void notifyEventSync(int evt) = 0;
// → 删除理由:没有调用方通过 IApplication 接口调用此方法
// → 这是 Application 内部的事件分发机制
//
// virtual void setDebugMode(bool on) = 0;
// → 删除理由:调试配置不是对外契约,是实现细节
//
// virtual void* getRawContext() = 0;
// → 删除理由:暴露内部数据结构,违反封装
};
5.2 第二招:将 notifyEventSync 放回 Application,不暴露给接口
// application.h(重构后)
// Application 实现 IApplication 的核心接口
// notifyEventSync 作为 Application 的私有/内部方法
#pragma once
#include "i_application.h"
#include <vector>
#include <functional>
class Application : public IApplication {
public:
// ---- 实现 IApplication 接口(对外承诺)----
bool init() override;
void start() override;
void stop() override;
void destroy() override;
bool isRunning() const override { return _running; }
// ---- Application 自己的扩展 API(不通过接口暴露)----
// 这些方法只有知道具体类型 Application 的调用方才能用
// 通过接口 IApplication* 的调用方看不到这些方法
// 注册事件监听器(Application 层面的功能,不是接口层面的)
void addEventListener(std::function<void(int)> listener) {
_listeners.push_back(std::move(listener));
}
private:
// notifyEventSync 降级为私有方法!
// 它只是 Application 内部的实现细节,外部不需要直接调用
void notifyEventSync(int evt) {
// 同步遍历所有监听器
for (auto& listener : _listeners) {
listener(evt); // 同步分发
}
}
void notifyEventAsync(int evt); // 同理,也是私有
bool _running = false;
std::vector<std::function<void(int)>> _listeners;
};
5.3 第三招:接口添加评估流程
// 每次有人想给 IApplication 添加方法时,必须回答这四个问题:
/*
评估清单(添加接口前必须回答):
Q1. 哪些调用方(具体列举)需要通过 IApplication 调用这个方法?
如果答案是"只有 Application 自己的内部代码"
→ 不应该加到接口,改为私有方法
Q2. 如果有第二个实现类(如 MockApplication),
它能合理地实现这个方法吗?
如果答案是"不知道怎么 Mock"或"Mock 没有意义"
→ 这个方法不属于接口
Q3. 这个方法在 6 个月后还会存在吗?
如果它依赖于当前特定的实现细节
→ 不应该加到接口(接口应该稳定)
Q4. 不加这个接口,能否通过其他方式(事件、回调、组合)解决需求?
如果答案是"能"
→ 用更好的方式解决,不要污染接口
*/
// 具体示例:notifyEventSync(int evt) 的评估
// Q1: 哪些调用方需要通过 IApplication 调用 notifyEventSync?
// → 经过排查:没有任何外部模块通过 IApplication* 调用此方法
// → 结论:不应该加到接口 ✗
// Q2: MockApplication 如何实现 notifyEventSync?
// → Mock 版本不知道该做什么,只能写个空函数
// → 说明这个方法对外部没有语义意义 ✗
// Q3: 6个月后还存在吗?
// → notifyEventSync 依赖内部的 _listeners 容器实现细节
// → 很可能随着实现变化而改变 ✗
// Q4: 能否不加接口方法解决?
// → 完全可以:Application 内部私有调用即可 ✓
// 评估结论:4个问题3个不通过 → 坚决不加到 IApplication
5.4 第四招:从接口中拆分非核心逻辑
// ✓ 如果事件通知确实需要对外暴露,
// 应该拆分为独立的、职责单一的接口,而不是塞进 IApplication
// 方案:把事件相关功能提取为单独接口
class IEventEmitter {
public:
virtual ~IEventEmitter() = default;
virtual void addEventListener(std::function<void(int)> cb) = 0;
virtual void removeEventListener(std::function<void(int)> cb) = 0;
};
// IApplication 保持干净,只管生命周期
class IApplication {
public:
virtual ~IApplication() = default;
virtual bool init() = 0;
virtual void start() = 0;
virtual void stop() = 0;
virtual void destroy() = 0;
virtual bool isRunning() const = 0;
// 绝对不加 notifyEventSync!
};
// Application 同时实现两个接口(各司其职)
class Application : public IApplication,
public IEventEmitter {
public:
// IApplication 实现
bool init() override { /* ... */ return true; }
void start() override { _running = true; }
void stop() override { _running = false; }
void destroy() override { /* ... */ }
bool isRunning() const override { return _running; }
// IEventEmitter 实现
void addEventListener(std::function<void(int)> cb) override {
_listeners.push_back(std::move(cb));
}
void removeEventListener(std::function<void(int)> cb) override {
// 移除对应监听器(实际用 token/id 更好)
}
private:
// notifyEventSync 依然是私有方法
void notifyEventSync(int evt) {
for (auto& cb : _listeners) cb(evt);
}
bool _running = false;
std::vector<std::function<void(int)>> _listeners;
};
// 使用:调用方根据需要选择接口
void moduleInit(IApplication* app) {
app->init(); // 只用生命周期接口,看不到事件方法
}
void setupEvents(IEventEmitter* emitter) {
emitter->addEventListener([](int evt) {
printf("event: %d\n", evt);
});
}
// App 组装时转换接口
Application app;
moduleInit(&app); // 当作 IApplication 使用
setupEvents(&app); // 当作 IEventEmitter 使用
六、接口设计的黄金法则
6.1 接口最小化原则(ISP:接口隔离原则)
// ✗ 胖接口:把所有东西塞在一起
class IApplication {
virtual bool init() = 0;
virtual void start() = 0;
virtual void stop() = 0;
virtual void notifyEventSync(int evt) = 0; // 事件
virtual void setLogLevel(int level) = 0; // 配置
virtual void dumpMemoryStats() = 0; // 调试
virtual std::string getVersionString() const = 0; // 版本
// ...共30个方法
};
// ✓ 瘦接口:每个接口只做一件事
class IApplicationLifecycle { // 生命周期(核心)
virtual bool init() = 0;
virtual void start() = 0;
virtual void stop() = 0;
virtual void destroy() = 0;
};
class IEventEmitter { // 事件(可选)
virtual void on(int evt, std::function<void()> cb) = 0;
virtual void off(int evt) = 0;
};
class IConfigurable { // 配置(可选,仅特定调用方需要)
virtual void configure(const Config& cfg) = 0;
};
// 调试相关:完全不应该在接口里,用条件编译或单独的调试模块
接口的方法数与维护代价的关系:
维护代价∝∣I∣2\text{维护代价} \propto |I|^2维护代价∝∣I∣2
因为每次修改接口,所有 nnn 个实现类都需要更新,而每个实现类的修改代价又与接口大小成正比:
总修改代价=n实现类×∣I∣×c单次修改\text{总修改代价} = n_{\text{实现类}} \times |I| \times c_{\text{单次修改}}总修改代价=n实现类×∣I∣×c单次修改
将 ∣I∣|I|∣I∣ 从 303030 减小到 555,总修改代价降低 305=6\frac{30}{5} = 6530=6 倍。
6.2 接口稳定性检验
// 接口方法的稳定性自检:
// ✓ 稳定的接口方法特征:
// - 描述"做什么",不描述"怎么做"
// - 参数/返回值是稳定的数据类型(不是内部实现类型)
// - 有清晰的语义,任何实现者都知道该做什么
virtual bool init() = 0;
// 稳定:init 的语义永远清晰,任何实现都知道怎么做
virtual void start() = 0;
// 稳定:start 语义明确,无歧义
// ✗ 不稳定的接口方法特征:
// - 方法名包含实现细节(Sync/Async/Internal/Raw)
// - 参数是内部数据结构
// - 只有当前这一个实现知道该怎么实现
virtual void notifyEventSync(int evt) = 0;
// 不稳定:
// "Sync"暗示了实现细节(同步 vs 异步)
// 第二个实现类应该实现同步还是异步?不清晰
// 如果改成异步,接口名字就错了
virtual void* getRawContext() = 0;
// 不稳定:
// "Raw"暗示内部实现
// 不同实现的 context 完全不同,接口毫无意义
virtual void setInternalThreadCount(int n) = 0;
// 不稳定:
// "Internal"已经说明这不该在接口里
// 线程数是实现细节,不是外部契约
6.3 Mock 测试作为接口合理性验证
// 最强的接口合理性验证手段:尝试写一个 MockApplication
// 如果某个方法难以 Mock,说明它不该在接口里
// ✓ 合理的接口:MockApplication 很容易实现
class MockApplication : public IApplication {
public:
bool init() override {
initCalled = true;
return true; // Mock 返回成功,简单直接
}
void start() override { startCalled = true; }
void stop() override { stopCalled = true; }
void destroy() override { destroyCalled = true; }
bool isRunning() const override { return startCalled && !stopCalled; }
// 测试断言用
bool initCalled = false;
bool startCalled = false;
bool stopCalled = false;
bool destroyCalled = false;
};
// Mock 非常自然,每个方法都有明确的 Mock 实现 ← 说明接口设计合理
// ✗ 不合理的接口:notifyEventSync 如何 Mock?
class MockApplication : public IApplication {
void notifyEventSync(int evt) override {
// 做什么?
// 空实现?那测试没有意义
// 记录调用?但调用方根本不通过接口调用这个方法!
// Mock 无意义 ← 强烈信号:这个方法不该在接口里
}
};
七、总结:接口的郑重承诺
接口的本质是一种契约,一旦发布就要承担维护责任:
承诺的代价=∣接口方法数∣×∣实现类数∣×时间\text{承诺的代价} = |\text{接口方法数}| \times |\text{实现类数}| \times \text{时间}承诺的代价=∣接口方法数∣×∣实现类数∣×时间
越多的承诺⇒越大的维护负担⇒越难修改的系统\text{越多的承诺} \Rightarrow \text{越大的维护负担} \Rightarrow \text{越难修改的系统}越多的承诺⇒越大的维护负担⇒越难修改的系统
正确的接口设计哲学:
宁可少承诺,不可乱承诺\boxed{\text{宁可少承诺,不可乱承诺}}宁可少承诺,不可乱承诺
具体操作:
| 操作 | 何时执行 |
|---|---|
| 果断删除接口方法 | 发现某个方法只有一个实现且无外部调用方时 |
| 降级为私有方法 | 方法只服务于本类内部逻辑时 |
| 拆分为独立接口 | 方法属于不同的关注点时 |
| 通过事件/回调替代 | 需要通知外界但不想暴露细节时 |
| 做 Mock 验证合理性 | 每次添加新接口方法时 |
接口是模块对外的郑重承诺——每一个虚函数都是对所有调用方、所有实现者的永久约定。加之前三思,删时果断,这是接口设计的核心修养。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)