7. MVC及其變種(譯)

原文:https://herbertograca.com/2017/08/17/mvc-and-its-variants/

這篇文章是軟件架構(gòu)編年史()的一部分桐腌,這部編年史由一系列關(guān)于軟件架構(gòu)的文章組成枚尼。在這一系列文章中,我將寫下我對(duì)軟件架構(gòu)的學(xué)習(xí)和思考皱碘,以及我是如何運(yùn)用這些知識(shí)的。如果你閱讀了這個(gè)系列中之前的文章雌隅,本篇文章的的內(nèi)容將更有意義烛芬。

創(chuàng)建可維護(hù)的應(yīng)用始終是構(gòu)建應(yīng)用的真正的長(zhǎng)期挑戰(zhàn)。

不久以前宜咒,我還為一家公司工作過惠赫,其核心業(yè)務(wù)應(yīng)用是擁有數(shù)千家公司客戶的 SaaS 平臺(tái)。這個(gè)至關(guān)重要的應(yīng)用已經(jīng)開發(fā)了三年故黑,代碼文件中混雜著 HTML儿咱、CSS庭砍、業(yè)務(wù)邏輯和 SQL。果然混埠,在發(fā)布兩年之后怠缸,公司決定完全重寫這個(gè)應(yīng)用。盡管這些情況時(shí)有發(fā)生钳宪,但如今我們?cè)S多人都知道這是不對(duì)的以及該如何避免揭北。

然而,在20世紀(jì)70年代使套,職責(zé)混雜還是常見的實(shí)踐罐呼,人們還在尋找更好的解決辦法鞠柄。隨著應(yīng)用程序復(fù)雜度的增長(zhǎng)侦高,修改 UI 必然也會(huì)引起業(yè)務(wù)邏輯的修改,修改越發(fā)復(fù)雜厌杜,耗費(fèi)的時(shí)間也越來越多奉呛,還可能帶來更多的問題(因?yàn)樾薷牡拇a更多了)。

MVC 因此應(yīng)運(yùn)而生夯尽,它提出前端和后端之間的“關(guān)注點(diǎn)分離”來解決上述問題瞧壮。

1979 – Model-View-Controller

為了解決上述問題,Trygve Reenskaug 于1979 年提出了 MVC 模式來分離關(guān)注點(diǎn)匙握,將 UI 和業(yè)務(wù)邏輯隔離咆槽。該模式當(dāng)時(shí)被應(yīng)用于1973 就已經(jīng)出現(xiàn)的桌面圖形界面的開發(fā)。

MVC 模式將代碼拆分成了三個(gè)概念單元:

  • 代表業(yè)務(wù)邏輯的 Model (模型)圈纺;
  • 代表 UI 控件的 View (視圖):按鈕秦忿、文本框等等;
  • 在視圖和模型之間居中協(xié)調(diào)的 Controller(控制器)蛾娶,這意味著:
    • 它決定顯示哪些視圖以及哪些數(shù)據(jù)灯谣;
    • 它將用戶操作(例如點(diǎn)擊按鈕)轉(zhuǎn)換成業(yè)務(wù)邏輯。

模型可以是單個(gè)對(duì)象(相當(dāng)無趣)蛔琅,也可以是對(duì)象組成的某種結(jié)構(gòu)胎许。——Trygve Reenskaug 1979, MVC

最初的 MVC 模式還有其它一些需要了解的的重要概念:

  1. View 直接使用 Model 數(shù)據(jù)對(duì)象來展示數(shù)據(jù)罗售;
  2. 當(dāng) Model 發(fā)生變化時(shí)辜窑,會(huì)觸發(fā)一個(gè)事件立即更新 View(記住,1979年還沒有 HTTP)寨躁;
  3. 每一個(gè) View 通常只關(guān)聯(lián)一個(gè) Controller谬擦;
  4. 每個(gè)界面可以包含多對(duì) View 和 Controller;
  5. 每個(gè)Controller 可以對(duì)應(yīng)多個(gè) View朽缎。

