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