神コントローラーにテスト駆動開発で機能追加

TDDBC オンラインの基調講演の録画を見た。とても面白かった。和田さんのセッションやスライドは何度も見て勉強してるので、それをひとつずつ再確認しながら見ることができて、とても良かった。話の流れが分かりやすくてすごいなー。

最近実際にやったテスト駆動開発

その録画を見ながら、最近自分がテスト駆動開発で機能追加をしたことを思い出してた。今日は、それをだらだらと書いてみようと思う。プライベートメソッドに対してテストを書いてたりするのが、なんか泥臭くて面白いかなと思って。

背景

  • 自動テストが全くないプロダクト
  • God コントローラー(1000行以上ある)
  • 既にあるアクションに1つ機能を付け加える

こんな感じ

f:id:bufferings:20200809142428p:plain

ほえー

色々やる前に

さすがに神アクションに対して「Code & Fix (コードを書いてみて、動作を確認して修正すること)」はつらいので、PHPUnit を入れたいなと思った。

そう、対象のプロダクトは PHP で書かれてるんだけど、僕自身 PHP はあまり詳しくないので、PHP 自体やそのプロダクトが採用してるフレームワークの動きを確認しながら進めたいという気持ちもある。

実は、ちょっと前から PHPUnit の導入を企んでいて、ある程度調査してたから、ちょうどいいタイミングだし投入してみることにした。

なので、まずは PHPUnit をプロジェクトに追加して、Hello World 的なテストが動くことを確認しといた。ちなみにここが一番苦労した。既存のコードやデプロイフローに影響を与えない形で導入しておいた。

あとは楽しいだけ。

集中

さて、機能追加をどんな風に実装しようかな。やりたいことを考えてみる。いくつかの変数の値を元に(入力)、ごにょごにょ処理をして、結果として次の処理に渡す配列を生成すること(出力)ができれば良さそう。

本来なら、ビジネスロジックとしてコントローラーから切り離したいところだけど、そこまでやっちゃうと変化としてちょっと大きそう。だから、今回は「PHPUnit の導入」だけに集中する。これだけでも十分大きな変化だろう。

だから、新たにビジネスロジックに切り出すのではなくて、これまでのやり方に合わせて GodController の中のプライベートメソッドとして実装することにしよう。

プライベートメソッドA

じゃ、そのプライベートメソッドAに対するテストを考えてみる。ちなみに、アクションに対するテストは、現時点では難しそうだから今回は考えない。

んー。メソッドAに対するテストかぁ・・・。でかいな。Aの中には、B・C・D・Eみたいなロジックが入りそう。それぞれ別のメソッドに切り出したいな。メソッドEが一番簡単だし重要だから、そこから始めるとシンプルで良さそう。

f:id:bufferings:20200809153049p:plain:w300

機能に対するテストクラス

メソッドEのテストを書いてみよう。渡したオブジェクト2つの特定のフィールドが同じ値を持ってたら true を返すメソッドを作りたい。

GodControllerTest を作って・・・いや、ちょっとこれ名前がでかいな。んー、普通はテスト対象のクラスと1対1でテストクラスを作るところだけど・・・テスト対象のクラスがでかいからな・・・今回は機能を表すクラスにしとこ。GodControllerFeatureATest クラスを作った。よし、テストメソッドを書こ。

プライベートメソッドのテスト

んだけども、メソッドEってコントローラーの中のプライベートメソッドだから、どうしようかな。

リフレクションで呼び出すのも一つの手だけど。。。ちょっと本質的なところから遠いから、とりあえずもっと簡単な方法ないかな?

コントローラーにパブリックメソッドとして実装するのもいいけど、もっと気楽に、まずはこのテストクラスの中に実装を書いてしまおっと。こんなイメージ↓

f:id:bufferings:20200809153533p:plain:w300

動作するテスト

やっと最初のテストを書けそう。2つのオブジェクトを渡すと false が返される。。。と。

で、まだ実装がなくてエラーが出るから、クイックフィックスからメソッドを生成して。

とりあえず true を返すようにして、実行。よし。 予定通りレッド。

次は false を返すようにしてグリーンになった。

テストはちゃんと動いてるみたいだな。良かった。

ここまでで「テストが動く」ことが確認できた。

f:id:bufferings:20200809153924p:plain:w300

実装

特定のフィールドが同じ値を持ってるオブジェクト2つを渡すと true が返される。。。と。実行してレッド。

じゃ、比較の実装を入れよっと。はい、グリーン。

不安をなくす

特定のフィールドの中で1つでも異なる値を持つオブジェクトを渡すと false が返される。。。と。実行してグリーン。

ふむ。ちょっと順調すぎるけど、シンプルだからそんなもんかな。いくつかパターンを書いて全部グリーン。

え?ちょっとレッドになるか、フィールドの値を変えてみよっと。お、よかった。レッドになった。

じゃ、メソッドEには不安はもうないな。

f:id:bufferings:20200809155159p:plain:w300

他のメソッド

他のメソッドB・C・Dもそんな風にテストと実装を書いた。まだメソッドAのテストと実装は書いてない。ここでB・C・Dの実装はイケてないなぁ、もっと良い実装方法がありそうだよなぁ、と思いつつ、ただ動きはするから、まずはキレイにするよりは前に進めることに集中することにする。

リフレクションユーティリティの導入

じゃ、今テストクラスに実装したメソッドB・C・D・Eをコントローラー側に移動しよっと。

まずはリフレクション用のユーティリティメソッドを作って・・・と。で、コントローラー側にメソッドEを置いて、リフレクションユーティリティ経由で呼び出して、テスト実行・・・OK全部グリーンだ。

念の為、ちょっとメソッドEの中身を変えてみよっと。お、良かった。テストがレッドになったや。

他のメソッドも移動して、テスト実行。全部グリーンね。安心。

f:id:bufferings:20200809155845p:plain

メソッドAを作成

メソッドAのテストについて考える。全然関係ないけどここでペアプロを始めた。

メソッドAはその中でB-Eを呼び出して、結果を返す。

メソッドAは、いくつかの変数の値を元に(入力)、ごにょごにょ処理をして、結果として次の処理に渡す配列を生成する(出力)

じゃ、まずは空の配列を返すテストを書いてレッド→仮実装(単に空の配列を返す)→グリーン。で、メソッドAの実装を書く。単にB-Eを呼び出すだけだから簡単。はい、グリーンのまま。