現(xiàn)在我所熟知的 HTTP 請(qǐng)求響應(yīng)范式并沒有使用最初的 MVC 風(fēng)格惨远。這是因?yàn)槊瘴颍凑赵嫉脑O(shè)想,數(shù)據(jù)從 View 流向 Controller北秽,這和我熟悉的一樣葡幸,但另一邊,數(shù)據(jù)直接從 Model 流向 View贺氓,并沒有經(jīng)過 Controller蔚叨。

而且,在現(xiàn)在的請(qǐng)求響應(yīng)范式中辙培,當(dāng)數(shù)據(jù)庫中的數(shù)據(jù)發(fā)生變化時(shí)蔑水,并不會(huì)觸發(fā)瀏覽器中展示 View 的更新(盡管可以用 Web Socket 實(shí)現(xiàn))。要看到更新后的數(shù)據(jù)扬蕊,用戶需要發(fā)起一次新的請(qǐng)求搀别,而更新的數(shù)據(jù)總是會(huì)通過 Controller 返回。

1987/2000 – PAC/Hierarchical Model-View-Controller

PAC 又稱 HMVC尾抑,在 UI 片段控件化的上下文中它能帶來更好的模塊化拆分歇父。

例如,我們會(huì)發(fā)現(xiàn) View 的一部分被其它一些 View 以同樣的格式使用再愈,甚至直接就在同一個(gè) View 重復(fù)使用榜苫。一個(gè)實(shí)際的例子就是網(wǎng)頁展現(xiàn) RSS 訂閱內(nèi)容的片段,它可以被其它頁面重用翎冲。

如果使用 HMVC垂睬,處理主請(qǐng)求的 Controller 會(huì)將子請(qǐng)求轉(zhuǎn)發(fā)給其它 Controller 讓這些控件進(jìn)行渲染,然后在主 View 的渲染中合并它們抗悍。

在 HTTP 請(qǐng)求/響應(yīng)范式的上下文里驹饺,我自己也曾遇到過幾次這種情況,但我發(fā)現(xiàn)了一個(gè)更簡(jiǎn)單的方法檐春,即讓 UI 向可以渲染控件的 Controller 發(fā)起 AJAX 調(diào)用逻淌。在保持模塊化優(yōu)勢(shì)的同時(shí)并沒有增加嵌套 Controller 調(diào)用帶來的復(fù)雜性,另一個(gè)優(yōu)勢(shì)就是這些子請(qǐng)求可以使用像 Varnish 這樣的緩存疟暖。

1996 – Model-View-Presenter

MVC 模式給當(dāng)時(shí)的編程范式注入了一劑強(qiáng)心針卡儒。然而,隨著應(yīng)用程序復(fù)雜度的增加俐巴,需要更進(jìn)一步地解耦骨望。

1996 年,IBM 的子公司 Taligent 公開了他們基于 MVC 的 模式 MVP欣舵。其思想是將 Model 對(duì) UI 的關(guān)注更徹底地分離:

  • View 是被動(dòng)的擎鸠,對(duì) Model 無感知;
  • 專注于輕量 Controller(Presenter)缘圈,它們不包含任何業(yè)務(wù)邏輯劣光,只是簡(jiǎn)單地調(diào)用命令和/或查詢模型袜蚕,將原始數(shù)據(jù)傳遞給 View;
  • 數(shù)據(jù)的變化不會(huì)直接觸發(fā) View 的更新:它始終要通過 Presenter绢涡,由 Presenter 來更新 View牲剃。這樣在更新視圖之前 Controller(Presenter) 還可以執(zhí)行一些和展現(xiàn)相關(guān)的額外邏輯。例如雄可,同時(shí)更新另一些數(shù)據(jù)凿傅,它們和數(shù)據(jù)庫中發(fā)生變化的數(shù)據(jù)有關(guān);
  • 每個(gè) View 對(duì)應(yīng)一個(gè) Presenter数苫。

這更接近我所見到的現(xiàn)在的請(qǐng)求/響應(yīng)范式:數(shù)據(jù)流始終要經(jīng)過 Controller/Presenter聪舒。不過,Presenter 仍然不會(huì)主動(dòng)更新視圖虐急,它始終需要執(zhí)行一次新的請(qǐng)求才能讓變化可見箱残,。

MVP 中的 Presenter 又被稱為 Supervisor Controller戏仓。

2005 – Model-View-ViewModel

