<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          『每周譯Go』Go Web 應(yīng)用中常見的反模式

          共 25987字,需瀏覽 52分鐘

           ·

          2021-08-20 03:31

          在我職業(yè)生涯的某個階段,我對我所構(gòu)建的軟件不再感到興奮。

          我最喜歡的工作內(nèi)容是底層的細(xì)節(jié)和復(fù)雜的算法。在轉(zhuǎn)到面向用戶的應(yīng)用開發(fā)之后,這些內(nèi)容基本消失了。編程似乎是利用現(xiàn)有的庫和工具把數(shù)據(jù)從一處移至另一處。到目前為止,我所學(xué)到的關(guān)于軟件的知識不再那么有用了。

          讓我們面對現(xiàn)實(shí)吧:大多數(shù) Web 應(yīng)用無法解決棘手的技術(shù)挑戰(zhàn)。他們需要做到的是正確的對產(chǎn)品進(jìn)行建模,并且比競爭對手更快的改進(jìn)產(chǎn)品。

          這起初看起來似乎是那么的無聊,但是你很快會意識到實(shí)現(xiàn)這個目標(biāo)比聽起來要難。這是一項(xiàng)完全不同的挑戰(zhàn)。即使它們技術(shù)上實(shí)現(xiàn)并沒有那么復(fù)雜,但時(shí)解決它們會對產(chǎn)品產(chǎn)生巨大影響并且讓人獲得滿足。

          Web 應(yīng)用面臨的最大挑戰(zhàn)不是變成了一個無法維護(hù)的屎山,而是會減慢你的速度,讓你的業(yè)務(wù)最終失敗。

          這是他們?nèi)绾卧?Go 中發(fā)生和我是如何避免他們的。

          松耦合是關(guān)鍵

          應(yīng)用難以維護(hù)的一個重要原因是強(qiáng)耦合。

          在強(qiáng)耦合應(yīng)用中,任何你嘗試觸動的東西都有可能產(chǎn)生意想不到的副作用。每次重構(gòu)的嘗試都會發(fā)現(xiàn)新的問題。最終,你決定字號從頭重寫整個項(xiàng)目。在一個快速增長的產(chǎn)品中,你是不可能凍結(jié)所有的開發(fā)任務(wù)去完成重寫已經(jīng)構(gòu)建的應(yīng)用的。而且你不能保證這次你把所有事都完成好。

          相比之下,松耦合應(yīng)用保持了清晰的邊界。他們允許更換一些損壞的部分不影響項(xiàng)目的其他部分。它們更容易構(gòu)建和維護(hù)。但是,為什么他們?nèi)绱撕币娔兀?/p>

          微服務(wù)許諾了松耦合時(shí)實(shí)踐,但是我們現(xiàn)在已經(jīng)過了他們的炒作年代,而難以維護(hù)的應(yīng)用仍舊存在。有些時(shí)候這反而變得更糟糕了:我們落入了分布式單體的陷阱,處理和之前相同的問題,而且還增加了網(wǎng)絡(luò)開銷。

          從強(qiáng)耦合單體應(yīng)用到分布式單體

          ? 反模式:分布式單體 在你了解邊界之前,不要將你的應(yīng)用切分成為微服務(wù)。

          微服務(wù)并不會降低耦合,因?yàn)椴鸱址?wù)的次數(shù)并不重要。重要的是如何連接各個服務(wù)。

          從模塊化單體應(yīng)用到松耦合微服務(wù)

          ? 策略:松耦合 以實(shí)現(xiàn)松耦合的模塊為目標(biāo)。如何部署它們(作為模塊化單體應(yīng)用或微服務(wù))是一個實(shí)現(xiàn)細(xì)節(jié)。

          DRY 引入了耦合

          強(qiáng)耦合十分常見,因?yàn)槲覀兒茉缇蛯W(xué)到了不要重復(fù)自己 (Don't Repeat Yourself, DRY) 原則。

          簡短的規(guī)則很容易被大家記住,但是簡短的三個單詞很難概括所有的細(xì)節(jié)。《程序員修煉之道: 從小工到專家》這本書提供了一個更長的版本:

          每條知識在系統(tǒng)中都必須有一個單一的、明確的、權(quán)威的表述。

          "每一條知識"這個說法相當(dāng)極端。大多數(shù)編程困境的答案是看情況而定,DRY 也不例外。

          當(dāng)你讓兩個事物使用相同抽象的時(shí)候,你就引入了耦合。如果你嚴(yán)格遵循 DRY 原則,你就需要在這個抽象之前增加抽象。

          在 Go 中保持 DRY

          相比于其他現(xiàn)代語言,Go 是清晰的,缺少很多特性,沒有太多的語法糖來隱藏復(fù)雜性。

          我們習(xí)慣了捷徑,所以一開始很難接受 Go 的冗長。就像我們已經(jīng)開發(fā)出一種去尋找一種更加聰明的編寫代碼的方式的本能。

          最典型的例子就是錯誤處理。如果你有編寫 Go 的經(jīng)驗(yàn),你會覺得下面的代碼片段很自然

          if err != nil {
              return err
          }

          但是對新手而言,一遍又一遍的重復(fù)這三行就是似乎在破壞 DRY 原則。他們經(jīng)常想辦法來規(guī)避這種樣板方法,但是卻沒有什么好的結(jié)果。

          最終,大家都接受了 Go 的工作方式。它讓你重復(fù)你自己,不過這并不是 DRY 告訴你的你要避免重復(fù)。

          單一數(shù)據(jù)模型帶來的應(yīng)用耦合

          Go 中有一個特性引入了強(qiáng)耦合,但會讓你認(rèn)為你自己在遵循 DRY 原則。這就是在一個結(jié)構(gòu)體中使用多個標(biāo)簽。這似乎是一個好主意,因?yàn)槲覀兘?jīng)常對不同的事物使用相似的模型。

          這里有一個流行的方式保存單個User模型的方法:

          type User struct {
              ID           int        `json:"id" gorm:"autoIncrement primaryKey"`
              FirstName    string     `json:"first_name" validate:"required_without=LastName"`
              LastName     string     `json:"last_name" validate:"required_without=FirstName"`
              DisplayName  string     `json:"display_name"`
              Email        string     `json:"email,omitempty" gorm:"-"`
              Emails       []Email    `json:"emails" validate:"required,dive" gorm:"constraint:OnDelete:CASCADE"`
              PasswordHash string     `json:"-"`
              LastIP       string     `json:"-"`
              CreatedAt    *time.Time `json:"-"`
              UpdatedAt    *time.Time `json:"-"`
          }

          type Email struct {
              ID      int    `json:"-" gorm:"primaryKey"`
              Address string `json:"address" validate:"required,email" gorm:"size:256;uniqueIndex"`
              Primary bool   `json:"primary"`
              UserID  int    `json:"-"`
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/01-tightly-coupled/internal/user.go

          這種方式通過很少的幾行代碼讓你可以只維護(hù)單一的結(jié)構(gòu)體實(shí)現(xiàn)功能。

          然而,在單一模型中擬合所有的內(nèi)容需要很多技巧。API 可能不需要保護(hù)某些字段,因此他們需要通過json:"-"隱藏起來。只有一個 API 使用到了Email字段,那么 ORM 就需要跳過它,并且需要在常規(guī)的 JSON 返回中通過omitempty進(jìn)行隱藏。

          更重要的是,這個解決方案帶來一個最糟糕的問題:API、存儲和邏輯之間產(chǎn)生了強(qiáng)耦合。

          當(dāng)你想要更新結(jié)構(gòu)體中的任何東西時(shí),你都不知道還有什么會發(fā)生修改。你會在更新數(shù)據(jù)庫 Schema 或者更新驗(yàn)證規(guī)則時(shí)破壞 API 的約定。

          模型越復(fù)雜,你面臨的問題就越多。

          比如,json標(biāo)簽表示 JSON 而不是 HTTP。但是讓你引入同樣是格式化到 JSON,但是格式與 API 不同的事件時(shí)會發(fā)生什么?你需要不停的添加 hack 讓所有功能正常工作。

          最終,你的團(tuán)隊(duì)會避免對結(jié)構(gòu)體的修改,因?yàn)樵谀銊恿私Y(jié)構(gòu)體之后你無法確定會出現(xiàn)什么樣的問題。

          ? 反模式:單一模型 不要給一個模型多個責(zé)任。每個結(jié)構(gòu)字段不要使用多個標(biāo)簽。

          復(fù)制消除耦合

          減少耦合最簡單的方法是拆分模型。

          我們提取 API 使用的部分作為 HTTP 模型:

          type CreateUserRequest struct {
              FirstName string `json:"first_name" validate:"required_without=LastName"`
              LastName  string `json:"last_name" validate:"required_without=FirstName"`
              Email     string `json:"email" validate:"required,email"`
          }

          type UpdateUserRequest struct {
              FirstName *string `json:"first_name" validate:"required_without=LastName"`
              LastName  *string `json:"last_name" validate:"required_without=FirstName"`
          }

          type UserResponse struct {
              ID          int             `json:"id"`
              FirstName   string          `json:"first_name"`
              LastName    string          `json:"last_name"`
              DisplayName string          `json:"display_name"`
              Emails      []EmailResponse `json:"emails"`
          }

          type EmailResponse struct {
              Address string `json:"address"`
              Primary bool   `json:"primary"`
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/02-loosely-coupled/internal/http.go

          數(shù)據(jù)庫相關(guān)部分作為存儲模型:

          type UserDBModel struct {
              ID           int            `gorm:"column:id;primaryKey"`
              FirstName    string         `gorm:"column:first_name"`
              LastName     string         `gorm:"column:last_name"`
              Emails       []EmailDBModel `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
              PasswordHash string         `gorm:"column:password_hash"`
              LastIP       string         `gorm:"column:last_ip"`
              CreatedAt    *time.Time     `gorm:"column:created_at"`
              UpdatedAt    *time.Time     `gorm:"column:updated_at"`
          }

          type EmailDBModel struct {
              ID      int    `gorm:"column:id;primaryKey"`
              Address string `gorm:"column:address;size:256;uniqueIndex"`
              Primary bool   `gorm:"column:primary"`
              UserID  int    `gorm:"column:user_id"`
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/02-loosely-coupled/internal/db.go

          起初,看上去我們會在所有地方使用相同的User模型?,F(xiàn)在,很明顯我們過早的避免了重復(fù)。API 和存儲的結(jié)構(gòu)很相似,但足夠不同到需要拆分成不同的模型。

          在 Web 應(yīng)用中,你 API 返回(讀模型)與存儲在數(shù)據(jù)庫中的視圖(寫模型)并不相同。

          存儲代碼無需知道 HTTP 的模型,因此我們需要進(jìn)行結(jié)構(gòu)轉(zhuǎn)換。

          func userResponseFromDBModel(u UserDBModel) UserResponse {
              var emails []EmailResponse
              for _, e := range u.Emails {
                  emails = append(emails, emailResponseFromDBModel(e))
              }

              return UserResponse{
                  ID:          u.ID,
                  FirstName:   u.FirstName,
                  LastName:    u.LastName,
                  DisplayName: displayName(u.FirstName, u.LastName),
                  Emails:      emails,
              }
          }

          func emailResponseFromDBModel(e EmailDBModel) EmailResponse {
              return EmailResponse{
                  Address: e.Address,
                  Primary: e.Primary,
              }
          }

          func userDBModelFromCreateRequest(r CreateUserRequest) UserDBModel {
              return UserDBModel{
                  FirstName: r.FirstName,
                  LastName:  r.LastName,
                  Emails: []EmailDBModel{
                      {
                          Address: r.Email,
                      },
                  },
              }
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/02-loosely-coupled/internal/http.go

          這就是所有你需要的代碼:將一種類型映射到另一種類型的函數(shù)。編寫這種平淡無奇的代碼可能看起來十分無聊,但是它對解耦至關(guān)重要。

          創(chuàng)建一個使用序列化或者reflect實(shí)現(xiàn)用于映射結(jié)構(gòu)體的通用解決方案看上去十分誘人。請抵制它。編寫模版比調(diào)試映射的邊緣情況會更節(jié)省時(shí)間和精力。簡單的函數(shù)對團(tuán)隊(duì)中每個人都更容易理解。魔法轉(zhuǎn)換器會在一段時(shí)間后變得難以理解,即使對你而言也是如此。

          ? 策略:模型單一責(zé)任。通過使用單獨(dú)的模型來實(shí)現(xiàn)松耦合。編寫簡單明了的函數(shù)用以在它們之間進(jìn)行轉(zhuǎn)換。

          如果你害怕太多的重復(fù),請考慮一下最壞的情況。如果你最終多了幾個隨著應(yīng)用程序增長不變的結(jié)構(gòu),你可以將它們合并回一個。與強(qiáng)耦合代碼相比,修復(fù)重復(fù)代碼是微不足道的。

          生成模版

          如果你擔(dān)心手寫這些所有代碼,有一個管用的方法可以規(guī)避。使用可以為你生成模版的庫。

          你可以生成諸如:

          • 由 OpenAPI 定義的 HTTP 模型和路由(oapi-codegen或者其他庫)。
          • 由 SQL schema 定義的數(shù)據(jù)庫模型和相關(guān)代碼([sqlboiler](https://github.com/volatiletech/sqlboilerORM)。和其他
          • 通過 Protobuf 文件生成 gPRC 模型。

          生成的代碼可以提供強(qiáng)類型保護(hù),因此你無需在通用函數(shù)中傳遞interface{}類型的數(shù)據(jù)。你可以保證編譯時(shí)檢查的同時(shí)無需手寫代碼。

          下面是生成的模型的例子。

          // PostUserRequest defines model for PostUserRequest.
          type PostUserRequest struct {

              // E-mail
              Email string `json:"email"`

              // First name
              FirstName string `json:"first_name"`

              // Last name
              LastName string `json:"last_name"`
          }

          // UserResponse defines model for UserResponse.
          type UserResponse struct {
              DisplayName string          `json:"display_name"`
              Emails      []EmailResponse `json:"emails"`
              FirstName   string          `json:"first_name"`
              Id          int             `json:"id"`
              LastName    string          `json:"last_name"`
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/03-loosely-coupled-generated/internal/http_types.go

          type User struct {
              ID           int64       `boil:"id" json:"id" toml:"id" yaml:"id"`
              FirstName    string      `boil:"first_name" json:"first_name" toml:"first_name" yaml:"first_name"`
              LastName     string      `boil:"last_name" json:"last_name" toml:"last_name" yaml:"last_name"`
              PasswordHash null.String `boil:"password_hash" json:"password_hash,omitempty" toml:"password_hash" yaml:"password_hash,omitempty"`
              LastIP       null.String `boil:"last_ip" json:"last_ip,omitempty" toml:"last_ip" yaml:"last_ip,omitempty"`
              CreatedAt    null.Time   `boil:"created_at" json:"created_at,omitempty" toml:"created_at" yaml:"created_at,omitempty"`
              UpdatedAt    null.Time   `boil:"updated_at" json:"updated_at,omitempty" toml:"updated_at" yaml:"updated_at,omitempty"`

              R *userR `boil:"-" json:"-" toml:"-" yaml:"-"`
              L userL  `boil:"-" json:"-" toml:"-" yaml:"-"`
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/03-loosely-coupled-generated/models/users.go

          有時(shí)你可能會想要編寫代碼生成工具。這其實(shí)并不難,結(jié)果需要是每個人都可以閱讀和理解的常規(guī) Go 代碼。常見的替代方案是使用reflect,但是這很難掌握和調(diào)試。當(dāng)然,首先要考慮的是付出的努力是否值得。在大多數(shù)情況下,手寫代碼已經(jīng)足夠快了。

          ? 策略:生成重復(fù)工作的部分 生成的代碼為你提供強(qiáng)類型和編譯時(shí)安全性。選擇它而不是reflect。

          不要過度使用庫

          只將生成的代碼用于它應(yīng)該做的事情。如果你想避免手工編寫模版,但仍需要保留一些專用的模型。不要以單一模型反模式作為結(jié)束。

          當(dāng)你想遵循 DRY 原則時(shí),很容易落入這個陷阱。

          例如,sqlc和sqlboiler都是從 SQL 查詢中生成代碼。sqlc 允許在生成的模型上添加 JSON 標(biāo)簽,甚至允許讓你選擇camelCase還是snake_case。sqlboiler 在所有模型上默認(rèn)添加了json,tomlyaml標(biāo)簽。這顯然是不是讓用戶僅僅把這個模型僅用于存儲。

          看一下 sqlc 的 issue 列表,我發(fā)現(xiàn)很多開發(fā)者要求更多的靈活性,比如重命名生成的字段和整個跳過一些 JSON 字段。有人甚至提到他們需要某種在 REST API 中隱藏某些敏感字段的方法。

          所有這些都是鼓勵在單一模型中擔(dān)負(fù)更多職責(zé)。它可以讓你寫更少的代碼,但是請務(wù)必考慮這種耦合是否值得。

          同樣,需要注意結(jié)構(gòu)體標(biāo)簽中隱藏的魔法,比如,gorm 中提供的權(quán)限功能:

          type User struct {
              Name string `gorm:"<-:create"` // 允許讀取和創(chuàng)建
              Name string `gorm:"<-:update"` // 允許讀取和更新
              Name string `gorm:"<-"`        // 允許讀取和寫入(創(chuàng)建和更新)
              Name string `gorm:"<-:false"`  // 允許讀取,禁用寫權(quán)限
              Name string `gorm:"->"`        // 只讀模式(除非單獨(dú)配置,否則禁用寫權(quán)限)
              Name string `gorm:"->;<-:create"` // 允許讀取和創(chuàng)建
              Name string `gorm:"->:false;<-:create"` // 只允許創(chuàng)建(禁止從數(shù)據(jù)庫中讀?。?br>    Name string `gorm:"-"`  // 在讀寫模型時(shí)忽略這個字段
          }

          完整代碼:gorm.io/docs/models.html#Field-Level-Permission

          你同樣可以使用 [validator] 庫進(jìn)行復(fù)雜的比較,比如參考其他字段:

          type User {
              FirstName    string `validate:"required_without=LastName"`
              LastName     string `validate:"required_without=FirstName"`
          }

          它為你節(jié)省了一點(diǎn)編寫代碼的時(shí)間,但是這意味著你放棄了編譯期檢查。在結(jié)構(gòu)體標(biāo)簽中很容易出現(xiàn)錯別字,在驗(yàn)證和權(quán)限等敏感地方使用這種會帶來風(fēng)險(xiǎn)。這同樣也會讓很多不那么熟悉庫的語法糖的人感到困擾。

          我并不是指摘這些提到的庫,他們都有自己的用途。但是這些示例展示了我們?nèi)绾伟?DRY 做到極致,這樣我們就不用編寫更多的代碼了。

          ? 反模式:選擇魔法來節(jié)省編寫代碼的時(shí)間 不要過度使用庫以避免冗余。

          避免隱式標(biāo)簽名

          大多數(shù)庫不要求標(biāo)簽必須存在,此時(shí)會默認(rèn)使用字段名稱。

          在重構(gòu)項(xiàng)目時(shí),有人可能會重命名字段,但是他沒有想過編輯 API 返回或者數(shù)據(jù)模型。如果沒有標(biāo)簽,這就會導(dǎo)致 API 約定或者數(shù)據(jù)存儲過程被破壞。

          請始終填寫所有標(biāo)間,即使你必須兼容同一名稱兩次,這并不違反 DRY 原則。

          譯者注:其實(shí) Go 之前有個類似 proposal 提過在 1.16 中簡化這一寫法,但是后面發(fā)現(xiàn)存在一些問題被回滾了。?反模式:省略結(jié)構(gòu)標(biāo)簽 如果庫使用它們,則不要跳過結(jié)構(gòu)標(biāo)簽。

          type Email struct {
              ID      int    `gorm:"primaryKey"`
              Address string `gorm:"size:256;uniqueIndex"`
              Primary bool
              UserID  int
          }

          ?戰(zhàn)術(shù):顯式結(jié)構(gòu)標(biāo)簽 始終填充結(jié)構(gòu)標(biāo)簽,即使字段名稱相同。

          type Email struct {
              ID      int    `gorm:"column:id;primaryKey"`
              Address string `gorm:"column:address;size:256;uniqueIndex"`
              Primary bool   `gorm:"column:primary"`
              UserID  int    `gorm:"column:user_id"`
          }

          將邏輯與實(shí)現(xiàn)細(xì)節(jié)分開

          通過生成模型將 API 與存儲解耦是一個好的開始。但是,我們?nèi)耘f需要保留在 HTTP 處理中的驗(yàn)證過程。

          type createRequest struct {
              Email     string `validate:"required,email"`
              FirstName string `validate:"required_without=LastName"`
              LastName  string `validate:"required_without=FirstName"`
          }

          validate := validator.New()
          err = validate.Struct(createRequest(postUserRequest))
          if err != nil {
              log.Println(err)
              w.WriteHeader(http.StatusBadRequest)
              return
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/03-loosely-coupled-generated/internal/http.go

          驗(yàn)證是你能在大多數(shù) Web 應(yīng)用中可以找到的業(yè)務(wù)邏輯中的一環(huán)。通常,他們會更加復(fù)雜,比如:

          • 僅在特定情況下顯示字段
          • 檢查權(quán)限
          • 取決于角色而隱藏字段
          • 計(jì)算價(jià)格
          • 根據(jù)幾個因素采取行動

          將邏輯和實(shí)現(xiàn)細(xì)節(jié)混在一起(比如將他們放在 HTTP handler 中)是一種快速交付 MVP 的方法。但是這也引入了最壞的技術(shù)債務(wù)。這就是為什么你會被供應(yīng)商鎖定,為什么你需要不停的添加 hack 拉支持新功能。

          ? 反模式:將邏輯和細(xì)節(jié)混在一起 不要將你的應(yīng)用程序邏輯與實(shí)現(xiàn)細(xì)節(jié)混在一起。

          商業(yè)邏輯需要單獨(dú)的層。更改實(shí)現(xiàn)(數(shù)據(jù)庫引擎、HTTP 庫、基礎(chǔ)架構(gòu)、Pub/Sub 等)應(yīng)是可能的,而無需對邏輯部件進(jìn)行任何更改。

          你做這種分離并不是因?yàn)槟阆胍臄?shù)據(jù)庫引擎,這種情況很少會發(fā)生。但是,關(guān)注點(diǎn)的分離可以讓你的代碼更容易理解和修改。你知道你在修改什么,并且有沒有副作用。這樣就很難在關(guān)鍵部分引入 bug。

          要分離應(yīng)用層,我們需要添加額外的模型和映射。


          type User struct {
              id        int
              firstName string
              lastName  string
              emails    []Emailf
          }

          func NewUser(firstName string, lastName string, emailAddress string) (User, error) {
              if firstName == "" && lastName == "" {
                  return User{}, ErrNameRequired
              }

              email, err := NewEmail(emailAddress, true)
              if err != nil {
                  return User{}, err
              }

              return User{
                  firstName: firstName,
                  lastName:  lastName,
                  emails:    []Email{email},
              }, nil
          }

          type Email struct {
              address string
              primary bool
          }

          func NewEmail(address string, primary bool) (Email, error) {
              if address == "" {
                  return Email{}, ErrEmailRequired
              }

              // A naive validation to make the example short, but you get the idea
              if !strings.Contains(address, "@") {
                  return Email{}, ErrInvalidEmail
              }

              return Email{
                  address: address,
                  primary: primary,
              }, nil
          }

          完整代碼:github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/04-loosely-coupled-app-layer/internal/user.go

          這就是當(dāng)我需要更新業(yè)務(wù)邏輯時(shí)我需要修改的代碼。這明顯很無聊,但是我知道我修改了什么。

          當(dāng)我們添加另一個 API(比如 gRPC)或者外部系統(tǒng)(如 Pub/Sub)時(shí),我們需要同樣的工作。每個部分都是用單獨(dú)的模型,我們在應(yīng)用層映射轉(zhuǎn)換它們。

          因?yàn)閼?yīng)用層維護(hù)了所有的驗(yàn)證和其他商業(yè)邏輯,他會讓我們無論是使用 HTTP 還是 gRPC API 都沒什么區(qū)別。API 只是應(yīng)用的入口。

          ? 策略:應(yīng)用層 將產(chǎn)品最重要的代碼劃分成單獨(dú)的層。

          上面的代碼片段都來自于同一個代碼庫,并且實(shí)現(xiàn)了經(jīng)典的用戶域。所有示例都暴露相同的 API 并且使用相同的測試套件。

          以下是他們的比較:

          標(biāo)準(zhǔn)的 Go 項(xiàng)目結(jié)構(gòu)

          如果你看過這個倉庫,你會發(fā)現(xiàn)在每個例子中只有一個包。

          Go 目前沒有官方的目錄組織結(jié)構(gòu),不過你可以找到很多微服務(wù)例子或者 REST 模版?zhèn)}庫建議你如何拆分。他們通常有精心設(shè)計(jì)的目錄機(jī)構(gòu),有人甚至提到他們遵循了簡潔架構(gòu)或者六邊形架構(gòu)。

          我一般第一件確認(rèn)的事情是如何存儲模型的。大多數(shù)情況下,他們使用了 JSON 和數(shù)據(jù)庫標(biāo)簽混合的結(jié)構(gòu)體。

          這是一種錯覺:包看起來進(jìn)行了很好的切分,但實(shí)際上他們?nèi)耘f通過一個模型被緊密的耦合在了一起。新人用來學(xué)習(xí)的很多流行例子中,這些問題也很常見。

          具有諷刺意味的是,標(biāo)準(zhǔn)的 Go 項(xiàng)目結(jié)構(gòu)仍舊在社區(qū)中繼續(xù)被討論,然而模型耦合反模式卻很常見。如果你的應(yīng)用程序的類型耦合了,任何目錄的組織形式都不會改變什么。

          在查看示例結(jié)構(gòu)時(shí),請記住他們可能是為另外一種不同類型的應(yīng)用程序設(shè)計(jì)的。對于開源的基礎(chǔ)設(shè)施工具、Web 應(yīng)用后端和標(biāo)準(zhǔn)庫而言,沒有一種方法同時(shí)對他們有效。

          包分層和切分微服務(wù)的問題非常類似。重要的不是如何劃分他們,而是他們彼此之間如何連接。

          當(dāng)你專注于松耦合時(shí),目錄結(jié)構(gòu)就會變得更加清晰。你可以將實(shí)現(xiàn)細(xì)節(jié)與業(yè)務(wù)邏輯區(qū)分開。你把相互引用的事物分組,并將不互相引用的事物拆分開。

          在我準(zhǔn)備的示例中,我可以輕松的將 HTTP 相關(guān)的代碼和數(shù)據(jù)庫相關(guān)的代碼拆分至單獨(dú)的包中。這會避免命名空間的污染。模型之間已經(jīng)沒有耦合,所以這些操作就變成了具體的細(xì)節(jié)。

          ?反模式:過度考慮目錄結(jié)構(gòu) 不要通過分割目錄來啟動項(xiàng)目。不管你怎么做,這是一個慣例。你不太可能在編寫代碼之前把事情做好。?策略:松耦合代碼 重要的部分不是目錄結(jié)構(gòu),而是包和結(jié)構(gòu)是如何進(jìn)行相互引用的。

          保持簡單化

          假設(shè)你想要創(chuàng)建一個用戶,這個用戶有一個 ID 字段。最簡單的方法可以看起來像這樣:

          type User struct {
              ID string `validate:"required,len=32"`
          }

          func (u User) Validate() error {
              return validate.Struct(u)
          }

          這段代碼能夠正常工作。但是,你無法判斷該結(jié)構(gòu)在任何時(shí)候都是正確的。你依靠一些額外東西來調(diào)用驗(yàn)證并處理錯誤。

          另一種方法是采用良好的舊式封裝。

          type User struct {
              id UserID
          }

          type UserID struct {
              id string
          }

          func NewUserID(id string) (UserID, error) {
              if id == "" {
                  return UserID{}, ErrEmptyID
              }

              if len(id) != 32 {
                  return UserID{}, ErrInvalidLength
              }

              return UserID{
                  id: id,
              }, nil
          }

          func (u UserID) String() string {
              return u.id
          }

          此片段更清晰、更冗長。如果你創(chuàng)建了一個新的UserID并且沒有收到任何錯誤,你可以確定創(chuàng)建是成功的。此外,你可以輕松地將錯誤映射到 API 的正確響應(yīng)。

          無論你選擇哪種方法,你都需要對用戶 ID 的基本復(fù)雜性進(jìn)行建模。從純粹的實(shí)現(xiàn)的角度來看,將 ID 保持在字符串中是最簡單的解決方案。

          Go 應(yīng)該很簡單,但這并不意味著你應(yīng)該只使用原始類型。對于復(fù)雜的行為,請使用反映產(chǎn)品工作方式的代碼。否則,你最終會獲得一個簡化的模型。

          ?反模式:過度簡化 不要用瑣碎的代碼來模擬復(fù)雜的行為。?策略:編寫明確的代碼 保證代碼是明確的,即使它很冗長。使用封裝來確保你的結(jié)構(gòu)始終處于有效狀態(tài)。即使所有字段都未導(dǎo)出,也可以在包外創(chuàng)建空結(jié)構(gòu)。唯一要做的是在接受UserID作為參數(shù)時(shí),你需要檢查一下合法性。你可以使用if id == UserID{}或編寫專門的IsZero()方法來進(jìn)行。

          從數(shù)據(jù)庫 Schema 開始

          假設(shè)我們需要添加一個用戶創(chuàng)建和加入團(tuán)隊(duì)的功能。

          按照關(guān)系型方法,我們需要添加一個teams表和另外一個將用戶和它進(jìn)行關(guān)聯(lián)的表。我們叫它membership。

          按照關(guān)系方法,我們將添加一張桌子和另一張加入它的表格。讓我們稱之為。teamsusersmembership

          我們已經(jīng)有了UserStorage,所以很自然的添加兩個新的結(jié)構(gòu)體:TeamStorageMembershipStorage。他們會為每個表格提供 CRUD 方法。

          添加新團(tuán)隊(duì)的代碼可能看起來是這個樣子的:

          func CreateTeam(teamName string, ownerID int) error {
              teamID, err := teamStorage.Create(teamName)
              if err != nil {
                  return err
              }

              return membershipStorage.Create(teamID, ownerID, MemberRoleOwner)
          }

          這種方法有一個問題:我們沒有在事務(wù)中創(chuàng)建團(tuán)隊(duì)和成員記錄。如果出現(xiàn)問題,我們可能最終擁有一支沒有分配所有者的團(tuán)隊(duì)。

          首先想到的第一個解決方案是在方法之間傳遞事務(wù)。

          func CreateTeam(teamName string, ownerID int) error {
              tx, err := db.Begin()
              if err != nil {
                  return err
              }

              defer func() {
                  if err == nil {
                      err = tx.Commit()
                  } else {
                      rollbackErr := tx.Rollback()
                      if rollbackErr != nil {
                          log.Error("Rollback failed:", err)
                      }
                  }
              }()

              teamID, err := teamStorage.Create(tx, teamName)
              if err != nil {
                  return err
              }

              return membershipStorage.Create(tx, teamID, ownerID, MemberRoleOwner)
          }

          但是,這樣的話實(shí)現(xiàn)細(xì)節(jié)(事務(wù)處理)就會泄漏到了邏輯層。它通過基于defer的錯誤處理污染了一個可讀的函數(shù)。

          下面是一個練習(xí):考慮如何在文檔數(shù)據(jù)庫中對此進(jìn)行建模。比如,我們可以將所有成員保留在團(tuán)隊(duì)文檔中。

          在這種情況下,添加成員就可以在TeamStorage中完成,這樣我們就不需要單獨(dú)的MembershipStorage。但是切換數(shù)據(jù)庫就變更了我們模型的假設(shè),這不是很奇怪嗎?

          現(xiàn)在很顯然,我們通過引入"成員身份"概念泄露了實(shí)現(xiàn)細(xì)節(jié)。"創(chuàng)建新成員身份",這只會困擾我們的銷售或者客戶服務(wù)同事。當(dāng)你開始說一種不同于公司其他成員的語言時(shí),這通常一個嚴(yán)重的危險(xiǎn)信號。

          反模式?:從數(shù)據(jù)庫 Schema 開始 不要將模型建立在數(shù)據(jù)庫模式的基礎(chǔ)上。你最終會暴露實(shí)現(xiàn)細(xì)節(jié)。

          TeamStorage用于存儲團(tuán)隊(duì)信息,但它不是與teams SQL 表無關(guān)。這是關(guān)于我們產(chǎn)品的團(tuán)隊(duì)概念。

          每個人都明白創(chuàng)建一個團(tuán)隊(duì)需要一個所有者,我們可以為此暴露一個方法。這個方法會將所有的查詢放在一個事務(wù)中執(zhí)行查詢。

          teamStorage.Create(teamName, ownerID, MemberRoleOwner)

          同樣,我們也可以有一個加入團(tuán)隊(duì)的方法。

          teamStorage.JoinTeam(teamID, memberID, MemberRoleGuest)

          membership表依舊存在,但是實(shí)現(xiàn)細(xì)節(jié)被隱藏在TeamStorage中。

          ?策略:從領(lǐng)域開始 你的存儲方法應(yīng)遵循產(chǎn)品的行為。不要他們的事務(wù)細(xì)節(jié)。

          你的網(wǎng)絡(luò)應(yīng)用程序不是單純的 CRUD

          教程通常都是以"簡單的 CRUD"為特色,因此它們似乎是任何 Web 應(yīng)用的基礎(chǔ)構(gòu)建模塊。這是個虛無縹緲的傳說。如果你所有的產(chǎn)品需要的是 CRUD,你就是在浪費(fèi)時(shí)間和金錢從零開始構(gòu)建。

          框架和無代碼工具使得啟動 CRUD 變得更容易,但我們?nèi)匀幌蜷_發(fā)人員支付構(gòu)建自定義軟件的費(fèi)用。即使是 GitHub Copilot 也不知道你的產(chǎn)品除了模版之外是如何工作的。

          正是特殊的規(guī)則和奇怪的細(xì)節(jié)使你的應(yīng)用程序與眾不同。這不是你分散在四個 CRUD 操作之上的邏輯。它是你銷售的產(chǎn)品的核心。

          在 MVP 階段,從 CRUD 開始快速構(gòu)建可工作版本是很誘人的。但這就像使用電子表格而不是專用軟件。一開始,你會獲得類似的效果,但每個新功能都需要更多的 hack。

          反模式?:從 CRUD 開始 不要圍繞四個 CRUD 操作的想法來設(shè)計(jì)你的應(yīng)用程序。?策略:了解你的領(lǐng)域 花時(shí)間了解你的產(chǎn)品是如何工作的,并在代碼中建模。

          我描述的許多策略都是眾所周知的模式背后的想法:

          • SOLID 中的單一責(zé)任原則(每個模型只有一項(xiàng)責(zé)任)。

          • 簡潔架構(gòu)(松耦合的包,將邏輯與實(shí)現(xiàn)細(xì)節(jié)隔離)。

          • CQRS(使用不同的讀取模型和寫入模型)。有些甚至接近域驅(qū)動設(shè)計(jì):

          • 值對象(始終保持結(jié)構(gòu)處于有效狀態(tài))。

          • 聚合和倉庫(無論數(shù)據(jù)庫表的數(shù)量如何,都以事務(wù)方式保存領(lǐng)域?qū)ο螅?/p>

          • 無處不在的語言(使用每個人都能理解的語言)。這些模式似乎大多與企業(yè)級應(yīng)用相關(guān)。但其中大多數(shù)是簡單明了的核心思想,比如本文中的策略。它們同樣適用于處理復(fù)雜的業(yè)務(wù)行為 Web 應(yīng)用程序、

          你不需要閱讀大量書籍或復(fù)制其他語言的代碼來遵循這些模式。你可以通過實(shí)踐檢驗(yàn)的技術(shù)編寫慣用的 Go 代碼。如果你想了解更多關(guān)于他們的內(nèi)容,可以看看我們的免費(fèi)電子書。

          如果你想在反模式倉庫中添加更多示例以及有關(guān)主題,請?jiān)谠u論中告知我們。




          作者:Mi?osz Smó?ka
          譯者:Kevin
          原文地址:https://threedots.tech/post/common-anti-patterns-in-go-web-applications/
          譯文地址:https://www.4async.com/2021/08/common-anti-patterns-in-go-web-applications/

          瀏覽 43
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本三级免费 | 操逼男人的天堂 | 伊人天天操天天色 | 人人鲁人人操 | 国产精品 久久久精品 |