【问题标题】:How to shrink this code and merge the if and else condition如何缩小此代码并合并 if 和 else 条件
【发布时间】:2021-03-04 20:23:31
【问题描述】:
    if category == 0 {
        rows, err := h.Repo.GetAllLatestProducts(c.Context())
        if err != nil {
            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
        }
        result := make([]interface{}, len(rows))
        for i, product := range rows {
            result[i] = dbrow.ConvertToAllLatestProducts(product)
        }
    } else {
        rows, err := h.Repo.GetLatestProductsByCategory(c.Context(), int16(category))
        if err != nil {
            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
        }
        result := make([]interface{}, len(rows))
        for i, product := range rows {
            result[i] = dbrow.ConvertToCategoryLatestProducts(product)
        }
    }

if和else条件都遵循相同的代码流程,只是functions和struct不同,如何合并,让代码更小。我的意思是:

    var rows []postgres.GetAllLatestProductsRow
    var rows []postgres.GetLatestProductsByCategoryRow
    if category == 0 {
        rows, err = h.Repo.GetAllLatestProducts(c.Context())
    } else {
        rows, err = h.Repo.GetLatestProductsByCategory(c.Context(), int16(category))
    }
    //Rest of the code ...

不能触摸 h.Repo.GetAllLatestProducts 或 h.Repo.GetLatestProductsByCategory,因为它们是外部函数。类型安全也很重要。

可以有多个函数,比如featuredproducts,newproducts,我想做一个通用函数,根据动态选择的sql函数将产品返回为json。


您遇到了问题,具有相同代码结构的数十个函数很糟糕,至少对我而言,它的可读性与否无关紧要,它只是复制/粘贴的东西,没有任何意义, 大部分是复制/粘贴,只有函数名/SQLC生成的函数名和结构名发生变化,其余代码流程相同。

SQLC 根据 SQL 查询生成自动代码,现在编写重复代码只是将结果转换为 JSON 并将其返回给客户端是浪费时间。 可能有几十个 SQL 函数来返回最新产品、特色产品、类别中的产品、愿望清单产品、等等等等……

网站所理解的只是 Product 结构,但 SQLC 返回不同的结构,因此没有单一的 dbResult 类型的东西。反正映射不是什么大事,使用反射我们可以在映射函数中检查同名字段,将SQL.NullString转换为字符串等。

对我来说,真正的问题是 if/else 语句。您已经在不同的函数中移动了代码,但对我来说,在这种情况下它没有意义。因为网络处理程序无论如何都必须检查 请求是否有效,是否定义了category,然后检查category是否为0,然后调用不同的函数,然后得到结果返回给客户端。 对于单个函数,它可能看起来更好,但对于实际生产,它会让事情变得更糟,而不是单个函数和一个 if/else 块,现在每个 API 都有 3 个函数。

我正在寻找它只是将 SQLC 结果映射到路由处理程序。代码流程总是一样的, 只有函数名称和结构名称发生变化。如何使其动态化,以便在我的 http 处理程序中,我可以简单地编写:

return SQLCResult(c.Query("category"), GetAllFeaturedProducts, GetFeaturedProductsByCategory)

然后根据来自 c.Query("category") 的类别值,SQLCResult 将自动调用 GetAllFeaturedProducts 或 GetFeaturedProductsByCategory。 类似于函数作为回调的东西,但函数签名不同,这是一个问题。

func (q *Queries) GetAllFeaturedProducts(ctx context.Context) ([]GetAllFeaturedProductsRow, error)
func (q *Queries) GetFeaturedProductsByCategory(ctx context.Context, idCategory int16)

映射函数不是必需的,因为在 SQLCResult 中,我们可以这样做:

MapDBStructToRestAPIStruct(&Product{}, &row, MapFields(&row))

这将创建一个字段名称和索引的映射,并传递 dbresult 行,它会使用反射将其转换为 Product 结构并返回相同的内容,即在修改其字段后返回第一个参数作为结果。

我仍在寻找如何编写 SQLCResult 函数,将 SQLC 函数名称作为输入然后返回结果,或者可以通过将 Product{} 结构本身放在 SQLCResult 函数中使其更通用,例如:

var result := SQLCResult(&Product{}, c.Query("category") == 0, GetAllFeaturedProducts, GetFeaturedProductsByCategory)
return c.Status(fiber.StatusOK).JSON(result)

SQLCResult 将根据布尔条件调用 GetAllFeaturedProducts 或 GetFeaturedProductsByCategory 并创建将函数结果映射到作为第一个参数传递的结构,并返回该结构。

或者可能是这样的最终目标:

