架构思维:如何让写程序像搭积木一样轻松?
Posted C语言与CPP编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了架构思维:如何让写程序像搭积木一样轻松?相关的知识,希望对你有一定的参考价值。
这篇算是来自读者【戚翔尔】的约稿,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 : pControllercontroller
3
4
当用户点击充值、自增、自减三个按钮时,同样将逻辑跳转到Controller。
1void BalanceView::OnClickTopUp(TNotifyUI* pObj)
2
3 CEditUI* etTopUpValue = static_cast<CEditUI*>(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<BalanceModelInterface> pModel;
15 std::unique_ptr<BalanceView> 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<BalanceModel>();
4 pModel->RegisterObservers(this);
5
6 // 启动View
7 pView = std::make_unique<BalanceView>(this);
8 pView->Create(nullptr, L"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<BalanceController*> 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<CEditUI*>(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<BalancePresenter> 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<CEditUI*>(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<CEditUI*>(m_PaintManager.FindControl(L"editCurrentBalance"));
13 etCurrentBalance->SetText(strValue);
14
可以看到,两个接口只是负责提供数据和设置数据。
现在来看另一个不同点,MVP在View当中创建了Presenter,
1BalanceView::BalanceView()
2
3 pPresenter = std::make_unique<BalancePresenter>(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<BalanceModelInterface> 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是如何连接它们的桥梁。
要依据理念来使用工具,而不是由工具来指导理念,否则会徒增许多争执。
大家可以根据具体需求,灵活选择组织策略,必要之时,自己修改也未尝不可。
以上是关于架构思维:如何让写程序像搭积木一样轻松?的主要内容,如果未能解决你的问题,请参考以下文章
微软开源项目 NeuronBlocks:像搭积木一样构建 NLP 深度学习模型!