メソッドAの他のテストを書く

既にあんまり不安はないけど、メソッドAに対するパターンを考えて、テストを一個ずつ実行しながらペアと交互に書いていく。

全部グリーンになったや。これで動作する実装は終わりだなー。

f:id:bufferings:20200809160631p:plain

メソッドAのテストをリファクタリング

メソッドAのテストは、パターンとしては大体網羅されてるけど、まずは実装することを重視したから、テストコードをキレイにして、意図が伝わるようにしよう。重複を取り除いたり、テストメソッドの名前を変えたり。都度グリーンをキープしながら。

よし。終わった。・・・ん?俯瞰して見ると、こういうパターンもあった方がいいな。足しとこ。逆に、このケースは冗長だな。消しとこ。

これでメソッドAのテストは「動作するキレイなテストコード」になった。良い感じ。

f:id:bufferings:20200809160925p:plain

要らないテストを削除

ここまで、メソッドEから始めて、一歩ずつテストを書いて、動作を確認しながらだったから、安心して最終的にメソッドAを実装することができた。

でも、メソッドAのテストがあれば、メソッドB-Eのテストって、要らないし、あると逆に邪魔になる。だから、削除しよっと。

ただ、その中でも、メソッドEのパターンは、メソッドAのテストだけだとカバーしづらいから、キレイに書き直して残しておこう。

最終的に、メソッドAのテストと、メソッドEのテストが残った。3ヶ月後の自分に意図が伝わる最小限のテストになったかな。これで安心して忘れられる。どっちもプライベートメソッドに対するテストだけどね。

f:id:bufferings:20200809161254p:plain

本実装のリファクタリング

さて、メソッドAのテストが揃ったから、本実装をリファクタリングしちゃおう。

B・C・Dの実装がイマイチだとずーっと気になってたから、メソッドFを作ってそっちにまとめてしまおっと。やっぱりこの方が読みやすいな。

メソッドAとEのテストはグリーンのままだから大丈夫。

f:id:bufferings:20200809161756p:plain

ということで

楽しかったー。安心して眠れる。みたいな気持ち。

E2E Web テスト自動化ツールの TestCafe を調べて触った

Java のチームの同僚と「最近だと UI Testing にはどういうフレームワークを使うのかなぁ?」みたいな話をしてて、僕だったら

  • Selenium を直接。WebDriver は結構書きやすそうな印象。
  • Geb は好き。Groovy + Spock は良い。けど Groovy。
  • Selenide は使ったことはないけど Java で書くのに書きやすそう?
  • Cypress.io はちょっと違う種類だけど、サクサクで好き。

とかかなぁ?って言ってたら「 TestCafe ってのもあるよ?」「へー知らないー!」って教えてもらったので調べてみた。

## TestCafe?

devexpress.github.io

目に止まった特徴は、こういうところ。

npm install するだけですぐ始められる。実際にやってみたらほんとに簡単に始めることができた。良いね。Selenium ベースではないので各ブラウザー用のドライバーのインストールも不要。プロキシを使うアーキテクチャーになってるみたい。

サポートしてるプログラミング言語JavaScript と TypeScript。async/await が使える。

オフィシャルにサポートしてるブラウザーは( Browsers | TestCafe より)

Headless Chrome/Firefox も使えるみたい。良いね。

## vs Cypress.js

Cypress.js との比較記事。2018年の3月の記事。

www.yld.io

テストランナー

  • Cypress は Mocha, Chai, Sinon などの標準的なライブラリーを使うので JS 使う人ならスムーズに始められる
  • TestCafe は独自のテストランナーを使うので最初はちょっと戸惑うけど、async/await とか使えて良い

サポートブラウザー

  • TestCafe はプロキシを使うアーキテクチャーなので、色んなブラウザーで実行できる。
  • Cypress は(この記事の時点では)Chrome のみをサポート。(でも、今見てみたら Firefox と Edge もサポートされてた)

実行場所

  • Cypress はブラウザの中で実行されるから DOM を直接触ることができる
  • TestCafe は Node で実行されるので DOM を直接触ることはできない。でも、Node のコードを使ってセットアップとかできる

とかとか。この方は TestCafe を選んだみたいね。この時点でのサポートブラウザーを見て。

## vs Selenium

www.quora.com

Selenium WebDriver の方が歴史が長いしユーザーも多いし、柔軟だし色んな言語をサポートしてるから Selenium がおすすめ。

TestCafe は Angular, Vue.js, React をサポートするライブラリーがあるし、色んなアプリをサポートしてるけど、プロキシを使うアーキテクチャーまわりで色々課題がある。言語が JS/TS のみ。シングルタブのみしかテストできない。要素の取得が CSS セレクターのみ。Job opportunity が少なすぎる。Selenium を1年、TestCafe を半年使って、結局 Selenium のほうがおすすめ。

へー。そっかー。

## TestCafe が Selenium を採用しなかった背景

testcafe-discuss.devexpress.com

  • まず第一に、セットアップを簡単にしたかった
  • 次に、Selenium を採用しないことで以下のようなことが可能になった
    • TestCafe をインストールしていないリモートデバイスでもテストを実行できる
    • 実行環境の分離。毎回シークレットタブを開いているみたいな感じでテストを実行できる
  • 最後に、WebDriver は各ブラウザベンダーが開発しているけど、互換性の問題が出ている

これに対して「SeleniumSelenium 1 のプロキシのアーキテクチャーの課題を解決するために、WebDriver に切り替えたのに、 TestCafe は Selenium 1 のアーキテクチャを採用してるってことよね。面白いね」ってコメントがついて

  • プロキシの問題は、色々ごにょごにょやって解決してるよ!

みたいな感じ。プロキシ自体は Hammerhead という名前で、この仕様は公開されてないみたい。TestCafe と Hammerhead の連携モジュールがこれなのかな↓

GitHub - DevExpress/testcafe-hammerhead: A powerful web-proxy used as a core for the TestCafe testing framework.

## アーキテクチャー概要

TestCafe のエンジニアの Alexander さんが書いた記事: TestCafe: An e2e Testing Tool That Doesn’t Use Selenium

Selenium はセットアップが大変だし色々ライブラリーが必要になる。そこで、さくっとセットアップできる TestCafe ですよ。という感じ。やっぱり最初の導入の簡単さを推してるね。