func (h *Handlers) GetLatestProducts(c *fiber.Ctx) error {
  if c.Query("category") == 0
    return c.JSON(SQLCResult(&Product{}, GetAllLatestProducts)
  else
    return c.JSON(SQLCResult(&Product{}, GetLatestProductsByCategory, c.Query("category"))
}

func (h *Handlers) GetFeaturedProducts(c *fiber.Ctx) error {
  if c.Query("category") == 0
    return c.JSON(SQLCResult(&Product{}, GetAllFeaturedProducts)
  else
    return c.JSON(SQLCResult(&Product{}, GetFeaturedProductsByCategory, c.Query("category"))
}

【问题讨论】:

  • 可读性强,书写方式清晰。为什么要改呢?
  • 嗨 Burak,有两件事,首先是代码重复,if/else 中还有很多其他代码适用于结果。其次,如前所述,有很多类似的功能,例如特色产品,新产品,我想最终将所有功能合并为通用的。
  • 嗨松饼,结果已经是接口{},这是 JSON 生成的。忽略 dbrow.ConvertToAllLatestProducts 中的部分。关于 GetAllLatestProductsRow 使变量类型动态化的部分是问题。
  • 结果是 []interface{}。 convert 函数只是将一个结构映射到另一个结构。像 struct a { b: int } 到 struct c { b: int }。数据库结果在 SQLC 定义的结构中,我只是将它们映射到通用结构。
  • 代码重复不是坏事。它使阅读变得更容易。如果你想抽象出代码的结构,你可以使用闭包来处理依赖类型的部分。它会更容易编写,但不一定更容易阅读。

标签: go refactoring go-fiber sqlc


【解决方案1】:

有很多需要考虑,代码确实没有问题,但它可能难以维护,如果有更多与预期相似的场景,它可能会更难,它的扩展性很差,从长远来看,可能会出现更多规则让它很容易成为意大利面。

我们想要的是关注点分离、可重复使用的相似部分和清晰性。我们可以在没有太多复杂性的情况下拥有它。

鉴于我们无法更改存储库 API(这可能是一种直截了当的方法),我们必须包装存储库,更像是装饰器或 Go 术语中的 Shadowing。

// ProductHandler shadows product repository
type ProductHandler struct {
    *Repo
}

这让我们可以更好地封装每个调用的兴趣

func (ph ProductHandler) GetLatestProductsByCategory(ctx context.Context, cat int) ([]interface{}, error) {
    if cat == 0 {
        return nil, nil
    }

    l, err := ph.Repo.GetLatestProductsByCategory(ctx, cat)

    return ResultSet(&ProductsByCategory{}, l), err
}

func (ph ProductHandler) GetAllLatestProducts(ctx context.Context) ([]interface{}, error) {
    l, err := ph.Repo.GetAllLatestProducts(ctx)

    return ResultSet(&Products{}, l), err
}

这样,我们通过自己的方法委派检索或不检索类别的责任,并自动将结果集包装到自己的类型中,从而相应地分离映射责任。

type Products struct {
    Id   string
    Name string
}

type ProductsByCategory struct {
    Category string
}

为了实现 db result-set 到特定类型的转换,我们必须公开一个公共接口,因此任何实现该接口的类型都可以转换(translate、map、hydrate 是同义词)本身

type Transformer interface {
    Transform(interface{}) interface{}
}

现在每种类型都可以有自己的从 -> 到

的转换
func (p Products) Transform(i interface{}) interface{} {
    v, _ := i.(*dbresult)

    p.Name = v.RawName
    p.Id = v.RawId

    return p
}

func (p ProductsByCategory) Transform(i interface{}) interface{} {
    v, _ := i.(*dbresult)

    p.Category = v.CategoryName

    return p
}

具有帮助我们转换数据列表的功能,我们可以随时重复使用

func ResultSet(t Transformer, d []interface{}) []interface{} {
    result := make([]interface{}, len(d))
    for i, p := range d {
        result[i] = t.Transform(p)
    }

    return result
}

现在我们的实现可以简单地看起来像这样,所有这些部分都可以重复使用

func main() {
    var category int
    // h.Repo
    repo := Repo{}
    ph := ProductHandler{&repo}

    pcat, _ := ph.GetLatestProductsByCategory(context.Background(), category)
    products, _ := ph.GetAllLatestProducts(context.Background())
    products = append(pcat, products...)

    for _, product := range products {
        fmt.Printf("%v\n", product)
    }
}

虽然代码使用了interface{},但这并没有什么不好的地方,最终你的数据已经像这样来自数据库了,我们只是将它们传递过去。 type 断言它们可能会在做得不好时代价高昂,这里没有这种情况,直到调用 json marshal。

你可以找到working copy here 为了支持这些案例,有一个可能的数据库响应调用的模拟。 尝试为category 赋值,看看会发生什么

【讨论】:

  • 由于字数限制,我无法在评论中输入回复,所以将其添加到问题本身。
  • 好吧,我建议的第一个想法是责任链模式,但你根本不喜欢它,因为即使是给定的也不喜欢你,从你的评论中我很清楚您最初的不适实际上反映了您正在使用 sqlc 的工具。你会找到最好的方法,关注点分离和封装显然不会加入那一方,这很好,没有什么问题,就像一开始一样:] @PriyankBolia
猜你喜欢
  • 1970-01-01
  • 2022-01-24
  • 2022-06-14
  • 2022-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-22
  • 1970-01-01
相关资源
最近更新 更多