由于應(yīng)用程序的復(fù)雜性還在增加疚宇,2005 年微軟的 WPF 和 Silverlight 架構(gòu)師 John Gossman 又提出了 MVVM 模式亡鼠,目標(biāo)是進(jìn)一步將 UI 設(shè)計(jì)從代碼中分離出來赏殃,并提供 View 到數(shù)據(jù)模型的數(shù)據(jù)綁定機(jī)制。

[MVVM] 是 [MVC] 的變種间涵,專為現(xiàn)代 UI 開發(fā)平臺(tái)設(shè)計(jì)∪嗜龋現(xiàn)代 UI 開發(fā)中,View 是由設(shè)計(jì)師負(fù)責(zé)而不是由傳統(tǒng)意義上的開發(fā)者負(fù)責(zé)勾哩。[…] 開發(fā)應(yīng)用程序 UI 使用的工具抗蠢、語言以及使用它們的人都和業(yè)務(wù)邏輯以及數(shù)據(jù)后端有著天壤之別∷祭停——John Gossman 2005, Introduction to Model/View/ViewModel pattern

Controller 被 ViewModel “取代”:

[View] 對(duì)鍵盤快捷鍵進(jìn)行編碼迅矛,而且控件自行管理與輸入設(shè)備的交互,這本該是 MVC 中的 Controller 的職責(zé)(現(xiàn)代 GUI 開發(fā)中 Controller 的變化說來話長(zhǎng)...我認(rèn)為它只是淡出了開發(fā)者的實(shí)現(xiàn)潜叛。它始終都存在著秽褒,而我們不需要像1979年那樣去思考它)⊥担——John Gossman 2005, Introduction to Model/View/ViewModel pattern

MVVM 背后的思想是:

  • ViewModel 和 View 一一對(duì)應(yīng)销斟;
  • 將 View 中的邏輯轉(zhuǎn)移到 ViewModel 來簡(jiǎn)化 View;
  • View 使用的數(shù)據(jù)和 ViewModel 中的數(shù)據(jù)一一對(duì)應(yīng)椒舵;
  • 將 ViewModel 中的數(shù)據(jù)綁定到 View 中的數(shù)據(jù)上蚂踊,這樣 ViewModel 中數(shù)據(jù)的變化會(huì)立即體現(xiàn)在 View 上。

和最初的 MVC 模式的情況相仿笔宿,對(duì)傳統(tǒng)的請(qǐng)求/響應(yīng)范式來說這種方法是行不通的犁钟,因?yàn)?ViewModel 無法主動(dòng)地更新 View(除非使用 Web Socket)棱诱,而 MVVM 對(duì)這一點(diǎn)是有要求的。還有涝动,根據(jù)我的經(jīng)驗(yàn)军俊,ViewModel 的屬性和 View 使用的數(shù)據(jù)做到完全匹配并不是 Controller 的常見實(shí)踐。

Model-View-Presenter-ViewModel

當(dāng)構(gòu)建云原生的復(fù)雜企業(yè)應(yīng)用時(shí)捧存,我傾向于將應(yīng)用的 UI 結(jié)構(gòu)合理地設(shè)計(jì)成 M-V-P-VM粪躬,這里的 View Model 是 Martin Fowler 在 2004 年提出的 Presentation Model,。

  • Model

    一組包含業(yè)務(wù)邏輯和用例的類昔穴。

  • View

    一個(gè)模板镰官,模板引擎用它來生成 HTML;

  • ViewModel(又叫做 Presentation Model)

    從查詢中接收(或者從 Model 實(shí)體中提取)原始數(shù)據(jù)吗货,持有這些會(huì)模板會(huì)用到的數(shù)據(jù)泳唠。它還要封裝復(fù)雜的展現(xiàn)邏輯,來簡(jiǎn)化模板宙搬。我發(fā)現(xiàn)運(yùn)用 ViewModel 十分重要笨腥,因?yàn)槲覀兘^不會(huì)想在模板中使用實(shí)體。這樣我們才能將 View 和 Model 完全隔離開:

    • Model 中的變化(比如實(shí)體結(jié)構(gòu)的變化)會(huì)上升并影響 ViewModel勇垛,但不會(huì)影響模板脖母;
    • 復(fù)雜的展現(xiàn)邏輯被封裝到了 ViewModel 之中,因此不會(huì)被泄露(例如闲孤,在業(yè)務(wù)實(shí)體中創(chuàng)建一些只和展現(xiàn)邏輯有關(guān)的方法)到領(lǐng)域之中谆级;
    • 模板的依賴變得很清晰,因?yàn)樗鼈儽仨氃?ViewModel 中設(shè)置讼积。例如肥照,暴露出依賴可以幫助我們決定應(yīng)該優(yōu)先從數(shù)據(jù)庫中加載哪些內(nèi)容來避免 N+1 問題。
  • Presenter

    接收 HTTP 請(qǐng)求勤众,觸發(fā)命令或查詢舆绎,使用查詢返回的數(shù)據(jù)、ViewModel们颜、模板和模板引擎生成 HTML 并將它返回給客戶端吕朵。所有 View 的交互都要經(jīng)過 Presenter形入。

