軟件架構編年史:MVC 及其變種

覃宇,Android 開發者 / ThoughtWorks 技術教練 // 譯者,熱衷於探究軟件開發的方方面面,從端到雲,從工具到實踐。喜歡通過翻譯來學習和分享知識,譯作有《Kotlin 實戰》、《領域驅動設計精粹》、《Serverless 架構:無服務器應用與 AWS Lambda》和《雲原生安全與 DevOps 保障》。

創建可維護的應用始終是構建應用的真正的長期挑戰。

不久以前,我還爲一家公司工作過,其核心業務應用是擁有數千家公司客戶的 SaaS 平臺。這個至關重要的應用已經開發了三年,代碼文件中混雜着 HTML、CSS、業務邏輯和 SQL。果然,在發佈兩年之後,公司決定完全重寫這個應用。儘管這些情況時有發生,但如今我們許多人都知道這是不對的以及該如何避免。

然而,在 20 世紀 70 年代,職責混雜還是常見的實踐,人們還在尋找更好的解決辦法。隨着應用程序複雜度的增長,修改 UI 必然也會引起業務邏輯的修改,修改越發複雜,耗費的時間也越來越多,還可能帶來更多的問題 (因爲修改的代碼更多了)。

MVC 因此應運而生,它提出前端和後端之間的 “關注點分離” 來解決上述問題。

**◐ **1979 – Model-View-Controller

爲了解決上述問題,Trygve Reenskaug 於 1979 年提出了 MVC 模式來分離關注點,將 UI 和業務邏輯隔離。該模式當時被應用於 1973 就已經出現的桌面圖形界面的開發。

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

模型可以是單個對象 (相當無趣),也可以是對象組成的某種結構。——Trygve Reenskaug 1979, MVC

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

現在我所熟知的 HTTP 請求響應範式並沒有使用最初的 MVC 風格。這是因爲,按照原始的設想,數據從 View 流向 Controller,這和我熟悉的一樣,但另一邊,數據直接從 Model 流向 View,並沒有經過 Controller。

而且,在現在的請求響應範式中,當數據庫中的數據發生變化時,並不會觸發瀏覽器中展示 View 的更新 (儘管可以用 Web Socket 實現)。要看到更新後的數據,用戶需要發起一次新的請求,而更新的數據總是會通過 Controller 返回。

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

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

例如,我們會發現 View 的一部分被其它一些 View 以同樣的格式使用,甚至直接就在同一個 View 重複使用。一個實際的例子就是網頁展現 RSS 訂閱內容的片段,它可以被其它頁面重用。

在 HTTP 請求 / 響應範式的上下文裏,我自己也曾遇到過幾次這種情況,但我發現了一個更簡單的方法,即讓 UI 向可以渲染控件的 Controller 發起 AJAX 調用。在保持模塊化優勢的同時並沒有增加嵌套 Controller 調用帶來的複雜性,另一個優勢就是這些子請求可以使用像 Varnish 這樣的緩存。

◐ 1996 – Model-View-Presenter

MVC 模式給當時的編程範式注入了一劑強心針。然而,隨着應用程序複雜度的增加,需要更進一步地解耦。

1996 年,IBM 的子公司 Taligent 公開了他們基於 MVC 的 模式 MVP。其思想是將 Model 對 UI 的關注更徹底地分離:

這更接近我所見到的現在的請求 / 響應範式:數據流始終要經過 Controller/Presenter。不過,Presenter 仍然不會主動更新視圖,它始終需要執行一次新的請求才能讓變化可見。

MVP 中的 Presenter 又被稱爲 Supervisor Controller。

◐ 2005 – Model-View-ViewModel

由於應用程序的複雜性還在增加,2005 年微軟的 WPF 和 Silverlight 架構師 John Gossman 又提出了 MVVM 模式,目標是進一步將 UI 設計從代碼中分離出來,並提供 View 到數據模型的數據綁定機制。

[MVVM] 是 [MVC] 的變種,專爲現代 UI 開發平臺設計。現代 UI 開發中,View 是由設計師負責而不是由傳統意義上的開發者負責。[…] 開發應用程序 UI 使用的工具、語言以及使用它們的人都和業務邏輯以及數據後端有着天壤之別。——John Gossman 2005, Introduction to Model/View/ViewModel pattern

Controller 被 ViewModel “取代”:

[View] 對鍵盤快捷鍵進行編碼,而且控件自行管理與輸入設備的交互,這本該是 MVC 中的 Controller 的職責 (現代 GUI 開發中 Controller 的變化說來話長... 我認爲它只是淡出了開發者的實現。它始終都存在着,而我們不需要像 1979 年那樣去思考它)。——John Gossman 2005, Introduction to Model/View/ViewModel pattern

MVVM 背後的思想是:

和最初的 MVC 模式的情況相仿,對傳統的請求 / 響應範式來說這種方法是行不通的,因爲 ViewModel 無法主動地更新 View(除非使用 Web Socket),而 MVVM 對這一點是有要求的。還有,根據我的經驗,ViewModel 的屬性和 View 使用的數據做到完全匹配並不是 Controller 的常見實踐。

◐ Model-View-Presenter-ViewModel

當構建雲原生的複雜企業應用時,我傾向於將應用的 UI 結構合理地設計成 M-V-P-VM,這裏的 View Model 是 Martin Fowler 在 2004 年提出的 Presentation Model,。

Model

一組包含業務邏輯和用例的類。

View

一個模板,模板引擎用它來生成 HTML;

ViewModel(又叫做 Presentation Model)

從查詢中接收 (或者從 Model 實體中提取) 原始數據,持有這些會模板會用到的數據。它還要封裝複雜的展現邏輯,來簡化模板。我發現運用 ViewModel 十分重要,因爲我們絕不會想在模板中使用實體。這樣我們才能將 View 和 Model 完全隔離開:

Presenter

接收 HTTP 請求,觸發命令或查詢,使用查詢返回的數據、ViewModel、模板和模板引擎生成 HTML 並將它返回給客戶端。所有 View 的交互都要經過 Presenter。

下面是我實現的一個非常簡單的例子:

<?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 一一對應,意味着 View 只能被一個特定的 ViewModel 使用,反過來也一樣。這會讓我進一步思考,也許我們可以將模板和 ViewModel 封裝成一個 View 對象,更有效地將 Controller 和模板以及 ViewModel 解耦,讓它只依賴一個通用的 View 接口;但我還沒有機會實驗這個想法。

◐ 總結

在網上,我們還能找到其它 MVC 的變種。但是,這裏列出是我覺得更有意義和 / 或與我的工作有關的一些模式。

然而,我在本文中引用的這些模式是爲桌面應用程序和 / 或富客戶端的上下文創建的,因此它們不是總能和請求 / 響應範式百分之百的匹配。

如果你開發的是雲原生的企業應用並且使用了 MVC,實際上你多半使用的是更接近 MVP 的某種模式。但無論如何,我想表達的不是應該尊崇某種特定的 MVC 變種或是刻板地理解它們的名字,而是我們應該學習所有的模式,按照需要去使用和調整它們。還是那句老話,最終目標就是高內聚低耦合:關注點分離。

◐ 引用來源

☼ 屐痕處處:2012 年 7 月 13 日攝於四川九寨溝。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/tDk0s9REd9L4tVwPRaQaJA