架构思维:如何让写程序像搭积木一样轻松?

共 11542字,需浏览 24分钟

 ·

2022-01-10 19:24

这篇算是来自读者【戚翔尔】的约稿,MVC主题,先给写上。
本篇文章属于启发型,会联系到各种知识,不一定皆是编程领域的,概念碰撞,思维摩擦,以飨读者。

1

开发思维

开发能力的提高,往往不在于你懂得几种语言、多少语法,因为这些都只是应用层面的东西。

开发者真正值得增加杠杆的地方在哪呢?解决问题的思维。

开发思维,就是利用编程来解决实际问题的思考方式。这需要多思考,写项目实践,再反思有效的方式,优化无效的方式,不断完善开发流程。

那么设计模式算不算开发思维?大家看得到的设计模式的结构图、代码这些,都不算是。

如何形成这种结构?为何要包含这些组件?为何同一问题存在多种相似的设计模式?为何要满足SOLID原则?这些背后的原理与依据,才是开发思维。

本篇讲解的MVC,只是开发流程的一个部分,在这部分中需要组织程序的结构。依据解决这个问题的思维方式,具体实现出来的一种形式就是MVC。MVC具体实现的技术用到了一些设计模式,所以它本身就属于是对设计模式的应用。也可以理解为,设计模式的组合运用,配合开发思维,可以创建架构。

简而言之,MVC是一种组织代码结构的思维,其具体实现使用了某些设计模式。

2

问题解决流程

解决问题一般分为三个步骤:确定问题、分析原因和选择策略。

第一步,确定问题。

什么是问题?问题就是理想和现实之间的差距。

确定问题,就是搞清楚真正的需求是什么,为目标定一个方向,以免南辕北辙。

大家一定都有过这样的经历:拿到一个需求,或是自己实现一些小项目,写了半天才忽然发现这种设计实现起来有问题,或是不符合需求。从而推倒重来,浪费了大量时间,这便是忽略了这一步。

因此这步有两个作用,一是定义,二是定向。定义用于明确需要做什么,定向用于清楚要做成什么样。

完成了以上两点,也就确定了需要解决的问题,接着便可以集中注意力来具体分析问题。

第二步,分析原因。

所谓分析,就是将复杂问题拆解成更具体、更熟悉的小问题,来一步步攻克它。

这样,就可以把抽象的问题,转化成易被解决的问题。在此基础上,便能够决定程序应该包含哪些组件与模块,从而搭建起程序的基本框架。

一个项目,由想法到落地,不是一开始就具备所有功能,实现所有需求,而是先搭建起一个基本框架,再往里面填充细节。最初,框架并不会很复杂,只会包含实现核心需求所需要的主要组件,通过这些主要组件,迅速让这个系统运转起来。然后在此基础上,发现缺少什么功能了,再慢慢添加进来,久而久之,简单的系统就变成了很大的项目。

打个比方。一个项目就像是一颗种子,起初可能只是一个不经意间的想法,被你敏感地捕捉到了。你不断给它浇水、施肥,让这个想法逐渐清晰、完善起来,使它生根发芽,慢慢成长,最终长成参天大树。

这一步可以说是项目能够落地的关键步骤,也是真正的难点所在。

最后一步,选择策略。

经过分解,抽象的问题变成了一个个具体的问题,我们现在要做的,就是去寻找攻下它们的方法。

其实大部分问题,都是不需要自己创造方法去解决的,前人早就给我们总结好了。我们只需要去学习这些总结好的策略,就能够应付绝大多数问题,这需要的不是天赋,而是勤奋。

设计模式本身就属于这一阶段,那么它自然是由前两个步骤总结、归纳、提炼而来的。说它难学、抽象,不是说代码本身有多复杂,而是解决问题的思维很难掌握。代码只是结果,如何产生的过程才是重点。

简言之,前两个步骤重点在于纵向洞察问题,这一步骤在于横向寻找若干解决方案。

组件划分好了,如何安排它们的结构?如何处理它们的逻辑?如何把它们组合起来使用?如何生成它们?它们之间应该如何交互?参数应该如何传递?等等等等问题,最终都在这里解决。

