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

Posted Frey_Liu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了架构思维:如何让写程序像搭积木一样相关的知识,希望对你有一定的参考价值。

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

开发思维

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

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

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

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

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

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

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

问题解决流程

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

  • 第一步,确定问题。

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

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

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

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

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

  • 第二步,分析原因。

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

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

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

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

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

  • 最后一步,选择策略。

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

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

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

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

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

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

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

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

三层思维模型

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

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

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

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

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

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

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

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

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

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

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

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

具体架构的多样性

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

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

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

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

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

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

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

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

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

MVC

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

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

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

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

 1 class BalanceView : public WindowImplBase
 2 
 3 public:
 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
12 protected:
13    CDuiString GetSkinFile() override;
14    LPCTSTR GetWindowClassName(void) const override;
15
16 private:
17    void OnClickTopUp(TNotifyUI* pObj);            // 充值事件
18    void OnClickBalanceIncrease(TNotifyUI* pObj);  // 余额自增事件
19    void OnClickBalanceDecrease(TNotifyUI* pObj);  // 余额自减事件
20
21 private:
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。代码很简单

1 BalanceView::BalanceView(BalanceController* controller)
2    : pControllercontroller
3 
4 

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

 1 void 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
10 void BalanceView::OnClickBalanceIncrease(TNotifyUI* pObj)
11 
12    // 交由Controller负责与Model沟通
13    pController->BalanceIncrease();
14 
15
16 void BalanceView::OnClickBalanceDecrease(TNotifyUI* pObj)
17 
18    // 交由Controller负责与Model沟通
19    pController->BalanceDecrease();
20 

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

接着来看Controller

 1 class BalanceController
 2 
 3  public:
 4    BalanceController();
 5
 6    void TopUp(int value);
 7    void BalanceIncrease();
 8    void BalanceDecrease();
 9
10    // Model处理成功后的回调函数
11    void OnSuccess();
12
13  private:
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,代码如下:

 1 BalanceController::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传来的三个按钮事件的处理,代码如下:

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

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

最后来看Model

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

1 struct 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定义:

 1 class BalanceModel : public BalanceModelInterface
 2 
 3 public:
 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
13 private:
14    void notifyObservers() const;
15
16 private:
17    int balance 0 ; // 余额
18    std::vector<BalanceController*> observers;
19 ;

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

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

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

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

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

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

 1 void BalanceModel::RegisterObservers(BalanceController* view)
 2 
 3    observers.push_back(view);
 4 
 5
 6 void 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
13 void BalanceModel::notifyObservers() const
14 
15    for (auto& observer : observers)
16        (*observer).OnSuccess();
17 

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

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

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

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

MVP

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

1 void 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,代码如下:

1 struct BalanceViewInterface
2 
3    virtual int GetBalance() = 0;
4    virtual void UpdateBalance(int value) = 0;
5 ;

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

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

 1 class BalanceView : public WindowImplBase, public BalanceViewInterface
 2 
 3 public:
 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
14 protected:
15    CDuiString GetSkinFile() override;
16    LPCTSTR GetWindowClassName(void) const override;
17
18 private:
19    void OnClickTopUp(TNotifyUI* pObj);            // 充值事件
20    void OnClickBalanceIncrease(TNotifyUI* pObj);  // 余额自增事件
21    void OnClickBalanceDecrease(TNotifyUI* pObj);  // 余额自减事件
22
23 private:
24    std::unique_ptr<BalancePresenter> pPresenter;
25
26    UIEventHandler m_ClickHandler;  // 点击事件处理
27    CButtonUI* btnTopUp;            // 充值按钮
28    CButtonUI* btnBalanceIncrease;  // 余额自增按钮
29    CButtonUI* btnBalanceDecrease;  // 余额自减按钮
30 ;

具体接口实现如下:

 1 int BalanceView::GetBalance()
 2 
 3    CEditUI* etTopUpValue = static_cast<CEditUI*>(m_PaintManager.FindControl(L"editTopUpValue"));
 4    return _ttoi(etTopUpValue->GetText().GetData());
 5
 6
 7 void 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,

1 BalanceView::BalanceView()
2 
3    pPresenter = std::make_unique<BalancePresenter>(this);
4 

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

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

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

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

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

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

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

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

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

1 void BalancePresenter::OnSuccess()
2 
3    int value = pModel->GetBalance();
4    pView->UpdateBalance(value微软开源项目 NeuronBlocks:像搭积木一样构建 NLP 深度学习模型!

牛逼 666,这个项目让网页制作像搭积木一样简单

机器人自己造自己,像搭积木一样轻松 | MIT

springboot实战开发全套教程,让开发像搭积木一样简单!Github星标已上10W+!

Jetpack从入门到精通全家桶(含项目实战 附Demo)

Jetpack组件库(含Jetpack Compose)从入门到精通全家桶附Demo