👋

宣言的UIの何がいいのか?

2023/04/25に公開

宣言的UIの何がいいのか?
var c = document.createElement('li'); p.appendChild(c); より <ul><li></li></ul> の方がいいよね、という簡単な話ではあります。UI(=HTML)を直接書き下せるのがいいですね。
ですがこの記事では、それ以外にもメリットがありますよ、という話をしようと思います。
UIプログラミングを宣言的に行うと他にもメリットがありますよ、ということです。

と、その前に用語のおさらいをしようと思います。

そもそも「宣言的」とは何なのでしょうか。
wikipediaで宣言型プログラミングを調べてみても何やら学術的に難しく書かれていて意味が分かりません。
なのでテキトーに嚙み砕いて言うと「宣言的プログラミングでは書いたものがそのまま手に入る」といった感じでしょうか。
たとえばjsxでは、HTML的な文法をjavascriptに付け足すことで「HTML的に書けばそれが手に入る」という感覚を実現させています。
このように、本来的なプログラミングより直観的なもの、jsxで言えばHTMLですが、そういったものを用いてプログラミングができるというのが宣言的プログラミングのエッセンスなわけです。

閑話休題。

「宣言的」の意味がなんとなく分かったところで、本題である そのメリット の話をしていこうと思います。
この記事ではそのために小さなプログラムを例として使います。最初はプログラムを本来的なプログラミング手法である命令的スタイルで書いて、次にそれを宣言的スタイルに書き直して、どんなメリットがあるかを見ていきます。
なお、この記事ではjsxみたいな特殊な文法は使いません。javascriptだけを使います。

命令的スタイルのコード

メールアドレスの入力ボックスを例にします。
入力された文字列が適切なメールアドレスでない場合にエラーメッセージを表示する、という仕様にします。
さっそくコードを見ていきましょう。

const input = document.getElementById('email-input')
const message = document.getElementById('email-message')
input.addEventListener('blur', (ev) => {
  if (isValidEmail(input.value)) {
    message.style.setProperty('display', 'none')
  } else {
    message.style.setProperty('display', 'block')
  }
})

簡単にコードを解説します。
input変数が入力ボックス、message変数がエラーメッセージです。
入力文字列が適切かどうかの判定は、blurイベント発生時(入力ボックスからフォーカスが外れたとき)に仕掛けることにしました。
isValidEmail関数が適切かどうかを判定してくれて、その結果に応じてif文で分岐します。
入力文字列が適切な場合はエラーメッセージを非表示にします。
入力文字列が不適切な場合はエラーメッセージを表示します。

宣言的スタイルのコード

上記のコードを今度は宣言的スタイルで書き直してみます。
変更はなるべく少なく留めます。
次のようになりました。

const input = document.getElementById('email-input')
const message = document.getElementById('email-message')
const reflectToUI = (valid) => {
  message.style.setProperty('display', valid ? 'none' : 'block')
}
input.addEventListener('blur', (ev) => {
  const valid = isValidEmail(input.value)
  reflectToUI(valid)
})

変更点は2つです。
1つ目は、表示・非表示を切り替える際、if文での条件分岐をやめて、1箇所で指定するようにしました。
2つ目は、UIの更新処理をreflectToUI関数に切り出して、イベントハンドラーから独立させました。

気持ちとしては、reflectToUI関数の中でUIを宣言的に記述しています。

なぜこのように変更したのか

まず1つ目の変更点「if文の除去」の理由を説明します。
なぜこうしたかと言うと、その方が宣言的っぽいからです。宣言的プログラミングではif文のようなフロー制御はほとんど使われません。フロー制御は本来的なプログラミングのものであって、「それより直観的な何か」には無いはずのものだからです。
このように、宣言的スタイルには「1つのことを1箇所で記述する」傾向があります。1つのことを条件分岐して2箇所で記述するような、本来的なプログラミングのやり方は使いません。

次に2つ目の変更点「UI更新の切り出し」の理由を説明します。
宣言的スタイルでは、(それがどれだけ直観的かは別として)宣言的に UIを記述したい ので、UIを記述する部分を関数として切り出しました。イベントハンドラーはイベントが起こったときに実行する処理なので、UIの記述とは視点というか文脈が違います。この文脈の違いを、関数に切り出すことで表現したわけです。
宣言的スタイルではUIの記述が他から分離されます。混じりあったりはしません。

余談:もっと宣言的に書きたい

余談ですが宣言的スタイルで使っているsetPropertyメソッドの呼び出しはなんとも本来的プログラミングっぽい感じで気に入りません。本当は下記にように、もっと直観的な書き方(下記はCSSっぽい感じ)をしたいところですが、javascriptのAPIが命令的に出来ているので仕方ないですね。

{
  "#email-message": {
    "style-display": valid ? 'none' : 'block'
  }
}

宣言的スタイルのメリット

それではいよいよ、宣言的スタイルのメリットを見ていきましょう。

メリット1:UIの変化が多い場合

