日々常々

ふつうのプログラマがあたりまえにしたいこと。

「あるエンジニアがプログラムを紡いでいく様を見てみる」ライブコーディング・リプレイ

あるエンジニアがプログラムを紡いでいく様を見てみるでしたライブコーディングで言ったことや言わなかったことを書いてみます。

意識してるのは「コードをどまんなかに」です。

speakerdeck.com

……あ、このスライドのブログ書き忘れてた。

スライド中の「えらぶ」はだいたいIDEの機能を指します。なのでライブコーディング中に使用したIDEの機能も挙げようと思います。基本的にデフォルトのつもりだけど、vimとの兼ね合いで変更してるのもあるので、そこはごめんなさい。あとMacです。今回はメソッド抽出とかクラス間移動とかダイナミックなのがなくて地味だけど、便利な子たちなので使ってあげてください。

リプレイ

ってことなので、コミットを追いながらいってみます。私の意図はどの程度コードに残ってるのかなー。

github.com

■最初はREADME

会場で何をするかを話して、READMEに書いてみました。思ったよりもでなくて、参加された方もどう参加したものかと手探りだったんだと思います。

■プロジェクトを作成して動作確認

Javaアプリケーションを作るときにまずやってること - 日々常々 で書いた内容ですね。 見事に 動作しませんでした 。本番あるある。

原因はオフライン時のライブラリ解決です。いつも通りと言いつつネットワークの無い状態なのでいつも通りじゃない。普段と違うことをしたら詰まるのは当たり前です。てことでネットワーク繋げて再開しました。(数時間前にネットワーク切断して素振りした時は大丈夫だったんです。ほんとですよ?)

SpringBootのApplicationクラスを作って実行、動作した、よし。ってところで最初の10分で目標にしてたところをクリア。

使用したIDEの機能

  • 前回 挙げたやつ
  • Alt + Ctrl + R Mainやテストの実行

使用するものの選択

build.gradle に何を使うかを書きます。今回はJava 8、Spring Boot 2.0とJUnit 5、Assert J。

Spring Bootを使うのは後でSpring Shellを使おうと思っていたと言うのもありますが、Isolating the Domainに準じて作るつもりだったので。

■パッケージ名変更

前段であがった「 tododototypoしている」の指摘はあえてスルーしてました。typoを直すよりも動作確認を優先したかったからです。仮にtypoを直したとしても動作しなきゃ無駄になるし、typoは動作に影響しないことは知っているので、後回しです。動作確認ができたので、ここでリネーム。もちろんリネーム後はアプリケーションを実行して確認します。

新たに使用したIDEの機能

  • Shift + F6 ファイル名のリネーム
  • Alt + R 前回実行したものの再実行

■初期モデル作成

環境ができたら「TODOリスト」に対して現在捉えてるモデルを書きます。 実装としてはYAGNIですが、ホワイトボードや紙に走り書きするのと同じ感じでラフにコーディング。

f:id:irof:20180308171121p:plain

たまにクラス図を眺めてみたり。この時点で頭の中で描けないようなのは詳細に立ち入りすぎかなーとも思います。

「先に設計しますよね?」

「先にある程度考えてたんですか?当然、普段は先に設計しますよね?」と言った質問がありましたが、冒頭に挙げた通り「コードをどまんなかに」なので、これが初期設計です。その時点でわかっていることを雑にコードで書きます。ただし、この時点はモデルの関連だけ。基本的に操作は書きません。この日は控えめだけど、基本的には認識してるものは全部書くので、もう少し多くなる。かも。

強調しておきますが、これは「初期モデル」です。絶対変わるし、間違ってる。立ち止まって落ち着いて確認する時間は後から何度でも取ります。欲しいのはコードからのフィードバック。間違っても完成と思わないくらいにラフなのを、ささっと短時間で書きます。間違ってたとしてもいいんです。と言うか、間違って捉えてたことのフィードバックを得るために書いてるところがあります。

新たに使用したIDEの機能

  • Alt + Enter (空気読め機能)コンパイルエラーのフィールド型名から新規クラス作成
  • + N コンストラクタ作成
  • Tab 補完
  • + E 最近開いたファイル

■タスクが取れる

