软件之禅(四)用观察者模式连接 Data 和 Output

文章介绍了如何通过观察者模式在QT应用中实现Data和Output的解耦,具体示例是一个名为AfcQtEdit的类从Output类继承并在QT对话框中显示数据。创建了一个DataString类作为字符串主题,当主题更新时,通知所有注册的观察者,如AfcQtEdit,从而更新显示。这种方式使得显示逻辑和数据源可以独立发展。

黄国强 2023/06/09

        为了将 Data 和 Ouput 解耦,我们这里的 Data 和 Ouput 是使用观察者模式来交互。Data 是主题,Ouput 是观察者。代码如下。

// 观察者
class Output
{
	// Regist 主题时,主题回调此函数,参数是 json 格式的主题信息
	void SubjectInfo(const Json::Value& val);
	// 观察者模式的主题调用此函数输出内容
	void SubjectUpdate(const std::string& val);
	void SubjectUpdate(int val);
	void SubjectUpdate(double val);
	void SubjectUpdate(const Json::Value& val);
};

// 主题
class Data
{
public:
	// 向观察者发布主题
	void Publish();
	// 注册指定 Output 观察者
	void Regist(Output * p);
	// 注销指定 Output 观察者
	void RegistUn(Output * p);
};

        我们准备在 QT 的对话框中显示 “Hello World”。所以,这个字符串观察类应该从 Output 继承。同时我们再创建一个字符串主题类。下面是类图,图中 AfcQtEdit 是观察类,DataString 是字符串主题类。

AfcQtEdit 代码如下。

///////////////////////////////////////////////////////////////////////////////////////////

class AfcQtEdit : public Output
{
public:
	AfcQtEdit(QLabel * pLable, QLineEdit * pEdit);

protected: // Output
	virtual void DoSubjectUpdate(const std::string& val);

private:
	QLabel * _pLable{ nullptr };
	QLineEdit *	_pEdit{ nullptr };
};

/////////////////////////////////////////////////////////////////////////////////////////// AfcQtEdit

AfcQtEdit::AfcQtEdit(QLabel * pLable, QLineEdit * pEdit)
	: _pLable(pLable)
	, _pEdit(pEdit)
{
}

void AfcQtEdit::DoSubjectUpdate(const std::string & val)
{
	_pEdit->setText(QString::fromLocal8Bit(val.c_str()));
}

        在QT 的对话框中添加如下代码。

/////////////////////////////////////////////////////////////////////////////////////////// QT 对话框

class TestAFC : public QDialog
{
    Q_OBJECT

public:
    TestAFC(QWidget *parent = nullptr);
    ~TestAFC();

private:
    Ui::TestAFCClass *ui;

	std::shared_ptr<AfcQtEdit>	_pEdit;
	AFC::DataPtr				_pData;
};

/////////////////////////////////////////////////////////////////////////////////////////// TestAFC

TestAFC::TestAFC(QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::TestAFCClass())
{
    ui->setupUi(this);

	_pEdit.reset(new AfcQtEdit(ui->label, ui->lineEdit)); // 创建一个字符串观察者
	_pData = Data::CreateObj("string"); // 创建 String 主题,默认值是 "Hello World!"
	_pData->Regist(_pEdit.get()); // 注册一个观察者
	_pData->Publish(); // 将主题发布给所有观察者
}

TestAFC::~TestAFC()
{
    delete ui;
}

 最终效果如下图。

        正如上面提到的,这样的设计达到了解耦的目的。后续主题和显示是独立发展的。比如,Output 可以增加对 MFC, WPF 甚至浏览器的支持,而 Data 无需进行任何修改。

[参考链接]

软件之禅(三) 0,1,n

软件之禅(五)Input 出场

