トップ 最新 追記

2019-02-03 [長年日記]

_ Vueで雑にContainer Component

Container Component が何かはあえてとりあえず省略。

こんな感じかな

Presentational Component

<template>
  <button @click="$emit(event, arg)">{{button_name}}</button>
</template>

<script>
export default {
  props: ['button_name']
}
</script>

<style>
  ..
</style>

Container Component

<template>
  <presentational
     @event="handler"
     :button_name=button_name />
</template>

<script>
import './Presentational'

export default {
  components: {
    Presentational
  },
  data() {
    return {
      button_name: 'default'
    }
  },
  methods: {
    handler(arg) {
    }
  }
}
</script>

Presentational では methods を持たず、$emit で Container 側に丸投げする。data は独自に持たずに props で受け取る、という形が Vue.js でいちばん雑に実装したものになるかな。

参考


2019-02-06 [長年日記]

_ 大雑把にViewコンポーネントから責務をひっぺがしていくフロントエンド設計

Vue.js を利用すると仮定して、だいたいこんな感じに登場人物を分けるといいんじゃないかというメモ。View フレームワークのコンポーネントの分類の話ではなくて、コンポーネントに限らずに単純に責務を分離しようとしているだけ。

考え方の基本は以下の2点。

  • 並行作業のしやすさ(クリティカルパスの短縮)
  • テスト容易性の確保

以下、仮に UI を決める component の名前を UI.vue とする。なお、☆の数は現時点で考えている重要度を表す。

☆☆☆ Model.jsとの分離

データによって UI に変化が起きるデータバインディングの部分は「データ取得の仕組み」に依存してはいけない。

  • View が Vue コンポーネントから data をセットされる際、その仕組みの詳細に依存する必要はない
    • YAML を直接読み込んでいようが API にアクセスしていようが View には影響しない
  • つまり Vue コンポーネントの中にその仕組みの詳細を置いてはいけない
    • 置いてあったら詳細の変更の影響を受ける

上の考え方を適用すれば「View」と「データ取得、反映の仕組み」の開発は分離して同時に行えるのでお互いの作業をブロックしないし、それぞれ自動テストに対応させやすい。

もちろん、設計段階でお互いに連携しなければいけない責務は 考慮はしておく。例えばデータの取得に失敗した場合のユーザーへのフィードバックの方法などは Model でエラーの種類などを整理したうえで View にどうやって反映するかを考えておかなければいけない。

この辺をまず大雑把に Model と呼び、View component から分離しておく。この考え方で言う Model は特定のフレームワークに依存する必要はないので VanillaJS で書いておけばよい。attribute の扱いやエラーの扱いなどの共通の処理を内包するなんらかのライブラリがあるならそれを使ってもよいかもしれないが、Model すなわち State を持つ Store というわけではない。ここ重要。

☆☆ Container.vueとの分離

Container と Presentational の分離の話。methods, data, props, $emit の話。

例えば分離しておけば Model を仮置きしておいて後で差し替えるといった際に UI.vue 側は何も変更する必要がない。

cf. Vueで雑にContainer Component - あーありがち(2019-02-03)

☆☆ Error.vueとの分離

  • エラー処理をいちいち if で分岐すると処理の本質を追いにくくなる
  • 例えば Error.vue を用意し <Error v-if="errors.length > 0" :errors=errors> で丸投げ

ViewModel から見て model.errors と this.errors の 2パターン用意できるかも。

※ ActiveModel::Errors の考え方を転用。

☆ ModelとModelState.jsの分離

  • Model と ModelState は分けてよい
  • Flux 系 Store は State だけ担っていると考えればよい

☆ ModelとInteractor.jsの分離

Clean Architecture 的な意味での Interactor みたいな何か。Model 一つで処理できなければ、複数の責務にまたがる一つの処理にまとめる人を用意する。

errors も relay していく感じ。

まとめ

こんな構造になるかな。

Container.vue (3)
  `-- UI.vue (1)
  |    `-- Error.vue
  `-- Model.js (2)
  |    `-- errors
  `-- ModelState.js ( nearly equal Store ) (4)

順番としては 1, 2, 3, 4 で考えたらいいんじゃないかと考えている。Interactor を入れるとこんな感じかな?

Container.vue (3)
  `-- UI.vue (1)
  |    `-- Error.vue
  `-- Interactor.js (5)
  |    `-- ModelA.js (2)
  |    |    `-- errors
  |    `-- ModelB.js
  `-- ModelState.js ( nearly equal Store ) (4)

