Обработка конкретных действий модели против действий базы данных - PullRequest
0 голосов
/ 29 мая 2018

У меня возникли некоторые проблемы, когда речь заходит о дизайне модели, в частности, обработка Действия для конкретной модели против Действия с базой данных .Хорошим примером может служить моя модель User.

При создании пользователя в моей БД я хочу:

  1. Проверить, что пароль соответствует критериям (действие модели)
  2. Создание дайджеста (действие модели)
  3. Установка меток времени (действие модели)
  4. Сохранение электронной почты, дайджеста и меток времени в БД (действие БД)

При тестировании я, очевидно, хочу иметь набор модульных тестов для всех 4, однако у # 4 есть вызовы к остальным 3, что-то, что я не хочу повторно тестировать, или рискую потерпеть неудачу при тесте # 4, если какой-либо из этих 3 сделает.

Я придумал создать отдельный интерфейс для ModelActions против StoreActions и, при необходимости, отправить интерфейс UserAction в действие хранилища, однако, когда я его записываю, я уже чувствую какой-то серьезный запах кода.

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
}

Есть ли лучший способ смоделировать эти отдельные действия, чтобы пользовательские действия можно было смоделировать при тестировании функций DB / Store?Я попытался сохранить структуру User как часть интерфейса, например:

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

Это, однако, вызывает циклический импорт при создании макетов, а также открывает все поля, которые уже доступны, так какполя модели можно экспортировать

1 Ответ

0 голосов
/ 31 мая 2018

I думаю, ваш пользователь должен иметь конкретный тип, и вы должны использовать интерфейс для макета вашего магазина.

Например, такая структура проекта имеет для меня смысл:

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

Ваша пользовательская модель находится в корне, в user.go.Этот файл будет содержать вашу User структуру и функции, которые будут работать с ним, например CreateDigest.Эти функции должны быть протестированы в user_test.go.

Стоит отметить, что в вашем корне ваш пакет не должен быть main, вашим именем пакета должно быть имя вашего проекта, мы назовем его myapp.

Ваши mysql, postgres и т. Д. Также должны быть конкретной реализацией.У вас может быть функция в этом пакете, например:

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

Эта функция должна быть протестирована в mysql/user_test.go.

И, наконец, мы можем собрать все это вместе в server.Это двоичный файл, который вы на самом деле развертываете или запускаете.

В cmd/server/store.go вы должны создать интерфейс, который будет реализован с помощью mysql.

В cmd/server/user_test.go это очень легкосмоделируйте это так, чтобы вам не приходилось сталкиваться с реальной базой данных.Я верю в то, что ваши интерфейсы должны жить в вашем клиенте.В этом случае server является клиентом mysql.

В cmd/server/user.go у вас могут быть функции, которые выглядят следующим образом:

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

Теперь, когда у вас есть лучшее разделениеЧто касается проблем, все должно быть легко протестировано, а вносить изменения в существующий код легко.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...