WIP Xiuno BBS插件开发经验谈之 面向对象编程并不忤逆程序本身的精神
# Xiuno BBS插件开发经验谈之 面向对象编程并不忤逆程序本身的精神
本文档由TRAE编写。本文档使用了AI辅助生成,可能包含错误或不完整的内容,需要校对。
本文包含大量代码,请在阅读前喝点水,并在阅读过程中适当休息。
本文对你的PHP水平有要求。
## 引言
当谈到Xiuno BBS的插件开发时,很多开发者可能会认为这是一个"轻量级"的任务,不需要复杂的编程范式。毕竟,Xiuno BBS本身的代码风格就是过程式编程,大量使用全局函数和数组来处理数据。
然而,随着插件功能越来越复杂,传统的开发方式往往会导致代码冗余、难以维护,甚至产生"认知负载过高"的问题。
本文将以 **任务中心重铸版** 插件为例,探讨如何在Xiuno BBS的生态中引入面向对象编程范式,从而降低开发和维护的认知负载,同时保持与Xiuno BBS核心精神的一致性。
## 一、认知负载问题:传统过程式开发的困境
### 1. 假设我们用纯过程式开发任务中心
想象一下,如果任务中心插件完全采用过程式编程,我们会面临什么?
#### 场景一:用户任务状态管理
```php
// 全局变量存储当前用户的任务状态
$abs_task_current_user_tasks = [];
$abs_task_current_user_activity = [];
// 更新任务进度的函数
function abs_task_update_task_progress($uid, $general_task_id, $progress) {
global $abs_task_current_user_tasks;
// 问题1:如果uid不是当前用户,就需要额外处理
if ($uid != $GLOBALS['uid']) {
// 从数据库读取该用户的任务状态
$user_tasks = cache_persist_get('abs_task__task_for_' . $uid);
// 更新进度...
// 保存回数据库...
// 认知负载+1:需要记住这个逻辑
} else {
// 使用全局变量
// 认知负载+1:需要区分当前用户和其他用户
}
}
```
**问题**:
- 全局变量污染:`$abs_task_current_user_tasks`、`$abs_task_current_user_activity`等变量散落在各处
- 状态管理混乱:当前用户和其他用户的处理逻辑不同
- 函数参数冗长:每个函数都需要传递`$uid`、`$task_id`、`$progress`等参数
- 认知负载高:开发者需要记住哪个全局变量存储什么数据,哪个函数处理哪种情况
#### 场景二:任务池与任务实例的关系
```php
// 任务池(通用任务ID)
$abs_task_pool = [
'thread_create' => [...],
'post_create' => [...],
];
// 用户任务列表(特定任务ID)
$abs_task_user_list = [
'thread_create__abc123' => [...], // 站长创建的"发帖达人"任务
'thread_create__def456' => [...], // 站长创建的"每日发帖"任务
];
// 更新进度时需要遍历
function abs_task_update_progress($uid, $general_task_id, $progress) {
global $abs_task_user_list;
foreach ($abs_task_user_list as $task_id => $task) {
// 检查是否匹配通用任务ID
if (strpos($task_id, $general_task_id . '__') === 0) {
// 更新进度...
// 认知负载+1:需要理解ID匹配规则
}
}
}
```
**问题**:
- ID匹配规则复杂:`thread_create__abc123`需要匹配`thread_create`
- 逻辑分散:遍历、匹配、更新、保存等逻辑分散在多个函数中
- 认知负载高:开发者需要理解"通用任务ID"和"特定任务ID"的关系
### 2. abs_task插件的实际解决方案
任务中心重铸版插件通过面向对象编程,优雅地解决了这些问题:
```php
// 创建用户专属的任务管理器
$tm = new TaskManager($uid);
// 更新进度——就这么简单!
$tm->updateTaskProgress('thread_create', 1);
```
这一行代码背后,隐藏了多少复杂性?让我们深入分析。
## 二、核心设计:面向对象的封装艺术
### 1. TaskPool类:任务模板的注册中心
```php
class TaskPool {
private $pool = [];
public function registerTask(
$id,
$name,
$description = '',
$required = 1,
$type = 'once',
$reward_credits = 0,
$reward_golds = 0,
$reward_rmbs = 0,
$reward_medal = null,
array $prerequisites = []
) {
// 确保前置任务存在,避免锁死任务
foreach ($prerequisites as &$value) {
if (isset($this->pool[$id])) {
continue;
} else {
unset($value);
}
}
$this->pool[$id] = [
'id' => $id,
'name' => $name,
'description' => $description,
'current' => 0,
'required' => $required,
'reward' => [
'credits' => $reward_credits,
'golds' => $reward_golds,
'rmbs' => $reward_rmbs,
],
'type' => $type,
'reward_medal' => (!is_null($reward_medal) && is_numeric($reward_medal) ? true : false),
'reward_medal_id' => (!is_null($reward_medal) && is_numeric($reward_medal) ? $reward_medal : 0),
'prerequisites' => $prerequisites,
'completed' => false,
'lastReset' => 0,
];
}
}
```
**设计亮点**:
- **单一职责**:TaskPool只负责注册和存储任务模板
- **封装**:任务模板的数据结构被封装在类内部
- **验证**:构造函数中验证前置任务是否存在,避免逻辑错误
**使用示例**(来自`hook/abs_task_registerTask.php`):
```php
$taskPool->registerTask(
/* id */ 'thread_create',
/* name */ '帖子创建',
/* description */ '',
/* required */ 10,
/* type */ 'daily',
/* reward_credits */ 10,
/* reward_golds */ 5,
/* reward_rmbs */ 0
);
```
### 2. TaskManager类:用户任务状态的核心管理器
这是整个插件的核心,让我们逐步拆解。
#### 2.1 构造函数:用户隔离的关键
```php
class TaskManager {
private $tasks = [];
private $taskPool;
public $uid; // readonly
private $gid;
private $totalActivity = 0;
private $dailyActivity = [0, 0, 0, 0, 0, 0, 0];
private $weeklyActivity = 0;
public function __construct($uid) {
$this->uid = intval($uid);
// 从缓存/数据库加载该用户的任务状态
$tasksData = cache_persist_get('abs_task__task_for_' . $uid);
$tasksRevision = cache_persist_get('abs_task__task_list_revision');
if (is_null($tasksData)) {
// 初始化任务列表
$this->tasks = $this->initTasks($uid);
} else {
if ($tasksData['_revision'] < $tasksRevision) {
// 版本更新,重新初始化
$this->tasks = $this->initTasks($uid);
} else {
// 直接加载数据
$this->tasks = is_array($tasksData) ? $tasksData : [];
}
}
// 加载活跃度数据
$activityData = cache_persist_get('abs_task__activity');
// ...
// 获取用户组信息
$temp_user = user_read_cache($this->uid);
$this->gid = intval($temp_user['gid']);
// 将用户添加到索引
$this->add_user_to_index($uid);
}
}
```
**设计亮点**:
- **用户隔离**:每个`TaskManager`实例对应一个用户,内部状态完全独立
- **延迟加载**:只有在创建实例时才从缓存/数据库加载数据
- **版本控制**:通过`_revision`字段检测任务列表更新,自动同步
**认知负载降低**:
- 开发者不需要关心数据从哪里来,只需要`new TaskManager($uid)`
- 不需要全局变量,每个用户的任务状态被封装在独立的实例中
#### 2.2 updateTaskProgress:核心业务逻辑的封装
```php
public function updateTaskProgress($generalTaskId, $progress) {
// 权限检查
if ($this->checkIsBanned()) {
xn_log('被封禁用户试图更新任务"' . $generalTaskId . '",已被阻止', 'abs_task_banned');
return;
}
// 遍历所有任务,寻找与通用任务ID相匹配的特定任务ID
foreach ($this->tasks as $taskId => $task) {
if (strpos($taskId, $generalTaskId . '__') === 0 && $progress !== 0) {
// 更新进度
$newCurrent = max(0, min($task['current'] + $progress, $task['required']));
if ($newCurrent != $task['current']) {
$this->tasks[$taskId]['current'] = $newCurrent;
xn_log('任务"' . $task['name'] . '"(ID ' . $taskId . ' )更新到 ' . $newCurrent . '(增量 ' . $progress . ' )', 'abs_task');
$this->saveTasks();
}
// 检查任务完成
$this->checkTaskCompletion($taskId);
// 添加活跃度
$this->addActivity($progress);
// 保存活跃度数据
$this->saveActivityData();
}
}
}
```
**设计亮点**:
- **自动匹配**:通过`strpos($taskId, $generalTaskId . '__') === 0`自动找到所有匹配的任务
- **原子操作**:更新进度、检查完成、添加活跃度、保存状态,一气呵成
- **边界保护**:`max(0, min(...))`确保进度在合理范围内
**认知负载降低**:
- 开发者只需要调用`$tm->updateTaskProgress('thread_create', 1)`
- 不需要知道"通用任务ID"和"特定任务ID"的匹配规则
- 不需要手动保存数据、更新活跃度等
#### 2.3 checkTaskCompletion:任务完成的自动化处理
```php
private function checkTaskCompletion($taskId) {
$task = $this->tasks[$taskId];
// 如果任务已经完成,直接返回
if (!empty($task['completed'])) {
return true;
}
// 检查进度是否达标
if ($task['current'] >= $task['required']) {
// 检查前置任务
if ($this->checkAllPrerequisitesCompleted($taskId)) {
// 双重保险,防止并发问题
if (!empty($this->tasks[$taskId]['completed'])) {
return true;
}
// 标记完成
$this->tasks[$taskId]['completed'] = true;
xn_log('任务"' . $task['name'] . '"(ID ' . $taskId . ')已完成', 'abs_task');
// 发放奖励
$this->grantRewards($taskId);
// 保存状态
$this->saveTasks();
return true;
}
}
return false;
}
```
**设计亮点**:
- **防重复**:多次检查`completed`状态,防止重复发放奖励
- **前置任务检查**:自动检查前置任务是否完成
- **自动化**:任务完成后自动发放奖励、发送通知
**认知负载降低**:
- 开发者不需要手动检查任务是否完成
- 不需要手动发放奖励、发送通知
### 3. Hook文件的极简化
在过程式开发中,hook文件可能包含大量业务逻辑。但在abs_task插件中,hook文件变得极其简洁:
```php
// hook/thread_create_thread_end.php
$TaskManager->updateTaskProgress('thread_create', 1);
```
**认知负载降低**:
- Hook文件只负责"触发",不包含业务逻辑
- 业务逻辑全部封装在`TaskManager`类中
- 开发者可以快速理解每个hook的作用
## 三、面向对象如何降低认知负载
### 1. 用户隔离:每个用户独立的任务空间
**过程式方式**:
```php
// 全局变量存储当前用户
$abs_task_current_user = $uid;
$abs_task_current_tasks = [...];
// 如果要操作其他用户
function abs_task_update_for_user($uid, $task_id, $progress) {
if ($uid == $GLOBALS['abs_task_current_user']) {
// 使用全局变量
} else {
// 从数据库加载
$tasks = cache_persist_get('abs_task__task_for_' . $uid);
// 更新...
// 保存...
}
}
```
**面向对象方式**:
```php
// 每个用户独立的实例
$tm_current = new TaskManager($current_uid);
$tm_other = new TaskManager($other_uid);
// 操作完全一致
$tm_current->updateTaskProgress('thread_create', 1);
$tm_other->updateTaskProgress('thread_create', 1);
```
**认知负载降低**:
- 不需要区分"当前用户"和"其他用户"
- 不需要全局变量
- 操作方式完全一致
### 2. 数据封装:状态管理的清晰边界
**过程式方式**:
```php
// 数据散落在各处
$abs_task_tasks = [...];
$abs_task_activity = [...];
$abs_task_settings = [...];
// 函数需要传递大量参数
function abs_task_do_something($uid, $tasks, $activity, $settings, ...) {
// ...
}
```
**面向对象方式**:
```php
class TaskManager {
private $tasks = [];
private $totalActivity = 0;
private $dailyActivity = [0, 0, 0, 0, 0, 0, 0];
private $weeklyActivity = 0;
private $abs_task_setting;
// 所有数据都在类内部,不需要传递
}
```
**认知负载降低**:
- 数据被封装在类内部,不会污染全局命名空间
- 不需要传递大量参数
- 数据的访问和修改受到控制
### 3. 自动化:复杂逻辑的隐藏
**过程式方式**:
```php
function abs_task_update_progress($uid, $task_id, $progress) {
// 1. 检查权限
if (abs_task_is_banned($uid)) {
return;
}
// 2. 获取任务数据
$tasks = cache_persist_get('abs_task__task_for_' . $uid);
// 3. 更新进度
foreach ($tasks as $id => $task) {
if (strpos($id, $task_id . '__') === 0) {
$tasks[$id]['current'] += $progress;
// 4. 检查完成
if ($tasks[$id]['current'] >= $tasks[$id]['required']) {
// 5. 检查前置任务
if (abs_task_check_prerequisites($tasks, $id)) {
// 6. 发放奖励
abs_task_grant_rewards($uid, $tasks[$id]);
// 7. 发送通知
abs_task_send_notice($uid, $tasks[$id]);
}
}
}
}
// 8. 保存数据
cache_persist_set('abs_task__task_for_' . $uid, $tasks);
// 9. 更新活跃度
abs_task_update_activity($uid, $progress);
}
```
**面向对象方式**:
```php
$tm->updateTaskProgress('thread_create', 1);
```
**认知负载降低**:
- 所有复杂逻辑被封装在方法内部
- 开发者只需要调用一个方法
- 不需要记住每个步骤
### 4. 可测试性:单元测试的友好支持
**过程式方式**:
```php
// 测试需要模拟全局变量和函数
function test_abs_task_update_progress() {
global $uid, $abs_task_tasks;
$uid = 1;
$abs_task_tasks = [...];
// 调用函数
abs_task_update_progress(1, 'thread_create', 1);
// 断言...
}
```
**面向对象方式**:
```php
// 测试可以独立创建实例
function test_task_manager() {
$tm = new TaskManager(1);
// 调用方法
$tm->updateTaskProgress('thread_create', 1);
// 断言
$task = $tm->getTask('thread_create__abc123');
$this->assertEquals(1, $task['current']);
}
```
**认知负载降低**:
- 不需要模拟全局变量
- 可以独立测试每个实例
- 测试代码更清晰
## 四、与Xiuno BBS精神的契合
### 1. 轻量级原则
Xiuno BBS的核心精神之一是轻量级。有人可能会认为面向对象编程会让代码变得臃肿,但实际上:
- **代码复用**:通过封装,避免了重复代码
- **逻辑集中**:相关逻辑集中在一个类中,而不是分散在多个文件中
- **简洁调用**:对外接口简洁,内部实现复杂
### 2. 扩展性原则
Xiuno BBS通过Hook机制实现扩展性,而abs_task插件通过面向对象实现了另一种扩展性:
- **Hook点**:在类内部预留了多个hook点,如`// hook abs_task_onUpdateTaskProgress.php`
- **继承扩展**:可以通过继承`TaskManager`类来扩展功能
- **组合扩展**:可以通过组合`TaskPool`和`TaskManager`来实现更复杂的功能
### 3. 性能原则
Xiuno BBS注重性能,而abs_task插件通过以下方式保持高性能:
- **延迟加载**:只有在创建`TaskManager`实例时才加载数据
- **缓存机制**:使用`cache_persist_get/set`实现缓存+数据库的双重存储
- **按需保存**:只有在数据变化时才保存
### 4. 兼容性原则
abs_task插件保持了与Xiuno BBS的兼容性:
- **面向过程接口**:提供了`abs_task_credits_display_html()`等面向过程的函数
- **全局变量**:在必要时使用全局变量,如`global $user`
- **Hook机制**:遵循Xiuno BBS的Hook机制
## 五、实践建议
### 1. 渐进式引入
对于现有插件,可以采用渐进式的方式引入面向对象编程:
1. **识别核心业务逻辑**:找出插件中最复杂的部分
2. **创建服务类**:将核心逻辑封装到服务类中
3. **重构Hook文件**:将Hook文件简化为调用服务类的方法
4. **提供面向过程接口**:为其他开发者提供面向过程的函数
### 2. 合理设计
在引入面向对象编程时,要注意:
- **单一职责**:每个类只负责一件事
- **开放封闭**:对扩展开放,对修改封闭
- **依赖注入**:通过构造函数注入依赖,而不是使用全局变量
### 3. 保持兼容
确保改造后的代码与Xiuno BBS保持兼容:
- **遵循命名规范**:类名、方法名遵循Xiuno BBS的命名规范
- **使用Xiuno BBS函数**:使用`db_`系列函数、`cache_`系列函数等
- **预留Hook点**:在类内部预留Hook点,方便其他开发者扩展
## 六、总结
通过abs_task插件的分析,我们可以看到面向对象编程在Xiuno BBS中的实践:
1. **用户隔离**:通过实例化,每个用户拥有独立的任务空间
2. **数据封装**:任务状态、活跃度数据被封装在类内部
3. **自动化**:复杂逻辑被封装在方法内部,对外提供简洁接口
4. **可测试**:可以独立测试每个实例,不需要模拟全局变量
面向对象编程并不忤逆Xiuno BBS的精神,反而可以作为一种有效的工具来降低插件开发的认知负载,提高代码的可维护性和可扩展性。
记住,编程范式只是工具,最终的目标是开发出高质量的插件。无论是过程式编程还是面向对象编程,只要能够降低认知负载、提高开发效率,就是好的编程方式。
## 附录:abs_task插件的核心类图
```
┌─────────────────┐
│ TaskPool │
├─────────────────┤
│ - pool: array │
├─────────────────┤
│ + registerTask()│
│ + getAllTemplates()│
│ + getTemplateById()│
└─────────────────┘
┌─────────────────────────────┐
│ TaskManager │
├─────────────────────────────┤
│ - tasks: array │
│ - uid: int (readonly) │
│ - gid: int │
│ - totalActivity: int │
│ - dailyActivity: array │
│ - weeklyActivity: int │
├─────────────────────────────┤
│ + __construct($uid) │
│ + updateTaskProgress() │
│ + getAllTasks() │
│ + getTask($taskId) │
│ + getCompletedTasks() │
│ + getUncompletedTasks() │
│ + getTasksByType($type) │
│ - checkTaskCompletion() │
│ - grantRewards() │
│ - resetPeriodicTasks() │
│ - saveTasks() │
│ - addActivity() │
│ - saveActivityData() │
└─────────────────────────────┘
┌─────────────────────────────┐
│ 辅助函数(面向过程接口) │
├─────────────────────────────┤
│ abs_task_credits_display_html()│
│ abs_task_is_debounce_triggered()│
│ abs_task_get_debounce_cache_key()│
│ abs_task_cron_daily_reset() │
└─────────────────────────────┘
```
## 附录:Hook文件示例
```php
// hook/abs_task_registerTask.php
// 注册任务到任务池
$taskPool->registerTask('thread_create', '帖子创建', '', 10, 'daily', 10, 5, 0);
// hook/thread_create_thread_end.php
// 帖子创建完成时更新任务进度
$TaskManager->updateTaskProgress('thread_create', 1);
// hook/post_post_end.php
// 回帖完成时更新任务进度
$TaskManager->updateTaskProgress('post_create', 1);
```
通过这些简洁的Hook文件,我们可以看到面向对象编程如何将复杂的业务逻辑隐藏在类内部,让插件开发变得简单而优雅。