前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在Go中使用服务对象模式

在Go中使用服务对象模式

作者头像
李海彬
发布2018-07-26 09:43:50
4040
发布2018-07-26 09:43:50
举报

原文作者:Jon Calhoun

NOTE: Most of the code and ideas in this post are things I have been experimenting with. That doesn't mean the ideas and lessons aren't valuable, but it does mean that you shouldn't just blindly follow this pattern. It has its own set of pros and cons that should be considered on a case-by-case basis. That said, the pattern has been working very well for me and using a design to isolate parsing data from application logic is a critical step in building web applications that support multiple formats (HTML and JSON API), as we will explore in a future post.

We've all likely seen a web application in Go with a handler function that looks something like this:

 1type WidgetHandler struct {
 2    DB *sql.DB
 3    // Renders the HTML page w/ a form to create a widget
 4    CreateTemplate *template.Template
 5    // Renders a list of widgets in an HTML page
 6    ShowTemplate *template.Template
 7}
 8func (handler *WidgetHandler) Create(w http.ResponseWriter, r *http.Request) {
 9    // Most HTML based web apps will use cookies for sessions
10    cookie, err := r.Cookie("remember_token")
11    if err != nil {
12        http.Redirect(w, r, "/login", http.StatusFound)
13        return
14    }
15    // Hash the value since we store remember token hashes in our db
16    rememberHash := hash(cookie.Value)
17    // Then look up the user in the database by hashed their remember token
18    var user User
19    row := handler.DB.QueryRow(`SELECT id, email FROM users WHERE remember_hash=$1`, rememberHash)
20    err = row.Scan(&user.ID, &user.Email)
21    if err != nil {
22        http.Redirect(w, r, "/login", http.StatusFound)
23        return
24    }
25    // From here on we can assume we have a user and move on to processing
26    // the request
27    var widget Widget
28    widget.UserID = user.ID
29    err = r.ParseForm()
30    // TODO: handle the error
31    widget.Name = r.FormValue("name")
32    // postgres specific SQL
33    const insertWidgetSql = `
34INSERT INTO widgets (user_id, name) 
35VALUES ($1, $2) 
36RETURNING id`
37    err = handler.DB.QueryRow(insertWidgetSql, widget.UserID, widget.Name).Scan(&widget.ID)
38    if err != nil {
39        // Render the error to the user and the create page
40        w.Header().Set("Content-Type", "text/html")
41        handler.CreateTemplate.Execute(w, map[string]interface{}{
42            "Error":  "Failed to create the widget...",
43            "Widget": widget,
44        })
45        return
46    }
47  // Redirect the user to the widget
48  http.Redirect(w, r, fmt.Sprintf("/widgets/%d", widget.ID), http.StatusFound)
49}

The exact details may vary - for instance the application may use another database, it might create UserService and WidgetService interfaces instead of directly writing SQL, and you might use a framework/router like echo, but generally speaking the code will look roughly the same. The first few lines of a handler will be used to parse data, then we will go about doing whatever we really wanted to do, then finally we will render any results or errors.

Handlers are a data parsing ‍and rendering layer

If we look back at our original code, it is shocking just how much of that code is actually just parsing and rendering. The entire cookie retrieval section is used just to get a remember token or redirect the user if there is an error. Once we have the token we perform a database lookup, but again this is quickly followed with a error handling and rendering logic. Then comes parsing the form, getting the widget name, and rendering any errors that occur while creating the widget. Finally, we are able to redirect the user to the new widget if it is created, but if you think about it the redirect is basically just rendering logic as well.

All in all, about 60% of our code is just parsing data and rendering results/errors.

Parsing this data isn't intrinsically bad, but what I do find disturbing is the fact that before data is parsed, requirements are unclear. Think about it - if I handed you this function definition and asked you to test it, could you tell me what data it expected?

1func (handler *WidgetHandler) Create(whttp.ResponseWriter, r *http.Request)

You might be able to infer from the WidgetHandler type and the function name - Create - that this is used to create a widget, so we need some information describing a widget, but would you know what format that data should be in? Would you know that the user needs to be signed in via a cookie based session?

Even worse, we can't even infer which parts of the WidgetHandler need to be instantiated for this to work. If we scan the code we can clearly see that we use the DB field, and it looks like we render the CreateTemplate when there is an error so we need to set that, but we had to look through all of the code to see what all was used.

NOTE: In this example which fields we use are obvious, but imagine our WidgetHandler was used to create, update, publish, and perform many other actions on a widget. In that case our WidgetHandler type would have a lot more fields and we surely wouldn't need them all to be set to test just this handler.

Handler functions need to be vague; there really isn't a viable way to create an http server without having a vague definition of what an incoming HTTP request looks like and then writing some code to parse that incoming data. Even if we created reusable middleware and leveraged the to store the parsed data, we still need to write and test those middleware, and it doesn't solve the problem of having unclear data requirements for our handler functions. So how do we fix that problem?

The service object pattern

Rather than fighting the fact that we need to parse data in our handlers, I have found that what works better is to embrace it and make those handlers strictly data parsing rendering layers. That is, in my http handlers I try to avoid any logic that isn't related to parsing or rendering data and instead embrace a pattern very similar to the ‍service objects pattern in Ruby.

NOTE: In reality, I even try to pull as much data rendering out of the handlers as possible too. See for more ideas on how to do this.