(画像は記事から引用)

f:id:bufferings:20200718175103p:plain

あとは大体これまでで理解してきたことが書いてあった。

Getting Started

と、大体気になるとこはチェックしたので、触ってみる。

Getting Started | TestCafe

インストール:

❯ npm install -g testcafe

(省略)

+ testcafe@1.8.8
added 477 packages from 297 contributors in 55.379s

お。本当に1分でセットアップ終わったw

テストを書いて:

import { Selector } from 'testcafe';

fixture `Getting Started`
    .page `http://devexpress.github.io/testcafe/example`;

test('My first test', async t => {
    await t
        .typeText('#developer-name', 'John Smith')
        .click('#submit-button')

        // Use the assertion to check if the actual header text is equal to the expected one
        .expect(Selector('#article-header').innerText)
        .eql('Thank you, John Smith!');
});

実行:

❯ testcafe chrome test1.js
 Running tests in:
 - Chrome 83.0.4103.116 / Linux 0.0

 Getting Started
 ✓ My first test


 1 passed (4s)

5分かからんかったな。

失敗させてみたらこんな感じ。分かりやすそう。色付きの方がいいかなと思ってスクリーンショットで:

f:id:bufferings:20200718180118p:plain

感想:Selenium でいいかな

確かにセットアップが簡単ですぐ始められるのはとても良いけど、こういうの使うときってセットアップが終わってからの時間の方が長いし、セットアップが終わってしまえば Selenium の方が柔軟だから Selenium でいいかなと思いました。

もし、採用したら、最初は大丈夫だけど、しばらくすると難しい問題にぶつかって、Selenium だったらコミュニティが大きいのでそれを解決するアイデアやツールがありそうだけど、TestCafe だと自分たちで解決しないといけなさそうで、苦しむ未来が見える。

でも、面白かった!

知識創造企業とアジャイル開発とスクラムと自分

気づかされる日々

最近は、海外出身の人たちと近い距離で仕事をしてて、文化の違いを感じて面白い。そういうところを気にするのか、そこは気にしないのか、とか学びながら、というか、日本人はあの部分は気にしないけど、そこは気にするってことか、みたいに、海外出身の人たちの文化を感じると同時に、日本の文化、それが当たり前すぎて気づくことが難しい部分、に気づかされるような日々を過ごしてた。

そんな中で、自分の中にこういう気持ちがあることにも目を向けさせられた。

スクラムとは違う何か

この10年間、スクラムを勉強して、そこから得た学びを元に実践の中に取り入れたりしつつ開発をしてきたのだけど、自分の中では、そろそろいいんじゃないかという気がしているんだなって気づかされた。これまで、スクラムの考え方を基準にして、組織の形や自分の役割やチームのあり方を疑って、改善してこれたと思う。そして、もう十分「良い活動をできる組織」になってきたと思う。(「良い組織」になったということよりも、そのベクトルのことを「良いな」と思っている)

なので、次の一歩として、こういうことを考えてみたいなと思った。

組織にあった開発スタイル

正直に言うと、この10年間、スクラムをちゃんとやったことはない。というか、できたことはない。そこで悩んでいる部分もあったけど、常に考えていたのは「今のみんなの頑張りをつなげたい」ということで、そのチームや組織の状況や現在のステージに合わせて「スクラムを学ぶ中で得たもの」を活用して、改善をしてきた。

なので、スクラムできてないなぁ、って目をそらすんじゃなくて、僕らの組織の形で目指せる開発スタイルってどういうものだろう?ということを考えても良い時期なのかなと思ったのだ。

日本からのスクラム・海外からのスクラム

さて、スクラムを学ぶ中で、自分の中にずっとふわっと置いてあった気持ちがある。それは「スクラムは海外の人が日本のやり方を参考にしたことが発端の開発手法」なんだよなということ。海外の人から見たスクラムと日本の人から見たスクラムは、違うものなのではないかということ。僕らはもっと暗黙知よりで動くことができそう。

最初に触れたように、僕は、海外出身の人たちと仕事をする中で、文化の違いを感じていた。良いとか悪いとかではなく、単に「違う」というだけなのだけど、海外の人の方が「個」が強くて、役割や境界の明文化を求める。(ここで僕が想像している「海外」は欧米だなと書いてて思った。アジア出身の人たちはまた違う)。それに対して日本の人は「場」が強くて、役割や境界はふわっとしていても気にせずに仕事をする。

この違いが面白いなぁと思うのと同時に、ここにヒントがありそうだなと感じ、もう少しスクラムと日本のことを知りたくなって、この本を手にとった。先に言うと、すばらしい本だった。

知識創造企業

books.rakuten.co.jp

この本は、日本企業が意識的・無意識的に開発し実践してきたその経営原理を「組織的知識創造」というコンセプトで捉え「知識創造企業」というコンセプトを提言している。25年前の本で、製造業の新製品開発を中心としたものなので、そのまま現在の自分の業界に当てはめることはできないのだけど、日本と欧米との文化の違いについての分析や考察など、はっとさせられることや考えさせられることがたくさんあった。

読んでいく中で「現在のスクラムアジャイル開発を野中さんが見たらどう感じるんだろう?」と考えずにはいられず、以前に買ったこの本を読み直してみた。

アジャイル開発とスクラム

books.rakuten.co.jp

これまた、良かった。特に野中さんがアジャイルなソフトウェア開発についてお話されているのが「それ、聞きたかったところです!」という気持ち。以前に読んだときには、形式的な部分だけを言葉で捉えていたのが、今回はもっと深いところで共感することができたと思う。この数年間の自分の経験や、知識創造企業を読んだことによる学びからの実感があったのだよなぁ。とても良かった。

そして、僕が感じているようなことは、平鍋さんが既に言葉にしていた。「本書を作るにあたって考えたのは、危機的な状況下で日本の経営のあり方が問われる中、日本ならではのイノベーションを今一度発信していけないかということでした。」僕は正直、イノベーションまでは考えてないけど、日本の文化ならではの開発手法・組織のあり方を見つけられたらいいなぁと思っている。

新しい一歩

日本の伝統的な部分には、あまり好きではない部分もあるので、その辺りを見直しつつ、欧米の考え方の良い部分から学びつつ、日本の文化の良い部分を土台にして、何か自分にとって新しい一歩を踏み出せたらいいなと思った。折角、日本の文化の上で、色んな国の出身の人たちと仕事をしているのだから、その強みをみんなと探していけたらいいなと思っている。

