一個可以自我進化的微服務框架

倚天碼農發表於2020-06-28

你是否遇到過這樣的框架,它非常簡單又是輕量級的,很容易上手,然而當你的專案變得複雜的時候它能自我進化成功能強大的重量級框架,而不需要把整個專案重寫? 我是從來沒見過。

先讓我們來看一下專案的生命週期。通常,當一個新專案開始時,我們不知道它能持續多久,所以我們希望它儘可能簡單。大多數專案都會在短時間內夭折,所以它們並不需要複雜的框架。然而,其中有一些擊中了使用者的痛點並受到歡迎,我們就會不斷地對它們改進,使它們變得越來越複雜。結果就是原來簡單的框架和設計已經遠遠不能滿足需求,剩下的唯一方法就是重寫整個專案,並引入強大的重量級框架。如果專案持續受歡迎,我們可能需要多次重寫整個專案。

這時一個能自我進化的框架就展現出優勢。我們可以在專案開始時使用這個輕量級框架,並只在確實需要時才將其進化為重量級框架, 在這個過程中我們不需要重寫整個專案或更改任何業務邏輯程式碼,當然你需要對建立結構(struct)的程式碼(也叫程式容器)做一些修改。但這個修改比起修改業務邏輯或重寫整個專案不知要容易多少倍。

這聽起來太棒了,但有這樣的東西嗎?很長一段時間以來,我都認為這是不可能的,直到最近竟然找到了一個。

去年,我建立了一個基於清晰架構(Clean Architecture)的框架,並寫了一系列關於它的文章。請檢視"清晰架構(Clean Architecture)的Go微服務" 。它使用工廠方法設計模式來建立物件(結構),功能非常強大,但有點重。我希望能把它改的輕一些,這樣簡單的專案也能使用。但我發現任何強大的框架都是重量級的。沒有一個框架是輕量級的但同時又非常強大,正如魚與熊掌不可兼得。我在這上面花了不少時間,最後終於找到了一個方法,就是讓框架能夠自我進化。

解決方案

我們可以將一個專案的程式碼分為兩部分,一部分是業務邏輯(Business Logic),其中所有呼叫都基於介面,不涉及具體物件(結構)。另一部分是為這些介面建立具體物件(結構(struct)),我們可以稱之為程式容器(Application Container)(詳情參見"清晰架構(Clean Architecture)的Go微服務: 程式容器(Application Container)") 。這樣,我們就可以讓業務邏輯保持不變,而使程式容器自我進化。大多數程式容器都用依賴注入來將物件(結構)注入到業務邏輯中,“Spring”就是一個很好的例子。但是,要使框架能夠自我進化,關鍵是不能直接使用依賴注入作為這兩部分之間的介面。相反,你必須使用一個非常簡單的介面。當然,你依然可以使用依賴注入,但這只是在程式容器內部,因此只是程式容器的實現細節。

下面就是框架的結構圖.

serviceTmpl1.jpg

程式容器和業務邏輯之間的介面

程式容器和業務邏輯之間的介面應該非常簡單。唯一的功能就是讓業務邏輯能獲取具體物件(結構)。在清晰架構中,大多數情況下你只需要獲取用例(Use Case)。

下面就是程式容器的介面:

type Container interface {
	// BuildUseCase creates concrete types for use case and it's included types.
	// For each call, it will create a new instance, which means it is not a singleton
	BuildUseCase(code string) (interface{}, error)

	// This should only be used by container and it's sub-package
	// Get instance by code from container.
	Get(code string) (interface{}, bool)

	// This should only be used by container and it's sub-package
	// Put value into container with code as the key.
	Put(code string, value interface{})

}

如何讓程式容器進化

我定義了三種模式的程式容器,從最簡單到最複雜,你可以直接使用。你也可以定義新的程式容器模式,只要它遵循上面的介面即可。你可以隨時將程式容器替換為其他模式,而無需更改業務邏輯程式碼。

初級模式

