Thunder_Class icon indicating copy to clipboard operation
Thunder_Class copied to clipboard

项目架构的一个参考

Open cnDengyu opened this issue 5 years ago • 21 comments

项目架构的一个参考

在我 fork 的 repo 中有一个分支,src 中包含了一个 core 文件夹和一个 Qt 文件夹。 core 文件夹中存放了一个 Workflow 抽象类,包含许多纯虚函数。 Qt 文件夹中存放了一组 GUI 类和一个 QtWorkflow 派生类,实现 Workflow 类的所有虚函数。 当然也可以新建其他文件夹(比如 MFC),然后创建 MFCWorkflow 派生类,把核心类链接到 MFC 的 GUI。 总而言之,核心类使用 Workflow 类操作 GUI,实现核心类和 GUI 框架的解耦。这样一来核心类开发和 GUI 类开发就成了两件独立的事情。

Workflow 接口说明

在我目前暂写的 Workflow 类中,纯虚函数分为两种: 第一种:由 GUI 调用,应当借助核心类实现的函数。命名为 on******()。 目前第一种函数一个都没实现。(逃

    virtual int onLogin(std::string, std::string) = 0;
    virtual void onAdminChangeList() = 0;
    virtual std::vector<std::string> onRequireAudioOutputList() = 0;
    virtual std::vector<std::string> onRequireAudioInputList() =0;
    virtual int onSelectAudioOutput() = 0;
    virtual int onSelectAudioInput() = 0;
    virtual void onShareScreen() = 0;
    virtual void onShareAudio() = 0;
    virtual void onRaiseQuestion() = 0;
    virtual void onCreateTest() = 0;
    virtual void onStudentEnter() = 0;
    virtual void onFocusChange() = 0;

第二种,由核心类调用,间接操作 GUI 的函数。 第二种函数不够全面,但目前列出来的都已经在 Qt 框架中实现。

    virtual void quitApplication() = 0;
    //登录
    virtual bool closeLoginWindow() = 0;
    virtual void showLoginWindow() = 0;
    //课室相关
    virtual void showClassroomWindow() = 0;
    virtual bool closeClassroomWindow() = 0;
    //管理相关
    virtual void showAdminWindow() = 0;
    virtual bool closeAdminWindow() = 0;
    virtual void addShowUsers(std::string, std::string, std::string, std::string) = 0;

这样的框架不会影响本项目之前所述的开发计划,原定先写的核心类可以直接放到 core 文件夹中。这个框架只是为了方便同时从 GUI 和核心两头掘进。

这种框架下,看似有两个业务流程类,实际上只有一个,因为 Workflow 作为抽象类只是名义上的业务流程类,实际执行者还是它的派生类。

另外,可以根据上面这些纯虚函数将项目分割。例如[我的任务板]。(https://github.com/cnDengyu/Thunder_Class/projects/1)

这也是我第一次做这样的项目,如有不足还望指出。

cnDengyu avatar Apr 24 '20 06:04 cnDengyu

很好!这也是目前这个设计方案的思想,我们希望把OSAPI和GUI这种环境相关的具体底层依赖与程序核心逻辑隔离开。理想情况下是做一个对应于OSAPIWrapper的GUIWrapper类,把GUI包装起来,然后Workflow只管逻辑,与GUI的互动由GUIWrapper对象来解决。但是作业要求只有一个业务流程类,兼具GUI控制和流程控制的功能,这个Workflow抽象基类相当于一个巧妙的解决方案,方便不同版本的GUI模块化拼装。手动赞!

profthecopyright avatar Apr 24 '20 07:04 profthecopyright

理想情况下是做一个对应于OSAPIWrapper的GUIWrapper类,把GUI包装起来,然后Workflow只管逻辑,与GUI的互动由GUIWrapper对象来解决。

想了很久,觉得我们对【业务流程类】这个词的理解可能有所不同。
我认为,【业务流程类】不应该管【业务逻辑】。【业务逻辑】应当由核心类来完成。
【业务流程类】是核心类与 GUI 类的桥梁,仅此而已。

cnDengyu avatar Apr 24 '20 15:04 cnDengyu

Workflow 抽象类还可以优化。

workflow.h

//此区域声明核心类
class Core;

//Workflow 类
class Workflow
{
private:
    //此区域写核心类
    Core* core;

public:
    //伪构造函数和伪析构函数
    void create();
    void destroy();

    //此区域写 on……()的函数
    void onUseCore();

    //此区域写 GUI 函数
    virtual void gui() = 0;
    
};

workflow.cpp

#include "workflow.h"
//引用核心类
#include "core.h"

void Core::create()
{
    this->core = new Core(this);
}
void Core::destroy()
{
    delete this->core;
}

//接口单行实现
void Core::onUseCore()
{
    return this->core->use();
}

uiworkflow.h

#include "../core/workflow.h"

//此处声明 GUI 类
class Window;

class UIWorkflow: public Workflow
{
private:
    //此处写 GUI 类
    Window* window;

public:
    //构造与析构
    UIWorkflow();
    ~UIWorkflow();

    //此处实现 GUI 函数
    void gui();
};

uiworkflow.cpp

#include "uiworkflow.h"
//引用 GUI 类
#include "window.h"

main()
{
    UIWorkflow u;
}

UIWorkflow::UIWorkflow()
{
    //构造成员
    this->window = new Window(this);
    this->create();
}

UIWorkflow::~UIWorkflow()
{
    //析构成员
    this->destroy();
    delete this->window;
}

//接口单行实现
void UIWorkflow::gui()
{
    return this->window->gui();
}

cnDengyu avatar Apr 24 '20 17:04 cnDengyu

不好意思那个伪析构函数踩着 delete 关键字了。手机码字没有高亮,疏漏请见谅。

cnDengyu avatar Apr 24 '20 17:04 cnDengyu

理想情况下是做一个对应于OSAPIWrapper的GUIWrapper类,把GUI包装起来,然后Workflow只管逻辑,与GUI的互动由GUIWrapper对象来解决。

想了很久,觉得我们对【业务流程类】这个词的理解可能有所不同。 我认为,【业务流程类】不应该管【业务逻辑】。【业务逻辑】应当由核心类来完成。 【业务流程类】是核心类与 GUI 类的桥梁,仅此而已。

其实我也是类似理解。来解释一下我的看法。 【业务逻辑】=时刻监督并控制程序下一步该干什么(抽象意义上),比如登录账户是Admin,那么下一步就是进入Server端模式,否则进入Client端模式;又如老师点击【点名】按钮,下一步就是生成一个随机数,给对应的Client发送点名信号。Workflow类只负责到这一步,我称为业务逻辑。

核心类如User类下的ServerUser和ClientUser实现,负责【业务的实际执行】,比如真正地创建Message对象,然后去传输点名信息,以及接收到点名信息以后解释并执行开麦操作等等。其实这部分架构和你的理解应该是一致的。

对于不同版本的GUI实现,可以加入另一层抽象去屏蔽它。如果没有作业中(只有业务流程类可以有GUI指针)的限制,我倾向于加一个GUIWrapper虚基类作为一切GUI的抽象代理去连接GUI端实际的用户操作(如单击Windows界面下的点名按钮)和对Workflow而言的抽象用户操作(用户发起了点名,Workflow要确定下一步做什么)。这样Workflow就是一个纯的Workflow,只实现程序逻辑(Controller),并不关心GUI长啥样(View),理论上可以和任何具体GUI实现和底层业务执行分离开。

但是目前有这个作业要求,我认为你的方法是很好的,用Workflow虚基类去抽象,相当于合并了我理想中的Workflow(具体)+GUIWrapper(抽象)类的功能,只是设计模式上的区别,思路上应该是一致的。

profthecopyright avatar Apr 24 '20 19:04 profthecopyright

不好意思那个伪析构函数踩着 delete 关键字了。手机码字没有高亮,疏漏请见谅。

帮你改成destroy()了,顺便改了下代码风格(

profthecopyright avatar Apr 24 '20 19:04 profthecopyright

【业务逻辑】=时刻监督并控制程序下一步该干什么(抽象意义上),比如登录账户是Admin,那么下一步就是进入Server端模式,否则进入Client端模式;又如老师点击【点名】按钮,下一步就是生成一个随机数,给对应的Client发送点名信号。 Workflow类只负责到这一步,我称为业务逻辑。

我认为,Workflow 连这一步都不必做。考虑到移植时 UIWorkflow 的代码需要重写,里面的东西越少越好。这也是上面的代码中“接口单行实现”的意义。
我理解的 Workflow 只负责告知某一个核心类(比如 LoginBot)点击了登录按钮。

void Workflow::onLogin()
{return this->loginbot->onLogin();}

判断用户是学生还是教师还是管理员,以及判断之后进入的客户端模式,以及关闭登录窗口,打开新窗口,都应该在核心类的成员函数 LoginBot::onLogin() 中实现。这样才能最大程度实现核心类和 GUI 解耦,并最大程度降低移植的工作量。

一个困惑 Workflow/UIWorkflow 是把一个类拆成了两个类,UI 为实,Core 为虚。也就是说,这两个类完全可以反过来写。(继承关系可以反) 但是拆分类之后,实的部分更贴近 main() 函数,虚的部分拥有完整的函数表。这是不对称的,但我始终想不明白怎样写更优。

还有一点就是,我认为 GUI 是 wrap 不起来的。 如果写出来,必定和 Workflow/UIWorkflow 相似。 调用操作系统 api 是单向调用,可以封装。 GUI 是双向调用,难以封装。(在写出这个抽象类之前,我最先是用回调函数指针,但函数指针不能传入类的成员函数,然后改来改去才做成了现在这个 Workflow)

cnDengyu avatar Apr 24 '20 23:04 cnDengyu

【业务逻辑】=时刻监督并控制程序下一步该干什么(抽象意义上),比如登录账户是Admin,那么下一步就是进入Server端模式,否则进入Client端模式;又如老师点击【点名】按钮,下一步就是生成一个随机数,给对应的Client发送点名信号。 Workflow类只负责到这一步,我称为业务逻辑。

我认为,Workflow 连这一步都不必做。考虑到移植时 UIWorkflow 的代码需要重写,里面的东西越少越好。这也是上面的代码中“接口单行实现”的意义。 我理解的 Workflow 只负责告知某一个核心类(比如 LoginBot)点击了登录按钮。

void Workflow::onLogin()
{return this->loginbot->onLogin();}

判断用户是学生还是教师还是管理员,以及判断之后进入的客户端模式,以及关闭登录窗口,打开新窗口,都应该在核心类的成员函数 LoginBot::onLogin() 中实现。这样才能最大程度实现核心类和 GUI 解耦,并最大程度降低移植的工作量。

一个困惑 Workflow/UIWorkflow 是把一个类拆成了两个类,UI 为实,Core 为虚。也就是说,这两个类完全可以反过来写。(继承关系可以反) 但是拆分类之后,实的部分更贴近 main() 函数,虚的部分拥有完整的函数表。这是不对称的,但我始终想不明白怎样写更优。

还有一点就是,我认为 GUI 是 wrap 不起来的。 如果写出来,必定和 Workflow/UIWorkflow 相似。 调用操作系统 api 是单向调用,可以封装。 GUI 是双向调用,难以封装。(在写出这个抽象类之前,我最先是用回调函数指针,但函数指针不能传入类的成员函数,然后改来改去才做成了现在这个 Workflow)

其实分歧本质在于“类叫什么名字”的问题。我们都认定需要有一个类完成GUI和程序逻辑之间的通讯(你叫它UIWorkflow,我叫它GUIWrapper),还有一个类完成流程控制(你叫它Core(包含很多具体的类),我叫它WorkflowManager(单个类))。

关键问题在于,负责业务流程逻辑并不是单个诸如LoginBot的类可以完成的。在用户按下某个按钮后具体做何响应,取决于当前的程序状态,对应同一GUI操作的响应机制可能随程序状态的变化而不同。比如某个Client已经登录后,如果Server再次接收到同一Client的登录请求,这个响应模式和此Client第一次登录时应该是不同的,因此Server端的LoginBot需要知道当前接入Server的Client有哪些(这就是一种程序状态),然后做出判断。然而我认为LoginBot这个模块不应该“主动”判断该不该让它登录,而只是负责“执行”允许/拒绝登录对应的操作,而决定权应该属于更高一级的我称为【业务流程逻辑控制】的东西(作业要求中仅允许出现一个业务流程类,我的理解就是管这种杂事的,这个类会很恶心,维护了整个程序的各种宏观状态,据此判断对应同一GUI操作的实际执行方式,然后向下调用具体的我称为ServerUser/ClientUser对象方法具体执行)。

另外GUIWrapper这个名字的确不太准确,应该是类似于GUIDelegate的东西,其实是在抽象逻辑层面代理GUI的操作(类似于Objective-C中的delegate机制),完成这个讨厌的和GUI的双向连接机制(你说我的GUIWrapper就是你的UIWorkflow,我完全赞同)。

关于虚实的问题,我认为你原来那样所有虚函数接口定义在虚基类Workflow里,然后根据不同的UI派生不同的UIWorkflow子类是很好的设计。在main函数创建对象里可以直接创建Workflow基类对象,然后调用的(形式上)全都是基类对象方法,不同之处只在于赋值号右边是什么。C++的虚基类相当于Java等语言的接口,定义的只是必须包含的方法类型,也就是说对于main()来讲,你的GUI长什么样不重要,有那几个(抽象的)按钮和对应的操作就行。

int main()
{
    Workflow* wf1 = new QtWorkflow();
    Workflow* wf2 = new MFCWorkflow();      // 选择一个,其余代码均不需要更改
}

profthecopyright avatar Apr 25 '20 04:04 profthecopyright

分歧的本质在于“类叫什么名字” 确实是,之前的讨论是类名字的问题。下面我想提出一种方式,“消灭”掉管杂事的类。

之所以要有一个管杂事的类,是因为核心类“需要知道当前程序的状态”。

Workflow 接口的设计是为了:
Core 调用 GUI
GUI 调用 Core

但它同时也实现了:
Core 调用 其他Core
GUI 调用 其他GUI

因此,为了保存程序状态,可以在 Workflow 中创建一个核心类 State ,然后由需要获取程序状态的核心类通过 Workflow 指针间接获取。

这样一来,消灭掉管杂事的类,降低代码的“恶心”程度。

cnDengyu avatar Apr 25 '20 05:04 cnDengyu

分歧的本质在于“类叫什么名字” 确实是,之前的讨论是类名字的问题。下面我想提出一种方式,“消灭”掉管杂事的类。

之所以要有一个管杂事的类,是因为核心类“需要知道当前程序的状态”。

Workflow 接口的设计是为了: Core 调用 GUI GUI 调用 Core

但它同时也实现了: Core 调用 其他Core GUI 调用 其他GUI

因此,为了保存程序状态,可以在 Workflow 中创建一个核心类 State ,然后由需要获取程序状态的核心类通过 Workflow 指针间接获取。

这样一来,消灭掉管杂事的类,降低代码的“恶心”程度。

那么最终程序状态数据结构State是要作为Workflow的成员对象,由Workflow对象去维护了。这样的话,所有的具体管事的核心类(如Loginbot)都要有一个当前Workflow的指针作为成员(至少要把Workflow的this指针作为参数传递到Loginbot的成员函数中),相当于底层核心类都要依赖于上层的理应只负责GUI和抽象逻辑的Workflow,同时Workflow又要调用底层核心类的方法去具体执行逻辑。这样的循环依赖并不很合适,容易纠缠不清,难以实现模块分离。

这种设计我的理解还是类似于把Status当成一个静态变量/全局变量一般的东西,只是由于面向对象的要求必须把它放到某个类中维护于是就选择了Workflow类,但并不是完全的面向对象思想。

profthecopyright avatar Apr 25 '20 07:04 profthecopyright

这里先阐述一下我执意要“消灭”管杂事的类的原因。
————————
设想一个场景:

程序员 A 开发了一个类 Net,用来进行 TCP 通信。
显然,这个类与 GUI 无关,不需要保有 Workflow 指针。
这个 Net 类称为底层核心类。

程序员 B 正在开发一个类 SingleFrame,这个类利用 Net 类从网络上获取一张图像,并显示到窗口中。
这个类处理“显示一张图像”的业务逻辑,因此需要保有 Workflow 类的指针,同时需要有一个 Net 类的成员变量。
这个 SingleFrame 类称为业务逻辑类。
同时,SingleFrame 并不是收到图像就显示出来,而是要获得服务端的准许才能显示。

这时程序员 B 想起来,程序员 C 此前开发了一个 Permission 类,提供了 bool getPermession() 函数。
这个 Permission 类也称为业务逻辑类。

于是程序员 B 写下了代码段

if(this->workflow->premission->getPermission())
{
    this->workflow->showSingleFrame(Image image);
}

B 开发完了 SingleFrame 业务逻辑类,想要添加到项目进行调试,于是在 core/workflow.h 中添加了

……
class SingleFrame;//added
……
private:
……
    SingleFrame *singleFrame;//added
……

在 core/workflow.cpp 中添加了

#include "singleframe.h"

在 Workflow::create() 中添加了

    this->singleFrame = new SingleFrame(this);

同时确保 singleFrame 的构造在 Permission 之后。
在 Workflow::destory() 中添加了

    delete this->singleFrame;

同时确保 singleFrame 的析构在 Permission 之前。

设想场景结束。
————————

这个 Permission 类是我们之前讨论的“状态类”,其实并不是全局变量或静态变量的为了封装而封装。
在上面的场景中,我提到了【业务逻辑类】,它是核心类的一种。
在上面的场景中,我提到了【底层核心类】,它和上一条 comment 中的【底层核心类】表达同一个意思。

明确了这些以后,我们可以感受到,所谓“消灭管杂事的类”实际上是将一个庞大的,你所说“恶心”的类,拆分成许许多多的“业务逻辑类”,并通过 Workflow 连接。
而那些底层核心类,由业务逻辑类调用,并不需要直接保有 Workflow 指针。

另外你应该已经注意到,所有的类都是通过指针创建的,任何一个类不包含实际的类对象,只包含类指针。
因此我要纠正上一个 comment 中的表述:可以说这是循环引用,但不可以说这是循环依赖。

另外上一个 comment 中所说,容易纠缠不清。确实,这样写容易在不经意间造成无限递归。这种写法以多重引用为代价,实现了GUI 与业务逻辑类的双向调用。

至于 Workflow 维护许多对象,我期望规定 Workflow 只做下面几件事:
1、按依赖顺序创建对象。
2、反依赖顺序销毁对象。
上述对象特指业务逻辑类对象。

综上所述,按照需求文档的定义和我们之前的交流,可以总结出以下几点:
1、核心类分为底层核心类和业务逻辑类。
2、业务流程类直接操作 GUI 与业务逻辑类。 3、只有业务逻辑类需要保有 Workflow 指针。 4、你理解的业务逻辑类是一个,我理解的业务逻辑类可以是多个。

cnDengyu avatar Apr 25 '20 09:04 cnDengyu

  1. 关于依赖的定义说明

你指的依赖/引用更多是代码层面的,而我说的依赖是软件工程中软件设计层面的。在软件工程中,类之间的依赖 (Dependency) 关系定义为:如果A类的改变会对B类造成影响(也就是说B类只要“用到”了A类),那么就认为B类依赖于A类。这个依赖其实是一个很松散的定义,在这种意义下,成员(包括指针)依赖或者函数参数依赖都称之为依赖。从这个角度讲,你的设计中,Workflow类持有SingleFrame类的对象指针,因此Workflow类依赖于SingleFrame类;与此同时SingleFrame类对象的成员方法需要传入Workflow类的对象指针,因此SingleFrame类依赖于Workflow类。

然而这两种依赖的“强度”是不同的,Workflow类对SingleFrame类的依赖更“强”一些,在UML里对这种依赖关系有另一个特定的名词,叫做关联 (Association)。Workflow对SingleFrame是关联性依赖,SingleFrame对Workflow是弱的,非关联性的依赖(但仍然是依赖)。在这个框架下,因为并不是双向关联,所以在代码层面上可以避免报错,但是从软件工程角度讲,这种循环依赖违背软件设计基本原则,是软件设计的大忌,因为会对软件的可维护性带来很大问题,基本上是无法接受的程度,也不符合你推崇的解耦思想。

关于关联和依赖的更多讨论和例子可以参阅这里,或者这里,或者任何一本软件工程教科书。

  1. 如何解决循环依赖

一般方法有很多,包括添加抽象层、分包、合并、反向依赖等等。具体到这个应用中,我认为一个很好的解决办法是把Status类独立出来。在你的设计中,Workflow类的对象并不需要确切知道当前的程序状态,只负责通知正确的业务逻辑核心类调用正确的函数,而具体的函数执行逻辑是由业务逻辑核心类参照Status对象中的数据做出判断。因此Status类指针并不需要作为Workflow类的成员,由Workflow类去维护,Workflow类所要做的工作至多是在程序初始化时创建Status类的对象,然后得到对象指针,将该指针注册到所有的业务逻辑核心类(即初始化业务逻辑核心类拥有的Status指针成员),之后Workflow和Status内的数据再无任何关系。这种架构下,Workflow依赖于Status,业务逻辑类也依赖于Status,同时Workflow依赖于业务逻辑类,但依赖有向图并不成环。

在此基础上如果进一步思考一下,我认为Status还可以下移。既然Workflow的功能定位只是个传话筒,负责GUI和程序核心业务逻辑类之间进行通信,那我们干脆可以把初始化Status并维护指针的任务都交给核心业务逻辑,这也就是我的设计中核心业务逻辑类有个顶层类User的原因。Status属于核心逻辑的一部分,理应由核心逻辑的执行者User来维护(User类似你早先说的Core)。Workflow只需要创建User对象,并告知User点击了什么,具体的操作由User执行。

  1. 如何反馈信息

那么现在还有最后一个问题,就是如果User类想更改GUI(比如User要求显示图像),如何做。由于不形成依赖环后,User类不能直接调用Workflow类的方法(不以on开头那些),因此需要一种新的机制。在此我建议使用事件(event)机制来传递消息(参见这里)。本质上,GUI的响应原理就是用户对GUI界面进行操作(如单击按钮)时,由操作系统产生一个事件对象,然后由Workflow捕获(因为Workflow中定义了事件处理(event handling)对应的函数挂钩),并触发相应函数,完成事件响应。类似地,我们可以人工定义由User类(程序核心逻辑)产生的event类型,然后把对应的event handler函数定义在Workflow类里。这样Workflow类处理GUI event和User event的逻辑没有任何本质区别,都用类似on开头的函数就可以。然后User需要Workflow对GUI做出更改的时候,只需要通过__raise关键字去产生一个对应类型的事件,便会自动被Workflow捕获。这样逻辑上实现了消息的双向传递,但从依赖关系上,User和Workflow共同依赖于自定义的InnerEvent类,Workflow依赖于User,但User并不依赖于Workflow。

对应这种架构的类图已更新。

profthecopyright avatar Apr 25 '20 20:04 profthecopyright

按照这个类图的架构确实更好。

之前提出的那个方案的出发点是方便各个业务逻辑单独开发,便于多个开发者共同参与。
我们的设计都需要通过一个顶层类把各种业务逻辑整合到一起。我一开始选择了 Workflow,但加一层 User 应该是更好的选择。 确实,选择 Workflow 作为顶层类并不是很好的选择。前几条 comment 中希望让 Workflow 做的事情,其实是我希望顶层类做的事情。 由于此前开发GUI把 UIWorkflow 作为顶层类的惯性思维(GUI并不需要复杂的逻辑),我把 Workflow 选择作为顶层类确实是一种失误。

那么在我发的上一条 comment 中,我希望的对 Workflow 的操作,实际上是我希望的对顶层类的操作。(由于 User 类又分了三种环境下的具体实现,这些操作又可能是我希望的对 ClientUser类的操作) 因此,Status 下沉到 User 当然是正确的做法。

当初那种把 Workflow 作为顶层类,实际上是我为了协作开发更方便而矫枉过正,牺牲了可维护性。

我的本意是业务功能模块化开发。

题外话:这样一来,你此前所述的“管杂事”的类,实际上是整合各种业务功能(而不是实现业务功能)的类。当“管杂事”的类有了明确的功能,它便不再是管杂事了,这或许也是消灭“管杂事的类”的另一种形式。

那么也许可以这样设计: 核心类分为三种:结构设计类,业务功能类,底层核心类。 目前的类图中已经列出的都是结构设计类,它们不实现具体的业务功能,但是帮助业务功能类分成不同的模块。它们只做两件事:构造和析构。 业务功能类就像之前的 LoginBot 例子,它们实现具体的功能。按照这个类图,User 类需要判断用户身份,因此 LoginBot 是 User 类中的实现具体功能的类。 业务功能类又像之前的 SingleFrame 例子,它应当是 ClientUser 中实现具体功能的类之一。

cnDengyu avatar Apr 25 '20 23:04 cnDengyu

  1. 关于管杂事的类:这种架构的确可以消灭管杂事的类,原因之一在于把Status独立出来成一个底层类,并不需要作为某个特定的超级管理者的成员一家独大去维护,而是可以在程序中动态维护,交由各个相关类去共同访问(只需要顶层User类负责内存的构造与析构)。另一个原因其实是把抽象意义的“杂事”交给了抽象层User(整合),杂事的具体实现逻辑进一步下沉到User的具体派生类中,加一个过渡层就可以把Workflow从杂事中彻底分开,其实更方便独立开发。

  2. User的确是一个程序内部逻辑的抽象,具体User怎么实现具体的逻辑,可能借助成员函数,也可能如你所说,把具体的业务功能分拆到相应的LoginBot或者相应的各种XXBot类中,各Bot由User统一维护(其实函数太多的话拆分是个自然而然的事情)。

那么也许可以这样设计: 核心类分为三种:结构设计类,业务功能类,底层核心类。

同意。

profthecopyright avatar Apr 26 '20 00:04 profthecopyright

具体User怎么实现具体的逻辑,可能借助成员函数,也可能如你所说,把具体的业务功能分拆到相应的LoginBot或者相应的各种XXBot类中,各Bot由User统一维护(其实函数太多的话拆分是个自然而然的事情)。

有些时候一些业务功能并不需要和其他功能共享状态。比如,需求里提到了密码错误三次退出程序。这个密码错误的次数只有 LoginBot 需要用到。这也是我个人倾向于对象实现而不是直接在 User 的成员函数的原因。

cnDengyu avatar Apr 26 '20 04:04 cnDengyu

有些时候一些业务功能并不需要和其他功能共享状态。比如,需求里提到了密码错误三次退出程序。这个密码错误的次数只有 LoginBot 需要用到。这也是我个人倾向于对象实现而不是直接在 User 的成员函数的原因。

所以我说拆分是自然而然的。如果功能足够繁多,User肯定无法一个类里面全部实现,那样会在类中实现几十个函数,极其臃肿且难以维护。可以把一类功能分别委派到分别的更底层的对象去实际操作,比如LoginBot之类的,User类只要维护一个LoginBot(以及其他相应XXBot)的指针即可。但是从WorkflowManager上层看,它需要管的只是报告给User,具体User什么样的实现细节,又引用了哪些具体的功能类,那是User的事情,WorkflowManager不需要知道,它只要知道对应的接口叫什么名字就好了。

一个比喻,WorkflowManager是领导秘书,User是领导,XXBot是干事。秘书只需要认识领导是谁,负责向领导报告公司外部动态(用户操作GUI事件),并根据领导的指示(InnerEvent)变动GUI(把图像显示出来)。具体领导怎么做,是自己能做了还是要派哪个干事做,秘书没有权利管也不必在意。领导要是觉得活少,就自己干了;要是事比较复杂,就下放给下级Bot部门;业务越来越多,就自然而然会多创造几个部门分管。这样秘书不需要直接和Bot干事通信,领导可以自由变动人力分配而不需要改动秘书的实现细节;反之亦然,换个秘书(GUI实现)只要可以完成要求的业务流程(操作GUI),就可以和领导直接对接。

所以其实WorkflowManager本质上是一个GUIWrapper/Delegate,但这里我觉得叫GUIAdaptor更合适,因为其实就是个适配不同GUI的接口定义,对应纯虚基类正合适。

这个接口标准和InnerEvent定义的消息标准一旦确定,具体的GUI和具体的核心类实现可以彻底分离,因为有两个抽象层领导和秘书的双重隔离机制,两边都可以随意自由更改实现细节而不必通知对方。

profthecopyright avatar Apr 26 '20 06:04 profthecopyright

上面讨论了项目架构的类的关系。

下面我想提出关于数据结构的存放的问题。

在上面的 GUIAdaptor 中有一个接口 addShowUsers(string, string, string, string)
我们肯定希望用一个结构体把 id,用户名,密码,身份这四个数据打包起来,也方便日后的拓展。 由于要求中禁止 GUI 类直接访问“其他类”,这些数据的打包只能通过结构体来实现。 那么问题来了,下面这段代码应该写在哪个文件中?

typedef struct
{
    string id;
    string name;
    string password;
    string role;
} studentInfo;

我觉得可以放在 GUIAdaptor.h 中,或者单独写一个 .h 文件 include 到 GUIAdaptor.h 中,或者不同的结构体写在不同的 .h 文件,放进同一个文件夹,再依次include 到 GUIAdaptor.h 中。因为 GUI 和其他类都可能用到这些。

在 OSAPIWrapper 的开发过程中,也会遇到结构体的问题(但和上面这个例子有所不同)。由于涉及到的内容过于复杂,还是在专门讨论 OSAPIWrapper 的 issue 中讨论比较好。

cnDengyu avatar Apr 26 '20 11:04 cnDengyu

还有一个问题——计时器。 计时器并不算 GUI 类,也不应该由 GUI 接口去频繁地发送时间信息。因此,OSAPIWrapper 可能也需要有一个回调机制。

cnDengyu avatar Apr 26 '20 15:04 cnDengyu

上面讨论了项目架构的类的关系。

下面我想提出关于数据结构的存放的问题。

在上面的 GUIAdaptor 中有一个接口 addShowUsers(string, string, string, string) 我们肯定希望用一个结构体把 id,用户名,密码,身份这四个数据打包起来,也方便日后的拓展。 由于要求中禁止 GUI 类直接访问“其他类”,这些数据的打包只能通过结构体来实现。 那么问题来了,下面这段代码应该写在哪个文件中?

typedef struct
{
    string id;
    string name;
    string password;
    string role;
} studentInfo;

我觉得可以放在 GUIAdaptor.h 中,或者单独写一个 .h 文件 include 到 GUIAdaptor.h 中,或者不同的结构体写在不同的 .h 文件,放进同一个文件夹,再依次include 到 GUIAdaptor.h 中。因为 GUI 和其他类都可能用到这些。

在 OSAPIWrapper 的开发过程中,也会遇到结构体的问题(但和上面这个例子有所不同)。由于涉及到的内容过于复杂,还是在专门讨论 OSAPIWrapper 的 issue 中讨论比较好。

我认为这些基本的数据类型(消息格式)结构体/宏定义属于最底层,应当放在basic包中的某个.h中定义。此包中还包括Status类、Message类、InnerEvent类和OSAPIWrapper类等的定义。

另外,我并没有看到界面类不得访问任何其他类的要求。界面类如果要通过Event机制接收User传来的指令,必定要能够解读InnerEvent的对象,也就是说GUIAdaptor需要包含InnerEvent定义的头文件。同理,另一个方向传递的消息(从GUI到USer)也是如此,不管从界面传来的用户登录输入是以结构体还是对象的形式(个人倾向于对象)去传送给User,必定要包含一个底层定义格式的头文件。这也就是我为什么坚持暂缓把人放进来直接写代码的原因。至少这些底层定义要先做好,别人再往上写才有个比较好的基础且可以直接调试,不然每人定义一个不同的底层格式会乱的。

关于计时器:程序中实现的计时相关功能主要有两种。

  1. 学生在接收到题目后在GUI中显示倒计时提醒按时作答。这部分内容并不需要操作系统的“绝对时间",因此可以不直接通过OSAPIWrapper实现。一般提供GUI开发的类库中会包含相应的Timer Object定义,会以固定的Interval产生时钟事件,然后由GUI中的onTimer()去响应时钟事件即可。如Qt中的Timer类型在这里。(当然本质上还是变相通过Qt调用了OS的API。。。但GUI本身就调用了太多OS的API了,所以这个也无妨。)比如每隔1s产生一个时钟事件,GUI上显示的读秒-1(手动苟头,此时并不需要和User通讯);然后时间到了以后产生另一个事件,User自动交卷等等。

  2. 服务器端的日志功能和学生端的注意力统计功能。这种需要用到“绝对时间”的功能可以交由User级通过OSAPI来统一记录和调用。比如注意力统计,如果GUI的当前窗口失去焦点/获得焦点时,都会产生一个事件,然后用GUIAdaptor的onLostFocus()和onGetFocus()一类的函数去告诉User有这么个事件,再由User的相应函数来具体维护(比如失去焦点时记录当前OS的时刻并与上一次记录相减,得到这次注意力的时长,加到Status的某个域里面)。

其实还有一个3. 是视频问题。 #11 据说老师建议不用FFmpeg,而用屏幕截图+增量jpeg压缩的方法去实现视频传输。如果需要手写这个,那么可能要考虑发送端的帧率控制和接收(显示)端的音画同步问题。这个地方现在还没想好具体怎么弄,但原则上应该还是可以分离的(比如发送端截图时间控制由User调用OS的API去做,显示端的控制由GUI去做)。

profthecopyright avatar Apr 26 '20 20:04 profthecopyright

关于 OSAPIWrapper,如果我们封装一个表面层,同时适配两种API的话,那么核心类中就要调用一个既不是WinAPI也不是MacAPI的一个API(ThunderClass API?) 考虑到 windows 用户较多,封装出来的 OSAPIWrapper 应该尽量贴近 Windows API ,降低核心部分的开发难度。 至于 mac ,看看能不能把调用 Windows API 的内容转译成 MacOS 对应的 API 。(WINE for mac?) 总而言之,跨平台应用不用跨平台库是真的麻烦。 个人认为要“封装后使用系统API”,还不如把用到API的模块写两个版本来得方便。这个作业要求简直有毒

cnDengyu avatar Apr 29 '20 01:04 cnDengyu

关于 OSAPIWrapper,如果我们封装一个表面层,同时适配两种API的话,那么核心类中就要调用一个既不是WinAPI也不是MacAPI的一个API(ThunderClass API?) 考虑到 windows 用户较多,封装出来的 OSAPIWrapper 应该尽量贴近 Windows API ,降低核心部分的开发难度。 至于 mac ,看看能不能把调用 Windows API 的内容转译成 MacOS 对应的 API 。(WINE for mac?) 总而言之,跨平台应用不用跨平台库是真的麻烦。 个人认为要“封装后使用系统API”,还不如把用到API的模块写两个版本来得方便。这个作业要求简直有毒

是啊 所以我现在的设计图中单独包含了一个API类,里面所有方法都是静态方法。其他类直接调用API类的类方法,然后API类再调用OSAPIWrapper以及相关音视频流/压缩算法之类的库。OSAPIWrapper可以有两个版本,但目前我也倾向于还是集中先做windows吧。

profthecopyright avatar Apr 29 '20 02:04 profthecopyright