Categories
程式開發

如何使用統一架構簡化全棧開發


現代的全棧應用程序通常由六層組成:數據訪問、後端模型、API服務端、API客戶端、前端模型和用戶界面。我們需要大量的膠水代碼才能將它們全部連接起來,並且領域模型在整個棧中存在重複。因此,開發的敏捷性受到了極大的影響。本文如何使用統一架構來構建全棧應用程序,以及統一架構語言擴展Liaison。

如何使用統一架構簡化全棧開發 1

現代的全棧應用程序(例如,單頁應用程序或移動應用程序)通常由六層組成:

  • 數據訪問
  • 後端模型
  • API服務端
  • API客戶端
  • 前端模型
  • 用戶界面

通過這種架構方式,我們可以實現某些設計良好的應用程序特性,例如關注點分離(separation of concerns,SoC)耦合(loose coupling)

但它也並非沒有缺點。它通常是以犧牲其他一些重要特性為代價的,比如簡單性、內聚性或敏捷性。

似乎我們不可能擁有上述全部特性。我們必須妥協。

但問題在於,通常每一層都是作為一個完全不同的世界被單獨構建的。

即使這些層都是使用相同的語言實現的,它們之間也不能很容易地通信和共享。

我們需要大量的膠水代碼才能將它們全部連接起來,並且領域模型重複地存在於整個棧中。因此,開發的敏捷性受到了極大的影響。

例如,向模型中添加一個簡單的字段通常需要修改棧中的所有層。難道您不覺得這有點可笑嗎?

最近我一直在思考這個問題,我相信我已經找到了解決的辦法。

訣竅在於:當然,應用程序的層必須是“物理上”的分割,但不需要是“邏輯上”的分割。

統一架構

如何使用統一架構簡化全棧開發 2

在面向對象編程中,當我們使用繼承時,我們可以得到一些類,並從兩種角度來觀察它們:物理和邏輯。這是什麼意思呢?

假設我們有一個繼承自A類的B類,那麼,可以將A和B看作是兩個物理類。但是在邏輯上,它們並不是分離的,B可以被看作是一個邏輯類,它是由A的屬性和其自身的屬性組成的。

例如,當我們在類中調用某個方法時,我們不必擔心這個方法是在這個類中實現的還是在它的父類中實現的。從調用方的角度來看,只需要擔心一個類即可。父類和子類被統一成一個邏輯類了。

如何將相同的方式應用到應用程序的各個層中呢?例如,如果前端可以以某種方式從後端繼承,這不是很好嗎?

這樣做,前端和後端將被統一到一個單一的邏輯層,這將消除所有通信和共享問題。實際上,可以從前端直接訪問後端的類、屬性和方法。

當然,我們通常不希望將整個後端都暴露給前端。但是類繼承也是如此,並且它有一個優雅的解決方案叫做“私有屬性”。類似地,後端也可以有選擇地暴露一些屬性和方法。

能夠從一個統一的世界中掌握應用程序的所有層並不是一件小事。它完全改變了遊戲規則。這就像是從三維世界降到二維世界。一切都變得容易多了。

繼承並不邪惡。是的,它可能被誤用了,並且在某些語言中,它可能非常僵化。但是,如果使用得當,它會是我們的工具箱中的一種寶貴機制。

不過,我們有個問題。據我所知,沒有一種語言允許我們可以跨多個執行環境繼承類。但我們是程序員,不是嗎?我們可以構建我們所需的一切,並且我們可以擴展語言來提供新的功能。

但在我們開始之前,讓我們先對技術棧進行下分解,看看每層應該如何適用於統一架構。

數據訪問

對於大多數應用程序,可以使用某種ORM來對數據庫進行抽象。因此,從開發人員的角度來看,無需擔心數據訪問層。

對於更複雜的應用程序,我們可能必須優化數據庫模式和請求。但我們不想因為這些問題而使後端模型變得混亂,因此此處可能需要額外附加一層。

我們構建一個數據訪問層來實現優化關注點,而這通常發生在開發週期的後期(如果真的會發生的話)。

不管怎樣,如果我們需要這樣一個層,我們可以稍後再構建它。通過跨層繼承,我們可以在後端模型層上再添加一個數據訪問層,而這幾乎不需要對現有代碼進行任何更改。

後端模型

通常,後端模型層具有如下職責:

  • 塑造領域模型。
  • 實現業務邏輯。
  • 處理授權機制。

