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)
        }
    }

Both the if and else condition follows the same code process, just the functions and struct are different, how to merge them, so that the code is smaller. I mean like:

    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 ...

can't touch the h.Repo.GetAllLatestProducts or h.Repo.GetLatestProductsByCategory as those are external functions. Type safety is also important.

There can be multiple functions like featuredproducts, newproducts, I want to make a generic function to return the products as json based on the sql function selected dynamically.


You got the problem, dozens of functions with the same code structure is bad, at least to me it doesn't matter its readable or not, its just duplicate copy/paste thing without any sense, mostly copy/paste with only the function name/SQLC generated function name and structure name changes, rest the code flow is the same.

The SQLC generates automatic code based on the SQL query, now its a wastage of time to write repeated code to just transform the result to JSON and return it to the client. There could be dozens of SQL functions to return the latest products, featured products, products in a category, wishlist product, blah blah blah...

All the website understand is the Product structure, but the SQLC returns different structures, so there is no single dbResult kind of thing. Anyway the mapping is not a big thing, using reflection we can check fields with same name, and convert SQL.NullString to string, etc. in the mapping function.

The real issue is the if/else statements for me. You have moved the code in different functions, but for me in the situation it doesn't make sense. As the web handler will anyway have to check the request is valid or not, whether category is defined or not, then check whether category is 0 or not, and then call different functions and then get the result and return to the client. For a single function it may be looking better, but for real production, it will make thing worse, and instead of a single function and an if/else block, now you have 3 functions for each API.

What I am looking it to just map the SQLC result to the route handler. The code flow is always the same, only the function name and the structure name changes. How to make it dynamic so that in my http handler,I can simply write:

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

and then based on the value of the category from c.Query("category"), the SQLCResult will automatically call either GetAllFeaturedProducts or GetFeaturedProductsByCategory. something like function as a callback, but the function signature is different, that's one issue.

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

the mapping function is not required because in the SQLCResult, we can do something like:

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

that will create a map of the field name and the index, and pass the dbresult row and it will convert it to Product structure using reflection and return the same, i.e. return the first parameter as result after modifying its fields.

I am still looking for how to write a SQLCResult function, to take the SQLC function name as input and then return the result, or may be make it more generic by putting the Product{} atructure itself in the SQLCResult function like:

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

where the SQLCResult will call the GetAllFeaturedProducts or GetFeaturedProductsByCategory based on the boolean condition and create map the function result to the structure passed as 1st argument, and return back that structure.

or may be like this is the final goal:

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"))
}
Priyank Bolia
  • 14,077
  • 14
  • 61
  • 82
  • 1
    It is readable and clear the way it is written. Why do you want to change it? – Burak Serdar Nov 21 '20 at 17:45
  • Hi Burak, 2 things, first is code duplication, there are lot of other code in the if/else that works on result. secondly, as mentioned, there are lots of functions similar, like featuredproduct, newproduct, I want to finally merge all functions into something generic. – Priyank Bolia Nov 21 '20 at 17:48
  • Hi Muffin, result is already interface{}, which JSON generation. Ignore the part in dbrow.ConvertToAllLatestProducts. the part about GetAllLatestProductsRow making the variable type dynamic is the question. – Priyank Bolia Nov 21 '20 at 17:51
  • result is []interface{}. The convert function is nothing just map one struct to another. like struct a { b: int } to struct c { b: int }. The database results are in the SQLC defined structure, I am just mapping them to generic structures. – Priyank Bolia Nov 21 '20 at 18:05
  • Oh, got it. Ignore my previous comments. – Charlie Tumahai Nov 21 '20 at 18:07
  • The sqlc returns result in like postgres.GetAllLatestProductsRow, which I am just mapping to a generic product structure. the SQLC result contains SQLnullstring, etc. which is mapped to string for json generation, so all db results are mapped to a generic product structure. – Priyank Bolia Nov 21 '20 at 18:09
  • 2
    Code duplication is not a bad thing. It makes reading it easier. If you want to abstract out the structure of the code, you can use closures for type-dependent pieces. It will be easier to write, but not necessarily easier to read. – Burak Serdar Nov 21 '20 at 18:10
  • From a readability standpoint,the only thing I would change is that I would put the two branches in different functions. There's no reason to de-duplicate this code. Doing so will only serve to make the code harder to read. – Jonathan Hall Nov 21 '20 at 21:13

1 Answers1

1

there is a lot to consider, the code does not have issues indeed, but it may be hard to maintain, it may be even harder with more similar scenarios as intended, it scales badly and in a long term more rules may appear making it easily a spaghetti.

what we want is separation of concerns, reusable similar parts and clearness. we can have that without much complexity.

given we cannot change the repository API — this could be a straight forward approach — we have to wrap the repository, more like a Decorator or in Go terms, Shadowing.

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

this allow us to encapsulate better the interest of each call

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
}

with this we delegate the responsibility of retrieving or not categories by its own method and automatically wrap the result set into its own type, separating the mapping responsibility accordingly.

type Products struct {
    Id   string
    Name string
}

type ProductsByCategory struct {
    Category string
}

in order to achieve transformation of db result-set into a specific type, we have to expose a common interface, so any type which implements this interface is allowed to transform(translate, map, hydrate are synonymous) itself

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

now every type can have its own transformation from -> to

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
}

with a function to assist us in transforming list of data we can reuse whenever we want

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

    return result
}

by now our implementation can simply look like this, and all those parts can be reused

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)
    }
}

although the code uses interface{} there is nothing really bad with that, in the end your data is coming from the db already like that, and we are just passing them by. type asserting them could potentially be costly when done badly, none here is the case, until a call to json marshal.

you can find a working copy here there is a mock of the possible db response calls in order to support the cases. try to give a value to category and see what happens

wmsan
  • 137
  • 6
  • I am unable to type the reply in the comment, due to word limit, so added it to the question itself. – Priyank Bolia Nov 24 '20 at 05:32
  • 1
    alright, my first idea to suggest was the Chain of Responsibility Pattern, but you would not like it at all, as even the given one did not please you, by your comment its very clear for me that your initial discomfort is actually reflect of the tool you are using sqlc. you will find the best way to work with that, separation of concerns and encapsulation will clearly not join that party, and it is fine, there is nothing wrong with that, like in the beginning :] @PriyankBolia – wmsan Nov 24 '20 at 21:27