有机编程环境(OPEN)






3.74/5 (12投票s)
OPEN 是一个原型开发项目,探索一种不同的数据管理范例。传统的应用程序是以进程为中心,由进程驱动数据传输;而有机编程环境则采用以数据为中心的方法。在这种范例中,数据会启动进程。
引言
GUI 应用程序是事件驱动的,它们响应用户和硬件事件。这些事件执行以下一个或多个过程:
- 数据收集;
- 数据转换;
- 数据分发。
数据收集包括诸如以下活动:
- 从数据库读取信息;
- 将 GUI 对象的值存储到变量中;
- 读取硬件寄存器。
数据转换包括任何操作数据的过程,如组合、拆分、格式化等。数据分发与数据收集相反,在此过程中,数据被写入数据库、更新 GUI 对象或写入硬件寄存器。
在这种范例中,数据通常非常局部化,包含在单个函数中或由单个对象管理。各种复杂度的接口方法用于在对象之间共享数据。最终,一个大型应用程序必须管理大量多样化的数据并在此数据上执行复杂的任务。这会导致管理数据各个方面出现复杂性:
- 生命周期;
- 版本/格式;
- 读/写访问。
这些任务留给程序员,据我经验,这些任务大多被忽略。
在有机编程环境(OPEN)中,采用了以数据为中心的方法。数据池(D-Pool)管理所有数据。处理函数将自己注册到 DPM(D-Pool Manager),指明它们操作的数据以及它们生成的数据。当数据放入 D-Pool 时,DPM 会自动将所有感兴趣的参与者作为线程启动。当所有线程完成后,数据会自动从 D-Pool 中移除。DPM 还可以根据数据生命周期、版本/格式信息和读/写访问进行指示。
名称
我之所以起这个名字,是因为我想描述一个类似于蛋白质如何从细胞核中转运出来并被处理成更有趣的分子,然后这些分子要么被细胞自身使用,要么被转运到细胞外,被其他细胞或器官使用的过程。在我看来,这个过程本质上是以数据为中心的,DNA/RNA 是数据,而各种过程(酶等)在细胞液中出现时会附加到数据上。请注意,此架构不基于基因编程(http://www.genetic-programming.org/)或有机编程语言 Gaea(http://www.carc.aist.go.jp/gaea/)。
优点
此架构有几个优点:
- 进程之间完全隔离,提高了可移植性和重用性;
- 每个进程都清楚地记录了它感兴趣的数据类型;
- 程序的运行完全可追溯,无需程序员记住添加跟踪消息。DPM 可以处理所有数据和进程调用的跟踪;
- 由于每个进程都作为线程调用,程序运行效率最高;
- 程序员完全无需考虑线程是否会提高性能,也无需考虑大多数数据同步问题。
缺点
此架构也有几个缺点(我确信我还没有想到所有缺点!):
- 程序操作变得非线性,难以调试;
- 事件序列的发生顺序可能因一次运行到下一次运行而不同,这同样使得程序难以调试;
- 由于数据必须通过密钥或名称来管理,可能会发生名称冲突?无论如何,即使是微不足道的信息也必须命名,这可能会变得很繁琐;
- 将许多不需要作为全局处理的数据全局化;
- 当数据库架构包含外键关联时(任何健壮的数据库设计都应实现),数据依赖关系,尤其是更新/插入数据库记录时,很难支持([抱歉了 MySQL!]);
- 现有的开发框架(如 MFC)不支持这种建模;
- 需要对现有框架(如 MFC)进行重大增强才能纳入此架构;
- 最重要的是,这是一种完全陌生的编程思维方式,因此可能不会被广泛认为实用。
设计
OPEN 有四个设计元素:
- 进程池 (P-Pool);
- 数据池 (D-Pool);
- 数据池管理器 (DPM);
- 数据收集容器。
此架构会快速创建成百上千个小型进程,这些进程接收一些数据、操作数据,然后将结果放回 D-Pool。手动注册所有这些进程会非常繁琐,因此我们有必要尽可能简化程序员的操作。
进程必须实现三个功能:
- 注册进程本身;
- 注册进程操作的数据。
- 提供进程的实际实现。
每个进程可以操作一个以上的数据。例如,如果一个进程使用两个数据,DPM 必须检查池中所有数据的排列组合,以确定是否应触发进程。随着 D-Pool 中数据量的增加,这会导致灾难。为避免这种情况,实现了一个专用容器来收集数据。收集完成后,将触发进程。此收集实现为 STL multimap。数据可以与一个或多个集合-进程关联。集合获得与其集合进程相同的名称,从而将集合与其进程关联。当数据放入 D-Pool 时,DPM 会遍历数据的 multimap,将数据添加到每个关联的集合中。然后将触发每个集合中数据列表已完成的进程。
此设计有不良的副作用:
- 集合中的某个数据在集合完成之前可能发生变化;
- 在进程退出之前清除数据可能会导致丢弃已为当前进程第二次迭代收集的数据。
对于此原型,这些副作用被忽略。
最后,D-Pool 不知道池中每个数据的类型。使用模板是不可行的,因为模板要求类型在编译时已知,这对程序员提出了非常烦人的要求。相反,D-Pool 维护一个通用数据容器集合。数据容器足够智能,可以与各种内置类型进行转换,并包含一个用于自定义派生的虚拟基类。由于数据容器不是本文的真正主题,因此忽略其设计和实现。欢迎浏览源代码。
实现
为了实现此设计,所有进程都必须声明为类,并派生自 OPEN_Process
类。
class OPEN_Process { public: virtual ~OPEN_Process() {} void RegisterDNames(void); bool SetData(const CString& dataName, const DataContainer& dc) { dataNameList[dataName]=dc; // this is quite the kludge to see if all datum for // the process has been set return nameList.size() == dataNameList.size(); } virtual void Run(void)=0; protected: OPEN_Process(const CString& s1, const CString& s2) : pName(s1), dName(s2) {} OPEN_Process(void) {}; OPEN_Process(const OPEN_Process& p) : pName(p.pName), dName(p.dName), nameList(p.nameList), dataNameList(p.dataNameList) {} protected: CString pName; CString dName; std::vector<CString> nameList; std::map<CString, DataContainer> dataNameList; };
此类实现为虚拟基类。除了构造函数(必须从派生类调用)之外,用户真正感兴趣的唯一方法是 Run(void)
,它必须在派生类中实现。还要注意,此类封装了进程名称、原始数据名称列表、分隔成 vector
的数据名称列表,以及将数据名称与维护实际值的容器关联的 STL map
。这些信息可用于生成调试信息或数据流的数据图(例如,在 Visio 中)。描述输出数据目前不是必需的,但可以轻松添加。
每个进程由一个进程池管理。OPEN_ProcessPool
基本上封装了一个 STL map
,该 map
将进程名称与指向该进程的指针关联起来。OPEN 中的其他对象也使用此类来与特定进程对象进行接口。
class OPEN_ProcessPool { public: OPEN_ProcessPool(void) {} virtual ~OPEN_ProcessPool() {} void Register(const CString& processName, class OPEN_Process* proc) { processList[processName]=proc; } bool SetData(const CString& processName, const CString& dataName, const DataContainer& dc) { ASSERT(processList.find(processName) != processList.end()); bool trigger=processList[processName]->SetData(dataName, dc); return trigger; } void Trigger(const CString& processName) { ASSERT(processList.find(processName) != processList.end()); AfxBeginThread(OPEN_ProcessPool::StartProcess, processList[processName]); } protected: static UINT StartProcess(void*); public: static OPEN_ProcessPool pool; protected: std::map<CString, class OPEN_Process*> processList; };
程序员没有特别的理由直接与此类进行接口。
OPEN_DataCollection
实现了一个数据名称与对此数据感兴趣的进程之间的一对多关联。此实现为一个 STL multimap
,此类本质上是 multimap
的一个包装器,提供注册和迭代方法。
class OPEN_DataCollection { public: OPEN_DataCollection(void) {} virtual ~OPEN_DataCollection() {} void Register(const CString& datumName, const CString& collName) { collectionList.insert(std::pair<const CString, CString>(datumName, collName)); } bool FindFirst(const CString& dataName, CString& collName) { iter=collectionList.find(dataName); ASSERT(iter != collectionList.end()); collName=(*iter).second; return iter != collectionList.end(); } bool FindNext(const CString& dataName, CString& collName) { ++iter; if (iter==collectionList.end()) { return false; } collName=(*iter).second; return (*iter).first == dataName; } public: static OPEN_DataCollection coll; protected: std::multimap<const CString, CString> collectionList; std::multimap<const CString, CString>::iterator iter; };
OPEN_DataPool
实现了一个对另一个 STL map
的包装。此映射将数据名称与实际值关联起来。应用程序就是通过此对象将数据放入 D-Pool。此类还实现了一个信号量,用于解除 DPM 的阻塞,DPM 然后解析 D-Pool 中的数据,将其移除并放入相应的进程容器。此类还实现了一个 CRITICAL_SECTION
,以确保 DPM(作为线程运行)可以读写 collectionList
,而不会与其他可能正在写入和删除此集合的其他线程发生冲突。
class OPEN_DataPool { public: OPEN_DataPool(void) { InitializeCriticalSection(&cs); dpSem=CreateSemaphore(NULL, 0, 0x7FFF, "OPEN_DP_SEM"); ASSERT(dpSem); } virtual ~OPEN_DataPool() { DeleteCriticalSection(&cs); CloseHandle(dpSem); } void Add(const CString& dataName, const DataContainer& dc) { EnterCriticalSection(&cs); dataPoolList[dataName]=dc; LeaveCriticalSection(&cs); ReleaseSemaphore(dpSem, 1, NULL); } void RemoveDatum(CString& s, DataContainer& d) { EnterCriticalSection(&cs); std::map<CString, DataContainer>::iterator iter = dataPoolList.begin(); ASSERT(iter != dataPoolList.end()); s=(*iter).first; d=(*iter).second; dataPoolList.erase(iter); LeaveCriticalSection(&cs); } public: static OPEN_DataPool pool; protected: std::map<CString, DataContainer> dataPoolList; CRITICAL_SECTION cs; HANDLE dpSem; };
OPEN_Mgr
实现 DPM。此类非常简单。
class OPEN_Mgr { public: OPEN_Mgr(void) {} virtual ~OPEN_Mgr() {} void Run(void); public: static OPEN_Mgr mgr; protected: HANDLE dpSem; };
更有趣的是 Run
方法的实现。
void OPEN_Mgr::Run(void) { dpSem=OpenSemaphore(SYNCHRONIZE, FALSE, "OPEN_DP_SEM"); ASSERT(dpSem); while (1) { DWORD ret=WaitForSingleObject(dpSem, INFINITE); if (ret==WAIT_OBJECT_0) { CString dataName; CString processName; DataContainer data; OPEN_DataPool::pool.RemoveDatum(dataName, data); bool ret=OPEN_DataCollection::coll.FindFirst(dataName, processName); while (ret) { bool trigger=OPEN_ProcessPool::pool.SetData(processName, dataName, data); if (trigger) { OPEN_ProcessPool::pool.Trigger(processName); } ret=OPEN_DataCollection::coll.FindNext(dataName, processName); } } else { break; } } }
此函数实现为线程,它等待数据放入数据池,然后线程被释放。它遍历数据池,将每个数据和值从池中移除,并将其存储在对该数据感兴趣的每个进程中。DPM 然后将进程作为线程“触发”,当进程所需的所有数据都已实例化时。
为了支持更易读的进程实现,定义了几个宏。
#define DECLARE_OPEN(x, y) \ class x : public OPEN_Process \ { \ public: \ x(void) : OPEN_Process(#x, y) {} \ virtual void Register(void) \ { \ RegisterDNames(); \ OPEN_ProcessPool::pool.Register(#x, this); \ } \ virtual void Run(void); \ static x _##x; \ }; \ x x::_##x; #define IMPLEMENT_OPEN(x) \ void x::Run(void) { #define FINISH_OPEN \ dataNameList.erase(dataNameList.begin(), dataNameList.end()); } #define REGISTER_OPEN(x) \ x::_##x.Register()
因此,进程的实现看起来可能像这样:
DECLARE_OPEN(AddCost, "itemCost"); IMPLEMENT_OPEN(AddCost) { double cost; dataNameList["itemCost"].Get(cost); double total=atof(dlg->total)+cost; OPEN_DataPool::pool.Add("totalCost", DataContainer().Set(AutoString(total))); } FINISH_OPEN
在应用程序的初始化部分,必须实例化进程:
REGISTER_OPEN(AddToList);
进程实例化的问题很烦人。您会注意到,几乎所有 OPEN 类都会自动实例化一个单例,实现为每个类的公共静态成员。进程也是如此。但是,实际的数据名称注册不能在程序启动时完成,因为其他必要的初始化(例如,STL 的初始化)尚未发生。据我所知,没有办法预先确定全局或静态数据的初始化顺序。
演示程序
演示程序是一个简单且相当糟糕的示例,展示了此范例的工作原理。“添加零件”、“移除零件”和“清除零件”事件都会将数据放入 D-Pool,由 DPM 读取。此示例中最有趣的是,数据项“itemCost”触发了两个事件:一个用于更新列表框,另一个用于更新运行总计。实际上,这个示例很糟糕,因为进程处理程序与对话框对象紧密耦合,这在现实生活中绝对不是理想的。另外请注意,我重写了 AssertValid
方法,以便在调试模式下从工作线程更新对话框。MFC 真是太棒了。
更有趣的思想实验
要使此模型真正有效,开发人员需要彻底重新思考应用程序的设计和实现方式。
例如,一个函数可能执行如下操作:
- 数据库查询 Q1
- 数据库查询 Q2
- 操作 A
- 数据库查询 Q3
- 数据库查询 Q4
- 操作 B
- 数据库更新 QQ
- 数据库更新 RR
本着此架构的精神,该函数应重写为多个进程:
进程 1
数据库查询 Q1
进程 2
数据库查询 Q2
进程 3:(依赖于 Q1 和 Q2)
操作 A
- 将结果 R1 输出到数据池
进程 4:(依赖于 R1)
数据库查询 Q3
进程 5:(依赖于 R1)
数据库查询 Q4
进程 6:(依赖于 Q3 和 Q4)
操作 B
- 将结果 R2 输出到数据池
- 将结果 R3 输出到数据池
进程 7:(依赖于 R2)
数据库更新 QQ
进程 8:(依赖于 R3)
数据库更新 RR
从上述架构可以看出,程序现在会自动在单独的线程中同时执行数据库查询和更新。这可以极大地提高程序性能,并且只需使用不同的数据管理范例即可实现。
挑战
我认为这种范例是对现有以进程为中心的编程风格的重大增强。它带来了:
- 更小、更简单的函数;
- 大大增加了并行处理能力;
- 自动提供详细的函数和数据跟踪;
- 记录所有函数输入和输出。
此模型存在尚未完全理解的复杂性。我向读者提出的挑战是识别这些复杂性并设计解决方案,最终使此范例健壮且易于使用。例如,在此原型实现中,DPM、D-Pool 和其他对象实现为全局单例。相反,应用程序拥有多个不同规模的数据池似乎更合理。这将扩展有机编程的整个概念?例如,(请原谅类比)程序器官。
致谢
如果您在任何使用 OPEN 的应用程序中,请注明作者 Marc Clifton 是 OPEN 的核心贡献者(我可能是在做梦,对吧?)。作者(我)还要求您在所做的任何对 OPEN 架构的增强中,向我提供源代码和所有增强功能的列表,以便在未来版本中包含它们,造福所有人。
结论
有机编程环境并非以进程为中心的建模的替代品。然而,OPEN 是程序员工具集中的一项重要增强,因为在许多情况下,以数据为中心的方法优于以进程为中心的方法。