VsHMM框架

DinS          Written on 2018/3/2

继续上文探讨VsHMM框架的实现。

一、信使类

第一步要做的是确定一个信使类,作为各个部分的统一交互格式。
这里面临的问题是既然非GUI对象是任意的,也就是说传入参数和返回参数可能是任意类型,那么我们需要的是一个万能型数据结构,这才能做信使。
非常幸运的是,nlohmann大神写的json完全可以担当此重任,其可以深入任意层次,装载任意类型(包括自定义类型,除了指针外全部可以),具体见《nlohmann::json万能数据结构》。

信使.h如下:

也就是把json包装了一下,还是public,信使的话没必要封装那么厉害。
辅助功能暂时没有用到,所以.cpp就不贴了。

二、现有类

第二步确定现有的类,也就是各种Objectn。
尽管这个不是VsHMM框架要考虑的问题,不过为了能让程序运转起来,这里提供两个dummy类。.h和.cpp如下:

另一个类:

就是接收参数,输出点东西,返回点东西。输出是为了测试方便。

注意,这两个类里并没有信使Messenger,实际上没有任何VsHMM框架的内容。
这是合理的,因为这些类的设计是不考虑GUI部分,也不知道VsHMM框架的。
只有这样才能保证VsHMM的通用性。如果设计任何一个类都必须想着VsHMM框架,那这个框架也没用了。

三、包装现有类

第三步对这些类进行包装。
这一步的意义就是指定调用哪个接口,就调用哪个接口,而且指定的参数是字符串。可以理解为脚本。
先声明Model基类:

基类很简单了,就两个接口,所以使用起来大概是这个样子的:
XXX->SetAPI(Method1)
XXX->Execute()
然后XXX就执行Method1了,如果再变更API为Method2,那么Execute()就是调Method2。显然这是一个状态模式能够解决的问题。

再进入具体代码之前,说一下约定。
状态模式的实现使用嵌套类,一开始看可能有些不适应,但是习惯就好了。之所以使用嵌套类是因为状态仅对一个类本身有效,甚至可以说是类内部的事情,所以不需要暴露给外界。
使用前置声明一开始也会比较奇怪,这个考虑是这样的:现有类可能非常复杂,其牵扯的头文件可能非常多,这样的话最好把现有类和Model隔离开,防止头文件纠缠起来,增加编译效率。

来看看如何进行包装,其实很简单:

费解部分就是嵌套类那一块和一个指向嵌套类的指针。实际上这几个嵌套类也有继承关系,APIBase是基类,后面的类都是其派生类,为了方便名字统一使用真正类的API的名字。如果理解有困难可参考《设计模式-行为型》。
看看实现:

实现从概念上分成两部分,上面这一部分是定义嵌套类。
嵌套类只有一个接口,就是Execute(),这是嵌套类存在的唯一意义。因为嵌套类不能访问外面的NetPortalModel,所以需要把指针穿进去,另外就是装载数据的数据结构。

然后在派生类中定义NetPortal的哪个接口被调用。比如在DownloadFile这个嵌套派生类里就调NetPortal的DownloadFile接口,顺便需要处理传入和返回参数。得益于Messenger的万能数据结构json,所以理论上可以处理任意参数类型。
还有一点要注意,这里会出现json获取的路径,这个要跟GUI准备数据的路径一致,不然就取不到了。

大图点这里

这一部分才定义NetPortalModel这个类。
构造时实例化NetPortal,这是前置声明的套路,不然指针是空的。然后看看怎么个状态模式法儿。
在SetAPI中,根据传入的字符串,我们把状态指针设置为对应的派生类。然后在Execute()里面,我们执行APIBase的Execute,因为动态绑定这个Execute()会执行派生状态对应的Execute(),从而执行了NetPortal对应的接口。
因为使用了智能指针,不需要繁琐的new和delete,可放心食用。

这种包装方法有通用性,对于任何一个类,都可以如法炮制,因为类只有接口,而这个包装方法只针对接口。
剩下那个类似,只给出声明不给实现了。

四、制作ModelManager

第四步我们制作一个ModelManager来装载这些Model。
在Model里我们实现了灵活调用API,在ModelManager里我们要实现灵活调用不同对象,这样非GUI部分的难点就完成了。

声明很简单。本质上就是一个hashtable,从string到基类Model的映射。
看实现:

初始化时将所有Model派生类实例化,并加入hashtable,如此一来我们可以利用类名访问到该类对应的wrapper。
Run也很简单,取得动作清单,里面记录了哪个类的哪个API要被调用。然后我们遍历清单,先设置API,再执行即可。这样达到的效果就是指定对象的指定接口被调用。
整个过程非常灵活,不需要我们进行编码,那么怎么变更呢?

五、制作RuleFactory

第五步制作RuleFactory。
关于RuleFactory可以有许多实现思路,这里我使用配置文件的方式进行。
先看看配置文件是什么样子的:

大图点这里

