Go 工程化之如何優(yōu)雅的寫出 Repo 層代碼
上篇文章中我們提到了事務的幾種解決方案,可以避免在 repo 中寫很多不同事務的方法,這篇我們看一下怎么讓 repo 層的代碼看起來優(yōu)雅一點
還是以獲取一篇文章為例,我們在獲取文章的時候大部分時候可能都是通過 id 獲取的,但是我們也可能通過標題等其它信息獲取文章的數(shù)據(jù),這時候我們的 repo 層代碼怎么寫呢?
最簡單的方式,就是我們直接在 repo 這里寫兩個方法
- // IArticleRepo IArticleRepo
- type IArticleRepo interface {
- GetArticleByTitle(ctx context.Context, title string) (*Article, error)
- GetArticleByID(ctx context.Context, id int) (*Article, error)
- }
這樣最簡單也最直觀,但是問題是我們的實際的業(yè)務需求往往比我們的例子復雜,如果我們需要通過 id 或者標題獲取呢?再添加一個 GetArticleByIDOrTitle ?
這么做的話也不是不行,但是這么做的話就會讓我們的 repo 的代碼隨著時間的增長越來越多不說,命名也是問題,因為組合的方式可能是多種多樣的
接下來給大家提供一種我們正在使用的一種思路,利用 Function Options 這種 Go 常見的編程范式,使我們的 repo 更優(yōu)雅,也可擴展
DBOption
注意: 筆者這里使用的是 GORM,但是這種方式不僅僅適用于 orm 的情況,只是相對方便一點而已
- type DBOption func(*gorm.DB) *gorm.DB
- // IArticleRepo IArticleRepo
- type IArticleRepo interface {
- WithByID(id uint) DBOption
- WithByTitle(title string) DBOption
- GetArticle(ctx context.Context, opts ...DBOption) (*Article, error)
- }
我們定義一個的 DBOption 這個 Option 方法會作為我們 repo 層方法中的最后一個參數(shù),這樣我們在定義方法的時候就可以簡潔一些,就不必定義很多 GetArticleByXXX 方法了,而是通過定義很多 WithByXXX 的 Option 方法來解決。
這樣在 usecase 層,我們只需要這么調(diào)用即可
- func (u *article) GetArticle(ctx context.Context, id int) (*domain.Article, error) {
- // 這里可能有其他業(yè)務邏輯...
- return u.repo.GetArticle(ctx, u.repo.WithByID(uint(id)))
- }
優(yōu)點
復用: 雖然看上去我們只是把 GetArticleByXXX 換成了 WithByXXX 該有的方法并沒有變少,但是我們拆分之后會發(fā)現(xiàn)很多可以復用的方法,例如 WithByID 這種幾乎是每個實體都會有的方法,我們就不用重復寫了。
- // GetArticle 和 GetAuthor 都能用上
- func (u *article) GetArticle(ctx context.Context, id int) (*domain.Article, error) {
- // 這里可能有其他業(yè)務邏輯...
- return u.repo.GetArticle(ctx, u.repo.WithByID(uint(id)))
- }
- func (u *article) GetAuthor(ctx context.Context, id int) (*domain.Author, error) {
- // 這里可能有其他業(yè)務邏輯...
- return u.repo.GetAuthor(ctx, u.repo.WithByID(uint(id)))
- }
最小化: 這么修改了之后,拆分組合更加方便了,很多查詢條件都可以最小化,例如我們可以添加一個 WithSelects 的方法,我們在 usecase 調(diào)用的時候就可以傳入當前場景只需要關注的字段就可以了
- // GetArticle 返回文章的同時也需要返回作者的名字
- func (u *article) GetArticle(ctx context.Context, id int) (*domain.Article, error) {
- article, err := u.repo.GetArticle(ctx, u.repo.WithByID(uint(id)))
- if err != nil {
- return err
- }
- article.Author, err = u.repo.GetAuthor(ctx, u.repo.WithByArticleID(id), u.repo.WithBySelects("id", "name"))
- return article, err
- }
可測性: repo 層的測試會變得更加方便,這樣修改之后我們可以將查詢條件拆分出來進行測試,會比之前耦合在一起測試簡單很多。
抽象: 這種方式可以讓我們抽象 CURD 接口更加方便,在 repo 層實現(xiàn)的時候,我們可以直接把 curd 的方法都給抽象出來
- // 這里以創(chuàng)建為例
- func (r *userRepo) optionDB(ctx context.Context, opts ...model.DBOption) *gorm.DB {
- db := r.db.WithContext(ctx)
- for _, opt := range opts {
- db = opt(db)
- }
- return db
- }
- func (r *userRepo) create(ctx context.Context, data any, opts ...model.DBOption) error {
- db := r.optionDB(ctx, opts...)
- err := db.Create(data).Error
- if err != nil {
- return pb.ErrorDbCreateFailf("err: %+v", err)
- }
- return nil
- }
總結
今天給大家介紹了使用 Function Option 的方式來寫 repo 層的代碼,接下來我們就簡單總結一下
- type DBOption func(*gorm.DB) *gorm.DB
- // IArticleRepo IArticleRepo
- type IArticleRepo interface {
- WithByID(id uint) DBOption
- WithByTitle(title string) DBOption
- GetArticle(ctx con
優(yōu)點
- 復用: 雖然看上去我們只是把 GetArticleByXXX 換成了 WithByXXX 該有的方法并沒有變少,但是我們拆分之后會發(fā)現(xiàn)很多可以復用的方法,例如 WithByID 這種幾乎是每個實體都會有的方法,我們就不用重復寫了。
- 最小化: 這么修改了之后,拆分組合更加方便了,很多查詢條件都可以最小化,例如我們可以添加一個 WithSelects 的方法,我們在 usecase 調(diào)用的時候就可以傳入當前場景只需要關注的字段就可以了
- 可測性: repo 層的測試會變得更加方便,這樣修改之后我們可以將查詢條件拆分出來進行測試,會比之前耦合在一起測試簡單很多。
- 抽象: 這種方式可以讓我們抽象 CURD 接口更加方便
缺點
- 最大的缺點就是有的問題在單測可能測試不出來了,usecase 的測試中,repo 層被 mock 掉了,repo 在測試的時候大部分我們只會測試當前的方法,所以 usecase 有使用比較復雜的查詢語句的時候,repo 測試最好測一測真實的使用場景,不要僅測試單個 Option 方法
今天的文章就到這里,下篇文章給大家介紹一下 API 定義上的一點小技巧
本文轉(zhuǎn)載自微信公眾號「mohuishou」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系mohuishou公眾號。
原文鏈接:https://lailin.xyz/post/operator-09-kubebuilder-code.html