書いてみて思ったんだけど、こういう考え方だから、世間でよく観測される ViewModel コンポーネントの分類の話や Model == Store みたいな話にピンとこないんだなぁということが分かってきた。

Tags: Web Vue.js

2019-02-10 [長年日記]

_ JavaScriptのイベントドリブンプログラミング周りの登場人物を雑に整理する

自分向けのダンプであって、後半にいくほど間違いが含まれる可能性が高まります。

イベントドリブンプログラミングとは

処理の流れに関するプログラミングモデル。

書いた順番の通りに処理が流れるわけではなく、起きたイベントによって様々なところから動作が始まる。歴史としては別に新しいものではなく、メインループとシグナル、と呼んでもよい。様々な分野、時代で様々な呼び方、様々なテクニックがあるはず。(ちょっと追いきれません。悪しからず)

基本的な登場人物は

  • Event Listener
  • Event Handler
  • ( 結果の Value )

くらいでよい。どのような Event の種類があるかは扱うプラットフォームによって変わる。

基本的には後発のものほど抽象度を上げるテクニックが増えていると考えればよい。

なぜイベントドリブンなのか

  • 人類には早すぎる問題 ( スレッド )
  • プラットフォームの制約

マルチスレッドで UI を制御するのはロックの問題が非常に難しいらしい。それで最近の UI 周りはスレッドではなくイベントで取り回しを行う流れになっている。

もう一つはこれは明確な根拠はないけどたぶんこうだろうという推測。JavaScript は 1995年に登場したわけだが、当時すでに恐らくこの UI をスレッドで制御するのは難しいという議論はあった。結果、Netscape ブラウザは UI を管理するスレッドを一つにしてあり、その中で JavaScript は動いていたので JavaScript はマルチスレッドを扱えない。ただし非同期の処理はやはり生まれるので、timer を用意してイベントドリブンの形にした。(言語自体がシンプルになるという意味もありそうな気がする。)

ということで特に UI 周りではイベントドリブンという考え方がポピュラーになっている。バックエンドもイベントドリブンで、という考え方はもちろんあって、それが Node.js や RxJava に繋がる。

cf.

Callback関数

JavaScript は Function が first class object になっており、当初から自由に closure が使えた。これで event に対して function を割り当てる 手法が一般的になる。擬似コードを示すとこんな感じ。

object.on(event, function(event) {
  ..
})

しかしこの方法には課題がある。

関心が混ざり、テスト、デバッグしにくい

もう一度擬似コードを示す。

object.on(event, function(event) {
  ..
})

この書き方には listen したい event と handle する処理の両方が密に書かれている。そして function は closure であり、名前がない。

この組み合わせはテストおよびデバッグを難しくする。

  • function に独立した名前がないので独立して呼ぶことができない。したがってユニットテストを書けない*1
  • function に独立した名前がないので trace を追いにくい
  • JavaScript の基本的な scope は function であり、外から中の状態にタッチするのは難しい
    • 高性能なデバッガがないとデバッグが難しいが、デバッガ依存の開発は再現性が低い(誰でも自動化されたテストで同じ恩恵を受けられるわけではない)*2

要するに callback での開発は難しい。

インターフェイスが定まっていない

callback は単に function を引数に取るということだが、この function に渡す引数の順番にはルールがないためインターフェイスがブレてしまいやすい。例えば jQuery では

$.each(array, function(index, element) {
  ..
})

jQuery.each() | jQuery API Documentation

$.map(array, function(element, index) {
  ..
})

jQuery.map() | jQuery API Documentation

のように引数のブレがある

※ さらに

$.map(function(index, element) {})

とも違う。.map() | jQuery API Documentation

jQuery のこの例は this として何を渡すと都合がよいか、また従来の構文との親和性などを考えた結果こうなったのだと思うが、このようなブレがあらゆる callback に対して存在し得る。常にリファレンスを参照し、常に挙動を確認しないと安心して使えない。