<think>我们参考了用户提供的两个引用(引用[1]引用[2]),它们都是关于JavaScript观察者模式的实现。用户的问题是“我想了解或实现JavaScript中的观察者模式 请问JavaScript 观察者模式 实现方法 示例”,因此我们需要给出一个清晰、实用的JavaScript观察者模式实现,并附上示例代码。 观察者模式JavaScript中通常有两种常见实现方式: 1. 基于类的面向对象实现(如引用[1]的PubSub类) 2. 使用函数对象字面量的实现(也可以使用构造函数) 我们将结合引用[1]引用[2]的实现思路,给出一个更完整的示例,并解释关键步骤。 ### JavaScript观察者模式实现 在JavaScript中,观察者模式(也称为发布-订阅模式)的核心是维护一个事件对象(event object),该对象存储事件类型(eventType)对应的回调函数数组(callbacks)。当触发(emit)某个事件类型时,所有注册的回调函数都会被执行。 #### 实现步骤: 1. **创建一个发布者对象(或类)**:用于存储事件处理函数,并提供绑定(on/ bind)、触发(emit/ touch)、解绑(off/ unbind)等方法。 2. **事件注册**:允许观察者通过事件类型注册回调函数(即订阅事件)。 3. **事件触发**:当事件发生时,执行所有注册的回调函数,并传递参数。 4. **取消订阅**:允许移除特定事件类型下的某个回调函数或移除整个事件类型。 下面是一个基于类的实现(参考引用[1]并改进): ```javascript class PubSub { constructor() { // 存储事件及其对应的回调函数数组 this.handlers = {}; } // 订阅事件:eventType为事件类型,callback为回调函数 on(eventType, callback) { if (typeof callback !== 'function') { throw new Error('callback must be a function'); } // 如果事件类型不存在,则初始化为空数组 if (!this.handlers[eventType]) { this.handlers[eventType] = []; } // 将回调函数添加到事件类型对应的数组中 this.handlers[eventType].push(callback); // 返回this以支持链式调用 return this; } // 发布事件:eventType为事件类型,...args为传递的参数 emit(eventType, ...args) { if (this.handlers[eventType]) { // 遍历执行所有回调函数 this.handlers[eventType].forEach(callback => { callback.apply(this, args); }); } return this; } // 取消订阅:移除特定事件类型下的某个回调函数 off(eventType, callback) { if (this.handlers[eventType]) { // 过滤掉要移除的回调函数 this.handlers[eventType] = this.handlers[eventType].filter( cb => cb !== callback ); // 如果该事件类型没有回调函数了,则删除该事件类型 if (this.handlers[eventType].length === 0) { delete this.handlers[eventType]; } } return this; } // 一次性订阅:执行一次后自动取消订阅 once(eventType, callback) { const onceWrapper = (...args) => { callback.apply(this, args); this.off(eventType, onceWrapper); }; this.on(eventType, onceWrapper); return this; } // 移除整个事件类型(引用[1]中的removeLisener方法,这里命名为removeEvent) removeEvent(eventType) { if (this.handlers[eventType]) { delete this.handlers[eventType]; } return this; } } ``` #### 使用示例 ```javascript // 创建发布者实例 const pubsub = new PubSub(); // 定义回调函数 function callback1(data) { console.log('Callback 1:', data); } function callback2(data) { console.log('Callback 2:', data); } // 订阅事件'event1' pubsub.on('event1', callback1); pubsub.on('event1', callback2); // 触发事件'event1',输出: // Callback 1: Hello // Callback 2: Hello pubsub.emit('event1', 'Hello'); // 取消订阅callback1 pubsub.off('event1', callback1); // 再次触发事件'event1',只输出: // Callback 2: World pubsub.emit('event1', 'World'); // 一次性订阅 pubsub.once('event2', (data) => { console.log('Once:', data); }); // 第一次触发event2,输出:Once: First time pubsub.emit('event2', 'First time'); // 第二次触发event2,无输出(因为已经取消订阅) pubsub.emit('event2', 'Second time'); // 移除整个事件类型event1 pubsub.removeEvent('event1'); pubsub.emit('event1', 'No output'); // 无输出 ``` ### 解释 - **on方法**:用于订阅事件,将回调函数存储到对应事件类型的数组中。 - **emit方法**:触发事件,执行所有订阅的回调函数,并传递参数。 - **off方法**:取消特定事件类型下的某个回调函数的订阅。 - **once方法**:订阅一次,触发后自动取消订阅。 - **removeEvent方法**:移除整个事件类型(包括所有回调函数)。 ### 应用场景 - **模块间通信**:在大型应用中,不同模块之间通过事件通信,避免直接依赖。 - **异步操作完成通知**:例如,多个组件等待某个数据加载完成。 - **用户界面交互**:如按钮点击事件被多个处理函数监听。 ### 注意事项 - **内存泄漏**:如果订阅后忘记取消,可能导致回调函数无法被垃圾回收。因此,在不需要时应及时取消订阅。 - **性能**:如果某个事件类型有大量回调函数,触发时可能影响性能,需注意优化。 通过以上实现,我们可以在JavaScript项目中灵活使用观察者模式来解耦代码。 §§相关问题§§ 1. 观察者模式发布-订阅模式有什么区别? 2. 如何避免观察者模式中的内存泄漏问题? 3. 观察者模式在哪些框架或库中被广泛应用? 4. 观察者模式适用于实时数据处理吗?为什么?</think>### JavaScript 观察者模式实现详解 观察者模式(Observer Pattern)在 JavaScript 中通常通过**发布-订阅机制**实现,它允许对象(发布者)维护一组依赖对象(订阅者),并在状态变化时自动通知所有订阅者。下面是完整的实现方法示例: #### 核心实现方法 ```javascript class PubSub { constructor() { // 存储事件类型及其对应的回调函数数组 this.handlers = {}; } // 订阅事件:eventType-事件类型, callback-回调函数 on(eventType, callback) { if (!(eventType in this.handlers)) { this.handlers[eventType] = []; } this.handlers[eventType].push(callback); return this; // 支持链式调用 } // 发布事件:eventType-事件类型, ...args-传递的参数 emit(eventType, ...args) { if (this.handlers[eventType]) { this.handlers[eventType].forEach(callback => { callback.apply(this, args); // 执行所有订阅的回调 }); } return this; // 支持链式调用 } // 取消订阅:移除特定回调 off(eventType, callback) { if (this.handlers[eventType]) { this.handlers[eventType] = this.handlers[eventType].filter( cb => cb !== callback ); // 若该事件类型无回调,删除空数组 if (this.handlers[eventType].length === 0) { delete this.handlers[eventType]; } } return this; // 支持链式调用 } // 清除所有订阅 clear(eventType) { if (eventType) { delete this.handlers[eventType]; } else { this.handlers = {}; } return this; } } ``` #### 使用示例 ```javascript // 1. 创建发布者实例 const eventBus = new PubSub(); // 2. 定义回调函数 const logWeather = (temp, humidity) => { console.log(`天气更新: ${temp}℃, 湿度${humidity}%`); }; const alertHotWeather = (temp) => { if (temp > 30) console.log("高温警报: 请避免户外活动!"); }; // 3. 订阅天气更新事件 eventBus.on('weatherUpdate', logWeather); eventBus.on('weatherUpdate', alertHotWeather); // 4. 发布天气更新事件 eventBus.emit('weatherUpdate', 32, 65); // 输出: // 天气更新: 32℃, 湿度65% // 高温警报: 请避免户外活动! // 5. 取消单个订阅 eventBus.off('weatherUpdate', alertHotWeather); // 6. 再次发布事件(只触发剩余订阅) eventBus.emit('weatherUpdate', 28, 70); // 输出: 天气更新: 28℃, 湿度70% // 7. 清除所有订阅 eventBus.clear('weatherUpdate'); eventBus.emit('weatherUpdate', 25, 60); // 无输出 ``` #### 关键特性说明 1. **松耦合设计** - 发布者无需知道订阅者的具体实现 - 订阅者只需实现约定的回调函数接口 2. **动态订阅管理** ```javascript // 动态添加订阅 const newSubscriber = (data) => console.log(`新订阅: ${data}`); eventBus.on('dataUpdate', newSubscriber); // 动态移除订阅 eventBus.off('dataUpdate', newSubscriber); ``` 3. **多事件类型支持** ```javascript // 订阅不同事件类型 eventBus.on('userLogin', user => console.log(`${user}登录`)); eventBus.on('payment', amount => console.log(`支付金额: ¥${amount}`)); // 触发不同事件 eventBus.emit('userLogin', '张三'); eventBus.emit('payment', 199.9); ``` 4. **参数传递** - 通过`emit`的第二个参数开始传递任意数量参数 - 回调函数通过参数获取数据 #### 实际应用场景 1. **UI组件通信** ```javascript // 按钮点击通知多个组件 document.getElementById('btn').addEventListener('click', () => { eventBus.emit('buttonClicked', { id: 'btn-1', time: Date.now() }); }); // 不同组件订阅事件 eventBus.on('buttonClicked', data => updateHeader(data)); eventBus.on('buttonClicked', data => logActivity(data)); ``` 2. **数据状态同步** ```javascript // 数据模型 const userModel = { name: '', setUserName(name) { this.name = name; eventBus.emit('userChanged', name); // 通知变化 } }; // 视图更新 eventBus.on('userChanged', name => { document.getElementById('username').textContent = name; }); ``` #### 注意事项 1. **内存泄漏预防** - 组件销毁时务必调用`off()`取消订阅 - 使用弱引用(WeakMap)存储处理程序(高级用法) 2. **错误处理** ```javascript emit(eventType, ...args) { if (this.handlers[eventType]) { this.handlers[eventType].forEach(callback => { try { callback.apply(this, args); } catch (e) { console.error(`处理${eventType}时出错:`, e); } }); } } ``` 3. **性能优化** - 避免在频繁触发的事件中执行重操作 - 对高频事件使用节流(throttle)或防抖(debounce) 此实现参考了发布-订阅模式的核心思想,适用于大多数JavaScript应用场景[^1][^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

acloud_csu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值