對於大多數後端,最好在一個單一層中實現它的全部職責。但是,如果我們希望單獨處理一些關注點,例如,如果我們希望將授權與業務邏輯分開,那麼我們可以在兩個相互繼承的層中實現它們。

API層

為了連接前端和後端,我們通常會構建一個Web API(REST、GraphQL等),這會使一切變得複雜。

Web API必須在兩側都實現:前端是API客戶端,後端是API服務端。這就是需要擔心的兩個額外層,並且它通常會導致需要復制整個領域模型的後果。

Web API無非就是膠水代碼,並且構建起來非常麻煩。所以,如果我們能避免,這將是一個巨大的進步。

幸運的是,我們可以再次利用跨層繼承。在統一架構中,不需要構建Web API。我們所要做的就是讓前端模型從後端模型中繼承,這樣就完成了。

然而,仍然存在一些需要構建Web API的很好用例。這時,我們需要向某些第三方開發人員公開後端,或者需要與某些遺留的舊系統進行集成。

但是說實話,大多數應用程序都沒有這樣的需求。而當它們需要這樣做時,事後處理也很容易。我們可以簡單地將Web API實現到繼承自後端模型層的新層中。

關於這個主題的更多信息可以在這篇文章中找到。

前端模型

因為後端是事實來源,所以它應該實現所有的業務邏輯,而前端不應該實現任何業務邏輯。因此,前端模型只是簡單地繼承自後端模型而已,幾乎沒有添加任何內容。

用戶界面

我們通常是在兩個獨立的層中實現前端模型和UI。但是,正如我在這篇文章中所展示的,它不是強制性的。

當前端模型由類構成時,可以將視圖封裝為簡單的方法。如果您現在不明白我的意思,請不用擔心,在後面的示例中我會給出更清楚解釋。

由於前端模型基本上是空的(請參見上文),所以可以直接在其中實現UI,因此技術棧本身就沒有用戶界面層了。

當我們想要支持多個平台(例如,Web應用程序和移動應用程序)時,仍然需要在單獨的層中實現UI。但是,由於這只是繼承一個層的問題,所以可以在開發路線圖的後期進行。

將一切組裝起來

統一架構使我們能夠將6個物理層統一為1個邏輯層:

  • 在最小的實現中,數據訪問被封裝到了後端模型中,UI也被封裝到了前端模型中。
  • 前端模型繼承自後端模型。
  • 不再需要API層。

結果如下圖所示:

如何使用統一架構簡化全棧開發 2

真是太壯觀了,您不覺得嗎?

Liaison

為了實現一個統一的架構,我們所需的只是跨層繼承,而我是通過構建Liaison來實現這一點的。

如果您願意的話,可以把Liaison看作一個框架,但是我更喜歡把它描述成一個語言擴展,因為它的所有特性都位於盡可能低的級別上:編程語言級別。

所以,Liaison並不會把您鎖定在一個預定義的框架中,而是可以在其之上創建一個完整的宇宙。您可以在這篇文章中閱讀到更多關於此主題的信息。

在後台,Liaison依賴於RPC機制。因此,從表面上看,它可以被看作是CORBAJava RMI.NET CWF之類的東西。

但是Liaison是完全不同的:

  • 它不是一個分佈式對象系統。實際上,Liaison的後端是無狀態的,因此沒有跨層的共享對象。
  • 它是在語言級別實現的(見上文)
  • 它的設計簡單明了,並且公開了最少的API。
  • 它不涉及任何樣板代碼、生成的代碼、配置文件或工件。
  • 它使用了一個簡單但功能強大的序列化協議(Deepr),該協議支持一些獨特的特性,比如鍊式調用、自動化批處理或部分執行。

Liaison始於JavaScript,但是它所解決的問題是通用的,並且可以將它移植到任何面向對象的語言中而不會帶來太多的麻煩。

Hello 計數器

讓我們通過將經典的“計數器”示例實現成一個單頁應用程序來說明Liaison是如何工作的吧。

首先,我們需要在前端和後端之間共享一些代碼:

// shared.js

import {Model, field} from '@liaison/liaison';

export class Counter extends Model {
  // 共享类定义一个字段来跟踪计数器的值
  @field('number') value = 0;
}

然後,構建後端以實現業務邏輯:

// backend.js

import {Layer, expose} from '@liaison/liaison';

import {Counter as BaseCounter} from './shared';