ネストが深くなる

例えば callback の中でさらに成否を分ける callback を呼ぶ必要が出てくるとネストが深くなる。jQuery で書くとこんな感じ。

$.on('click', function(ev) {
  $.ajax(url, {
    data: {},
    error: function(error) {
      ..
    },
    success: function(data) {
      ..
    }
  }}
})

success はさらに callback function を与えることができる。

さきほど挙げた名前のない function の階層がいとも簡単に増えるのは非常にあぶない。

Promise

Promise は Future パターンの一種。パラダイムではなくデザインパターン。1977年考案。

Future パターン - Wikipedia

callback の持っている問題のうち、

  • 引数および戻り値のルールを決める
  • ネストせずに書けるようにする

ことが Promise の最大の意義。ざっくりイメージを書くと以下のような感じ。

promise
.then(function(resolve) {
  ..
})
.catch(function(reject) {
  ..
})

これで旧来の if-then-else のように書ける。ここが最大のポイント。

ただし、Promise を定義する側は気をつけるべきポイントが残っている。

return new Promise((reject, resolve) => {
  if ( !true ) {
    return reject()
  } else {
    resolve()
  }
})

大きくは上のような書き方にするのがよい。

ポイントは return reject() を先に並べていく形にしておかないと、reject すべきものが resolve として処理されてしまいやすいところ。

となると、Promise を 使っている部分 については if-then-else のように分かりやすく読めるが、Promise を 書く部分 については Promise の特徴を意識しながら読み書きする必要があり、必ずしも扱いやすいばかりではない。

しかし、ネストが深くなって扱いにくいという問題と引数の順番が固定していないという問題は解消されている。callback にまつわるすべてが解消されているわけではないが、前進している感じはする。

JavaScript では Promise が標準に入ったし Google などが積極的にライブラリで Promise を活用しているので今後は Promise を扱うのは標準と言ってよい。また仕様が言語の外で決まっているので、どの言語のエンジニアと会話しても同じ意味で会話できることの意味は大きいと思う。

参考
async/awaitはPromiseの処理の流れではなく結果の値の方に注目するもの

Promise を受け取る部分で await を書いてあげるとこの Promise の解決を待ってくれる。これで

val = await promise

と書ける。

イベントドリブンは

  1. イベントが主
  2. それに対応して処理を書く
  3. 処理の結果を受け取れない(callbackなので中に処理を書かざるを得ない)

という形だったが、async/await の登場でようやく callback を使わないコードと同じように並べることができるようになった。

ただし async/await を意図通りに動作させるためにはいくつかルールがあって、かつそれを 守れていなくてもプログラムは正しく動作してしまう ので、繊細であることは否めない。

Promise.allはゼロイチだがイチはジュウやヒャクかもしれない

一度に10のPromiseを処理していたらエラーの数も10になってしまうことがよくある。これは嬉しくない。しかしPromise.allの単位で「まとめる」などはPromiseの範囲外。

Reactive Programming

javascript - How is reactive programming different than event-driven programming? - Stack Overflow

event そのものではなく event によって得られるデータに注目する。そういう意味では async/await と同じ発想だけど、

  • async/await はデータを得るまでの話
  • Reactive はデータの変化に注目して、そこから処理を始める

という考え方になっている。大雑把に言うと Observer パターン。

イベントそのものではなくデータの方に注目することで本当にやりたかったことに注力することができるという考え方。

もう一つは async/await ( Promise ) では一つしか解決済みの値を扱えないので、複数回データが更新されたという事実は扱えない。これをもっと汎用的にした考え方が Reactive Programming.

最近の View フレームワーク ( React とか Vue とか ) はこの考え方を DOM にいかに速く適用するかということに基本的に特化していて、それが data ( state ) と data binding になる。データには注目しているがイベントとしてはストリームを扱えるものはメジャーにはなっていないと思う。

ReactiveX

ReactiveX

Functional Reactive Programming を一般的な Web やアプリと親和性の高いツール上で実現する恐らく一番有名なライブラリ。

event を stream として捉え、stream を加工する function を提供してくれるもの。

これによって event handler 内で条件分岐するとか、複数の event が絡むことを考慮して event handler 内で状態を扱う必要性を減らすことができる効果がある。