這是最簡單的模式,它不涉及任何設計模式。它的最大優點是簡單,易學,易用。絕大多數的專案都可以從此模式開始。使用這種模式可以在一天之內建立整個專案。如果專案很簡單,在一小時內完成都是有可能的。如果你不再需要這個專案,就可以一點也不可惜地丟棄它。缺點是它提供的功能非常簡單,所有配置資訊都是以硬編碼的形式寫在程式中,既不靈活也不強大。最適合POC(概念驗證)型別的專案。具體例項可檢視 "訂單服務" 。這是一個事件驅動的微服務專案,旨在提供訂單服務。

以下是初級模式的結構圖,框內是程式容器:

orderApp.jpg

增強模式

這種模式類似於初級模式,主要改進是增加了配置引數管理。在這種模式下,配置引數不再是硬編碼在程式碼中的,它們是在結構(struts)中定義的。你也可以對它們進行校驗。更改程式配置要容易得多,你可以在單個檔案裡看到專案的所有配置引數,從而掌握整個程式的全貌。該框架仍然非常簡單,不涉及任何設計模式。當專案已經穩定並且需要某種結構時,可以切換到這種模式。具體例項可檢視"支付服務". 這是一個事件驅動的微服務專案,旨在提供支付服務。

以下是增強模式的結構圖,框內是程式容器:

paymentApp.jpg

高階模式

當你有一個複雜專案時,你需要一個功能強大的框架來與之匹配。你可能會有一些比較複雜的需求,如更改所用的資料庫或動態更改配置引數(不需更改程式碼)。這時,你可以將專案升級為高階模式。它將在程式容器中使用依賴注入。具體例項可檢視"Service template 1"。 這是一個清晰架構(Clean Architecture)的微服務框架。

以下是高階模式的結構圖,框內是程式容器,它的檔案結構看起來有很大的不同。

serviceTmpl1App.jpg

如何升級

假設你有一個新專案,最容易的啟動方式的是複製整個“訂單服務”專案,然後將裡面的結構(struct)更改為你的結構,並完成業務邏輯程式碼。在這個過程中,你可以保留“訂單服務”專案的目錄結構和一些介面。過了一段時間,你發現需要升級到高階模式。這時,最簡單的方法是從“servicetmp1”專案中複製“app”資料夾,並替換你的專案中的“ app”資料夾,然後對程式容器進行相應的修改。完成之後,你無需更改業務邏輯中的任何程式碼,一切都應該可以正常工作。如果你瞭解這個框架,整個過程應該不會超過一天時間,甚至更短都有可能。

此方案的關鍵元素

要想框架能夠自我進化,它必須按照特定的方式進行設計和建立。以下是框架的四個關鍵元素。

  • 程式結構
  • 程式容器
  • 基於介面的業務邏輯
  • 可插拔的第三方介面庫

基於介面(Interface)的業務邏輯

前面已經講了程式結構和程式容器,這裡主要講解業務邏輯。基於介面的業務邏輯是框架能自我進化的關鍵。在應用程式的業務邏輯部分,你可能有不同型別的元素,例如“用例(use case)”,“域模型(domain model)”,“儲存庫(repository)”和“域服務(domain service)”。除了“域模型(domain model)”或“域事件(domain event)”之外,業務邏輯中的幾乎所有元素都應該是介面(而不是結構(struct))。有關程式設計和專案結構的詳細資訊,請檢視"清晰架構(Clean Architecture)的Go微服務: 程式設計"

內部介面

在業務邏輯中有兩種不同型別的介面。一種是內部介面,另一種是外部介面。內部介面是在應用程式內部使用的介面(通常不能與其他程式共享),例如“用例”,它是清晰架構中的重要元素。以下是“RegistrationUseCaseInterface”用例的介面。

type RegistrationUseCaseInterface interface {
	RegisterUser(user *model.User) (resultUser *model.User, err error)

	UnregisterUser(username string) error
	
	ModifyUser(user *model.User) error
	
	ModifyAndUnregister(user *model.User) error
}