The way the pattern works is pretty simple - rather than writing logic in my handlers to do things like create a widget, I instead pull that ocde out into a function that has clear data requirements and is easy to test. For instance, in the widget creation example I might create something like this:

 1func CreateWidget(db *sql.DB, userID int, name string) error {
 2  var widget Widget
 3  widget.Name = name
 4  widget.UserID = userID
 5    const insertWidgetSql = `
 6INSERT INTO widgets (user_id, name) 
 7VALUES ($1, $2) 
 8RETURNING id`
 9    err = db.QueryRow(insertWidgetSql, widget.UserID, widget.Name).Scan(&widget.ID)
10    if err != nil {
11    return err
12  }
13  return nil
14}

Now it is much clearer that in order to create a widget, we need to have a database connection, the ID of the user creating the widget, and the name of the widget.

NOTE: You don't have to create such specific requirements here. For instance, I'll often create functions like this that expect both a User and a Widget as its arguments instead of the more specific userID and name arguments. That choice is up to you to make.

A more interesting example

This particular example is pretty boring, so let's look at a more interesting example. Let's imagine we wanted to handle having a user sign up for our application, and when this happens we create the user in our database, send the user a welcome email, and add them to our mailing list tool. A traditional handler might look something like this:

 1func (handler *UserHandler) Signup(w http.ResponseWriter, r *http.Reqeust) {
 2  // 1. parse user data
 3  r.ParseForm()
 4  email = r.FormValue("email")
 5  password = r.FormValue("password")
 6  // 2. hash the pw and create the user, handling any errors
 7  hashedPw, err := handler.Hasher.Bcrypt(password)
 8  if err != nil {
 9    // ... handle this
10  }
11  var userID int
12  err := handler.DB.QueryRow("INSERT INTO users .... RETURNING id", email, hashedPw).Scan(&userID)
13  if err != nil {
14    handler.SignupForm.Execute(...)
15    return
16  }
17  // 3. Add the user to our mailing list
18  err = handler.MailingService.Subscribe(email)
19  if err != nil {
20    // handle the error somehow
21  }
22  // 4. Send them a welcome email
23  err = handler.Emailer.WelcomeUser(email)
24  if err != nil {
25    // handle the error
26  }
27  // 5. Finally redirect the user to their dashboard
28  http.Redirect(...)
29}

As you can see, we have a good bit of error handling, and in each of those if blocks we could easily need to render an error page, send the user back to the signup page, or anything else. We also end up using quite a few pieces of the handler - the MailingService, SignupForm, Emailer, and the Hasher - and none of these are obvious for testing purposes.

What makes this even worse is that testing each of these individual pieces is somewhat annoying. If we just wanted to verify that calling this endpoint created a user in the database we would still need to at least stub out all of those other pieces.

In cases like this, splitting our code into a few service objects that have clear requirements and can be independently tested is incredibly useful.

 1type UserCreator struct {
 2  DB *sql.DB
 3  Hasher
 4  Emailer
 5  MailingService
 6}
 7func (uc *UserCreator) Run(email, password string) (*User, error) {
 8  pwHash, err := uc.Hasher.BCrypt(password)
 9  if err != nil {
10    return nil, err
11  }
12  user := User{
13    Email: email,
14  }
15  row := uc.DB.QueryRow("INSERT INTO users .... RETURNING id", email, hashedPw)
16  err = row.Scan(&user.ID)
17  if err != nil {
18    return nil, err
19  }
20  err = uc.MailingService.Subscribe(email)
21  if err != nil {
22    // log the error
23  }
24  err = uc.Emailer.WelcomeUser(email)
25  if err != nil {
26    // log the error
27  }
28  return &user, nil
29}

Now we can easily test the code used to create a user; the dependencies are clear and we don't need to mess around with HTTP requests. It is just regular old Go code.

We also have the added benefit of simplifying our handler code. It no longer needs to mess around dealing with non-fatal errors that just need information logged, and we can instead focus on just parsing data.

 1type UserHandler struct {
 2  signup func(email, password string) (*User, error)
 3}
 4func (handler *UserHandler) Signup(w http.ResponseWriter, r *http.Reqeust) {
 5  // 1. parse user data
 6  r.ParseForm()
 7  email = r.FormValue("email")
 8  password = r.FormValue("password")
 9  user, err := handler.signup(email, password)
10  if err != nil {
11    // render an error
12  }
13  http.Redirect(...)
14}

To instantiate this code, we would write something like:

1uc := &UserCreator{...}
2uh := &UserHandler{signup: uc.Run}

And then we would be free to use the methods on uh as http.HandlerFuncs in our router.

More, but clearer code

This approach clearly requires more code. We now need to setup a UserCreator type and then set its Run function to the signup field in the UserHandler, but by doing this we have clearly separated the role of each function and made it much easier to to test our code. We no longer need to even have a database connection to test our handler, and could instead test it with code like this:

1uh := &UserHandler{
2  signup: func(email, password) (*User, error) {
3    return &User{
4      ID: 123,
5      Email: email,
6    }, nil
7  }
8}

Similarly, when testing our UserCreator we don't need to use the httptest package at all. Neato! ?

Finally, as we will see in a followup post (I'm working on it still - it's long), this also opens the door for writing applications that are mostly agnostic of their input/output formats. That is, we could take an existing web application and add JSON API support with fairly minimal effort.

Did you enjoy this article?‍ Join my mailing list!

If you enjoyed this article, please consider joining my mailing list.

I will send you roughly one email every week letting you know about new articles (like this one) or screencasts (ex) that I am working on or have published recently. No spam. No selling your emails. Nothing shady - I'll treat your inbox like it was my own.

As a special thank you for joining, I'll also send you both screencast and ebook samples from my course, Web Development with Go.

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档