もとは .NET の世界から始まっているが、Netflix の RxJava を契機に他の言語にも広まっている。JavaScript の世界でも React や Vue ではなく「RxJS + 何か」という選択をする流派もある。

この辺は扱う領域によって選択は変わるだろうなぁという印象。典型的な View フレームワークは HTML がそもそもツリー構造であることを前提にしているのかコンポーネントもツリー構造が前提になっているが、View を扱うこととイベント、データを扱うことの両方の関心が混ざっていてそのどちらに比重を置くかの難しい決断を迫られている感じがする。

言い換えるとどちらの比重が大きいかがはっきりしている場合、例えば View の比重が大きい場合は View から考えればよく、イベントとデータの比重が大きい場合はイベントとデータを中心に考えればよいと思うのと、同じ発想を広く適用したいということを考えるなら、React や Vue, Angular という「フレームワーク」を Web からネイティブアプリに適用していくアプローチと「Rx なんとか + 言語」という「考え方」を両方に適用していくというアプローチがありそう。息が長いのは考え方の方だろうけど、目の前の課題をより素早く解決するのはフレームワークの方だろうなぁ。

Tags: JavaScript

*1 これについては必ず callback function に名前を付けて独立させるという縛りで解決はできる。

*2 事実 Firebug 以降ブラウザのデバッグ環境は飛躍的によくなった。おかげで手作業で頑張る開発自体は以前より楽にできるようになった。


2019-02-13 [長年日記]

_ JSON Schemaの初歩的な整合性のメモ

以下のような構造の schema を用意して validate する際、

{
  "$schema": ..,
  "type": [
    "object"
  ],
  "definitions": {
    <parent>: {
      "$schema": ..,
      "title": ..,
      "description": ..,
      "links": [
      ],
      "type": [
        "object"
      ],
      "definitions": {
        <prop1>: {
          "type": [
          ]
        },
        <prop2>: {
          "type": [
          ]
        }
      }
    },
    "properties": {
      <prop1>: {
        "$ref": "",
      },
      <prop2>: {
        "$ref": "",
      }
    },
    "required": [
      <prop1>
    ]
  },
  "properties": {
    <parent>
  }
}

data としては

{
  <parent>: {
  }
}

こういう構造になっていないといけない。

data の構造が上のようになっていなくても エラーにはならない。

最上位の構造で "required": ["user"] を追加するとエラーになってくれる。

※ combine 前提で prmd で書き始めると最上位の構造に対して "required" ってどうやって定義するの?

Tags: JSON

2019-02-14 [長年日記]

_ Validationのことを考えてたらFunctional Validationという考え方に行き着いた

問題意識

  1. Rails の validation の基本は ActiveModel::Validations だが、Strong Parameters を別に書くのは二重化してしまう 部分 がある
  2. さらに API クライアントみたいな SPA みたいなものがあると三重化してしまう
  3. Conditional Validation はやめたい

Rails内でvalidationをDRYにするdry-validation

例えば Laravel には独立した Validator があるし、Hanami::Validations も独立している。

で、Hanami::Validations は dry-validation を利用している。

dry-rb - dry-validation - Introduction

まず 1 の Rails 内で validation が DRY でないということで、そのまんまの名前の dry-validation を代わりに使うことを検討した。

独自の DSL で Schema を定義し、それで data を validate する。

Schema = Dry::Validation.Schema do
  required(<attr>).filled
  ..
end

result = Schema.(data).inspect

こんな感じ。

例えば Model では AciveModel::Validations を一切定義せずに

def valid?
  result = Schema.(self.attributes).inspect

  if result.errors.size == 0
    true
  else
    errors = result.errors
    false
  end
end

とすれば雑な感じでは validation を置き換えることができる。(たぶん考慮しなきゃいけないことは足りてない。)Controller では

before_action do
  params.permit!
end

def <action>
  result = Schema.(params.to_h).inspect

  if ( result.erros.size > 0 )
    head 400
  else
    head 200
  end
end

みたいな感じになると思う。宣言的に書けず分岐が一つ深くなるのはイヤということであればなんか raise する guard 節でもよい。

 raise NankaError if Schema.(params.to_h).failure?