今回の例では、UIの変化は表示・非表示の切り替えだけでした。が、本番のプログラムではUIの変化はもっと多くなります。たとえば、未入力だったらエラーメッセージの内容を「入力してください」にするとか、エラーがあったら入力ボックスの枠を赤くするとかです。
そのとき、命令的なコードでは、if文による分岐先2箇所でコードの書き足しが必要ですので、漏れが発生する可能性が出てきます。
一方、宣言的なコードでは性質1つにつき1箇所で書き下していっているので、書いたか書いてないかのどちらかになります。書き漏れが発生する可能性はぐんと減ります。

命令的スタイルのコード(テキトーに抜粋)

input.addEventListener('blur', (ev) => {
  if (isValidEmail(input.value)) {
    message.style.setProperty('display', 'none')
    if (input.value === "") {  // 追加
      message.textContent = '入力してください'  // 追加
    } else {  // 追加
      message.textContent = 'メールアドレスが不正です'  // 追加
    }  // 追加
    input.style.setProperty('border-color', '#FF0000')  // 追加
  } else {
    message.style.setProperty('display', 'block')
    input.style.setProperty('border-color', '#000000')  // 追加
  }
})

宣言的スタイルのコード(テキトーに抜粋)

const reflectToUI = (valid, errMsg) => {  // パラメータerrMsgを追加
  message.style.setProperty('display', valid ? 'none' : 'block')
  message.textContent = valid ? '' : errMsg  // 追加
  input.style.setProperty('border-color', valid ? '#000000' : '#FF0000')  // 追加
}
input.addEventListener('blur', (ev) => {
  const valid = isValidEmail(input.value)
  const errMsg = input.value === "" ? '入力してください' : 'メールアドレスが不正です'  // 追加
  reflectToUI(valid, errMsg)  // 引数errMsgを追加
})

メリット2:UI変化のきっかけが増える場合

今回の例では、blurイベントでのみUI変化が発生することになっていました。が、たとえばフォームのサブミットボタンが押されたときにもUIを変化させたいなど、UI変化のきっかけが増えることは往々にしてあります。
そのとき、命令的なコードでは、イベントハンドラーとUIへの反映が混じって書かれているので、片方だけを使い回すことはできません。普通はコードを再構成することになります。
一方、宣言的なコードではあらかじめイベントハンドラーとUIへの反映が分離しているので、再構成は不要でちょっとした書き足しをするだけで済みます。

命令的スタイルのコード(あえて再構成はしない。テキトーに抜粋)

const submit = document.getElementById('submit-button')  // 追加
input.addEventListener('blur', (ev) => {
  if (isValidEmail(input.value)) {
    message.style.setProperty('display', 'none')
  } else {
    message.style.setProperty('display', 'block')
  }
})
submit.addEventListener('click', (ev) => {  // この行から最終行まで追加。blurのハンドラと同じ内容
  if (isValidEmail(input.value)) {
    message.style.setProperty('display', 'none')
  } else {
    message.style.setProperty('display', 'block')
  }
})

宣言的スタイルのコード(テキトーに抜粋)

const submit = document.getElementById('submit-button')  // 追加
const reflectToUI = (valid) => {
  message.style.setProperty('display', valid ? 'none' : 'block')
}
input.addEventListener('blur', (ev) => {
  const valid = isValidEmail(input.value)
  reflectToUI(valid)
})
submit.addEventListener('click', (ev) => {  // この行から最終行まで追加。blurのハンドラと同じ内容
  const valid = isValidEmail(input.value)
  reflectToUI(valid)
})

まとめ

このように、宣言的スタイルのコードは拡張工事がしやすい、というメリットがあります。
読者の中には「なんだコードが整理されただけじゃないか」と言う方もいるかもしれません。
それはその通りです。実は、「if文の除去」のようなフロー制御よりも値の計算を優先するのは関数型プログラミングのやり方だし、「UI更新の切り出し」はMVCやDDDのようなレイヤー分けのパターンです。
宣言的スタイルを意識することで、そのような良い手法をプログラムに取り入れられるというわけです。

最初に話したように、宣言的プログラミングとは、本来的なプログラミングとは違ったもっと直観的な何かを使ってプログラミングをしよう、という手法です。
その「何か」はプログラミング言語よりも直観的なのでだからそのコードも良くなる、というのはなんとなく合点のいくところではないでしょうか。
ぜひ日頃のプログラミングで活用していただけたらと思います。

告知

先ほど、こんなふうに書けたらいいな、というコードを示しました。

{
  "#email-message": {
    "style-display": valid ? 'none' : 'block'
  }
}

実はこのように書ける宣言的UIライブラリがあって、私の会社で開発しているbatonjsというものです。
batonjsは CSS的な記述 でもって、既存のHTMLページに対してダイナミズムを与えられるフレームワークです。

batonjsの特徴は、jsxなど特殊な文法を使わないので導入がしやすく、独自の概念が少ないので学習も容易なことです。
競合としてはReactよりはjQueryの方が近いような、他とは違った適性を持つ宣言的UIライブラリです。「SPAじゃなくてももっと宣言的UI使おうぜ」というわけです。
選択肢の1つとして知っておいてもらえると嬉しいです。

Discussion