也就是说,真正的编码实现是在这一步,它是前两步完成之后,自然而然确定的结果。

MVC就属于这一步,Model是什么?就是拆解问题所确定的组件构成的模块。组件与模块之间是什么关系呢?模块由组件构成。比如网络模块,其中含有的TCP、UDP、HTTP这些工具就是组件。

可以这样理解,模块就像是积木,功能就是部件,积木构成部件,部件组成了程序整体。

3

三层思维模型

拆解划分的诸多模块,其实就是程序的「逻辑部分」。

如何将这些逻辑更好地展现出来,就涉及程序的「结构部分」。

可以类比摄影。模块就好似拍摄的素材,它是一个个小的视频片段,把这些片段进行组合拼接,就可以形成一部完整的作品。整合形式的不同,产生的效果也不同,如何让视频的观感更佳?便产生了许多「蒙太奇手法」,蒙太奇指的就是把零碎的片段整合成作品的方法。

那么在程序开发领域,是不是也存在一些整合模块的蒙太奇手法呢?的确是有的,MVC、MVP、MVVM这些都属此列。

那么它们到底是做了个什么工作呢?

这里向大家分享一个我一直在践行以及迭代的模型——「三层思维模型」。它可以帮助你从更高的维度来理解这些「蒙太奇手法」是如何来组织程序的结构的。

可以把程序架构整体上分为三个层次:应用层、结构层和原理层。

应用层指的是能够被用户感知的、可交互的东西,例如界面、接口这些。

结构层指的是构成程序的组件、模块、功能,也就是在源码中直接呈现的、能够被开发者感知到的东西。

原理层指的是底层的、不变的理念思路,是一切的基础和支撑。

这就好似写作,原理层是作者的写作风格和思路,结构层是作者使用的词语句式,应用层则是最终呈现出来的作品。一旦作者的风格与思路确定了,整部作品就是顺势而为,遣词造句只是技巧上的功力。

所以看不见的原理层才是关键,程序开发实际上是从原理层入手,确定设计理念与原理,再描绘出最终想要呈现的形式,次再依据原理与形式,拆解出程序需要包含的功能,划分出程序的模块和组件。

4

具体架构的多样性

拆解出程序的模块与组件之后,便产生了新的问题:如何合理地安排它们之间的结构

要与用户交互,模块与组件必须呈现出来,因此,实际上就是处理「结构层与应用层」之间的关系。

结构层的所有东西,就称之为Model;应用层的东西,称之为View。

Model和View自然可以直接关联,杂糅到一起,但如此一来,程序结构将会非常混乱。

结构从本质上来说,是一种逻辑。结构是程序功能的逻辑体现,清晰的结构是项目需求确定后的自然选择。

因此,往往会通过一个桥梁来连接Model和View。这样,让Model和View更加关注自身的职责,它们之间如何沟通,则由这个桥梁来负责。

在这个理念下,具体实现方式的不同,便产生了多种策略。比如MVC、MVP、MVVM。

也可以理解为,采用的技术不同,逼近理念的程度也不同,正因为技术上存在局限,才产生了不同的实现方式,来尽可能更加完美地实现理念。

所以,只要你的实现能够满足项目需求,遵循这个理念,即便不使用常见架构,又有何不可呢?太过拘泥于架构的某种具体实现形式,未免胶柱鼓瑟,过于穿凿了。

5

MVC

MVC连接Model和View之间的桥梁便是Controller,它的工作是创建合适的View并与Model沟通,从而进一步配置View的数据。

下面以一个例子来进行讲解。

这是用Duilib写的一个小Demo,模拟了一个钱包界面,可以充值余额、显示余额、并对余额进行自增和自减操作,操作结果将自动更新到当前余额。

首先来看View部分,主要由BalanceView类负责。