まぁ、まわりまわって「スクラムに戻ってきた」ってなったりするかもなぁとか、何もしないうちに年を重ねてしまうかもなぁとか思いつつ、とりあえずこの2冊の本を読んで、今感じていることを書いてみた。

読み終わった。JavaScript: The Definitive Guide, 7th Edition

とても良かった。2ヶ月くらいかけて読んだのかな。

www.oreilly.com

最新の情報だけじゃなくて、歴史的経緯から順を追って説明してくれてるので、ちょっと前のスタイルのコードも読めそうだし。どういう背景で新しい仕様が導入されたのかとかも納得だし。とても良かった。

↓ let, const, var の違いや、Destructuring Assignment が面白かったなー。

↓ ES2020 の Conditional Property Access や First Defined で、ほほーってなったし、 === は真面目に読んだ。

↓ for/of やオブジェクトリテラル。ES2018 の Spread Operator は普通にたくさん出てきそう。

↓ Sparse Array って普段は使わなさそうだなーとか、map() や includes() は使いそうだなーとか。Array-like オブジェクトかーとか。

↓ Function と this と Closure はなんとなくでしか捉えてなかったので学べて良かった。昔のやり方から始まる class の説明は分かりやすくて良かった。

↓ Module の歴史。Closure Base から Node を経て、ES6 の Module への流れも分かりやすかった。ES2020 の Dynamic Imports は Node の require() を置き換えるために必要そうだなとか。

↓ Set や Map と。Iterable と Generator Function。

↓ からの Promise と aync/await。面白い。

メタプログラミングと Symbol。

この後に Web Browser の章と Node.js の章をさらっと読んで、最後にツールの紹介があった。

### JavaScript に入門できたかな

これで、一旦 JavaScript に入門できたかな。

なんとなくコードは読めそうだし、それが古いのか新しいのかも分かりそう。それに、分からないコードがでてきても、調べることはできそう。

Node.js のコードは CommonJS と ES の間で難しそうだなぁと思いつつ。

### 良い流れだった

JSPrimer と Promise の本で基本をなぞったあとに、JavaScript: The Definitive Guide, 7th Edition を読んで知識を深める、という流れだったのだけど、これはとても良かった。もういちど JSPrimer と Promise の本 を読み直して、一旦 JS 入門は終わりにしようと思う。

jsprimer.net

azu.github.io

### 次は

Vue.js でも触ってみようかなぁ?

JavaScriptの勉強中:その9 Metaprogramming

週末だー!ということで本の続きを読む。

bufferings.hatenablog.com

## Ch. 14 Metaprogramming

今日はメタプログラミングメタプログラミングは普段使うことはあんまりないから書かなくてもいいかなぁと思いつつ、普段使わないから書かないと全く記憶にも残らないだろうなとも思ったのでメモくらい残しておく。

### Property Attributes

データプロパティとアクセサプロパティはそれぞれ4つの属性を持つ:

  • データプロパティ: value, writable, enumerable, configurable
  • アクセサプロパティ: get, set, enumerable, configurable

ちなみに

  • value: 値
  • get: getter 関数
  • set: setter 関数
  • writable: 値を変更可能かどうか
  • enumerable: for/in ループや Object.keys() で列挙可能かどうか
  • configurable: プロパティを削除したり、プロパティの属性を変更したりできるかどうか

こんな感じ:

> const sample = {
...     x: 1,
...     get octet() { return Math.floor(Math.random()*256); },
... };
undefined

> Object.getOwnPropertyDescriptor(sample, "x");
{ value: 1, writable: true, enumerable: true, configurable: true }

> Object.getOwnPropertyDescriptor(sample, "octet");
{
  get: [Function: get octet],
  set: undefined,
  enumerable: true,
  configurable: true
}
  • Object.getOwnPropertyDescriptor() は own property しか取得できないので、継承しているプロパティの属性を取得したい場合はプロトタイプチェーンから対象のオブジェクトを取得して使う。
  • 普通にプロパティを定義すると、書き込み可能、列挙可能、属性変更可能になるけど、Object.defineProperty()Object.defineProperties() を使うと属性を指定してプロパティを作成・変更できる。継承してるプロパティは変更できないので注意。
  • プロパティの追加やプロパティ属性の変更をできるかは、色々な条件の組み合わせになっててめんどくさそう。今は覚えずに、そういうことをするときがあったら確認しよっと。
  • Object.assign() は列挙可能なプロパティの値のみをコピーする。属性はコピーしない。つまり例えばアクセサプロパティの場合は、getter をコピーするんじゃなくて、getter が返してきた値をコピーするので注意

さっきのやつをそのまま使ってみるとデータプロパティになった。面白い:

> Object.assign({}, sample);
{ x: 1, octet: 107 }

### Object Extensibility

オブジェクトが拡張可能かどうかは extensible 属性で決まる。

  • 通常は拡張可能
  • Object.isExtensible() で確認できる
  • Object.preventExtensions() で拡張不可に変更できる
  • 一度拡張不可にするともう拡張可能には戻せない

オブジェクトの変更をできないようにするために、プロパティの configurable writable 属性と組み合わせて使うことが多い

  • Object.seal() はオブジェクトを拡張不可にすることに加えて、全ての own properties を nonconfigurable にする。
    • プロパティの追加や削除、属性の変更ができない
    • writable なプロパティの値の変更は可能
    • sealed オブジェクトを unsealed に戻すことはできない
    • チェックには Object.isSealed() を使う
  • Object.freeze() はもっと厳しい。オブジェクトを拡張不可にする・全ての own properties を nonconfigurable にすることに加えて、データプロパティをリードオンリーにする(setter を持ってるアクセサプロパティは影響を受けない)
    • Object.isFrozen() で確認可能

ただ、プロトタイプオブジェクトも含めて全部ロックダウンしたい場合は、チェーンをたどって適用する必要がある。

### Well-Known Symbols

Symbol.iterator と Symbol.asyncIterator

  • これはもう知ってるやつだ

Symbol.hasInstance

  • ES6 以降ではもし instanceof の右側のオブジェクトに [Symbol.hasInstance] メソッドが定義されてたら、そのメソッドが呼ばれる
> 10 instanceof {[Symbol.hasInstance](v){console.log("called");return v > 5;}}
called
true
> 10 instanceof {[Symbol.hasInstance](v){console.log("called");return v > 10;}}
called
false

