ドメイン駆動設計 with Vue/Nuxt(Composition API)でリアルタイム・バリデーション


# 追記
2020/05/31　別記事として[発展編](https://qiita.com/JDM/items/4b7cf703ceae36d4c445)書きました

# 問題意識
ドメイン駆動設計に従うVue/Nuxtアプリケーションを作っていて、
名前フォームに文字列を打ち込んで8文字より多く書かれたとき、
そのフォームの一つ上に赤い文字で「名前は8文字以内です」というエラーをリアルタイムで表示してほしい、
……という要件があるとしましょう。

このときVue側のコードで、インプットフォームから受け取った変数に8文字という制限をすることも可能なのですが、
「それって **利口なUI** という奴なのでは？」と考えていました。
一般に利口なUIはアンチパターンとして知られていて、(議論はありますが)たしかに、ドメイン知識がUI層に流出しています。
どのみちエンジニア的にも同じ内容を２度書くような気がして、利口なUIは利口じゃないコードになりかねません。

そういうわけで自分なりにその解決をしてみたいと思います。
# 完成品
[devinoue/realtime-validation-ddd](https://github.com/devinoue/realtime-validation-ddd)

# 環境
Nuxt2.12.2
Composition API
TypeScript

# ドメインを書く
今回はNameクラスをTypeScriptで書きますが、必要そうな所だけです。
ひとまずdomainというディレクトリに以下のような値オブジェクトとしてName.tsを入れておきます。

```typescript:domain/Name.ts
export default class Name {
  constructor(private _name: string) {
    Name.validation(this._name)
  }

  static validation(name: string): never | void {
    if (name === '') {
      throw new Error('名前を入力してください')
    }
    if (typeof name !== 'string') {
      throw new TypeError('名前は文字列にしてください')
    }
    if (name.length > 8) {
      throw new Error('名前は8文字以内にしてください')
    }
  }
}

```

ついでに、Eメールアドレス用値オブジェクトも作っておきます。
コード的にはほぼ同じになってしまうので、[こちら](https://github.com/devinoue/realtime-validation-ddd/blob/master/domain/EmailAddress.ts)から御覧ください。

# ハンドラを書く
Composition APIで作るので、ハンドラも切り分けておきます。
別にそういうvue界の慣習があるわけではありませんし、切り分けなくてもいいのですが、
この方が見晴らしがいいという理由で分けています。
ちなみにDDDでいうプレゼンテーション層に属するものとして扱っています。
ここからドメイン知識にアクセスしていますが、生成メソッドではないため許容されるという認識です。

```typescript:handler/InputHandler.ts
import { ref, watch } from '@vue/composition-api'
import Name from '~/domain/Name'
import EmailAddress from '~/domain/EmailAddress'

interface IForm {
  name: string
  email: string
}

export default function() {
  const defaultInput: IForm = { name: '', email: '' }

  const forms = reactive({ ...defaultInput })
  const errors = reactive({ ...defaultInput })

  watch(
    () => forms.name,
    () => {
      errors.name = ''
      try {
        Name.validation(forms.name)
      } catch (e) {
        errors.name = e.message
      }
    },
    { lazy: true }
  )
  watch(
    () => forms.email,
    () => {
      errors.email = ''
      try {
        EmailAddress.validation(forms.email)
      } catch (e) {
        errors.email = e.message
      }
    },
    { lazy: true }
  )

  return { forms, errors }
}


```

ここ、無駄が多い気がしますが、watch関数を使っている手前まとめにくい、、、、

# vueファイルを完成させる
さて、残りはpages以下のindex.vueで、さきほど作ったファイルを読み込むだけです。


```vue:pages/index.vue
<template>
  <div class="container">
    <span v-show="errors.name" class="error">{{ errors.name }}</span>
    <span>名前 : <input v-model="name" type="text"/></span>
    <br />
    <span v-show="errors.email" class="error">{{ errors.email }}</span>
    <span>メールアドレス : <input v-model="email" type="text"/></span>
    <br />
  </div>
</template>

<script lang="ts">
import useInputHandler from '~/handler/InputHandler'
export default {
  setup() {
    return { ...useInputHandler() }
  }
}
</script>
// スタイル省略
```

御覧ください！！　ほとんど空っぽ！　なんとスッキリしているのでしょう！！！😭
今までのVueファイルがウソのようにキレイにまとめられました！


# 動作イメージ
何か書き込むたびにwatchメソッドが変数を監視して、エラーを報告してくれます。
これで「利口なUI」とならずに済みます😄
![コメント 2020-05-16 231755.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294146/ec4f6427-501f-8631-920c-53ee16def16f.png)

# 終わりに
 **Vue3素晴らしい……！**

GitHub:
[devinoue/realtime-validation-ddd](https://github.com/devinoue/realtime-validation-ddd)