下面是我實(shí)現(xiàn)的一個(gè)非常簡(jiǎn)單的例子:

<?php
// src/UI/Admin/Some/Controller/Namespace/Detail/SomeEntityDetailController.php
namespace UI\Admin\Some\Controller\Namespace\Detail;
// use ...
final class SomeEntityDetailController
{
    /**
     * @var SomeRepositoryInterface
     */
    private $someRepository;
  
    /**
     * @var RelatedRepositoryInterface
     */
    private $relatedRepository;
    /**
     * @var TemplateEngineInterface
     */
    private $templateEngine;
    public function __construct(
        SomeRepositoryInterface $someRepository,
        RelatedRepositoryInterface $relatedRepository,
        TemplateEngineInterface $templateEngine
    ) {
        $this->someRepository = $someRepository;
        $this->relatedRepository = $relatedRepository;
        $this->templateEngine = $templateEngine;
    }
    /**
     * @return mixed
     */
    public function get(int $someEntityId)
    {
        $mainEntity = $this->someRepository->getById($someEntityId);
        $relatedEntityList = $this->relatedRepository->getByParentId($someEntityId);
        return $this->templateEngine->render(
            '@Some/Controller/Namespace/Detail/details.html.twig',
            new DetailsViewModel($mainEntity, $relatedEntityList)
        );
    }
}

M-V-C-VM_-_Controller_example.php

<?php
// src/UI/Admin/Some/Controller/Namespace/Detail/DetailsViewModel.php
namespace UI\Admin\Some\Controller\Namespace\Detail;
// use ...
final class DetailsViewModel implements TemplateViewModelInterface
{
    /**
     * @var array
     */
    private $mainEntity = [];
    /**
     * @var array
     */
    private $relatedEntityList = [];
    /**
     * @var bool
     */
    private $shouldDisplayFancyDialog = false;
    /**
     * @var bool
     */
    private $canEditData = false;
    /**
     * @param SomeEntity $mainEntity
     * @param RelatedEntity[] $relatedEntityList
     */
    public function __construct(SomeEntity $mainEntity, array $relatedEntityList)
    {
        $this->mainEntity = [
            'name' => $mainEntity->getName(),
            'description' => $mainEntity->getResume(),
        ];
        foreach ($relatedEntityList as $relatedEntity) {
            $this->relatedEntityList[] = [
                'title' => $relatedEntity->getTitle(),
                'subtitle' => $relatedEntity->getSubtitle(),
            ];
        }
        
        $this->shouldDisplayFancyDialog = /* ... some complex conditional using the entities data ... */ ;
        
        $this->canEditData = /* ... another complex conditional using the entities data ... */ ;
    }
    public function getMainEntity(): array
    {
        return $this->mainEntity;
    }
    public function getRelatedEntityList(): array
    {
        return $this->relatedEntityList;
    }
    public function shouldDisplayFancyDialog(): bool
    {
        return $this->shouldDisplayFancyDialog;
    }
    public function canEditData(): bool
    {
        return $this->canEditData;
    }
}

M-V-C-VM_-_ViewModel_example.php