へー

Symbol.toStringTag

  • toString() で表示される文字列表現の部分を指定することができる。"[object Range]" みたいに。指定してなかったら "[object Object]" になる。
> Object.prototype.toString.call({})
'[object Object]'
> Object.prototype.toString.call({[Symbol.toStringTag]:"Sample"})
'[object Sample]'
> Object.prototype.toString.call({get [Symbol.toStringTag](){return "Sample2";}})
'[object Sample2]'

ふーむ。まぁ、使わなさそう。

Symbol.species

これはまたなんか面白いなー。そうそう使うことなさそうだけど。

  • 例えば Array() コンストラクター関数は、このプロパティを読み取り専用で持っている
  • Arraymap() 関数などは、このプロパティからコンストラクターを取得している
  • それによって継承先のクラスを使って結果を返すことができる

へー。MyArray に対して map() を呼んだら MyArray で返されてる:

> class MyArray extends Array {}
undefined
> new MyArray(1,2,3).map(v => v * 2)
MyArray(3) [ 2, 4, 6 ]

コンストラクター関数にこのプロパティが定義されてるや:

> Object.getOwnPropertyDescriptor(Array, Symbol.species)
{
  get: [Function: get [Symbol.species]],
  set: undefined,
  enumerable: false,
  configurable: true
}

> Array[Symbol.species]
[Function: Array]
> MyArray[Symbol.species]
[Function: MyArray]

this.constructor[Symbol.species]コンストラクター関数を取ってるってことか。・・・ん? this.constructor にもコンストラクター関数が入ってると思うんだけど・・・、あぁ、用途が違うから分けてるってことかな。

書き換えるときは、直接値を設定することはできなくて、 getter だけだから。でも configurable だから、こうすれば書き換えられる:

> Object.defineProperty(MyArray, Symbol.species, {value: Array});
[Function: MyArray]
> MyArray[Symbol.species]
[Function: Array]

Array になった。じゃもういちど map() 呼び出してみよう:

> new MyArray(1,2,3).map(v => v * 2)
[ 2, 4, 6 ]

へー。今回は MyArray じゃなくて Array になった。

ふーん。この場合、 this.constructor には MyArray が入ったままだけど、this.constructor[Symbol.species] には Array が入ってるってことか。

> myArray.constructor
[Function: MyArray]
> myArray.constructor[Symbol.species]
[Function: Array]

他にも MapSet はこのプロパティを持ってるみたいね:

> Object.getOwnPropertyDescriptor(Map, Symbol.species)
{
  get: [Function: get [Symbol.species]],
  set: undefined,
  enumerable: false,
  configurable: true
}
> Object.getOwnPropertyDescriptor(Set, Symbol.species)
{
  get: [Function: get [Symbol.species]],
  set: undefined,
  enumerable: false,
  configurable: true
}

へー。そういうののサブクラスを作るときに覚えておいたら良いのかな。

Symbol.isConcatSpreadable

  • Arrayconcat をするときに spread するかどうかを判別するためのプロパティ。
  • ES6 より前は Array.isArray() で判別してたけど、ES6 以降は、まずこのプロパティをチェックして、なければ Array.isArray() で判別する。
# 普通に配列を渡すと展開される
> [].concat([1,2,3])
[ 1, 2, 3 ]

# ArrayLike オブジェクトを普通に渡しても、そのまま追加される
> let arrayLike = {length:2, 0:"a", 1:"b"}
undefined
> [].concat(arrayLike)
[ { '0': 'a', '1': 'b', length: 2 } ]


# Symbol.isConcatSpreadable プロパティをつけてあげると展開される
> arrayLike = {length:2, 0:"a", 1:"b", [Symbol.isConcatSpreadable]: true}
{
  '0': 'a',
  '1': 'b',
  length: 2,
  [Symbol(Symbol.isConcatSpreadable)]: true
}
> [].concat(arrayLike)
[ 'a', 'b' ]

面白いなー

Pattern-Matching Symbols

  • string.method(pattern, arg) は実際にはこう呼び出されてる→ pattern[symbol](string, arg)

RegExp のプロトタイプ見てみたらこうなってた:

> Object.getOwnPropertySymbols(RegExp.prototype);
[
  Symbol(Symbol.match),
  Symbol(Symbol.matchAll),
  Symbol(Symbol.replace),
  Symbol(Symbol.search),
  Symbol(Symbol.split)
]

へー。

例えば string.search(pattern, arg) の pattern のところに RegExp じゃなくて独自で Symbol.search メソッドを実装したクラスを渡せばそれが呼び出されるみたい。やらんけど。

Symbol.toPrimitive

  • オブジェクトをプリミティブに変換するのには toString()valueOf() が使われるけど、ES6以降では Symbol.toPrimitive でこの処理を上書きできる:

こういうことなのかな?:

> Number(1)
1
> Number({})
NaN
> Number({[Symbol.toPrimitive](){return 10;}})
10
> String({[Symbol.toPrimitive](){return 10;}})
'10'
> String({[Symbol.toPrimitive](){return "aaa";}})
'aaa'

### Template Tags

  • backtick で囲まれた文字列はテンプレートリテラルと呼ばれてるけど、関数の直後にテンプレートリテラルが続くものを “tagged template literal” と呼ぶ。
  • DSL を定義するのによく使われる。GraphQL用の gql や、Emotion の css など。

tagged template literal の関数定義には特別な文法はなくて、通常の関数定義と同じ。引数が以下の通りになっている:

  • 第一引数に文字列の配列
    • 変数の部分で区切られている。n個変数があったら、n+1個の文字列になっている。
  • 第二引数以降に変数の値

これは覚えておきたいなー。

### The Reflect API

  • これまでリフレクション系の処理は Object の static メソッドとかでやってきたけど、分かりやすいように Reflect というオブジェクトにもまとめ直したんかな。

### Proxy Objects

  • ES6 以降
  • ターゲットオブジェクトに対する処理をプロキシしてハンドラーに渡すことができる

まぁ、これもフレームワークを作るんじゃなくて、使う場合には、ほしい場面に出会うことはないだろうな。

### 今日の中で

普通に使うのは Symbol.iterator と Symbol.asyncIterator と tagged template literal くらいかな。

今日も面白かったー。残りはあと3章だけど、長そうな章たちだな。

