まえおき
SPAを作っていると、入力フォームを持つコンポーネントはわりかし頻繁に作る機会がありますよね
フロントエンドのバリデーションというのは、大抵の場合それっぽいnpmを使ったりして済ませてしまいがちな部分がありますが、実際にはアプリケーション・アーキテクチャにおいてバリデーションというのは必ずしも軽視できないものであることが多いです。特に大規模なアプリケーションのフォームであれば、バリデーションのロジックがビューに絡みついてビューレイヤが肥大化していくことがよくあります。
ということで、今回はVue.jsのコンポーネント実装を例にとって、どのようにしてバリデーション・ロジックをコンポーネントに粗結合な形で実装すればよいかを、validatable-recordというOSSの利用例とともに考えてみます1。
実装例
実際に動くコードはこちらのURLから
https://www.webpackbin.com/bins/-L-kLd1mknBzNr67zNxW
HTML
HTMLは、このような構造のフォームを作ります。
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<style>
[v-cloak] { display: none; }
.field { margin: 10px 10px; }
.error { color: red; }
</style>
</head>
<body>
<div id="app" v-cloak>
<form @submit.prevent="submit">
<div class="field error">
<p v-if="errors">{{ errors[0] }}</p>
</div>
<div class="field">
<input type="text" v-model="form.name" placeholder="Your name" />
</div>
<div class="field">
<input type="number" v-model="form.age" placeholder="Your age" />
</div>
<div class="field">
<button type="submit">Submit</button>
</div>
</form>
</div>
<script src="main.js"></script>
</body>
</html>
フォームは@submit.prevent
とすることで、エンターキーでの送信処理をブロックし、Vueコンポーネントへ移譲します。
JavaScript
JavaScript側ではvalidatable-recordを使ったVueコンポーネントの実装を行っていきます。
validatable-recordとは
今回使うvalidatable-recordは簡単に言うと、immutable.jsのRecordにvalidate.jsを足したライブラリで、イミュータブルオブジェクトへの属性の定義とバリデーションルールの設定を行えるものです。
// ベースとなる属性の構造とバリデーションルールを定義
const ManRecord = ValidatableRecord({
name: null,
age: null
}, {
name: {
presence: true
},
age: {
presence: {
message: "is invalid"
}
}
});
// 上で定義したRecordを継承するクラスを定義
class Man extends ManRecord {
...
}
const man = new Man({
name: "Justine";
age: 25
});
man.validate() // == true
const agelessMan = new Man({
name: "Michael"
});
agelessMan.validate() // == false
agelessMan.getErrors() // == [ "Age is invalid" ]
実際のVueコンポーネントでの使用例
まずは、ValidatableRecord
の定義を継承したクラスを作ります。
ここでname
やage
などのフォームの属性の定義や、そのバリデーション・ルール、そしてそれらのデータを使った振る舞い(submit
などのようなインスタンスメソッド)を定義します。
class Login extends ValidatableRecord({
name: null,
age: null
}, {
name: {
presence: true,
length: {
maximum: 20
}
},
age: {
presence: true,
numericality: {
greaterThanOrEqualTo: 0
}
}
}) {
submit() {
//
// Ex: インスタンスのデータを使ってWebAPIをコールしたりする。
//
}
}
コンポーネント自体にはバリデーションのルールなどを具体的に定義せず、単にLogin
クラスを使った実装のみにすることで、バリデーションのロジックをコンポーネント(ビューレイヤ)から粗結合にすることができます。
new Vue({
el: '#app',
data: {
errors: null,
name: null,
age: null
},
methods: {
submit() {
// フォームのデータをもとにフォームモデルのインスタンスを作る
const login = new Login({
name: this.name,
age: this.age
});
// バリデーションを行い、エラーがあればerrorsにセットし、submitせずreturnする
if (!login.validate()) {
this.errors = login.getErrors();
return
}
// エラーがなければsubmitを実行する
login.submit();
}
}
})
ちなみに、動かしてみるとこのようなかたちになります。
ご覧のように、AgeとNameをそれぞれバリデーションし、どちらかが空白の場合にはフォームの上に、対応するエラーの文言が出ていることがわかりますね。
また、この実装の最も重要な点はビューレイヤ(VueコンポーネントやHTML)の中にバリデーションロジックの記述がないというところです。コンポーネントの実装はバリデーションロジックの実装によってFatにならず、またテストの際もバリデーション・ロジックの変更とビューの変更は別のものになり、Login
クラスが単体でテストをすれば良いことになります。
この実装のなにがいいの?
「なんか面倒くさそうだし、わざわざ
validatable-record
なんて使わなくても、コンポーネントの中でfilter
を使って入力のタイミングでバリデーションしたり、computed
を使った実装のほうがいいんじゃない?」
ふむ、たしかにこんな声も聞こえてきそうですね
ビューとバリデーションの関係を考える
今回の実装の最も重要な目的は、validatable-recordを使うことではなく、ビューレイヤから、アプリケーションに固有のルール(ドメイン・ロジック)としてのバリデーションを切り離すことで、変化に強いアプリケーションへとカイゼンする、というものです。
例えば、今回触れたVue.jsは、フレームワークの性質上Vueコンポーネントがデフォルトでfilter
やcomputed
などの様々なビューに関連する機能を提供しており、便利な反面、スケーラビリティを意識せず実装を進めてしまうと、どうしてもビューレイヤとしてのVueコンポーネントが膨らんでしまいがち2になります。
とりわけ、SPAなどのフロントエンド・アプリケーションは、ビューレイヤというデザイン要件の変更などの影響を強く受けやすい部分と近いため、極端に言うとビューはデータを表示するという責務のみを持ち、できるだけ複雑なif
分岐がビューレイヤには現れないように意識をすることが大事です
ドメインとバリデーション
Ex) 「選択できる商品は3つまで」
Ex) 「総額3000円になるように選択必須」
たとえば、ECサイトを作っていて現れる上のような条件は、確実にドメイン・ロジックの一部です。なぜなら、これらはそのアプリケーションに固有の条件だからです。そのバリデーションが、あなたのアプリケーションによって行われるビジネスによって生まれるものかどうかを考えると、自ずとそのバリデーションがドメインに属するものかどうか? という点が見えてきます
バリデーションがドメイン・ロジックの一部に属するものであるということが分かれば、そのバリデーション・ルールがHTMLやVueコンポーネントの中に記述されていてはいるべきではない、ということも分かります。なぜなら、ビューレイヤの変更によって意図しない影響を受ける可能性が生まれるからです。アプリケーションの中核としての「ドメイン」を関係のないレイヤから切り離して守ることで、保守・運用のしやすい変化に強いアプリケーションにすることができます
実装と設計とのトレードオフ
とはいえ、このような実装をするかしないか、という点は完全にアプリケーションを開発するチームや組織に依存します。フレームワークによっても、その実装パターンは多種多様で、必ずしもバリデーションがビューにおかれるべき責務ではないと必ずしも言いきれはしません。それでも、今回のvalidatable-recordを用いたVueコンポーネントのカイゼンパターンが何らかの手助けになれば幸いです。
-
元ネタはこのスライドです https://speakerdeck.com/izumisy/hurontoendobaridesiyon ↩
-
このような観点では、Reactのようなビューのコンポーネント化のみに特化している「薄い」ビューレイヤのフレームワークには利点があります。 ↩