TODOリストなんで、タスクがみれなきゃ困ります。 テストから書きたいところですが、テスト対象クラスでテストへ切り替えのキーを押せばテストクラスを作ってくれることは知っています。必ずセットで作るのはわかっているので、テスト対象クラスから作ります。無理矢理テストから作っても遅くなるだけなので。クラスを作り、テストクラスを生成し、テストを書き始める。ファイルの移動も少なくてスムーズです。メソッド名が微妙だなーと思いつつ、REDからGREENへ、とにかく通す。

テストメソッドは忠実にアサートファーストで書きます。だけど先走って型をStringにしてしまって「あれ?」と思ってたら、うらがみさんに突っ込まれました。素直に直したら「感心してたのに・・・」と残念がられました。くそう。

新たに使用したIDEの機能

  • + Shift + T 新規テスト作成
  • test + Tab (LiveTemplate)テストメソッド作成
  • Alt + Enter (空気読め機能)
    • コンパイルエラーのローカル変数名からローカル変数定義作成
    • コンパイルエラーのローカル変数型名から新規クラス作成
    • コンパイルエラーのメソッド呼び出しから対象クラスにメソッド作成
  • TodoService.new + TabPostfix)コンストラクタ呼び出しに変換

■メソッド名変更

スペルチェックで警告だされてるし、それほどこだわりのない名前なので、メソッド名を変えます。割れ窓理論信者なので、コードを健全な状態で保ちたいのです。警告が多いと気づかなくなるのが勿体無い。もしこの名前にこだわりがあるなら、スペル辞書に登録して警告を出なくします。警告が出ている状態はREDに近い扱いですね。

新たに使用したIDEの機能

  • スペルチェック(勝手に波線が出てるだけ)
  • Ctrl + Tリファクタリングメニュー)メソッド名のリネーム
    • Shift + F6 でもいいのだけど、押しにくいので Ctrl + T で選ぶ(と言っても一番上なのでEnter押すだけ)ほうが早い

■タスクが登録できる(RED)

ツッコミがあったところ。賛否両論、と言うか賛はあまりない感じでしたかね。

TODOリストの肝であるタスク登録に着手します。ベイビーステップで行くこともありますが、これくらいならいけるだろうと大きめの歩幅でいきます。ダメだったら戻ればいいんですよ。

まず検証方法を考えます。先ほどとれるようになったタスクをユーザーが見る場合、何かしらの形で文字列化されるはずです。なのでタスクを文字列化したらタスク名が入っている、としましょう。

-        assertThat(actual).isNotNull();
+        assertThat(actual.asText()).contains("yarukoto");

で、これが登録できればいい。タスクを登録する。

+        sut.add(task);

タスクはタスク名をつけて作る。タスク名はさっきアサーションで書いたやつ。

+        Task task = new Task(new TaskName("yarukoto"));

アサートファーストで下から順番。アサート書いて、それを実現するための操作(タスク登録)を書いて、必要な準備(タスク生成)をする。

新たに使用したIDEの機能

  • Alt + Enter (空気読め機能)
    • コンパイルエラーのコンストラクタ呼び出しから対象クラスにコンストラクタ作成
    • コンストラクタの仮引数名からフィールドを作成

「テスト変えちゃうの?」

元のテストを残さずに変更しています。タスク登録してない状態のアサーションがなくなりました。これで「タスク登録しないとnullかもしれない」と疑心暗鬼になれます。

変更してしまう理由はいくつかありますが、まず第一にタスク未登録でnullになるような実装はしないので変更前の検証がエラーになることはありません。基本的に初っ端に書いたクラスやメソッドを導くためのテストは、メンテに向いていません。テストリファクタリングをしてもいいのですが、落ちないテストに価値はありませんし、未登録時のふるまいなんかはそのうちモデルとテストが要求してきます。なので「未登録時はnullでない」程度のテストは維持せず変えちゃうわけです。

ドメインモデルとしては……」

