物联网
您现在所在的位置:首页>企业动态>物联网

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

编辑:学到牛牛IT培训    发布日期: 2022-01-12 10:09:52  

本篇文章属于启发型,会联系到各种知识,不一定皆是编程领域的,概念碰撞,思维摩擦,以飨读者。

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类负责。

class BalanceView : public WindowImplBase
{
public:
BalanceView(BalanceController* controller);
void InitWindow() override;
void Notify(TNotifyUI& msg) override;
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override;
void UpdateBalance(int value);
protected:
CDuiString GetSkinFile() override;
LPCTSTR GetWindowClassName(void) const override;
private:
void OnClickTopUp(TNotifyUI* pObj); // 充值事件
void OnClickBalanceIncrease(TNotifyUI* pObj); // 余额自增事件
void OnClickBalanceDecrease(TNotifyUI* pObj); // 余额自减事件
private:
BalanceController* pController;
UIEventHandler m_ClickHandler; // 点击事件处理
CButtonUI* btnTopUp; // 充值按钮
CButtonUI* btnBalanceIncrease; // 余额自增按钮
CButtonUI* btnBalanceDecrease; // 余额自减按钮
};

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

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

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

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

BalanceView::BalanceView(BalanceController* controller)
: pController
{
}

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

void BalanceView::OnClickTopUp(TNotifyUI* pObj)
{
 CEditUI* etTopUpValue = static_cast(m_PaintManager.FindControl(L"editTopUpValue"));
 int value = _ttoi(etTopUpValue->GetText().GetData());
// 交由Controller负责与Model沟通
pController->TopUp(value);
}
void BalanceView::OnClickBalanceIncrease(TNotifyUI* pObj)
{
// 交由Controller负责与Model沟通
pController->BalanceIncrease();
}
void BalanceView::OnClickBalanceDecrease(TNotifyUI* pObj)
{
// 交由Controller负责与Model沟通
pController->BalanceDecrease();
}

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

接着来看Controller。

class BalanceController
{
public:
BalanceController();
void TopUp(int value);
void BalanceIncrease();
void BalanceDecrease();
// Model处理成功后的回调函数
void OnSuccess();
private:
std::shared_ptr pModel;
std::unique_ptr pView;
};

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

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