这应该是一个很自然的格式。
第一项是UserEvent的名称,后面跟ActionList,每一项是类名.接口,看起来跟脚本相似。所以RuleFactory的功能就是读入配置文件,根据UserEvent名称提供对应的ActionList。

大图点这里

依然使用hashtable装载,声明那里应该能看懂。
顺便提供了一些解析配置文件的辅助函数,这些实现就不给了,直接看实质性内容。

大图点这里

重点在初始化,本质上是处理字符串,个人认为还是比较简洁的。
给出ActionList就是查表,赋值即可。

六、制作Hub

第六步制作Hub。
Hub仅仅是中转,所以逻辑不复杂。实际上如果有复杂逻辑,那也是其他部分要干的事,不归Hub管。这样才能够保证Hub的简洁性。

大图点这里

实现:

很简单,拿到信使,调RuleFactory接口获得ActionList,然后调ModelManager接口,就完了。
这里返回参数都省略了,因为全部是引用,执行完后信使里自然就携带了结果。

此处的Hub虽然简单,但是保留了极大地扩展余地。
比如在这里可以加个日志器,能记录用户的所有操作。实际上凡是涉及到中转的都可以在这里增加。这也是Hub的本质意义。

七、制作View

第七步,也就是最后一步,制作View。
制作View本身不是VsHMM框架的事,是GUI框架的事。只不过需要在View里按照一定的思路写代码即可。
这里演示一个dummy类:

假设这是一个GUI程序,GUI程序一般都提供了各种控件类,该怎么用怎么用。
需要设置的就是与Hub的交互,用一个智能指针实现。这个是很有必要的,因为通常我们会在一个窗体内建立其他窗体,比如点击按钮弹出选择框。为了保证这个新的窗体也能够与同一个Hub正常交互,我们需要把该智能指针传进去。
现代的GUI程序都是事件驱动型,所以一定会有OnXXX()这么个东西,与Hub的交互就在这里实现。

大图点这里

通常的套路是准备数据->调Hub->处理返回数据。
至于准备哪些数据,以及返回数据在哪里取得,需要查配置文件,并且跟ModelWrapper里保持一致。这个比起错综复杂的逻辑来说是简单太多了。
况且真的没对上的话,json也会抛出异常,很快就能查明原因。

八、使用示例

至此VsHMM框架就完成了。
测试一下,在程序主入口写如下代码:

大图点这里

然后运行,可以看到配置文件中定义的UserEvent对应的ActionList顺利执行:

返回值也取到了:

有了这个结构,代码维护起来非常方便。我们只要关注在Model派生类里对象真正API的传入和返回参数的处理,然后就是GUI部分收集数据和处理数据,最后根据需要调整RuleConfig文件即可。
各种逻辑全部拆开了,灵活应对不断变化的世界。

到目前为止,VsHMM框架能够解决我能想到的和遇到过的有关GUI程序代码结构的相关问题。当然还有待时间和实践的检验。

九、有关View部分逻辑处理的进一步探讨

VsHMM框架分离了业务逻辑和显示逻辑,“凯撒的归凯撒,上帝的归上帝”。
业务逻辑是对象内部的事,如何设计可参考OO专题
但是显示逻辑并非特别好处理,这里我们来谈谈这个问题。

一种常见的情况是根据控件的状态执行不同的路线。
比如有4个复选框和1个按钮,点击按钮后根据复选框是否勾上计算结果不一样,这种情况如何处理?
思路一:把4个控件的状态作为参数加入信使,然后不管了。
好处是GUI部分代码简洁,坏处是我们把实质问题推给了非GUI对象。
那么问题就是是否应该把这个问题交给对象处理?
这取决于类的设计,不过根据传入参数进行不同处理的接口有违背单一责任原则的风险。

思路二:把2^4共16个可能组合情况视为16种独立UserEvent。
好处是严格执行单一责任原则,方便后期进行扩展。比如对于某一种组合的需求变了,我们只需要改RuleConfig即可变更。坏处是GUI代码部分会看上处比较乱,各种If。

子曰:人弘道,非道弘人。对于这个问题,应该坚持的是“GUI弘OBJ,非OBJ弘GUI”。
也就是说思路取决于类是如何设计的。
如果类依靠不同参数做执行路径区分,那么就是思路一。
如果类依靠不同API做执行路径区分,那么就是思路二。

另一种常见的问题跟控件本身的显示有关。
通常而言我们总会涉及到更改控件的位置、可见性、style等等,而且总是根据某些值(比如其他控件的状态、配置文件)来作改动,并且一旦改变还会牵扯到其他类的位置、可见性等等(比如为了美观)。

对局部而言,涉及到的控件数量有限,直接在Event的实现中写即可。
对整体而言,现在流行的GUI都是分层式的,所以设计时要考虑控件分层。
对parent做操作可以直接改变全部child的属性,可以依靠分层合理来简化代码,把其他工作交给GUI框架去做。