ただし、標準の機能ではないし、許可されていない値を突っ込もうとするといきなり例外で死ぬ、みたいな「うっかり防止」感は弱いので、まず「Mass Assignment 脆弱性ってのがあってね」といった基本をしっかり押さえてから切り替えた方がよさそう。

ActiveModel::Validations はともかく Strong Parameters の記述はちっとも分かりやすさがなく、もう「とりあえずホワイトリストにしか使わない」感じはあって全然嬉しくないなーと思っていたので、独立した Validator を使うのは一時的には道具は増えるけど、いろんな書き方を覚えられなくて時間を浪費するよりはよさげだなぁという感触。

ただし許可されていないパラメータは明示的に禁止しないとダメで、書き方としては

optional(:id).value(:empty?)

みたいな感じになる。ここが少し罠っぽい*1

もう一つ、Schema 定義が独立すると当然だけど「登場人物が増えるとどっちをテストすればよいのか迷う問題」が発生し得る。例えば Schema に対してテストコードを書くか、Model に対して書くか、といった問題。

まぁ自分がやるなら特に難しくは感じてなくて、

  • Model に対しては本当に validation が間違いなく実行されるかどうか
  • 詳細は Schema に対して

テストコードを書くという作り方をすると思う。そこが「関心」なので。

そういうサンプルが手近にないと Schema のテストはバッチリしてたけど実際に validation が意図通りに実行されていないことが e2e のテストの終盤で見つかりました、みたいなことは起きかねないかもなぁと思っている。丁寧に commit を分けたサンプルがあるといいんだろう。

cf.

JSON Schemaはprmdのサンプルはクソ面倒だけどまぁまぁか?

JSON Schema | The home of JSON Schema

JSON Schema は狙いがいくつもあって非常に分かりにくいし、prmd init するとサンプルがてんこ盛りで分かりにくい。分かりにくい。

ただまぁ、言語非依存で schema 定義を行えるのでサーバとクライアントで共有できる、クライアントの言語を問わない、といったメリットは間違いなくある。

とりあえず我慢して作ってみて Vue.js にぶっこんでみた。

使った npm は tdegrunt/jsonschema: JSON Schema validation

実際に書くとこんな感じ。

<template>
  <Error :errors=errors v-if=errors />
  ..
</template>

<script>
import {validate} from 'jsonschema'
import Schema from 'schema.json'
import Error from 'Error.vue'

components: {
  Error
},
data() {
  ..
},
computed: {
  errors() {
    return validate(this.$data, Schema).errors
  }
}
</script>

ここでは computed に入れてあるが、実際には validate を走らせるタイミングが大事になってくるはずなので、あくまで上のコードはイメージ。また、Error の component が独立してるのは「大雑把にViewコンポーネントから責務をひっぺがしていくフロントエンド設計」で考えていた話。

JSON Schema はよく JSON Hyper Schema の文脈で語られたり API サーバの validation とクライアントの validation の両方に適用できるという意味で使われたりしている。

が、個人的には

  • API サーバの validation とクライアントの Model 相当のものの validation は意味が違うのでは?
  • むしろ API サーバ上の Model の validation とクライアントの Model の validation が近いのでは?
  • サーバの Model は状態を持つので条件によって validation ルール変わったりする(Conditional Validation)よね?
  • ということは JSON Schema は何種類必要なの?
  • でも当然重複もあるよね?
  • Schema 表現としてどの程度の自由度があるの?

など、非常に気になることが多い。これは次の話に続く。

cf.

Conditional ValidationではなくFunctional Validationがよさそう?

Conditional Validation という考え方が実際にはよくあって、例えばブログ一つ取っても

  • 下書き保存の際には何が欠けててもよい
  • 「公開」時にはタイトルもカテゴリも本文もアイキャッチの画像も全部埋まってないとダメ

みたいなことがあちこちで生まれる。

上の例だと素朴には「公開」かどうかを表す attribute をまず見て、それに応じて validation が追加設定されるという感じになりがち。イメージとしては以下のようになる。

validates :body presence: true, if: -> { public? }
validates ...                 , if: -> { public? }
...

だが、これはやめたいと思っている。理由は

