UML与Visual C++实战:仓库管理系统从设计到实现全解析
1. 项目概述与核心价值
最近在整理过去的项目资料,翻到了一个用Visual C++ 6.0配合UML完成的仓库管理系统设计。这个项目虽然技术栈现在看来有些“复古”,但其中关于如何用UML进行面向对象分析与设计,以及如何在C++中落地实现的完整思路,对于理解软件工程的核心流程依然非常有价值。很多新手朋友一上来就想写代码,结果发现类之间的关系理不清,功能模块耦合严重,后期维护和扩展举步维艰。这个项目恰恰展示了从需求到设计,再到编码的规范化路径。
简单来说,这个项目就是利用统一建模语言(UML)作为设计工具,对一个小型仓库管理系统的业务逻辑、数据结构和交互流程进行可视化建模,然后使用Visual C++(特别是其MFC框架)将设计图转化为一个可运行的Windows桌面应用程序。它解决了从“想法”到“软件”过程中最令人头疼的环节——如何确保你写的代码结构清晰、易于理解且能准确反映业务需求。无论你是正在学习软件工程的学生,还是希望夯实设计基础的初级开发者,通过剖析这个经典案例,你都能掌握一套实用的、可复用的系统分析与设计方法。
2. 系统整体设计与UML建模思路拆解
2.1 需求分析与用例图构建
在动手画任何一张UML图之前,我们必须先搞清楚系统要“为谁服务”和“做什么事”。对于仓库管理系统,核心角色(Actor)通常包括仓库管理员和系统管理员。仓库管理员负责日常的仓储操作,而系统管理员则负责基础数据的维护。
基于此,我绘制了系统的用例图。这张图是整个设计的起点,它从用户视角定义了系统的功能边界。核心用例包括:
- 物品管理:涵盖物品信息的增、删、改、查。
- 入库管理:处理采购到货、生产入库等业务,核心是创建入库单,更新库存。
- 出库管理:处理销售发货、领料出库等业务,核心是创建出库单,校验并扣减库存。
- 库存查询:按物品、类别、仓库位置等多维度查询实时库存。
- 报表统计:生成库存流水、出入库汇总等报表。
- 用户与权限管理(系统管理员专属):管理登录账号和操作权限。
注意:在绘制用例图时,一个常见的误区是把“登录”作为一个核心用例。实际上,“登录”是系统的一种约束机制,更应作为“包含”关系关联到其他需要身份验证的用例上,例如“<包含>登录”。这能更准确地反映业务逻辑。
2.2 静态结构建模:类图设计
用例图明确了“做什么”,类图则要回答“用什么来做”以及“它们之间如何关联”。这是面向对象设计的核心,直接决定了后续代码的结构。
我首先识别出系统中的核心实体类。经过分析,主要包含以下几类:
- 业务实体类:
CItem(物品)、CStorage(仓库/库位)、CInboundOrder(入库单)、COutboundOrder(出库单)、CInventory(库存记录,这是一个关联类,连接物品和仓库,并记录数量)。 - 控制类:
CItemManager、COrderManager、CInventoryManager。这些类不直接对应现实事物,而是负责协调多个实体类完成复杂的业务逻辑,例如“入库”这个动作,就需要COrderManager协调创建CInboundOrder、调用CInventory更新库存等。 - 边界类:对应于MFC中的对话框类,如
CItemDlg、CInboundDlg等,负责与用户交互。
接下来是定义类之间的关系。这里有几个关键设计点:
- 聚合关系:
CInboundOrder(入库单)与CInboundOrderDetail(入库明细)之间是聚合关系。一个入库单包含多个明细项,明细项不能脱离订单独立存在,但订单销毁时,明细项通常也随之销毁(在C++中表现为订单对象持有明细对象列表的指针,并在析构时释放)。 - 关联关系:
CInventory(库存)关联了CItem和CStorage。它记录了“某个物品CItem在某个仓库CStorage中有多少数量”。这是一个多对多的关联,通过CInventory这个关联类化解。 - 依赖关系:
COrderManager(订单管理器)依赖于CInventory和CItem等类,因为它需要调用这些类的方法来完成业务。
在Visual C++中,这些类最终会被定义为C++的类。例如,CItem类可能包含m_strID、m_strName、m_strCategory等私有成员变量,以及相应的Get/Set方法和一个SaveToDatabase()成员函数。
2.3 动态行为建模:时序图与状态图
类图描述了系统的静态骨架,而系统的“行为”如何发生,则需要用时序图和状态图来描绘。
对于入库流程,我画了详细的时序图。它清晰地展示了用户点击“入库”按钮后,系统内部对象的协作过程:
- 用户操作
CInboundDlg界面,填写数据后点击“提交”。 CInboundDlg调用COrderManager::CreateInboundOrder(...)方法。COrderManager首先验证数据,然后创建一个CInboundOrder对象和多个CInboundOrderDetail对象。- 接着,
COrderManager遍历明细,为每一个物品调用CInventoryManager::IncreaseInventory(itemID, storageID, quantity)。 CInventoryManager查找或创建对应的CInventory对象,更新其数量属性。- 最后,
COrderManager调用数据库操作类(如CDatabaseHandler)将订单和库存变更持久化到数据库(如Access或SQL Server),并将成功或失败的结果逐层返回给界面。
这个时序图的价值在于,它将一个复杂的业务流程分解为对象之间清晰的消息传递序列,是后续编写COrderManager类方法的核心依据。
此外,对于像CInboundOrder(入库单)这样的对象,其生命周期并非一成不变。它可能处于“草稿”、“已提交”、“已审核”、“已完成”等状态。为此,我绘制了状态图来描述状态之间的转换条件和触发的操作。例如,从“草稿”到“已提交”的状态转换,触发条件是用户执行提交操作,伴随的动作可能是执行库存预占。状态图帮助我们设计订单类的m_Status成员变量,以及Submit()、Audit()等方法内部的状态校验逻辑。
2.4 数据库设计:类图到数据表的映射
UML类图最终需要映射到关系型数据库的表结构。这是一个从面向对象模型到关系模型的转换过程。基本规则如下:
- 实体类通常映射为一张表:
CItem->Item表,包含ID、Name、Category等字段。 - 关联类映射为一张表:
CInventory->Inventory表,包含ItemID、StorageID、Quantity等字段,其中ItemID和StorageID是外键。 - 聚合关系中的“整体”和“部分”:
CInboundOrder->InboundOrder表(主单),CInboundOrderDetail->InboundOrderDetail表(明细)。明细表包含一个OrderID外键指向主单表。 - 继承关系:可以使用“所有类一张表”、“每个具体类一张表”或“每个类一张表”的策略。在本系统中若无复杂继承,可暂不考虑。
设计表结构时,需要仔细考虑主键、外键、索引以及字段的数据类型和约束(如非空、唯一),这些是保证数据完整性和查询效率的基础。
3. Visual C++开发环境搭建与MFC框架应用
3.1 开发环境配置与常见陷阱
这个项目基于经典的Visual C++ 6.0开发,当然,你也可以使用更高版本的Visual Studio(如VS 2019/2022),它们对C++标准和MFC的支持更好。使用MFC应用程序向导可以快速搭建起一个单文档或多文档的应用程序骨架。
这里有一个必须注意的坑,也是网络热词中频繁出现的error MSB3428问题的根源。当你使用更高版本的Visual Studio,并尝试编译一些依赖原生C++模块(特别是那些涉及node-sass等需要本地编译的npm包,或某些旧的C++项目)时,可能会遇到“未能加载Visual C++组件’vcbuild.exe’”的错误。这通常是因为缺少对应版本的Microsoft Visual C++ Redistributable运行库或构建工具。
实操心得:对于仓库管理系统这类纯原生C++/MFC项目,在Visual Studio中创建项目时,应确保已安装“使用C++的桌面开发”工作负载,并勾选“MSVC v142 - VS 2019 C++ x64/x86生成工具”和“Windows 10 SDK”。如果是从旧版本(如VC6)迁移项目,可能还需要在项目属性中调整“平台工具集”为合适的版本。彻底避免
vcbuild.exe错误的关键是保证开发环境的构建工具链完整。
3.2 MFC框架下的三层架构实现
在MFC项目中,我采用了经典的三层架构进行组织,这能让代码结构更清晰:
- 表示层:由
CView的派生类和各类CDialog对话框组成,对应UML中的边界类。它们负责显示数据、接收用户输入,但几乎不包含业务逻辑。例如,CItemDlg中只有一个OnOK()函数,其内部仅仅是收集对话框控件中的数据,然后调用业务逻辑层的CItemManager::AddItem(...)方法。 - 业务逻辑层:由UML中的控制类和实体类的业务方法构成。例如
COrderManager、CInventoryManager等。这一层是系统的核心,实现了所有的业务规则和流程。 - 数据访问层:抽象出数据库操作。可以定义一个
IDatabase接口类,然后派生出CAccessDatabase或CSqlServerDatabase类。业务逻辑层通过接口调用数据访问层,从而与具体的数据库解耦。在MFC中,也可以直接使用CDatabase和CRecordset类,但将其封装在数据访问层内是更好的实践。
这种分层确保了“高内聚、低耦合”。界面改动不影响业务逻辑,数据库更换(如从Access换到MySQL)也只需修改数据访问层的具体实现。
3.3 核心模块的MFC实现详解
以物品管理模块为例,我们看看UML类图如何转化为MFC代码。
首先,实体类CItem的定义:
// Item.h class CItem { public: CItem(); virtual ~CItem(); // Get/Set 方法 CString GetID() const { return m_strID; } void SetID(const CString& id) { m_strID = id; } // ... 其他属性如Name, Category, Unit等的Get/Set方法 // 业务方法 BOOL Validate() const; // 验证数据有效性 CString ToString() const; // 用于显示的字符串表示 private: CString m_strID; // 物品编号 CString m_strName; // 物品名称 CString m_strCategory;// 类别 CString m_strUnit; // 单位 double m_dUnitPrice; // 单价 // ... };对应的,控制类CItemManager负责物品的增删改查逻辑:
// ItemManager.h class CItemManager { public: static CItemManager* GetInstance(); // 单例模式,方便全局访问 BOOL AddItem(const CItem& item); BOOL DeleteItem(const CString& strID); BOOL UpdateItem(const CItem& item); CItem* GetItemByID(const CString& strID); std::vector<CItem> GetAllItems(); private: CItemManager(); // 私有构造函数,实现单例 std::map<CString, CItem> m_mapItems; // 内存缓存,实际应从数据库加载 // 数据访问对象 std::unique_ptr<IItemDAO> m_pItemDAO; };在对话框CItemDlg中,OnOK函数的实现非常简洁:
void CItemDlg::OnOK() { UpdateData(TRUE); // 将控件数据更新到变量 CItem item; item.SetID(m_strID); item.SetName(m_strName); // ... 设置其他属性 if (!item.Validate()) { AfxMessageBox(_T("物品信息无效!")); return; } CItemManager* pManager = CItemManager::GetInstance(); if (pManager->AddItem(item)) { AfxMessageBox(_T("添加成功!")); CDialog::OnOK(); } else { AfxMessageBox(_T("添加失败,可能ID重复!")); } }可以看到,界面只负责数据收集和展示,真正的业务逻辑(如ID重复校验、数据持久化)都封装在CItemManager和CItem中。这完全符合UML设计时的职责划分。
4. 数据库操作与数据持久化实现
4.1 使用ODBC进行数据库连接
在Visual C++中,连接数据库的常用方式是ODBC。首先需要在Windows系统的ODBC数据源管理器中配置一个DSN,指向你的数据库文件(如.mdb或.accdb)。
在MFC应用程序初始化时(如CWinApp::InitInstance中),建立数据库连接:
BOOL CWarehouseApp::InitInstance() { // ... 其他初始化 CDatabase* pDB = new CDatabase(); try { // “WarehouseDB”是配置的ODBC数据源名称 if (pDB->OpenEx(_T("DSN=WarehouseDB;"), CDatabase::noOdbcDialog)) { // 将pDB指针保存在应用程序类或全局可访问的地方 SetDatabaseConnection(pDB); return TRUE; } } catch (CDBException* e) { AfxMessageBox(e->m_strError); e->Delete(); } return FALSE; }4.2 封装CRecordset实现数据访问层
MFC提供了CRecordset类来简化数据库操作。我们可以为每张表创建一个CRecordset的派生类。
例如,对于Item表:
// ItemRecordset.h class CItemRecordset : public CRecordset { public: CItemRecordset(CDatabase* pDatabase = NULL); // 字段数据成员,对应数据库表的列 CString m_strID; CString m_strName; CString m_strCategory; // ... // 数据库列与变量绑定的实现 virtual void DoFieldExchange(CFieldExchange* pFX); }; // ItemRecordset.cpp CItemRecordset::CItemRecordset(CDatabase* pdb) : CRecordset(pdb) { m_strID = _T(""); m_strName = _T(""); m_strCategory = _T(""); m_nFields = 3; // 字段数量 m_nDefaultType = snapshot; } void CItemRecordset::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Text(pFX, _T("[ID]"), m_strID); RFX_Text(pFX, _T("[Name]"), m_strName); RFX_Text(pFX, _T("[Category]"), m_strCategory); }然后,在之前提到的CItemManager中,通过CItemRecordset来执行具体的数据库操作:
BOOL CItemManager::AddItem(const CItem& item) { // 1. 检查缓存中是否已存在(业务逻辑) if (m_mapItems.find(item.GetID()) != m_mapItems.end()) { return FALSE; } // 2. 通过数据访问层插入数据库 CDatabase* pDB = GetGlobalDatabaseConnection(); // 获取全局连接 CItemRecordset rs(pDB); try { if (!rs.IsOpen()) { rs.Open(CRecordset::dynaset, _T("SELECT * FROM Item WHERE 1=0"), CRecordset::none); } rs.AddNew(); // 进入添加模式 rs.m_strID = item.GetID(); rs.m_strName = item.GetName(); // ... 赋值其他字段 rs.Update(); // 执行插入 rs.Close(); } catch (CDBException* e) { AfxMessageBox(_T("数据库操作失败: ") + e->m_strError); e->Delete(); return FALSE; } // 3. 更新内存缓存 m_mapItems[item.GetID()] = item; return TRUE; }4.3 事务处理在出入库中的关键应用
出入库操作涉及多张表的更新(订单主表、明细表、库存表),必须保证原子性,即要么全部成功,要么全部失败。这就需要用到数据库事务。
在COrderManager::CreateInboundOrder方法中,事务的使用至关重要:
BOOL COrderManager::CreateInboundOrder(const CInboundOrder& order) { CDatabase* pDB = GetGlobalDatabaseConnection(); BOOL bSuccess = FALSE; try { pDB->BeginTrans(); // 开始事务 // 步骤1: 插入入库单主记录 CInboundOrderRecordset rsOrder(pDB); // ... 执行插入操作 // 步骤2: 循环插入每条入库明细 for (const auto& detail : order.GetDetails()) { CInboundDetailRecordset rsDetail(pDB); // ... 执行插入操作 } // 步骤3: 更新库存(这是最可能出错的地方) for (const auto& detail : order.GetDetails()) { if (!CInventoryManager::GetInstance()->IncreaseInventory( detail.GetItemID(), detail.GetStorageID(), detail.GetQuantity())) { // 如果某次库存更新失败,抛出异常触发回滚 throw std::runtime_error("库存更新失败"); } } pDB->CommitTrans(); // 所有操作成功,提交事务 bSuccess = TRUE; AfxMessageBox(_T("入库单创建成功!")); } catch (CDBException* e) { pDB->Rollback(); // 发生数据库异常,回滚 AfxMessageBox(_T("数据库错误,操作已回滚: ") + e->m_strError); e->Delete(); } catch (const std::exception& e) { pDB->Rollback(); // 发生业务逻辑异常,回滚 AfxMessageBox(_T("业务逻辑错误,操作已回滚。")); } return bSuccess; }重要提示:务必确保在异常处理中调用
Rollback(),并且CommitTrans()只在所有操作确认成功后调用。忘记回滚是导致数据不一致的常见原因。同时,库存更新前的检查(如库存是否充足、库位是否存在)应在事务开始前或事务中尽早进行,以减少无效的事务开销。
5. 系统界面设计与用户体验优化
5.1 使用MFC控件构建数据密集型界面
仓库管理系统的界面通常是数据密集型的,例如物品列表、出入库单明细。MFC的CListView控件(报表视图)非常适合展示这类表格数据。
首先,在资源编辑器中创建一个List Control,设置View属性为Report。然后在对话框的OnInitDialog函数中初始化列表的列:
BOOL CItemListDlg::OnInitDialog() { CDialog::OnInitDialog(); CListCtrl* pList = (CListCtrl*)GetDlgItem(IDC_LIST_ITEMS); pList->InsertColumn(0, _T("物品编号"), LVCFMT_LEFT, 80); pList->InsertColumn(1, _T("物品名称"), LVCFMT_LEFT, 120); pList->InsertColumn(2, _T("类别"), LVCFMT_LEFT, 80); pList->InsertColumn(3, _T("单位"), LVCFMT_LEFT, 60); pList->InsertColumn(4, _T("当前库存"), LVCFMT_RIGHT, 80); // ... 插入更多列 // 设置扩展样式,支持整行选择、网格线等,提升体验 pList->SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER); // 从数据库加载数据并填充列表 LoadItemDataToList(pList); return TRUE; } void CItemListDlg::LoadItemDataToList(CListCtrl* pList) { pList->DeleteAllItems(); // 清空现有项 std::vector<CItem> items = CItemManager::GetInstance()->GetAllItems(); int nIndex = 0; for (const auto& item : items) { pList->InsertItem(nIndex, item.GetID()); pList->SetItemText(nIndex, 1, item.GetName()); pList->SetItemText(nIndex, 2, item.GetCategory()); // ... 设置其他列 nIndex++; } }5.2 实现数据的增删改查对话框
对于数据的添加和修改,通常使用同一个对话框,通过传入不同的参数来区分模式。例如,在CItemDlg类中增加一个m_bEditMode成员变量和一个m_strEditID变量。
void CItemListDlg::OnAddItem() { CItemDlg dlg; dlg.m_bEditMode = FALSE; // 添加模式 if (dlg.DoModal() == IDOK) { LoadItemDataToList(); // 刷新列表 } } void CItemListDlg::OnEditItem(NMHDR* pNMHDR, LRESULT* pResult) { LPNMITEMACTIVATE pNMIA = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR); int nSelected = pNMIA->iItem; if (nSelected >= 0) { CListCtrl* pList = (CListCtrl*)GetDlgItem(IDC_LIST_ITEMS); CString strID = pList->GetItemText(nSelected, 0); CItemDlg dlg; dlg.m_bEditMode = TRUE; // 编辑模式 dlg.m_strEditID = strID; // 可以在这里根据ID加载物品信息到dlg的成员变量中 if (dlg.DoModal() == IDOK) { LoadItemDataToList(); } } *pResult = 0; }在CItemDlg::OnInitDialog中,根据m_bEditMode初始化对话框内容:
BOOL CItemDlg::OnInitDialog() { CDialog::OnInitDialog(); if (m_bEditMode) { SetWindowText(_T("编辑物品信息")); // 根据m_strEditID从数据库或缓存加载物品信息 CItem* pItem = CItemManager::GetInstance()->GetItemByID(m_strEditID); if (pItem) { m_strID = pItem->GetID(); m_strName = pItem->GetName(); // ... 赋值其他控件变量 UpdateData(FALSE); // 将变量更新到控件 GetDlgItem(IDC_EDIT_ID)->EnableWindow(FALSE); // 编辑时禁止修改ID } } else { SetWindowText(_T("添加新物品")); } return TRUE; }5.3 数据验证与用户反馈优化
良好的用户体验离不开及时有效的反馈。在数据提交前进行验证至关重要。
- 控件级验证:使用DDV(Dialog Data Validation)规则。在
DoDataExchange函数中,可以为编辑框添加长度、范围等限制。void CItemDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Text(pDX, IDC_EDIT_ID, m_strID); DDX_Text(pDX, IDC_EDIT_NAME, m_strName); DDX_Text(pDX, IDC_EDIT_PRICE, m_dUnitPrice); // 验证:ID不能为空,且长度不超过20 DDV_MaxChars(pDX, m_strID, 20); if (pDX->m_bSaveAndValidate && m_strID.IsEmpty()) { AfxMessageBox(_T("物品编号不能为空!")); pDX->Fail(); // 验证失败,焦点回到对应控件 } // 验证:单价必须大于等于0 DDV_MinMaxDouble(pDX, m_dUnitPrice, 0.0, 1e6); } - 业务级验证:在
OnOK函数中,调用实体对象的Validate()方法进行更复杂的检查,如ID格式、名称唯一性等。 - 操作反馈:所有数据库操作、业务逻辑操作都应提供明确的成功或失败提示。对于耗时操作(如生成大型报表),应考虑使用进度条(
CProgressCtrl)或后台线程,避免界面假死。
6. 项目部署、调试与常见问题排查
6.1 项目编译与依赖项处理
在Visual C++ 6.0时代,项目部署相对简单,通常只需要将生成的.exe文件和可能用到的.dll(如MFC42.dll)一起拷贝到目标机器即可。但在现代Visual Studio中,需要注意运行时库的依赖。
在Visual Studio项目属性中,关于运行库的配置(C/C++->代码生成->运行库)有几个选项:
- 多线程调试 (/MTd):静态链接调试版运行时库。生成的exe较大,但无需额外dll,适合调试阶段。
- 多线程 (/MT):静态链接发布版运行时库。同上,无需额外dll,适合发布。
- 多线程调试 DLL (/MDd):动态链接调试版运行时库。需要目标机器有对应的调试版MSVCRxxD.dll,一般仅用于开发环境。
- 多线程 DLL (/MD):动态链接发布版运行时库。这是最常用的发布设置。生成的exe较小,但要求目标机器安装相应版本的Visual C++ Redistributable。
部署建议:对于要分发给其他用户的发布版本,选择
/MT静态链接可以避免用户安装运行库的麻烦,但exe体积会增大。如果选择/MD,则必须在安装包中附带或要求用户预先安装对应版本的Microsoft Visual C++ Redistributable。这正是网络热词中频繁提及的组件,可以从微软官网下载。
6.2 典型问题排查实录
在开发此类系统时,我遇到过不少典型问题,这里记录下排查思路:
问题1:列表控件刷新后数据错乱或闪烁。
- 现象:调用
LoadItemDataToList后,列表显示异常或快速闪烁。 - 排查:
- 检查是否在每次插入新项前调用了
DeleteAllItems()。 - 确认
InsertItem和SetItemText的索引参数是否正确。 - 关键技巧:在数据量较大时,在批量更新列表前使用
SetRedraw(FALSE)禁止控件重绘,更新完成后再SetRedraw(TRUE),可以极大减少闪烁。pList->SetRedraw(FALSE); pList->DeleteAllItems(); // ... 批量插入数据 pList->SetRedraw(TRUE); pList->Invalidate(); // 触发一次重绘
- 检查是否在每次插入新项前调用了
问题2:数据库操作失败,错误信息不明确。
- 现象:捕获到
CDBException,但e->m_strError信息过于笼统。 - 排查:
- 首先检查ODBC数据源配置是否正确,数据库文件路径是否有效。
- 在
try-catch块中,输出更详细的信息。CDBException类还有m_strStateNativeOrigin成员,有时包含更具体的SQL错误。 - 使用数据库管理工具(如Access、SQL Server Management Studio)直接执行相同的SQL语句,看是否报错。
- 检查SQL语句中的表名、字段名是否正确,特别是是否使用了数据库保留字(如
Name,Order等),如果是,需要用方括号[]括起来。
问题3:内存泄漏。
- 现象:程序长时间运行后,内存占用持续增长。
- 排查:在Debug模式下,Visual Studio会在输出窗口显示未释放的内存块信息。重点检查:
- 所有
new操作是否有对应的delete。 - 所有
Open的数据库连接(CDatabase)、记录集(CRecordset)是否在最后都正确Close了。 - 是否在堆上分配了MFC对象(如
new CMyDialog)而未删除。对于非模态对话框尤其要注意。
- 所有
- 工具:可以使用Visual Studio自带的内存诊断工具,或第三方工具如Visual Leak Detector (VLD)来辅助定位。
问题4:在多文档/视图架构中,不同视图间数据状态不同步。
- 现象:在一个窗口中修改了数据,另一个打开的数据列表窗口没有实时更新。
- 解决:这是典型的观察者模式应用场景。可以建立一个简单的消息通知机制。例如,在数据管理器(如
CItemManager)中维护一个观察者列表。当数据发生变更时,通知所有注册的视图。视图接收到通知后,调用自己的刷新方法。// 简化的观察者模式示例 class IDataObserver { public: virtual void OnDataChanged() = 0; }; class CItemManager { public: void AddObserver(IDataObserver* pObs) { m_observers.push_back(pObs); } void NotifyDataChanged() { for (auto* obs : m_observers) { obs->OnDataChanged(); } } BOOL AddItem(...) { // ... 添加逻辑 if (success) NotifyDataChanged(); return success; } private: std::vector<IDataObserver*> m_observers; }; class CItemListView : public CListView, public IDataObserver { // ... 在适当的时候(如OnInitialUpdate)向CItemManager注册自己 // 实现OnDataChanged(),在其中调用LoadItemDataToList() };
回顾整个项目,从UML建模到MFC代码实现,最深的体会是:设计先行,编码在后。UML图不是给老师或领导看的“作业”,而是开发者自己理清思路、与团队成员沟通的利器。在画类图、时序图的过程中,很多潜在的设计缺陷(如职责不清、循环依赖)就已经暴露出来了。用Visual C++ MFC实现虽然繁琐,但它强迫你关注内存、资源、消息循环这些底层细节,对于深入理解Windows桌面程序开发大有裨益。如果你正在学习,不妨也尝试用这个组合,从一个简单的模块开始,亲手实现一遍这个“麻雀虽小,五脏俱全”的仓库管理系统,相信你对软件工程和C++的理解会上一个台阶。