可插拔的第三方介面庫

通常業務邏輯需要與外部世界互動並使用它們提供的服務,例如,日誌服務、訊息服務等等。這些都是外部介面,常常可以被很多應用程式共享。在領域驅動設計中,它們被稱為“應用服務(application service)”。 通常有許多庫或應用程式可以提供這樣的服務, 但你不希望將應用程式與它們中的任何一個繫結。最好是能隨時替換任何服務而又不需要更改程式碼。

問題是每個服務都有自己的介面。理想的情況是,我們已經有了標準介面,所有不同的服務提供者都遵循相同的介面。這將是開發者的夢想成真。Java有一個“JDBC”的介面,它隱藏了每個資料庫的實現細節,使我們能按照統一的方式處理不同的SQL資料庫。不幸的是,這種成功並沒有擴充套件到其他領域。

要想讓框架變得很輕量的一個關鍵是把服務都變成標準介面,並把它們移到框架之外,使之成為第三方庫,其中不僅包含了標準介面,同時也封裝了支援這個介面的庫。這樣這個第三方庫就變成了可插拔的標準元件。為了讓應用程式基於介面設計,我建立了三個通用介面分別用於日誌記錄、訊息傳遞和事務管理。建立一個好的標準介面是非常困難的,由於我在上面這些領域都不是專家,因此這些自建的介面離標準介面有一定差距。但對於我的應用程式來說,這已經足夠。我希望各個領域的專家能儘快制定出標準介面。在沒有標準介面之前,可以自定義介面,為以後切換到標準介面做好準備。

下面是日誌的通用介面:

type Logger interface {
	Errorf(format string, args ...interface{})
	Fatalf(format string, args ...interface{})
	Fatal(args ...interface{})
	Infof(format string, args ...interface{})
	Info(args ...interface{})
	Warnf(format string, args ...interface{})
	Debugf(format string, args ...interface{})
	Debug(args ...interface{})
}

這個第三方庫的結構是與框架或應用程式的結構相匹配的,這樣才能與框架很好地對接。關於如何建立一個第三方庫,我會單獨寫一篇文章["事件驅動的微服務-建立第三方庫"]來講解。

框架(framework)或者庫(Lib)?

框架和庫之間的爭論已經持續了很久了。大多數人更喜歡庫而不是框架,因為它是輕量級的並更加靈活。但為什麼我要建立一個框架而不是一個庫呢? 因為你仍然需要一個框架來將所有不同的庫組織在一起(不論它是自建的或是第三方的)。因此你通常要用很多庫,但只要一個框架。問題是有用的框架都太重了,我們需要一個輕量級的好用的框架。

因為業務邏輯中的元素都是基於介面的,我們可以把框架視為匯流排(介面匯流排),將任何基於介面的服務插入其中。這就是所謂的可插拔框架,它實現了框架與庫的完美結合。

在這個框架之下,一個應用程式的生態由三部分組成,一個是可進化的框架;另一個是可插拔的第三方標準介面(這個介面是可以不依賴於任何框架而單獨使用的),例如上面提到的日誌介面;最後是支援標準介面的具體實現庫,例如對日誌功能來講就是"zap""Logrus"。 而可進化的框架就成了把它們串接起來的主線。

與其它框架的比較

本文的框架是基於清晰架構(Clean Architecture) 的。你可以在很多其他框架中看到相似的元素,比如Java中的“Spring”,它也有程式容器並大量地使用了依賴注入。本框架唯一的新東西是自我進化。