そもそもの validation の意味、field の内容を見て追加される validation の意味、意図がコードから読み取りにくい

から。

ではどうするかというと Validator というか Schema というか、それに名前を付けて、

この条件が成り立つ時はこういう意図のこの名前の validation が行われる

という形にすべき。validation が複雑になればなるほど意図が大切になる。

しかし、ここでも schema 定義の多重化が起きてしまう。そこで「Schema の merge ができれば便利なのではないか?」と思ったら dry-validation にはしっかりその機能があった。

Extending a validation schema - Support / dry-validation - dry-rb discussion forum

では JSON Schema はどうだろうか?と思ったが、ここでハタと困ってしまった。そもそも JSON Schema 自体が分かりやすいとは言えないのにこいつを merge とかできるのか?というかそんな機能を持ってる JSON Schema ライブラリは存在しないように見える…。

ここでしばらく悩んだが、

を読んでいたのと、最近 Functional Reactive Programming のことを考えていたので、JSON Schema 自体に詳しくなろうとするより考え方を変えた方が良いなと思い直し、

Schema を merge するのではなく Validation の結果を compose すればよいのでは?

と思いついた。

実現方法は簡単で、複数の schema を使った複数回の validation の結果を一つにまとめておく result オブジェクトがあればよい。しかもこれなら JSON Schema での validate だろうが dry-validation の validate だろうが何も気にしなくてよい。Schema の定義方法も Validation Engine の実装も何もかも違っていてよい。これなら JSON Schema と dry-validation で Schema が重複してしまうといったことも気にする必要がない。Rails 以外だろうと他にどんなツールが増えようと、ずっと使える考え方だ。

「なんてこった天才かオレ」と思ったけど、やはりすでにこの考え方をしている人はいます。そういうことかー。

A Functional Approach to Data Validation

※ protocol に依存せずに結果の errors が欲しいので、発想としてはたまによく gem で見かける :context を使えばよいのかな?

実は… が、今回の発見はどんな schema validator でも組み合わせようと思えばできること。です。

cf.

Tags: JSON Rails

*1 Mass Assignment脆弱性対策のためだけに許可する params を列挙するだけに使うという使い方は残してもよいのかもしれない。でもそのためだけに private メソッドを使うのもイマイチなんだよねぇ。Action メソッドの中に宣言的に書けるならいいと思う。でも Action がメソッドだと難しいよね、これ。Hanami のように Action が class になってるとよさそう。


2019-02-15 [長年日記]

_ ESLintでObject.assignを一部だけゴルァできるようになってた

※ "using an object literal as the first argument" でした。literal じゃなかったら怒られないので完全に抑止はできません。

"rules": {
  "prefer-object-spread": "error"
}
Use an object spread instead of `Object.assign` eg: `{ ...foo }`  prefer-object-spread

た、たしかにー。

Object.assign はすでに標準の規格に入っているが、だからこそなのか Babel と ESLint の両方がスルーしてしまい、ちょっとお茶目なブラウザが踏み抜いて死ぬ、ということが起きていてつらかった。

また babel-polyfill で動くらしいことは知っていたのだが、

babel-polyfill 自体がそこそこ大きいのと、2回読み込むと死ぬらしいということで、asset 管理がしっかりしていないところに導入するには結構リスキーな仕様で困っていた。

2019年初頭現在、Object.assign は弾けるようになっていたということだな。知らなんだ。

Tags: JavaScript

2019-02-16 [長年日記]

_ ブラウザ対象でユーザーを縛れない場合はもうしばらくはObject.assignを使わない

具体的には IE と Android のため。

ではどうするのがよいか

  1. まずESLintで弾く
  2. 代替コードにする

根拠

  • Babel は Object.assign をトランスパイルしない
    • しかし古めのブラウザでは動かずエラーで死ぬ
  • babel-polyfill という手もあるが、asset bundler が適切にセットされていないと polyfill 二重読み込みで死ぬ可能性が残る
    • あとサイズがでかい

ESLint が面倒見てくれない時は気をつけるしかなかったが今は大丈夫。

Node.js ネイティブならバージョンに従ってください。

cf.

別解

Babel などのトランスパイル環境が整っているのなら babel-plugin-transform-object-assign を常に入れておく。