VS Code Remote - Containers を Docker Compose で使うのだー!

VS Code の Remote - Containers プラグインを使うと Docker の中で開発ができて最高だよ、という記事を読んで面白そうなので触ってみた。

www.keisuke69.net

ちょっとぐぐったら、このプラグインに関する記事がいっぱい出てきた。結構前から人気なのね。知らなかったや。

## Remote - Containers って何なの?

は、だいたいこんな印象。Remote - Containers プラグインVS Code Server をコンテナの中にインストールして、ローカル側の VS Code がその VS Code Server とコミュニケーションする。

f:id:bufferings:20200608000048p:plain

それによって、実際はコンテナの中にある開発環境が、あたかもローカルにあるみたいな気持ちで VS Codeを使うことができる。だから、ローカル側に例えば PHP を入れてなくても、VS Code では PHPプラグインを動かして、コード補完やデバッグ実行などができる。便利。

  • VS Code の UI 拡張はローカル側で、Workspace 拡張はコンテナ側で動くらしい。雰囲気は分かる。
  • Docker のポートをローカル側にマッピングしておけば、Webアプリの動作確認なども可能。ですよね。

## Dockerfile or docker-compose.yml ?

最初に触ってみたときは少し戸惑った。

ローカルのソースコードは自動でコンテナの中にマウントされるっぽいのにされないし、ポートは特に devcontainer.json に書かなくてもマッピングされてそうだし、んー?ってなった。

けど、しばらく触ってみて、あぁ、これって Dockerfile を使ってる場合と docker-compose.yml を使ってる場合で設定の方法が違うっぽいな。と気づいた。僕は docker-compose.yml を使ってた。

もうしばらく触ってみて分かったのは、Dockerfile だけではできない部分を devcontainer.json で設定できたり、 Remote - Containers プラグインが自動的にサポートしたりしてるってこと。ポートのマッピングとか、ボリュームのマウントとか、環境変数の指定とか。ほほー。

docker-compose.yml を使う場合は、そういうサポートをほとんど必要としない。元々 Docker Compose にそういう機能がだいたい備わってるから。

じゃ、どっちを使おうかなと考えたんだけど、 docker-compose.yml を使えば Dockerfile でできることはカバーできるだろうから、 Dockerfile よりも docker-compose.yml を使う方向で進めてみることにした。

## やらないこと

  • メインで使う言語は、ローカルマシンで環境を整えたいなと思うので、このプラグインでは僕は使わないだろうなと思った。なので Java で使うつもりはない。今回は PHP で試してみてる。
  • 実行中のコンテナや k8s のコンテナにアタッチすることもできるみたいだけど、やらない。docker-compose.yml を用意してコンテナをビルドするー。
  • 複数プロジェクトで1つのコンテナ設定を共有することもできるみたいだけど、やらない。プロジェクトごとにコンテナ設定を用意する。
  • 設定は .devcontainer/devcontainer.json.devcontainer.json のどちらかが選べるけど、後者は使わない。ディレクトリを用意する。中に色々置きたいから。

さて、進もう。

## Docker Compose を使う場合の情報

を知りたいなという観点で、公式ドキュメントを読んだ。で、満足した。2020-06-07 時点の情報。

code.visualstudio.com

code.visualstudio.com

こういう観点でメモを残しておこうと思う。ほぼ忘れているであろう来月の自分のため。

  • (1) devcontainer.json の仕様 (for docker-compose.yml)
  • (2) Dockerfile を使った場合と docker-compose.yml を使った場合の違い
  • (3) その他、覚えておきたいことのメモ

## (1) devcontainer.json の仕様 (for docker-compose.yml)

https://code.visualstudio.com/docs/remote/containers#_devcontainerjson-reference

に仕様が書いてある。docker-compose.yml を使う場合は、以下の部分だけを知ってれば良さそう:

"Docker Compose" の部分

dockerComposeFile

  • 文字列または配列。必須。
  • devcontainer.json からの相対パスで docker-compose.yml を指定
  • 元々存在しているファイルなどを書き換えずに拡張したい場合には、複数ファイルを指定することが可能で、その場合は配列で指定。
  • プロジェクトのルートから .env を読み込むっぽい。docker-compose.yml の env_file で別のファイルを指定できるっぽい。

感想

  • たぶん僕は配列での複数ファイル指定は使わないし、基本的には .devcontainer の下に docker-compose.yml を置くと思うので docker-compose.yml と書くだけになりそう。
  • Laravel みたいに .env を使ってる場合は、変な感じにならないといいんだけどな。覚えとこ。

service

  • 文字列。必須。
  • docker-compose.yml の中のどのサービスに VS Code が接続するかを指定

感想

  • この設定自体は問題ないんだけど、ここで指定した以外のサービスは起動するのかな?どうなのかな?と思ったら次に書いてあった。

runServices

  • 配列
  • docker-compose.yml の中のどのサービスを実行するか
  • デフォルトは全てのサービス

感想

  • 基本的に docker-compose.yml には起動したいサービスを書くので、この設定は書かないだろう。デフォルトの動きで問題ない。

workspaceFolder

  • 文字列
  • VS Code がコンテナに接続するときに開くパス(コンテナの中のパス)
  • デフォルトは"/"
  • 普通はマウントしたソースコードのフォルダーを指定する

感想

  • そだね。マウントしたソースコードのフォルダーを指定すると思う

remoteEnv

  • name-value ペアのオブジェクト
  • リモート側の VS CodeVS Code が起動するプロセス(ターミナルなど)で利用できる環境変数を上書き。コンテナ全体の環境変数ではない。

感想

  • 今の所、必要なさそう。

remoteUser

  • 文字列
  • リモート側の VS Code を実行するユーザー
  • デフォルトはコンテナが使うユーザーと同じ

感想

  • Linux を使う場合は気にしておいたほうが良さそうだな。ファイルのオーナーが root にならないように。MacWindows だとその辺りは気にしなくて良さそう。

shutdownAction

  • none または stopComposestopCompose がデフォルト。
  • VS Code (Remote) が閉じられたときに、コンテナを停止するかどうか

感想

  • VS Code (Remote) を閉じたら docker-compose は停止してほしいので、デフォルトのままでいいな。

General の部分

General の部分にはこれ以外にも指定できる項目があるけど、 Docker Compose 使うならこれくらいで大丈夫だと思う:

name

  • 文字列
  • 表示名

感想

  • 適当で大丈夫。

extensions

感想

  • コンテナ側で開いた後にプラグインを右クリックすると devcontainer.json に追加できるので、そこから追加するのが楽
  • 例えば PHP だと PHP Extension Pack を追加したりとかそういうの

settings

  • VS Code の設定を書いておける

感想

  • PHP Extension Pack の設定とかあったら書いておくと便利なのかな

postCreateCommand

  • 文字列または配列
  • コンテナの作成が終わって起動するときに実行したいコマンドを書いておくことができる

感想

  • ソースコードもマウントされた状態で実行されるので composer install とか書くと良さそう
  • 毎回なのかな?1回だけなのかな?と思ってみてみたら1回だけっぽいな。 /root/.vscode-server/data/Machine/.postCreateCommandMarker というファイルがなければ実行してそのファイルを作成するっぽい。

結局

のところ、書くのはこれくらいだな:

{
  "name": "My PHP Application",

  "dockerComposeFile": "docker-compose.yml",
  "service": "php-fpm",
  "workspaceFolder": "/workspace",
  "extensions": [
    "felixfbecker.php-pack"
  ],
  "settings": [],
  "postCreateCommand": "composer install"
}

.devcontainer の中身

.devcontainer ディレクトリの中に docker-compose.yml を入れて、その隣にビルドに必要な Dockerfile などを置いておくのが好きだなと思った。こんな感じ:

❯ tree .devcontainer 
.devcontainer
├── devcontainer.json
├── docker
│   ├── app
│   │   └── Dockerfile
│   └── web
│       └── default.conf
└── docker-compose.yml

## (2) Dockerfile を使った場合と docker-compose.yml を使った場合の違い

以下の設定は docker-compose.yml を使う場合は devcontainer.json じゃなくて docker-compose.yml の設定で書く

  • ポートマッピング
  • 環境変数
  • ボリュームマウント
  • コンテナのユーザー
  • コンテナの起動コマンドや引数

でも、デフォルトの動作が Dockerfile を使う場合と異なり、 docker-compose.yml では明示的に指定しないといけないこともある。

起動コマンド

Dockerfile の場合はコンテナのデフォルトコマンドを上書きして /bin/sh -c "while sleep 1000; do :; done" にしてしまうらしい。コンテナが終了してしまわないように。

でも docker-compose.yml の場合は自分で command にこれを指定しないといけない。

# プロセスが終了してコンテナが終了してしまわないように上書きする
command: /bin/sh -c "while sleep 1000; do :; done"

ただ、今回僕が使ってみたのは php-fpm のコンテナでコマンドが終了しないので、これはやらなくても良さそう。

ボリュームマウントのタイプ

MacOS の場合は Remote - Containers プラグインはデフォルトでボリュームマウントの consistency に cached を使うらしいんだけど、docker-compose.yml を使う場合は明示的に指定しないといけない(んだと思う):

volumes:
  - ..:/workspace:cached

cached はホストOS側の変更がコンテナ側に伝わるのに遅延があるけど、パフォーマンスが良くなる。

Use bind mounts | Docker Documentation

コンテナユーザーの UID/GID

僕は Ubuntu を使ってるんだけど Linux の場合はボリュームマウントを使ったときに、コンテナのユーザーの UID/GID でファイルが書き込まれてしまうから例えば root ユーザーを使うと全部 root で書き込まれてしまう。Mac だとホスト側のユーザーになるんだけどね。Windows は知らないけど Mac と同じなんじゃないかなと予想。

なので Linux でボリュームマウントを使うときは UID/GID を指定しておかないといけない。Remote - Containers で Dockerfile を使ってる場合は自動でホスト側の UID/GID を使ってくれるみたい。いいなぁ。 docker-compose.yml の場合はそういう対応はないので、自分で指定する。僕のユーザーは 1000 だからユーザーさえ指定してれば基本的には大丈夫だけど。

## (3) その他、覚えておきたいことのメモ

Git

https://code.visualstudio.com/docs/remote/containers#_sharing-git-credentials-with-your-container

  • Git はコンテナの中に入れておくと良さそう。VS Code が変更点とかを表示してくれるから。
  • でも push とかの操作はローカル側からやろうかな。SSH キーのことを考えたり、コンテナの中のターミナルを自分に合うようにするの面倒だし。

とはいえ、コンテナの中から Git を使うのに便利なように考えてくれてる。

  • ~/.gitconfig は自動的にマウントされる
  • SSH Agent を使ってると自動的にフォワードされる

なので、特に気にせずに Git を VS Code (Remote) の中で利用できそうではある。

Named Volume

https://code.visualstudio.com/docs/remote/containers-advanced#_improving-container-disk-performance

MacWindows のボリュームマウントは遅いので、 vendor や node_modules のようにファイルがたくさん置かれるものを bind してしまうと読み込みがすごく遅くなってしまう。

かといってボリュームを使わなかったら毎回クリアされてしまって、毎回 composer install でライブラリーを引っ張ってくるのは、ない。

ということで、 named local volume を使う。そうすれば bind はされないけどコンテナを再起動しても情報は残る:

version: '3'
services:
    app:
        build: docker/app
        working_dir: /workspace
        volumes:
            - ..:/workspace:cached
            - app-vendor:/workspace/vendor

(...)

volumes:
    app-vendor:

root 以外でコンテナを実行している場合は postCreateCommand で初回にオーナーを更新しておく:

"postCreateCommand": "sudo chown user-name-goes-here vendor"

だいたいこれくらい知っておけば良さそうかなぁ。

JavaScriptの勉強中:その8 Promise / async await

ハイボール片手に。今日は Promise やるぞー。

bufferings.hatenablog.com

## 13.2 Promises

  • callback だとネストが深くなる→ Promise だとリニアに書ける
  • callback だとエラーハンドリングが難しい→ Promise だとエラー処理の方法が標準化されてる

わかる。

Promiseの用語

  • "fulfillled" - 最初のコールバックが呼び出されるとき
  • "rejected" - 2番目のコールバックが呼び出されるとき
  • "settled" - "fulfilled" か "rejected" になったとき
  • "pending" - まだ "fulfilled" でも "rejected" でもないとき
  • "resolved" - あとで説明する

