C++ 任务管理系统设计实战:需求分析、核心类建模与 TaskManager 实现详解(awesome-low-level-design)
C++ 任务管理系统设计实战:需求分析、核心类建模与 TaskManager 实现详解(awesome-low-level-design)
本文基于开源仓库 awesome-low-level-design 中 C++ 任务管理系统实现 的完整设计文档,逐步拆解一个任务管理系统(Task Management System)的需求分析、类与枚举建模,并结合仓库源码(Task / User / TaskManager 及演示程序)深入讲解每个类的职责、关键方法的调用链与边界处理。读完本文,你将掌握一个典型的 Low Level Design 面试题从需求到 C++ 源码落地的完整过程,并理解依赖任务的状态流转、任务分配的一致性维护等实战细节。
需求分析:一个任务管理系统需要具备哪些能力
设计文档 README 开篇给出了 7 条核心需求,它们共同决定了后续类设计的方向:
- 任务生命周期管理:系统应允许用户创建、更新和删除任务;
- 任务属性完整:每个任务应包含标题(title)、描述(description)、截止日期(due date)、优先级(priority)和状态(status,如 pending、in progress、completed);
- 协作与提醒:用户应能将任务分配给其他用户,并为任务设置提醒;
- 检索能力:系统应支持基于多种条件(如优先级、截止日期、被分配用户)搜索和过滤任务;
- 状态推进与历史:用户应能将任务标记为已完成,并查看其任务历史;
- 并发一致性:系统应能处理任务的并发访问并保证数据一致性;
- 可扩展性:系统应易于扩展,以便容纳未来的功能增强。
在 C++ 实现中,这些需求被映射为 User、Task、TaskManager 三个核心类以及 TaskStatus、TaskPriority 两个枚举,入口演示由 TaskManagerDemo 承担。仓库根目录的 problems/task-management-system.md 还给出了该题的标准 UML 类图(taskmanagementsystem-class-diagram.png),可用于对照源码理解整体结构。
任务管理系统 UML 类图
枚举设计:任务状态与优先级
在 Task.hpp 中,任务状态与优先级被建模为两个 enum class(C++11 强类型枚举,避免与整型隐式混用):
enum class TaskStatus {
TODO,
IN_PROGRESS,
COMPLETED,
BLOCKED
};
enum class TaskPriority {
LOW,
MEDIUM,
HIGH,
URGENT
};
TaskStatus相比设计文档举例的"pending / in progress / completed"多了一个BLOCKED状态,用于表达任务因依赖未满足而无法推进的场景;TaskPriority提供四档优先级(低/中/高/紧急),MEDIUM作为构造任务的默认值(见 TaskManager.hpp 的默认参数TaskPriority::MEDIUM)。
状态与优先级的可读输出在 Task.cpp 的 displayInfo() 中通过 switch 完成,便于调试与演示。
核心类建模:User 与 Task
User:用户实体的最小闭环
User.hpp 中的 User 类持有一个用户的最小信息集:
class User {
private:
std::string userId;
std::string username;
std::string email;
std::vector<std::string> assignedTasks; // Task IDs
bool active;
...
};
要点:
userId由TaskManager统一生成(格式U1、U2…,见 TaskManager.cpp),Task一侧只保存用户 ID 字符串而非对象指针,两者通过 ID 解耦;assignedTasks保存的是任务 ID 列表,属于"User 知道它被分配了哪些任务"的反向索引,便于 TaskManager::displayUserTasks 直接列出某个用户的任务,对应需求"按被分配用户过滤任务";addTask/removeTask在 User.cpp 中用std::find做去重,保证同一任务不会重复出现在用户的分配列表里。
Task:任务的完整属性与自维护结构
Task.hpp 中的 Task 类对应需求 2 的完整属性集:
class Task {
private:
std::string taskId;
std::string title;
std::string description;
std::string assignedTo;
TaskStatus status;
TaskPriority priority;
std::time_t dueDate;
std::vector<std::string> dependencies; // Task IDs
std::vector<std::string> comments;
...
};
值得注意的实现细节:
- 截止日期:
dueDate使用std::time_t表示,构造函数将其初始化为0表示"未设置截止日期"(Task.cpp),输出时仅当非 0 才打印(Task.cpp); - 任务依赖:
dependencies存放前置任务 ID,addDependency在 Task.cpp 同样做了去重,这是后续"依赖未完成则不能开工"状态机的基础; - 评论:
comments支持为任务追加评论,对应设计文档中"设置提醒/备注"的协作诉求; - 新任务默认状态为
TODO(Task.cpp)。
核心控制器:TaskManager 的设计权衡
设计文档明确指出 TaskManager 是系统的核心,并描述了两种关键设计决策:
- 单例模式:确保全局只有一个任务管理器实例;
- 并发安全:使用
ConcurrentHashMap与CopyOnWriteArrayList处理并发访问(这是 Java 版实现的线程安全策略)。
从源码结构看,C++ 版本做了取舍:TaskManager 采用普通类而非单例,存储使用 std::vector(TaskManager.hpp):
class TaskManager {
private:
std::vector<Task*> tasks;
std::vector<User*> users;
int taskIdCounter;
int userIdCounter;
...
};
这带来两点可以明确说明的事实:
- README 中关于
ConcurrentHashMap/CopyOnWriteArrayList的表述描述的是 Java 实现的并发策略;C++ 实现面向教学演示,选择了最直观的std::vector线性存储,并未引入锁机制。读者在工程化时应自行叠加std::mutex或改用std::shared_mutex; - 尽管未使用单例,
TaskManager仍通过 ID 计数器(T1/U1自增)集中管理身份分配,承担了"唯一入口"的职责;如需单例化,仅需将构造私有化并增加getInstance(),类的职责边界无需改动——这正是需求 7"可扩展性"的体现。
TaskManager 的方法集覆盖了设计文档要求的全部能力,下面按职责拆解。
用户与任务的创建/删除
registerUser(TaskManager.cpp)生成用户 ID、构造User并加入users;createTask(TaskManager.cpp)生成任务 ID 并以MEDIUM为默认优先级构造Task;removeUser(TaskManager.cpp)在删除用户前,会先遍历所有任务,把分配给该用户的任务的assignedTo清空(置为""),避免出现悬空的分配关系;removeTask(TaskManager.cpp)是一个一致性维护的范例:删除任务时同步执行三步清理——① 从被分配用户的assignedTasks中移除该任务;② 从所有其他任务的dependencies中移除对该任务的依赖引用;③ 再释放内存并从tasks中移除。
任务分配的一致性
assignTask(TaskManager.cpp)演示了双向索引的同步维护:
// 先把任务从前任被分配者名下移除
if (!task->getAssignedTo().empty()) {
if (User* prevUser = findUser(task->getAssignedTo())) {
prevUser->removeTask(taskId);
}
}
task->setAssignedTo(userId); // Task 侧记录被分配者
user->addTask(taskId); // User 侧记录任务
只有任务与被分配者都存在时才返回 true,否则返回 false,保证了 Task.assignedTo 与 User.assignedTasks 永远保持对称,对应需求"将任务分配给其他用户"。
依赖驱动的状态机
updateTaskStatus(TaskManager.cpp)是本实现中最有业务价值的逻辑:只有当任务的全部依赖都处于 COMPLETED 状态时,任务才允许进入 IN_PROGRESS:
if (status == TaskStatus::IN_PROGRESS && !checkDependenciesMet(task)) {
std::cout << "Cannot start task: dependencies not met" << std::endl;
return false;
}
task->setStatus(status);
checkDependenciesMet(TaskManager.cpp)逐个校验依赖任务:依赖不存在或依赖状态非 COMPLETED 都视为"依赖未满足"。这一机制让 BLOCKED 语义落到实处——依赖未完成的任务无法开工,保证了任务推进顺序的正确性。
搜索与过滤
设计文档要求支持按条件搜索过滤任务,C++ 实现提供了两个方法:
getTasksByStatus(TaskManager.cpp):用std::copy_if按状态过滤;getTasksByPriority(TaskManager.cpp):按优先级过滤。
配合 displayUserTasks(按用户列出)与 displayAllTasks(列出全部并带分隔线),四类查询方式恰好覆盖了"按优先级、按状态、按被分配用户"的检索需求。
演示程序:从注册用户到依赖任务全流程
入口文件 TaskManagerDemo.cpp 完整演示了系统的运行闭环,流程如下:
- 注册用户:注册
john_doe与alice_smith两名用户,并调用displayAllUsers()输出初始状态(L8-L12); - 创建任务:创建 "Design Database"(HIGH)、"Implement API"(MEDIUM)、"Write Tests"(MEDIUM)三个任务(L15-L17);
- 建立依赖链:
addTaskDependency形成DB → API → Tests的链式依赖(L20-L21):- "Implement API" 依赖 "Design Database";
- "Write Tests" 依赖 "Implement API";
- 分配任务:任务 1 分给 john,任务 2、3 分给 alice(L24-L26);
- 验证依赖拦截:将任务 1 置为
IN_PROGRESS并加评论;随后尝试把任务 2 置为IN_PROGRESS,由于依赖(任务 1)尚未完成,该调用会被拒绝并打印 "Cannot start task: dependencies not met"(L29-L33); - 完成前置任务后放行:将任务 1 置为
COMPLETED后,任务 2 才能成功进入IN_PROGRESS(L36-L40); - 结果展示:分别输出全部任务、按用户分组的任务、以及处于
IN_PROGRESS状态的任务列表(L43-L55)。
这一流程直观展示了"依赖未满足 → 阻塞开工 → 依赖完成 → 放行"的完整状态流转,是阅读 updateTaskStatus 与 checkDependenciesMet 实现的最佳入口。
编译与运行
该实现目录下没有 CMakeLists.txt 或 Makefile,四个源文件(Task.cpp、User.cpp、TaskManager.cpp、TaskManagerDemo.cpp)与对应头文件直接组成一个可执行程序。可以推断直接用任意支持 C++11 及以上的编译器即可构建,例如:
g++ -std=c++17 -o task_manager_demo \
Task.cpp User.cpp TaskManager.cpp TaskManagerDemo.cpp
./task_manager_demo
运行输出将包含用户注册信息、三任务创建结果、依赖拦截提示("Cannot start task: dependencies not met")以及按用户/按状态过滤后的任务清单。
与仓库其他语言实现的横向对照
该问题在仓库中属于多语言实现的标准例题(problems/task-management-system.md),除 C++ 外还有:
其中 README 提到的 ConcurrentHashMap/CopyOnWriteArrayList 线程安全方案,正是 Java 实现 中 TaskManager 的具体落地;对比阅读 C++ 版(std::vector + 手动维护一致性)与 Java 版(并发容器 + 单例)的差异,可以更深刻地理解"同一设计文档在不同语言下的实现取舍"——这正是本仓库作为 LLD 面试准备的独特价值:需求与类设计是通用的,而线程安全与存储策略需要结合语言特性因地制宜。
小结
本仓库的 C++ 任务管理系统实现 用不足百行的核心代码,完整覆盖了设计文档中的 7 条需求:User 与 Task 提供属性建模,TaskStatus/TaskPriority 提供状态与优先级语义,TaskManager 以集中式控制器完成创建/删除/分配/状态流转/过滤查询,并以依赖检查实现了"前置任务未完成则不能开工"的业务规则,同时通过删除时的引用清理保证了数据一致性。对照设计文档与源码逐行研读,是掌握 LLD 面试"需求 → 类图 → 代码"全链路的最佳方式。