LoginSignup
4

More than 5 years have passed since last update.

goでClean Architectureをやるときのvalidation

Posted at

validatonをどう書くか

handler
  ↓
usecase (usecase, input)
  ↓
domain (model, repository)
  ↑
infra

みたいな構造のWebアプリケーションを構築する場合、validationをどう書くかという話です。考慮する点としては、

  1. validation ロジック(定義)をどこに書くか
  2. validation をどこで実行するか

の2点を考えていく必要がある。
1. については

  • usecase の中でも入力された値を使うので、そこでもvalidationをすると二度手間になる。
  • usecase より下では理想的で「綺麗な」値だけを考えたい
  • usecase 層のinputを構造体として定義している

あたりを考慮すると、usecase層のinput定義にロジックを記述するのがいいように思う。

2.については、「usecaseにvalidationしていない値を持ち込みたくない」という考えを推し進めるとhandlerで個別にやりたくなってくる。
ただusecaseの先頭でやるとエラーをまとめてcatchできるメリットがあって、コード的には収まりがいい。

サンプルコード

上記の点を考慮したサンプルコードを以下のまとめる。サンプルコードではusecase層でvalidationを実行しつつ、handlerでやる場合もコメントアウトして併記している。
(色々端折ってます)

handler

func GetPagingList(c *echo.Context) error {
    page, err := strconv.Atoi(ctx.QueryParam("page"))
    if err != nil {
        page = 1
    }
    perPage, err := strconv.Atoi(ctx.QueryParam("per_page"))
    if err != nil {
        perPage = 20
    }

    in := input.ListGet{
        UserID:  ctx.currentUserID,
        Page:    page,
        PerPage: perPage,
    }

    // handlerでvalidationするならここで
    //if err := in.Validate(); err != nil {
    //  return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    //}

    list, err := usecase.GetList(in)
    if err != nil {
        switch err.(type) {
        case *errors.InvalidParameter:
            return echo.NewHTTPError(http.StatusBadRequest, err.Error())
        case *errors.NotFoundError:
            return echo.NewHTTPError(http.StatusNotFound, "not found")
        default:
            return echo.NewHTTPError(http.StatusInternalServerError, "internal server error")
        }
    }
}

usecase.input

type ListGet struct {
    Page    int
    PerPage int
    UserID  int64
}

func (c *ListGet) Validate() error {
    if c.Page < 1 {
        return errors.NewInvalidParameter("page parameter must be upper 0")
    }
    if c.PerPage < 1 {
        return errors.NewInvalidParameter("per_page parameter must be upper 0")
    }
    if c.PerPage > 100 {
        return errors.NewInvalidParameter("per_page parameter must be under or equal 100")
    }

    return nil
}

usecase

func GetList(in input.ListGet) (list *model.List, err error) {
    if err = in.Validate(); err != nil {
        return
    }

    // ~~ list 取得処理 ~~
}

最後に

サンプルにあるc.PerPage > 100みたいなのって完全にドメインの知識だよね?というのはあって、ある程度仕方ないとはいえドメイン知識がusecaseに染み出してる感は否めません。
その辺りは「これはAPIの定義の話だ」と割り切ってしまえばそんなに違和感はないかもしれません。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4