@babel/plugin-transform-object-assign &#183; Babel

これが入っていると static な Object.assign() を spread operator に変換してくれるので、結果的に IE にも対応できるようになる。ただし、完璧ではない。

Tags: JavaScript

2019-02-18 [長年日記]

_ エラーの伝え方を調べてて今さらNotifcation Patternを知る

安易に例外を使ったり安易に例外をつぶしたりするコードになんか違和感があって調べてたんだけど、我らが Martin Fowler 先生の記事にたどりついた。で、

Replacing Throwing Exceptions with Notification in Validations

からの

Notification

からの

Separated Presentation

ですよ。

API クライアントが UI を持つ場合、何らかのアクションに応じてサーバでエラーが起きたらそれをフィードバックする必要があって、そのためには

  • サーバ側の Error を詰め込んだオブジェクトをバケツリレー(?)*1して
    • これは Model から独立しててよい
  • API(presentation)まで運んで
  • クライアントに返して
  • クライアントは Model で受け取って
  • いい具合に Error を表示するだけに集中する人に渡すといい

って考えてたんだけど、ほぼまんま書いてあったわ…。15年前にガイシュツやったわ…。

※ Data Transfer Object も出てくるね

Notification て名前は今だと UI の Pattern のようにも見えるけど、ここでは observeに対する notify くらいの意味かな。

にしても JS ヘビーにはしない方がよいとずっと思ってたけど、間違ってなかったな。そっちに舵を切るにはフレームワークのケアしない領域の羅針盤が必要だけど Web 側の知見は不十分で、いわゆるクライアント-サーバモデルの GUI アプリケーションの知見がまさにハマってきそう。

フロントエンドの世界では .NET のノウハウが Angular 1 辺りから入ってきてたけど、最近 Scala で DDD って話も結局 JavaEE 的な世界の話なのかなーくらいに思えてきた。あとはどんだけ言葉をそぎ落とすか、かなぁ。言葉が多いと言葉が目的化しちゃうのはフレームワークでもデザインパターンでもなんでも一緒な気がする。入り口に言葉を並べすぎるのはよくない。奥が深い程度の状態に留めておくのがよい。

*1 別ルート作ってもよいかもしれない


2019-02-24 [長年日記]

_ あとから@vue/cli 3 + Jest

まとめ

いろいろやって、結局丸一日くらいは掛かることが分かった。

気をつけるべきポイント

  • Babel がすでに入っているなら 7系 にちゃんと上げておく
  • @vue/cli は Webpack と一部概念が違う
  • 関係するツールが増えると当然だが検証時間は伸びる

狙い

大きなフレームワークが入っておらず、中途半端に JavaScript が散らかっている状態の小規模のプロジェクトがある。このうち一部のコードに対して

I/O 周り(HTTPS? や localStorageアクセス)に変更を施す必要がある

ことが分かった。そのため

  1. ビルドプロセスに手を入れる
  2. 既存のコードをテスタブルな状態にもっていく

を行う必要があると判断した。

方向

すでに動いているコードは以下の状態。

  • jQuery ベースのコードがある
  • ビルドプロセスを伴わない状態で Vue も採用されている

テストの欲しい変更を行うコードは Vue の方。そのため、

  • @vue/cli
    • webpack
  • jest

を入れることとした。

理由はimport / exportを書くとasset bundlerが必要になるから

I/O 周りのコードに対する変更を行いたいわけだが、このために

  • I/O 周りのコードを Model として分離する

こととした。

必要なのは View の方ではないので、Vue を利用して構築している部分から I/O 周りを分離し、Model だけをテスタブルにしたうえで変更を加えていくのはごく当たり前の判断と言ってよいと思う*1。View 周りまで含んだコードをテスタブルな状態に持っていくのはコストが大きすぎる。幸い、Vue の場合は jQuery ベースよりは anonymous function だらけにはなりにくく、「継ぎ」*2を作ればテスタブルな状態を作るのはそれほど難しくなさそうに見えた。

そして Model をテストするわけだが、ここで問題がある。

モダンな開発を行うに当たり、コードを責務の単位で分離したうえで import / export して作っていくのは当たり前のことである。しかし、import / export は Babel の領域ではなく、asset bundler の領域。

