dongpa5207 2018-05-29 16:20
浏览 43
已采纳

处理模型特定操作与数据库操作

I am having some issues when it comes to model design, specifically handling Model specific actions vs Database actions. A good example would be my User model.

When creating a user in my DB, I want to:

  1. Verify the password meets criteria (Model action)
  2. Create the digest (Model action)
  3. Set timestamps (model action)
  4. Save the Email, Digest, and Timestamps to the DB (DB Action)

When testing, I obviously want to have a set of unit test for all 4, however #4 has calls to the other 3, something I don’t want to retest, or risk having #4 test fail if any of those 3 do.

I have come up with creating a separate Interface for ModelActions vs StoreActions, and sending the UserAction interface to a store action when needed, however as I write it out, I am already sensing some serious code smell.

type User struct {
        ID                   int    `json:"id"`
        Email                string `json:"email"`
        Password             string `json:"password"`
        ConfirmationPassword string `json:"confirmationPassword"`
        passwordDigest       string `json:"-"`
        CreatedAt            time.Time `json:"createdAt,omitempty"`
        ModifiedAt           time.Time `json:"modifiedAt,omitempty"`
}

//UserStore is the interface for all User functions that interact with the database
type UserStore interface {
        GetUserByEmailAndPassword(email, password string) (User, error)
        UpdatePassword(u UserAction, previousPassword, password, confirmationPassword string) error
        UserExists(email string) (bool, error)
        CreateUser(u UserAction) error
}

// I am going against design Principles by having GetID, GetEmail, since JSON unmarshalling needs the struct fields to be capitalized, which is already a warning sign for me

type UserAction interface {
        GetID() int
        GetEmail() string
        Timestamps() (time.Time, time.Time)
        SetID(id int)
        SetTimestamps()
        SetPassword(password, confirmation string)
        SetDigest(digest string)
        CreateDigest() (string, error)
        VerifyPassword() error
        ComparePassword(password string) error
}

// Example of UserActions
func (u *User) CreateDigest() (string, error) {
        var digest string
        if err := u.VerifyPassword(); err != nil {
                return digest, err
        }

        passwordByte, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
        if err != nil {
                return digest, err
        }

        digest = string(passwordByte)
        return digest, nil
}

func (u *User) VerifyPassword() error {
        if len(u.Password) < 6 {
                return &modelError{"Password", "must be at least 6 characters long"}
        }

        if u.Password != u.ConfirmationPassword {
                return &modelError{"ConfirmationPassword", "does not match Password"}
        }

        return nil
}

// Example of DB Action
func (db *DB) CreateUser(ua UserAction) error {
        if exists, err := db.UserExists(ua.GetEmail()); err != nil {
                return err
        } else if exists {
                return &modelError{"Email", "already exists in the system"}
        }

        // set password
        digest, err := ua.CreateDigest()
        if  err != nil {
                return err
        }

        ua.SetDigest(digest)
        ua.SetTimestamps()

        createdAt, modifiedAt := ua.Timestamps()

        rows, err := db.Query(`
                INSERT INTO users (email, password_digest, created_at, modified_at)
                        VALUES ($1, $2, $3, $4)
                        RETURNING id
                `, ua.GetEmail(), digest, createdAt, modifiedAt)

        if err != nil {
                return err
        }

        defer rows.Close()

        var id int
        for rows.Next() {
                if err := rows.Scan(&id); err != nil {
                        return err
                }
        }

        ua.SetID(id)

        return nil
}

Is there a better way to model these separate actions so the UserActions can be mocked when testing the DB/Store functions? I tried storing the User struct as part of the interface, something such as:

type UserAction {
    SetTimestamps()
    CreateDigest() (string, error)
    VerifyPassword() error
    ComparePassword(password string) error
    User() *User
}

This however causes cyclical imports when creating mocks, and also opens up all fields, which are already arguably available since the model's fields are exportable

  • 写回答

1条回答 默认 最新

  • dpevsxjn809817 2018-05-31 00:55
    关注

    I think your user should be a concrete type, and you should use an interface to mock your store.

    For instance, a project structure like this makes sense to me:

    cmd/
       server/
          user.go
          user_test.go
          main.go
          store.go
    mysql/
       mysql.go
       user.go
       user_test.go
    user.go
    user_test.go
    

    Your user model is at the root, in user.go. This file will contain your User struct, and the functions that will operate on it, like CreateDigest. These functions should be tested in user_test.go.

    It is worth mentioning that at your root, your package should not be main, your package name should be the name of your project, we will call it myapp.

    Your mysql, postgres, etc. should also be a concrete implementation. You may have a function in that package like:

    func (m *MySQL) InsertUser(u *myapp.User) error
    

    This function should be tested in mysql/user_test.go.

    And finally, we can put it all together in server. This is the binary you actually deploy or run.

    In cmd/server/store.go, you should create an interface that will be implemented by mysql.

    In cmd/server/user_test.go it is very easy to mock this so that you do not have to hit the real database. I am a believer in that your interfaces should live in your client. In this case, server is a client of mysql.

    In cmd/server/user.go you may have functions that look like this:

    func CreateUser(w http.ResponseWriter, r *http.Request) {
      var u myapp.user
      err := json.NewDecoder(r.Body).Decode(&u)
      if err != nil {
        panic(err) // don't do this for real
      }
    
      d := myapp.CreateDigest(u.Password)
      u.Digest = d
    
      // s is the interface, defined in `cmd/server/store.go`, but is implemented by mysql
      err = s.InsertUser(&u)
      if err != nil {
        panic(err)
      }
    
      // Since we pass a pointer, you can have your store set the ID of the user
      fmt.Println(u.ID)
    }
    

    Now that you have a better separation of concerns, everything should be easy to test, and making changes to existing code is easy.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 python中合并修改日期相同的CSV文件并按照修改日期的名字命名文件
  • ¥15 有赏,i卡绘世画不出
  • ¥15 如何用stata画出文献中常见的安慰剂检验图
  • ¥15 c语言链表结构体数据插入
  • ¥40 使用MATLAB解答线性代数问题
  • ¥15 COCOS的问题COCOS的问题
  • ¥15 FPGA-SRIO初始化失败
  • ¥15 MapReduce实现倒排索引失败
  • ¥15 ZABBIX6.0L连接数据库报错,如何解决?(操作系统-centos)
  • ¥15 找一位技术过硬的游戏pj程序员