模板和 ViewModel 一一對(duì)應(yīng)闽晦,意味著 View 只能被一個(gè)特定的 ViewModel 使用,反過來也一樣纱耻。這會(huì)讓我進(jìn)一步思考波岛,也許我們可以將模板和 ViewModel 封裝成一個(gè) View 對(duì)象茅坛,更有效地將 Controller 和模板以及 ViewModel 解耦,讓它只依賴一個(gè)通用的 View 接口;但我還沒有機(jī)會(huì)實(shí)驗(yàn)這個(gè)想法贡蓖。

總結(jié)

在網(wǎng)上曹鸠,我們還能找到其它 MVC 的變種。但是斥铺,這里列出是我覺得更有意義和/或與我的工作有關(guān)的一些模式彻桃。

然而,我在本文中引用的這些模式是為桌面應(yīng)用程序和/或富客戶端的上下文創(chuàng)建的晾蜘,因此它們不是總能和請(qǐng)求/響應(yīng)范式百分之百的匹配邻眷。

如果你開發(fā)的是云原生的企業(yè)應(yīng)用并且使用了 MVC,實(shí)際上你多半使用的是更接近 MVP 的某種模式剔交。但無論如何肆饶,我想表達(dá)的不是應(yīng)該尊崇某種特定的 MVC 變種或是刻板地理解它們的名字,而是我們應(yīng)該學(xué)習(xí)所有的模式岖常,按照需要去使用和調(diào)整它們驯镊。還是那句老話,最終目標(biāo)就是高內(nèi)聚低耦合關(guān)注點(diǎn)分離竭鞍。

引用來源

1979 – Trygve Reenskaug – MVC XEROX PARC 1978-79
1979 – Trygve Reenskaug – MVC
1987 – Joelle Coutaz – PAC, an Object Oriented Model for Dialog Design
1996 – Mike Potel – MVP: Model-View-Presenter: The Taligent Programming Model for C++ and Java
2000 – Jason Cai, Ranjit Kapila, Gaurav Pal – HMVC: The layered pattern for developing strong client tiers
2003 -Trygve Reenskaug – The Model-View-Controller (MVC): Its Past and Present
2004 -Martin Fowler – Presentation Model
2005 – John Gossman – Introduction to Model/View/ViewModel pattern for building WPF apps
2006 – Martin Fowler – Supervising Controller
2006 – Martin Fowler – GUI Architectures
2011 – Mārti?? Tere?ko – Architecture more suitable for web apps than MVC?
2017* – Tracy-Gregory J. Gilmore – Never the twain shall meet. The tale of MV*
2017* – Tech notes – MVVM vs MVP vs MVC: The differences explained
2017* – Wikipedia – Model–view–controller
2017* – Wikipedia – Presentation–abstraction–control
2017* – Wikipedia – Model-view-presenter
2017* – Wikipedia – Hierarchical model–view–controller
2017* – Wikipedia – Model–view–viewmodel
2018* – Wikipedia – History of the graphical user interface

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末板惑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子偎快,更是在濱河造成了極大的恐慌冯乘,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滨砍,死亡現(xiàn)場(chǎng)離奇詭異往湿,居然都是意外死亡妖异,警方通過查閱死者的電腦和手機(jī)惋戏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來他膳,“玉大人响逢,你說我怎么就攤上這事∽厮铮” “怎么了舔亭?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蟀俊。 經(jīng)常有香客問我钦铺,道長(zhǎng),這世上最難降的妖魔是什么肢预? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任矛洞,我火速辦了婚禮,結(jié)果婚禮上烫映,老公的妹妹穿的比我還像新娘沼本。我一直安慰自己噩峦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布抽兆。 她就那樣靜靜地躺著识补,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辫红。 梳的紋絲不亂的頭發(fā)上凭涂,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音贴妻,去河邊找鬼导盅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛揍瑟,可吹牛的內(nèi)容都是我干的白翻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼绢片,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼滤馍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起底循,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤巢株,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后熙涤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阁苞,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年祠挫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了那槽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡等舔,死狀恐怖骚灸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情慌植,我是刑警寧澤甚牲,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站蝶柿,受9級(jí)特大地震影響丈钙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜交汤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一雏赦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦喉誊、人聲如沸邀摆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栋盹。三九已至,卻和暖如春敷矫,著一層夾襖步出監(jiān)牢的瞬間例获,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工曹仗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榨汤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓怎茫,卻偏偏與公主長(zhǎng)得像收壕,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子轨蛤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容