class Counter extends BaseCounter {
  // 我们将“value”字段暴露给前端
  @expose({get: true, set: true}) value;

  // 我同样将increment() 方法暴露给前端 
  @expose({call: true}) increment() {
    this.value++;
  }
}

// 我们将后端类注册到导出层中
export const backendLayer = new Layer({Counter});

最後,讓我們來構建前端:

// frontend.js

import {Layer} from '@liaison/liaison';

import {Counter as BaseCounter} from './shared';
import {backendLayer} from './backend';

class Counter extends BaseCounter {
  // 目前,前端类只是继承共享类
}

// 我们将前端类注册到一个继承自后端层的层中
const frontendLayer = new Layer({Counter}, {parent: backendLayer});

// 最后,我们实例化一个计数器
const counter = new frontendLayer.Counter();

// 运行计数器
await counter.increment();
console.log(counter.value); // => 1

這是怎麼回事呢?通過調用counter.increment(),我們可以使計數器的值遞增。請注意,increment()方法既沒有在前端類中實現,也沒有在共享類中實現。它只存在於後端。

那麼,我們為什麼能從前端調用它呢?這是因為前端類註冊在從後端層繼承的層中。因此,當前端類中缺少某個方法,而後端類中公開了具有相同名稱的方法時,則會自動調用該方法。

從前端的角度來看,該操作是透明的。它不需要知道哪個方法被遠程調用了。它只是調用。

實例的當前狀態(即,counter的屬性)會被自動地來回傳輸。當方法在後端執行時,將發送在前端修改的屬性。相反,當某些屬性在後端發生變化時,它們也會反映到前端。

注意,在這個簡單的示例中,後端並不是完全遠程的。前端和後端都在同一個JavaScript運行時中運行。為了使後端真正處於遠程狀態,我們可以通過HTTP輕鬆地公開它。請看此處的示例。

如何向(從)遠程調用的方法傳遞(返回)值呢?可以傳遞(返回)任何可序列化的內容,包括類實例。只要在前端和後端使用相同的名稱註冊一個類,就可以自動傳輸它的實例。

如何跨前端和後端重寫一個方法呢?這與常規的JavaScript沒有什麼不同,我們可以使用super。例如,我們可以重寫 increment()方法以在前端的上下文中運行額外的代碼:

// frontend.js

class Counter extends BaseCounter {
  async increment() {
    await super.increment(); // 后端的`increment()` 方法被调用
    console.log(this.value); // 在前端添加额外的运行代码
  }
}

現在,讓我們使用React和前面所示的封裝方法構建一個用戶界面:

// frontend.js

import React from 'react';
import {view} from '@liaison/react-integration';

class Counter extends BaseCounter {
  // 我们使用`@view()`装饰器来观察模型,并在需要时重新渲染视图
  @view() View() {
    return (
      
{this.value}
); } }

最後,為了顯示計數器,我們需要的是:


瞧!我們構建了一個具有兩個統一層和一個封裝UI的單頁應用程序。

概念驗證

為了試驗統一架構,我使用Liaison構建了一個 RealWorld 示例應用程序

我可能有些自誇了,但結果看起來真的非常驚人:實現簡單,代碼高內聚,100% DRY(Don’t repeat yourself),沒有膠水代碼。

就代碼量而言,我的實現比我使用過的其他任何實現都要輕得多。點擊這裡查看結果。

當然,RealWorld示例是一個小型應用程序,但是由於它涵蓋了所有應用程序都共有的最重要的概念,因此我相信統一架構可以擴展到更複雜的應用程序中。

結論

關注點分離、鬆散耦合、簡單性、內聚性和敏捷性。

似乎這一切都實現了。

如果您是一位經驗豐富的開發人員,那麼我想您對此會有所懷疑,這也很正常。我們很難把多年的習慣拋諸腦後。

如果您不喜歡面向對象編程,那麼就不要使用Liaison了,這也是完全沒有問題的。

但是,如果您對OOP感興趣,請在腦海中打開一扇小窗,下一次您必須構建一個全棧應用程序時,請試試看它是如何適用於統一架構的。

Liaison仍處於早期階段,但我正在積極研究中,我希望在2020年初發布第一個測試版本。

如果您有興趣的話,請為代碼庫加註star標,並通過關注博客或訂閱newsletter的方式來保持更新。

如果這篇文章對您有幫助,您可以發推或者分享它

原文鏈接:

https://www.freecodecamp.org/news/full-stack-unified-architecture/