BalanceController::BalanceController()
{
pModel = std::make_shared();
pModel->RegisterObservers(this);
// 启动View
pView = std::make_unique(this);
pView->Create(nullptr, L"BalanceView", UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
pView->CenterWindow();
pView->ShowModal();
}

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

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

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

void BalanceController::TopUp(int value)
{
pModel->SetBalance(value);
}
void BalanceController::BalanceIncrease()
{
int value = pModel->GetBalance();
pModel->SetBalance(value + 1);
}
void BalanceController::BalanceDecrease()
{
int value = pModel->GetBalance();
pModel->SetBalance(value - 1);
}

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

最后来看Model。

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

struct BalanceModelInterface
{
virtual int GetBalance() = ;
virtual void SetBalance(int value) = ;
virtual void RegisterObservers(BalanceController* view) = ;
virtual void RemoveObserver(BalanceController* view) = ;
};

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

来看具体的Model定义:

class BalanceModel : public BalanceModelInterface
{
public:
int GetBalance() override;
void SetBalance(int value) override;
void RegisterObservers(BalanceController* view) override;
void RemoveObserver(BalanceController* view) override;
private:
void notifyObservers() const;
private:
int balance{ }; // 余额
std::vector observers;
};

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

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

int BalanceModel::GetBalance()
{
return balance;
}
void BalanceModel::SetBalance(int value)
{
 balance = value;
 notifyObservers();
}

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

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

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

void BalanceModel::RegisterObservers(BalanceController* view)
{
observers.push_back(view);
}
void BalanceModel::RemoveObserver(BalanceController* view)
{
 auto iter = std::find(observers.begin(), observers.end(), view);
 if (iter != observers.end())
observers.erase(iter);
}
void BalanceModel::notifyObservers() const
{
for (auto& observer : observers)
(*observer).OnSuccess();
}

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

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

void BalanceController::OnSuccess()
{
 int value = pModel->GetBalance();
pView->UpdateBalance(value);
}

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

6、MVP

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

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

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

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

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

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

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

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

struct BalanceViewInterface
{
virtual int GetBalance() = ;
virtual void UpdateBalance(int value) = ;
};

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

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

class BalanceView : public WindowImplBase, public BalanceViewInterface
{
public:
 BalanceView();
 void InitWindow() override;
 void Notify(TNotifyUI& msg) override;
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override;
// 公共接口
int GetBalance() override;
 void UpdateBalance(int value) override;
protected:
 CDuiString GetSkinFile() override;
 LPCTSTR GetWindowClassName(void) const override;
private:
  void OnClickTopUp(TNotifyUI* pObj); // 充值事件
  void OnClickBalanceIncrease(TNotifyUI* pObj); // 余额自增事件
  void OnClickBalanceDecrease(TNotifyUI* pObj); // 余额自减事件
private:
 std::unique_ptr pPresenter;
 UIEventHandler m_ClickHandler; // 点击事件处理
 CButtonUI* btnTopUp; // 充值按钮
 CButtonUI* btnBalanceIncrease; // 余额自增按钮
 CButtonUI* btnBalanceDecrease; // 余额自减按钮
};

具体接口实现如下:

int BalanceView::GetBalance()
{
 CEditUI* etTopUpValue = static_cast(m_PaintManager.FindControl(L"editTopUpValue"));
 return _ttoi(etTopUpValue->GetText().GetData());
}
void BalanceView::UpdateBalance(int value)
{
 CDuiString strValue;
 strValue.Format(L"%d", value);
 CEditUI* etCurrentBalance = static_cast(m_PaintManager.FindControl(L"editCurrentBalance"));
 etCurrentBalance->SetText(strValue);
}

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

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

BalanceView::BalanceView()
{
 pPresenter = std::make_unique(this);
}

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

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

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

class BalancePresenter
{
public:
 BalancePresenter(BalanceViewInterface* view);
 void TopUp();
 void BalanceIncrease();
 void BalanceDecrease();
 void OnSuccess();
private:
 std::shared_ptr pModel;
 BalanceViewInterface* pView;
};

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

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

void BalancePresenter::TopUp()
{
 int value = pView->GetBalance();
 pModel->SetBalance(value);
}

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

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

void BalancePresenter::OnSuccess()
{
 int value = pModel->GetBalance();
 pView->UpdateBalance(value);
}

7、MVC versus MVP

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

架构思维.png

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

8、总结

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

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

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

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

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

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


这篇算是来自读者【戚翔尔】的约稿,MVC主题,先给写上。

声明:转载此文是出于传递更多信息之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系,我们将及时更正、删除,谢谢。 

免费试学
课程好不好,不如实地听一听

封闭学习

2

1

联系我们

电话:028-61775817

邮箱:1572396657@qq.com

地址:成都市金牛区西城国际A座8楼

  • 物联网_物联网专题新闻_物联网IOT资讯-学到牛牛
    物联网_物联网专题新闻_物联网IOT资讯-学到牛牛

    扫一扫,免费咨询

  • 物联网_物联网专题新闻_物联网IOT资讯-学到牛牛
    物联网_物联网专题新闻_物联网IOT资讯-学到牛牛

    微信公众号

  • 物联网_物联网专题新闻_物联网IOT资讯-学到牛牛
物联网_物联网专题新闻_物联网IOT资讯-学到牛牛

学一流技术,找高薪工作

物联网_物联网专题新闻_物联网IOT资讯-学到牛牛

7-24小时服务热线:

028-61775817

版权声明 网站地图

蜀ICP备2021001672号

课程问题轻松问