13.3 Designing controllers

Most traditional MVC frameworks are based on suffix action mapping. Nowadays, the REST style web architecture is becoming increasingly popular. One can implement REST-style URLs by filtering or rewriting them, but why not just design a new REST-style MVC framework instead? This section is based on this idea, and focuses on designing and implementing a controller based, REST-style MVC framework from scratch. Our goal is to simplify the development of web applications, perhaps even allowing us to write a single line of code capable of serving “Hello, world”.

The controller’s role

The MVC design pattern is currently the most used framework model for web applications. By keeping Models, Views and Controllers separated, we can keep our web applications modular, maintainable, testable and extensible. A model encapsulates data and any of the business logic that governs that data, such as accessibility rules, persistence, validation, etc. Views serve as the data’s representation and in the case of web applications, they usually live as templates which are then rendered into HTML and served. Controllers serve as the “glue” logic between Models and Views and typically have methods for handling different URLs. As described in the previous section, when a URL request is forwarded to a controller by the router, the controller delegates commands to the Model to perform some action, then notifies the View of any changes. In certain cases, there is no need for models to perform any kind of logical or data processing, or for any views to be rendered. For instance, in the case of an HTTP 302 redirect, no view needs to be rendered and no processing needs to be performed by the Model, however the Controller’s job is still essential.

RESTful design in Beego

The previous section describes registering route handlers with RESTful structs. Now, we need to design the base class for a logic controller that will be composed of two parts: a struct and interface type.

  1. type Controller struct {
  2. Ct *Context
  3. Tpl *template.Template
  4. Data map[interface{}]interface{}
  5. ChildName string
  6. TplNames string
  7. Layout []string
  8. TplExt string
  9. }
  10. type ControllerInterface interface {
  11. Init(ct *Context, cn string) //Initialize the context and subclass name
  12. Prepare() //some processing before execution begins
  13. Get() //method = GET processing
  14. Post() //method = POST processing
  15. Delete() //method = DELETE processing
  16. Put() //method = PUT handling
  17. Head() //method = HEAD processing
  18. Patch() //method = PATCH treatment
  19. Options() //method = OPTIONS processing
  20. Finish() //executed after completion of treatment
  21. Render() error //method executed after the corresponding method to render the page
  22. }

Then add the route handling function described earlier in this chapter. When a route is defined to be a ControllerInterface type, so long as we can implement this interface, we can have access to the following methods of our base class controller.

  1. func (c *Controller) Init(ct *Context, cn string) {
  2. c.Data = make(map[interface{}]interface{})
  3. c.Layout = make([]string, 0)
  4. c.TplNames = ""
  5. c.ChildName = cn
  6. c.Ct = ct
  7. c.TplExt = "tpl"
  8. }
  9. func (c *Controller) Prepare() {
  10. }
  11. func (c *Controller) Finish() {
  12. }
  13. func (c *Controller) Get() {
  14. http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
  15. }
  16. func (c *Controller) Post() {
  17. http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
  18. }
  19. func (c *Controller) Delete() {
  20. http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
  21. }
  22. func (c *Controller) Put() {
  23. http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
  24. }
  25. func (c *Controller) Head() {
  26. http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
  27. }
  28. func (c *Controller) Patch() {
  29. http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
  30. }
  31. func (c *Controller) Options() {
  32. http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
  33. }
  34. func (c *Controller) Render() error {
  35. if len(c.Layout) > 0 {
  36. var filenames []string
  37. for _, file := range c.Layout {
  38. filenames = append(filenames, path.Join(ViewsPath, file))
  39. }
  40. t, err := template.ParseFiles(filenames...)
  41. if err != nil {
  42. Trace("template ParseFiles err:", err)
  43. }
  44. err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data)
  45. if err != nil {
  46. Trace("template Execute err:", err)
  47. }
  48. } else {
  49. if c.TplNames == "" {
  50. c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt
  51. }
  52. t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames))
  53. if err != nil {
  54. Trace("template ParseFiles err:", err)
  55. }
  56. err = t.Execute(c.Ct.ResponseWriter, c.Data)
  57. if err != nil {
  58. Trace("template Execute err:", err)
  59. }
  60. }
  61. return nil
  62. }
  63. func (c *Controller) Redirect(url string, code int) {
  64. c.Ct.Redirect(code, url)
  65. }

Above, the controller base class already implements the functions defined in the interface. Through our routing rules, the request will be routed to the appropriate controller which will in turn execute the following methods:

  1. Init() initialization routine
  2. Prepare() pre-initialization routine; each inheriting subclass may implement this function
  3. method() depending on the request method, perform different functions: GET, POST, PUT, HEAD, etc. Subclasses should implement these functions; if not implemented, then the default is 403
  4. Render() optional method. Determine whether or not to execute according to the global variable "AutoRender"
  5. Finish() is executed after the action been completed. Each inheriting subclass may implement this function

Application guide

Above, we’ve just finished discussing Beego’s implementation of the base controller class. We can now use this information to design our request handling, inheriting from the base class and implementing the necessary methods in our own controller.

  1. package controllers
  2. import (
  3. "github.com/astaxie/beego"
  4. )
  5. type MainController struct {
  6. beego.Controller
  7. }
  8. func (this *MainController) Get() {
  9. this.Data["Username"] = "astaxie"
  10. this.Data["Email"] = "astaxie@gmail.com"
  11. this.TplNames = "index.tpl"
  12. }

In the code above, we’ve implemented a subclass of Controller called MainController which only implements the Get() method. If a user tries to access the resource using any of the other HTTP methods (POST, HEAD, etc), a 403 Forbidden will be returned. However, if a user submits a GET request to the resource and we have the AutoRender variable set to true, the resource’s controller will automatically call its Render() function, rendering the corresponding template and responding with the following:

13.3. Design controllers - 图1

The index.tpl code can be seen below; as you can see, parsing model data into a template is quite simple:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>beego welcome template</title>
  5. </head>
  6. <body>
  7. <h1>Hello, world!{{.Username}},{{.Email}}</h1>
  8. </body>
  9. </html>