asTextドメインにあるのがおかしい」と言った指摘がありましたが、ドメインは自身が文字列で表現されるときにどうするかを知っていると思います。「メソッド名 asTextドメイン感がない」と言ったところはあるかもしれませんが、妥当な名前が出てくればその時変えればいいです。この時点ではそれなりな名前で十分だと思っています。名前には閉じた世界の中での一貫性が重要で、その検討をするにはドメインの理解や登場するモデルの数が不足しています。この時点でメソッド名の検討は時期尚早。早く作ってフィードバックを得るのが良いです。材料が少ない状態で検討しても的外れになりやすいですし、何より分析麻痺になるのが怖い。よほど精通したドメインでもない限り、モデルが出揃ってない状態で検討できることではありません。

コードスメル: 知識がテストに書かれている

このコードは少し異臭がします。「TaskのコンストラクタがTaskNameを受け取る」と言う知識はテストクラスが持っているべきではないことでしょう。Taskインスタンスの生成方法を知る必要はありません。こう言うのがテストにあると、Taskを要求する全ての場所に同じコードがクローンされます。そこはかとなく臭いので、たぶん近い将来このコードはテストからは消え去ると思われます。でもまだ重複していないので、何かを考えるには時期尚早という判断をします。本当に複数箇所にクローンされるのか、生成のバリエーションが出ないか、などを少し待ってみてもいいかなー、と思ったり。もちろん油断すると大変なことになるので、臭いには気をつける。

■タスクを登録できる(GREEN)

さて、最速でテストを通しましょう。addされたタスクをリストに保持し、asText メソッドはタスクリストを文字列表現すればよい。タスクはタスク名を文字列表現できればよい。タスクの文字列表現は、とりあえず toString でいいや、と。余計な色気は出さず、とにかく最速でGREENにします。(それでもなんか詰まった気がするけど、忘れた。)

実はこの時点でリストを持ち出すのはYAGNIで、テストを通すだけならフィールドで十分だったりします。でも気にせずリストにしちゃいます。フィールドだとnullが登場しかねないから避けたいと言うのもある。リストを導くために複数登録するテストにしちゃうのはありかもだけど、面倒なのでしない。

新たに使用したIDEの機能

  • + Alt + F ローカル変数のフィールド化

「テストの検証が厳密でない」

この実装だと実際の文字列は "[yarukoto]" で、テストの検証は contains("yarukoto") になっていますす。当然 isEqualTo("[yarukoto]") と書いてもテストは通りますが、後者でないことに違和感があったようで、疑問の声がありました。

前段のテストで描いた未来は「登録したタスク名が読み取れる」です。登録した文字列が含まれてればOK。前後に何がついていようと別に困りません。困るのは "yarukoto" を登録したのに "yarukoto" って文字列が見当たらないこと。なので contains を選択しています。Taskのequalsなどで検証していないのは、同じTaskかどうかにあまり興味はないことと、文字列化はアプリケーションに必要なことと、テストのためだけのhashCode/equalsは開発を加速しないからです。

「登録したタスク名が読み取れる」は、おそらく完成したとしても変わりません。テストはピンポイントで重要なところだけ抑えないと、テスト対象をコールタールに沈めて身動き取れないものにしてしまい、開発の足を引っ張ります。例えば仮に isEqualTo("[yarukoto]") としてしまうと、テストが ArrayList の文字列表現に依存します。本来そこに依存関係はないので実装詳細に寄りすぎる検証となり、例えば「改行区切りにしたい」となったら即死です。(「改行区切り」を実現したくなったら、複数件を登録して「改行区切りになるよー」ってテストを書くとは思いますが。)

mdstoy.hatenablog.com

現地ではうまく説明できなかったと思っていたのだけど、存外伝わってたようでほっとしました。

やってみて

ってところで、ちょいちょい説明しながら、ネットワークとキーボードのトラブルもあったけど、合計20分とちょっとかな?最後はSpringShellを紹介するのに使いました。

「説明できるのが良いコード」としょっちゅう言ってる私ですが、だいたいこんな感じで自分に説明(言い訳)はしてる感じです。無意識にやってるものも多いですが、たまに確認はしてます。 でも今回のイベントで他人に説明するのは面白かったです。思った以上に言葉が出てこず、伝わってる感じもしなくて。変なこと言っちゃってるなーと内心ドキドキしながらやってました。

そういえばスライドでは「説明する機会はない」とか言ってたけど、なんかその機会貰えましたね。ありがとうございました。

テスト駆動開発

テスト駆動開発

せっかく買ったし、久々に読み直そう。