0
    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 因为它们是外部函数。类型安全也很重要。

可以有多个功能,如特色产品,新产品,我想制作一个通用函数,根据动态选择的 sql 函数将产品返回为 json。


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

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

所有网站都了解产品结构,但 SQLC 返回不同的结构,因此没有单一的 dbResult 类型的东西。无论如何映射不是什么大事,使用反射我们可以在映射函数中检查具有相同名称的字段,并将 SQL.NullString 转换为字符串等。

真正的问题是我的 if/else 语句。您已经在不同的函数中移动了代码,但对我来说,在这种情况下它没有意义。因为web handler无论如何都要检查请求是否有效,是否定义了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"))
}
4

1 回答 1

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{}并没有什么不好的地方,但最终您的数据已经像这样来自数据库,我们只是将它们传递过去。如果做得不好,类型断言它们可能会付出高昂的代价,在调用 json marshal 之前,情况并非如此。

您可以在此处找到工作副本, 其中模拟了可能的数据库响应调用以支持这些案例。尝试给一个值category,看看会发生什么

于 2020-11-22T18:13:15.180 回答