"resolved" かー、なるほど?(わかってない

Promiseのチェーン

  • then() は新たな Promise オブジェクトを返す(p1 と呼ぶことにする)。この Promise は then() に渡した処理が終わると fulfilled になる。

もう少し詳しく言うと(resolvedの話)

  • then() に渡した処理が Promise 以外の値を返すと、then() の返した Promise (p1) は fulfilled になる
  • then() に渡した処理が Promise を返すと(p2)、then() の返した Promise (p1) は resolved にはなるが fulfilled にはならない
    • p2 が fulfilled になると p2 と同じ値で p1 も fulfilled になる
    • p2 が rejected になると p2 と同じ値で p1 も rejected になる

へー。 resolved と settled が違うんだねー。 Promise が Promise を処理するときに差が出てくる感じだな。

エラーハンドリング

  • .then() の2番目の引数に関数を渡してあげると、処理中に例外が発生したときにその関数が呼び出される
  • けど、実際はそうすることは少ない。 .catch() を使う。 .catch(handleError).then(null, handleError) のショートハンド
  • 例外が発生したら、そこから .catch() があるところまでチェーンを降りていく
  • .catch() の後に処理が続く場合、.catch() が普通に値を返すと、その次の処理にうつる

finally()

  • ES2018
  • コールバック関数は引数を受け取らない
  • .finally() のコールバック関数が返す値は基本的には無視される
  • .finally() で返された Promise は、その前の Promise の結果の resolved または rejected の値を返す
  • ただし .finally() で例外が投げられた場合はその値で rejected になる

へー。1個前の値が渡されるのかー。

Promise.all()

  • を使うと並列に実行することができる
  • Promise の配列を引数として受け取る
  • 戻り値の Promise は、入力値の Promise が1つでも rejected だったら rejected になる
  • 最初に rejected になった Promise が出た時点で、まだ実行中の Promise があっても、すぐに rejected になる
const urls = [ /* URLs */ ];
let promises = urls.map(url => fetch(url).then(r => r.text()));
Promise.all(promises)
    .then(bodies => { /* 処理 */ })
    .catch(e => console.error(e));
  • Promise.all() は実はもうちょっとフレキシブルで、入力値の配列の要素は Promise じゃなくても大丈夫。その場合は fulfilled された Promise のように扱われて、そのまま出力配列にコピーされる

あんまり使わなさそうかなぁとは思うけど、一応片隅に置いとくかな。

Promise.allSettled()

  • ES2020
  • all() のように Promise の配列を受け取る
  • rejected になることはない。全ての Promise の処理が終わるのを待って fulfilled になる
  • 結果はオブジェクトの配列になっていて、各要素に status プロパティがあり "fulfilled" または "rejected" がセットされている
  • "fulfilled" の場合は value プロパティに値が、"rejected" の場合は reason プロパティにエラーまたはリジェクトの値が入っている
> Promise.allSettled([Promise.resolve(1), Promise.reject(2), 3]).then(results => console.log(results));
Promise { <pending> }
> [
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 2 },
  { status: 'fulfilled', value: 3 }
]

こっちのが all() より扱いやすいね。好きな感じ。

Promise.race()

  • 同時に実行して最初に fulfilled または rejected になったものだけを返す。

Promise を書く

  • 非同期に処理する必要がないものを Promise にして返したい場合は Promise.resolve()Promise.reject() を使えばいい
  • 新たに Promise を生成したい場合は Promise コンストラクターを使う
Promise p = new Promise((resolve, reject) => {
});
  • resolve 関数を使うと resolved または fulfilled になる
  • reject 関数を使うと rejected になる

## 13.3 acync and await

  • ES2017
  • この2つの新しいキーワードのおかげで Promise がすごく簡単に使えるようになって、Promise ベースの非同期のコードが同期のコードみたいに書けるようになった

await

  • await キーワードは Promise を受け取って、それを通常の値または例外に変換する。
let response = await fetch("/api/user/profile");
  • 同期処理のように書けるけど、実際は非同期処理になっている。なので await を使う関数それ自体も非同期な関数じゃないといけない。

try catch 書けるの幸せ。

async

  • await キーワードは async キーワードがついた関数の中でしか使えない、というルール。
async function getHighScore() {
    let response = await fetch("/api/user/profile");
    let profile = await response.json();
    return profile.highScore;
}
  • async な関数の戻り値は Promise になる。通常の値を返していたらそれが resolve された Promise を、例外を投げたらその例外で reject された Promise を返す。

お、あっさり async await が終わってしまった。シンプルでパワフルだなー。

## 13.4 Asynchronous Iteration

for/await ループ

  • ES2018

単に Promise の配列の場合、こんな風に書くことができるけど:

for(const promise of promises) {
    response = await promise;
    handle(response);
}

for/await ループを使うとシンプルにこう書ける:

for await (const response of promises) {
    handle(response);
}

これは普通の Iterator を使った場合で、ちょっとシンプルになるくらいだけど、Asynchronous Iterator の場合はもっと面白い。

Asynchronous Iterator

  • Symbol.iterator じゃなくて Symbol.asyncIterator でメソッドを定義する。
  • next() は Promise を返す。

ちなみに for/awaitSymbol.iterator に対しても動くけど、まず先に Symbol.asyncIterator が定義されていないかを探す。

Asynchronous Generators

  • async function * で Asynchronous Generator を定義できる
  • 内部では await を利用できる
  • yield で返される値は自動的に Promise でラッピングされる
async function* clock() {
    for(let count = 1; count <= 10; count++) {
        await new Promise(resolove => setTimeout(resolove, 100));
        yield count;
    }
}

async function test() {
    for await (let tick of clock()) {
        console.log(tick);
    }
}

test();

今まで勉強してきたのの組み合わせって感じね。

Asynchronous Iterators

Symbol.asyncIterator でメソッドを定義するだけだな。

class Clock {
    async *[Symbol.asyncIterator]() {
        for(let count = 1; count <= 10; count++) {
            await new Promise(resolove => setTimeout(resolove, 100));
            yield count;
        }
    }   
}

async function test() {
    for await (let tick of new Clock()) {
        console.log(tick);
    }
}

また別の例で、AsyncQueue を実装して、enqueue より先に dequeue を呼び出すことができるのとか面白かった。dequeue が unresolved な Promise を返すから。

Async なイテレーションは、実際にそういうのを使いたい場面に出くわしたら、もっとしっくりくるんだろうな。

はい、今日も面白かったー。