1class BalanceView : public WindowImplBase
2{
3public:
4    BalanceView(BalanceController* controller);
5
6    void InitWindow() override;
7    void Notify(TNotifyUI& msg) override;
8    LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override;
9
10    void UpdateBalance(int value);
11
12protected:
13    CDuiString GetSkinFile() override;
14    LPCTSTR GetWindowClassName(void) const override;
15
16private:
17    void OnClickTopUp(TNotifyUI* pObj);            // 充值事件
18    void OnClickBalanceIncrease(TNotifyUI* pObj);  // 余额自增事件
19    void OnClickBalanceDecrease(TNotifyUI* pObj);  // 余额自减事件
20
21private:
22    BalanceController* pController;
23
24    UIEventHandler m_ClickHandler;  // 点击事件处理
25    CButtonUI* btnTopUp;            // 充值按钮
26    CButtonUI* btnBalanceIncrease;  // 余额自增按钮
27    CButtonUI* btnBalanceDecrease;  // 余额自减按钮
28};

即便不懂Duilib也没关系,因为只是借助它来谈论MVC。

这里主要关注BalanceView的ctor,在这里保存了Controller的指针。为什么呢?

MVC的行为流程是这样的:用户通过View产生事件,Controller根据事件选择相应的策略,交由Model处理,Model处理完成后通知Controller,Controller再更新View。

因为View产生事件后要交给Controller去负责处理事件,所以需要保存Controller。代码很简单:

1BalanceView::BalanceView(BalanceController* controller)
2    : pController{controller}
3{
4}

当用户点击充值、自增、自减三个按钮时,同样将逻辑跳转到Controller。

1void BalanceView::OnClickTopUp(TNotifyUI* pObj)
2{
3    CEditUI* etTopUpValue = static_cast(m_PaintManager.FindControl(L"editTopUpValue"));
4    int value = _ttoi(etTopUpValue->GetText().GetData());
5
6    // 交由Controller负责与Model沟通
7    pController->TopUp(value);
8}
9
10void BalanceView::OnClickBalanceIncrease(TNotifyUI* pObj)
11{
12    // 交由Controller负责与Model沟通
13    pController->BalanceIncrease();
14}
15
16void BalanceView::OnClickBalanceDecrease(TNotifyUI* pObj)
17{
18    // 交由Controller负责与Model沟通
19    pController->BalanceDecrease();
20}

所以View的职责就只跟界面相关,获取事件,如何处理全权交给Controller。

接着来看Controller。

1class BalanceController
2{

3public:
4    BalanceController();
5
6    void TopUp(int value);
7    void BalanceIncrease();
8    void BalanceDecrease();
9
10    // Model处理成功后的回调函数
11    void OnSuccess();
12
13private:
14    std::shared_ptr pModel;
15    std::unique_ptr pView;
16};

Controller作为桥梁,只有它知道View和Model的存在,View和Model互不相知。

因此,Controller需要生成View和Model,Controller和Model之间是Observer的关系,Model作为subject,View作为observer。忘记的可以参考:C++ DP.11 Observer

在ctor中创建View和Model,代码如下:

1BalanceController::BalanceController()
2{
3    pModel = std::make_shared();
4    pModel->RegisterObservers(this);
5
6    // 启动View
7    pView = std::make_unique(this);
8    pView->Create(nullptrL"BalanceView", UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
9    pView->CenterWindow();
10    pView->ShowModal();
11}

可以看到,第4行Controller将自己作为observer注册到了Model中,如此一来,当Model处理完成时,就可以通知Controller。注:为了例子的简单,没有使用泛化的observer组件,其实那样将会更加灵活。

此处,Controller创建了View,于是Controller其实可以对应多个View,只要View有相似的行为。比如,同一数据分为柱状图、表格、饼图三个View,行为相同,多个View就可以使用一个Controller。

继续来看由View传来的三个按钮事件的处理,代码如下:

1void BalanceController::TopUp(int value)
2{
3    pModel->SetBalance(value);
4}
5
6void BalanceController::BalanceIncrease()
7{
8    int value = pModel->GetBalance();
9    pModel->SetBalance(value + 1);
10}
11
12void BalanceController::BalanceDecrease()
13{
14    int value = pModel->GetBalance();
15    pModel->SetBalance(value - 1);
16}

Controller只是起了逻辑分派的作用,它分析出事件应该使用哪些Model进行处理,调用相应的Model。

最后来看Model。

这里为Model定义了一个接口,

1struct BalanceModelInterface
2{

3    virtual int GetBalance() 0;
4    virtual void SetBalance(int value) 0;
5    virtual void RegisterObservers(BalanceController* view) 0;
6    virtual void RemoveObserver(BalanceController* view) 0;
7};

此处直接定义一个具体Model当然也是可以的,这只是单一性和多样性的差别,当需要多样性时,就抽象出一个接口。这里只是演示一下用法,Controller若是需要多样性,当然也应该定义一个接口,此时不同的具体Controller就是View的不同策略。

来看具体的Model定义:

1class BalanceModel : public BalanceModelInterface
2{
3public:
4
5    int GetBalance() override;
6
7    void SetBalance(int value) override;
8
9    void RegisterObservers(BalanceController* view) override;
10
11    void RemoveObserver(BalanceController* view) override;
12
13private:
14    void notifyObservers() const;
15
16private:
17    int balance{ 0 }; // 余额
18    std::vector observers;
19};

前面说过,Model属于结构层,包含模块与组件,由于程序很小所以没有体现出来。

实际上这就相当于是一个使用数据库组件的模块,充值时更新数据库中的余额,查询时从数据库返回余额。而模拟的代码则很简单:

1int BalanceModel::GetBalance()
2{
3    return balance;
4}
5
6void BalanceModel::SetBalance(int value)
7{
8    balance = value;
9    notifyObservers();
10}

只是返回并设置了成员变量,设置完成就相当于事件处理完成,所以需要进行通知。

通知当然既可以直接通知View,也可以通知Controller,再由Controller更新View。这在技术上都可以做到,只是谁作为observer的差别,但后者可以避免View和Model的耦合。

这里采用了后者,代码如下:

1void BalanceModel::RegisterObservers(BalanceController* view)
2{
3    observers.push_back(view);
4}
5
6void BalanceModel::RemoveObserver(BalanceController* view)
7{
8    auto iter = std::find(observers.begin(), observers.end(), view);
9    if (iter != observers.end())
10        observers.erase(iter);
11}
12
13void BalanceModel::notifyObservers() const
14{
15    for (auto& observer : observers)
16        (*observer).OnSuccess();
17}

这些代码都是Observer的内容,在此不再赘述。

当处理成功后,Model会回调Controller,也就是调用OnSuccess,

1void BalanceController::OnSuccess()
2{
3    int value = pModel->GetBalance();
4    pView->UpdateBalance(value);
5}

Controller再更新View的界面显示,将更新后的余额显示到界面上去。

6

MVP

MVP将View的职责划分的更加彻底,再来看看MVC的View处理:

1void BalanceView::OnClickTopUp(TNotifyUI* pObj)
2{
3    CEditUI* etTopUpValue = static_cast(m_PaintManager.FindControl(L"editTopUpValue"));
4    int value = _ttoi(etTopUpValue->GetText().GetData());
5
6    // 交由Controller负责与Model沟通
7    pController->TopUp(value);
8}

这里,View处于一个主动地位,它需要获取数据,再把数据传递给Controller。

也就是说,View需要关心数据,这就附带了部分逻辑。

MVP将View的这部分逻辑去除,使View由主动地位变为被动地位。也就是说,View不再主动传递数据,而是提供数据接口,需要数据之时,由Presenter通过接口获取数据;更新数据之时,由Presenter通过接口设置数据。

因此,MVP为View创建了一个ViewInterface接口,在这个接口当中,只提供输入和输出的逻辑。换言之,View通过实现这个接口,它本身不处理任何数据,只是提供数据输入和输出的接口。

而Presenter也不再直接和View交互,转而与ViewInterface交互。

于是首先来看ViewInterface,代码如下:

1struct BalanceViewInterface
2{

3    virtual int GetBalance() 0;
4    virtual void UpdateBalance(int value) 0;
5};

GetBalance属于输出接口,用其获取余额数据;UpdateBalance属于输入接口,用其设置余额数据。

View需要实现这个接口,以获取和显示界面上的数据:

1class BalanceView : public WindowImplBase, public BalanceViewInterface
2{
3public:
4    BalanceView();
5
6    void InitWindow() override;
7    void Notify(TNotifyUI& msg) override;
8    LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override;
9
10    // 公共接口
11    int GetBalance() override;
12    void UpdateBalance(int value) override;
13
14protected:
15    CDuiString GetSkinFile() override;
16    LPCTSTR GetWindowClassName(void) const override;
17
18private:
19    void OnClickTopUp(TNotifyUI* pObj);            // 充值事件
20    void OnClickBalanceIncrease(TNotifyUI* pObj);  // 余额自增事件
21    void OnClickBalanceDecrease(TNotifyUI* pObj);  // 余额自减事件
22
23private:
24    std::unique_ptr pPresenter;
25
26    UIEventHandler m_ClickHandler;  // 点击事件处理
27    CButtonUI* btnTopUp;            // 充值按钮
28    CButtonUI* btnBalanceIncrease;  // 余额自增按钮
29    CButtonUI* btnBalanceDecrease;  // 余额自减按钮
30};

具体接口实现如下:

1int BalanceView::GetBalance()
2{
3    CEditUI* etTopUpValue = static_cast(m_PaintManager.FindControl(L"editTopUpValue"));
4    return _ttoi(etTopUpValue->GetText().GetData());
5}
6
7void BalanceView::UpdateBalance(int value)
8{
9    CDuiString strValue;
10    strValue.Format(L"%d", value);
11
12    CEditUI* etCurrentBalance = static_cast(m_PaintManager.FindControl(L"editCurrentBalance"));
13    etCurrentBalance->SetText(strValue);
14}

可以看到,两个接口只是负责提供数据和设置数据。

现在来看另一个不同点,MVP在View当中创建了Presenter,

1BalanceView::BalanceView()
2{
3    pPresenter = std::make_unique(this);
4}

因此,一个View对应了一个Presenter,这里View处于主导地位,而MVC中则是Controller处于主导地位,多个View可以对应一个Controller。

若是View比较复杂,那么也可以让一个View对应多个Presenter,来让逻辑更加清晰。

接着来看Presenter,先看其定义:

1class BalancePresenter
2{

3public:
4    BalancePresenter(BalanceViewInterface* view);
5
6    void TopUp();
7    void BalanceIncrease();
8    void BalanceDecrease();
9
10    void OnSuccess();
11
12private:
13    std::shared_ptr pModel;
14    BalanceViewInterface* pView;
15};

注意一下,这里不再保存View,而是保存ViewInterface。

此外,TopUp()也不再需要参数,因为View不再主动提供,需要从其提供的接口中主动拿,代码如下:

1void BalancePresenter::TopUp()
2{
3    int value = pView->GetBalance();
4    pModel->SetBalance(value);
5}

这个地方,Presenter从接口拿到数据,将数据交给Model处理,Model的代码和MVC的一样,此外不再展示。

Model处理完成之后,依旧回调OnSuccess(),Presenter在这里调用View的接口更新界面,代码如下:

1void BalancePresenter::OnSuccess()
2{
3    int value = pModel->GetBalance();
4    pView->UpdateBalance(value);
5}

7

MVC versus MVP

MVC和MVP的差异其实在上两节已经穿插着谈论了,这里给个图总结一下。

该图贯穿了前面所有章节,大家可以体会一下。

8

总结

本篇信息密度不小,穿插了许多知识点,有广度有深度,大家可以多看两遍。

核心在于三层思维模型,MVC和MVP都是以此为基础进行演绎而写的。

侧重点在于讨论程序的结构,也就是如何组合使用拆解后的组件和模块的问题。

MVC和MVP是解决这个问题的两种具体方式,Model和View分别属于结构层与应用层,Controller和Presenter是如何连接它们的桥梁。

要依据理念来使用工具,而不是由工具来指导理念,否则会徒增许多争执。

大家可以根据具体需求,灵活选择组织策略,必要之时,自己修改也未尝不可。

浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报