通常,大多數框架都試圖通過使用多種設計模式來應對未來的不確定性。而它需要複雜的邏輯,這就不可避免地將這種複雜性寫入到程式碼中。這就使得多數有用的框架都很重,不論學習和使用都難度較高。但如果未來的情況與預計的並不相符,那麼這種內建的複雜性就得不到利用,而變成巨大的負擔。“Spring”就是一個很好的例子,它非常強大但也很重,適合複雜的專案,但是對於簡單的專案就很浪費。本框架在設計時徹底改變了思路,不對未來做任何假設,因此就不需預先在程式碼中引入複雜的設計模式。你可以從最簡單的框架開始,只有當你的程式變得很複雜並需要與之匹配的框架時,才進化成複雜的框架。當然你的程式必須遵從一定的設計結構,這裡面的關鍵是基於介面的設計。當前,我們已進入了微服務時代,大多數專案都是小的服務,這對能夠自我進化框架的需求就變得更為強烈。

應用程式如何使用框架?

在清晰架構中,“用例”是一個關鍵元件。如果你想了解一個應用程式,就從這裡開始。業務邏輯只需要獲得用例一個介面,就可以完成需要的任何操作,因為所有其它需要的介面都包含在“用例”中。

在業務邏輯中,“用例”被定義成介面而不是結構(struct)。在執行時,你需要獲得用例的具體實現結構(struct)並將其注入到業務邏輯中。它的步驟是這樣的,首先建立容器,然後構建具體的用例,最後呼叫“用例”中的函式。

如何呼叫“用例”

下面是構建程式容器的程式碼。

func buildContainer(filename string) (container.Container, error) {
	container, err := app.InitApp(filename)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	return container, nil
}

下面是程式容器中的函式"InitApp()"(在檔案"app.go"裡),呼叫它來初始化容器。

func InitApp(filename...string) (container.Container, error) {
	err := initLogger()
	if err != nil {
		return nil, err
	}
	return initContainer()
}

下面是用來建立"Registration"用例的幫助函式,它在檔案"serviceTmplContainer.go"裡。

func GetRegistrationUseCase(c container.Container) (usecase.RegistrationUseCaseInterface, error) {
	key := config.REGISTRATION
	value, err := c.BuildUseCase(key)
	if err != nil {
		//logger.Log.Errorf("%+v\n", err)
		return nil, errors.Wrap(err, "")
	}
	return value.(usecase.RegistrationUseCaseInterface), nil
}

下面是呼叫"Registration"用例的程式碼,它先呼叫"GetRegistrationUseCase"來得到用例,然後再呼叫“用例”裡面的"RegisterUser()"函式。

func testRegisterUser(container container.Container) {
	ruci, err := containerhelper.GetRegistrationUseCase(container)
	if err != nil {
		logger.Log.Fatal("registration interface build failed:%+v\n", err)
	}
	created, err := time.Parse(timea.FORMAT_ISO8601_DATE, "2018-12-09")
	if err != nil {
		logger.Log.Errorf("date format err:%+v\n", err)
	}

	user := model.User{Name: "Brian", Department: "Marketing", Created: created}

	resultUser, err := ruci.RegisterUser(&user)
	if err != nil {
		logger.Log.Errorf("user registration failed:%+v\n", err)
	} else {
		logger.Log.Info("new user registered:", resultUser)
	}
}

結論

本文介紹了一個能夠自我進化的輕量級的清晰架構框架。當建立一個新專案時你可以從最簡單的輕量級的框架開始。當此專案不斷髮展變得複雜時,框架可以自我進化為一個功能強大的重量級框架。在此過程中,不需要更改任何業務程式碼。目前它有三種模式,分別是初級模式,增強模式和高階模式。最複雜的是高階模式,它基於依賴注入,非常強大。我建立了三個簡單的應用程式來說明展示如何使用它,每個程式對應一種模式。

原始碼:

完整的原始碼:

索引:

1 "清晰架構(Clean Architecture)的Go微服務"

2 "清晰架構(Clean Architecture)的Go微服務: 程式容器(Application Container)"

3 "訂單服務"

4 "支付服務"

5 "Service template 1"

6 "zap"

7 "Logrus"

8 "清晰架構(Clean Architecture)的Go微服務: 程式設計"

9 ["事件驅動的微服務-建立第三方庫"]

10 The Clean Architcture

相關文章