asset bundler と言えば Webpack ... ?

Webpack の設定はしたくない。絶対にだ。

ということですでに Vue が入っているなら Webpack の面倒くささを避けるために @vue/cli だろうという当然の帰結。

やることリスト

  1. 既存の Babel があれば上げておく
  2. まず @vue/cli プロジェクトを別なところで作る
  3. .babelrc などは babel.config.js へ変更*3
  4. package.json などなどを別プロジェクトからコピー
  5. webpack の multi entry は @vue/cli 側で multi page として再定義されているのでそっちを参考に
  6. .eslintrc.js を追加*4
  7. Jestの導入(以下のようになる場合アリ)
    • [NG] @vue/cli-plugin-jest-unit
    • [OK] jest

意外とハマる

特に

  • Multi Page という @vue/cli 独自の概念
    • このために HtmlWebpackPlugin とか、あまり欲しくない知識が必要になる*5
    • @vue/cli のデフォルト設定の entry 周りから multi entrypoints に持っていく方が面倒
  • @vue/cli が .eslintrc.js を無視してしまう問題
  • @vue/cli-plugin-jest-unit がなぜか babel-jest を適切に適用できない問題

辺り。

@vue/cli は抽象化のレベルがなかなかよいと思っていたのだが、やはりレイヤーが増えればややこしさは増える。まだまだ伸び代だらけだなぁ。Vue に関してはほぼ本家扱いと言ってよいのでそこまで心配は要らないだろうが、Babel, Webpack, Jest は繋ぎの部分の作り込みのレベルにバラツキがあるように見えるので注意が必要。

また Jest が Babel 7 と合わない時代もあったりしたようで、Babel と Webpack の migration は今後も面倒ごとになり続けそう。

参考

*1 この Model 相当のコードが複数箇所から参照されるのは分かっていたのでその判断を加速させたが、それがなくても分離はした方がよい。

*2 sprout classとかアンチレガシーのプラクティスのこと。

*3 @vue/cli-service, jest が自動で認識してくれる

*4 @vue/cli 3.1.1 はバグで .eslintrc.js を無視するので直っているバージョンに上げておくこと

*5 読み込み用の HTML も不要だし、後始末が必要になったり。


2019-02-28 [長年日記]

_ 2019-02時点でJestの設定で最後までハマったもの

Vue cli 3 はよいという話を以前書いたんだけど、Jest でテストできるようになるまで一つだけすごくハマったものがあって、それは Babel runtime の除外設定。

ちなみにバージョンは以下の通り。

  • @babel/core 7.3.3
  • jest 24.1.0

エラーはこういうもので、

Jest encountered an unexpected token

This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.

By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".

Here's what you can do:

  • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatters" in your config.
  • If you need a custom transformation specify a "transform" option in your config.
  • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

You'll find more details and examples of these config options in the docs: https://jestjs.io/docs/en/configuration.html

Details:

xxx/node_modules/@babel/runtime-corejs2/helpers/esm/classCallCheck.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest)
{export default function _classCallCheck(instance, Constructor) {
 ^^^^^^

babel-jest 経由で Babel を通したコードなのになぜか ESM版の runtime-corejs2 というコードを通るので、その中に export があって死ぬという怪奇現象。

で、今となってはどこをどう調べたか分からないんだけど、自分が1月に作った設定では以下の記述を足して回避していた。

transformIgnorePatterns: ["/node_modules/(?!@babel/runtime-corejs2)"]

恐らく Babel 7 + Jest ではこういう落とし穴がちょこちょこあるのだろう。

transformIgnore したら export を回避できるというのもよく分からん…。(そしてなぜこうしようと思ったのだ、過去の自分よ)

もしかしたら

Configuring Jest &#183; Jest

transformIgnorePatterns のデフォルト値が <rootDir> 含んでなくてプロジェクト内の node_modules を ignore し忘れてるってことだから明示的に設定したのかな。いやでも新しく足したのも <rootDir> 入ってないけど…。分かってるんならよりよいデフォルトに変えてほしい。

ちなみに最小設定は

  • rootDir
  • moduleNameMapper ( Webpack の alias 相当 )
  • transformIgnorePatterns

でイケるっぽい。

Tags: JavaScript