iPad でクラス図をサクサク描ける astah pad を久々に使ってみた
iPad Air を弄っていたら数年前にインストールした astah pad を見つけたので久しぶりに起動してみた。
リビングのソファーでクラス図を
このアプリは何と言ってもUXがとても使いやすくてタッチ操作でも直感的にサクサクとクラス図を描けてしまうのが良い。
特に、配置したクラスを指先で自由に動かせたりクラス間の関連線を簡単なドラッグ操作で繋げられたり出来るので、デスクトップアプリを起動して「クラス図を描くぞ!」と意気込まなくても、フリーハンドに近いラフな操作感でリビングのソファーでテレビを観ながらでも気軽に描き始められてしまう。
SpecDown の概念図を描いてみた
試しに マークダウンのリストから spec の雛形に変換するツールを作ってみた - volpe’s diary で作った SpecDown の概念図みたいなものを描いてみた。 クラス図として完全にはソースコードと一致しているわけではないけれど、「こんなオブジェクトがあってここは抽象化して関連があって具象オブジェクトがあって..」みたいな概念を表現している。
このくらいの図であれば 10分くらいでサクッと描ける。
趣味プロダクト向きかも
機能としては描いたクラス図を画像として出力することしか出来ないので、ドキュメント管理をしっかりやるチーム開発向きとは言えないけれど、趣味のプロダクトでセカンドディスプレイ代わりに iPad を手元に置いといて、ラフにモデリングしながらコード書いたりするにはぴったりかも。
実運用でもコードを書く前の事前設計だったり部分的なコードをヴィジュアル化して、画像を Github のプルリクエスト等に貼り付けたりして共有するのには使えそう。
iPad にとりあえず入れておくと良さそう
調べてみるとまだちゃんと提供されているっぽい。
apps.apple.comiPad があればとりあえず入れておいて、いつでもどこでもクラス図を気軽に描けるのって素晴らしいですね。
やっぱり素晴らしいUXは何年経っても色褪せないなぁ。
マークダウンのリストから spec の雛形に変換するツールを作ってみた
新しいクラスを追加するときなど、ある程度まとまった spec を書く必要がある場合、どんな感じでテストケースの設計をしているでしょうか?
マークダウンを使ったテスト設計
僕はマークダウンエディタ(notionなど)を使って、リスト記法でテストパターンを箇条書きにして設計することが多い。 リスト記法だと階層の深さで事前条件や期待値を表現できるし全体の構造を俯瞰しやすいし、改行時に入力補完されたりタブキーでの階層の上げ下げが容易だったりして試行錯誤するための敷居がとても低く感じる。 少なくともいきなりプログラミング言語で書き始めるよりはテスト設計に集中できる。
例えばこんな感じ。
- d: 新しいクラス - d: モード取得 - c: ほげの場合 - i: モード1が返ること - c: ふがの場合 - i: モード2が返ること - c: 上記以外の場合 - i: XXX例外をスローすること
各行にはマークダウンのリスト記法に独自のシンボルを付けて意味づけをしている。 (d は describe, c は context, i は it)
マークダウン上での試行錯誤がある程度済んだらそれを実際の spec ファイルにコピペして1行1行を言語に変換する。 ただこれが結構しんどい。単純作業で置換コマンドなどを駆使しても閉じ括弧とかもあるしなかなかのストレスフルな作業だ。
SpecDown
そこで、変換ツールがあったらいいなー、と思って探してみたけど、ありそうで意外と見つけられなかったので今回は SpecDown というWebアプリを作ってみた。
左側のテキストエリアにマークダウンを貼ると右側に実際の spec のコードテンプレートが表示される。 右上のセレクトボックスで言語の選択も可能で、今のところ良く使いそうな rspec と jest に対応してみた。
右上のボタンを押すとクリップボードにコピーされるのでそのままコードエディタに貼り付けられる。 これで、これまで多少時間のかかっていた「マークダウンでテスト設計」 → 「テストコードの実装」 がシームレスに移行出来るのでテストの実装が捗る!
対応言語の追加
各言語に依存した部分は、本体のロジックから分離しているので比較的容易に追加出来ると思う。
例えば rspec の場合は以下のような感じで、リストのシンボルに対応したコードを定義する。
{ name: 'rspec', format: 'ruby', symbols: { d: (body) => `describe '${body}' do`, c: (body) => `context '${body}' do`, i: (body) => `xit '${body}' do`, b: (body) => `before do ${body.length !== 0 ? '# ' + body : ''}`, a: (body) => `after do ${body.length !== 0 ? '# ' + body : ''}` }, comment: (body) => `# ${body}`, terminal: 'end' },
https://github.com/volpe28v/Specdown/blob/master/services/SpecRenders.js
このファイルに追加しておけば、実行時にセレクトボックスの言語に追加されて選択可能になる。
システム構成
構成は Nuxt のフレームワークで書いたシンプルな SPA で、 heroku に上げて運用している。 こういう単機能のシンプルツールは Nuxt + heroku だと簡単に作れて捗る。 Github と heroku を連携しておけば、master に push するだけで heroku へデプロイされてとても楽チン。
実はこのWebアプリを作る前に、rubyの練習がてらコマンドラインツールとして作ってみたんだけど、マークダウンを一旦ファイルに保存してからそのファイルを食わすというインターフェースだと変換までの手順が多くて実際にあまり使わなくなってしまった。 やっぱりWebインターフェースはいいですね。
実際に使えるデモページはこちら https://specdown.herokuapp.com/
Github の PullRequest をコマンドラインで表示出来るようにしてみた
モチベーション
フリーランスになって Github のプルリクエスト駆動で開発するようになって、今現在アクティブなプルリクエストのリストをいちいちブラウザを開かなくても手元のシェルで確認したい気持ちが高まってきた。
また、人のプルリクをレビューする際などにちょっと階層が深い複雑なブランチ構造のプルリクを開くと、自分は一体何の差分をレビューしているのか見失うことがあるので、プルリクのブランチ階層構造をサクッと把握したい。
コマンドラインツールを作ってみた
そこで、Github のAPIを使って pr-tree
というコマンドラインツールを作ってみた。
なるべく簡単にインストール出来るように ruby の実行ファイル一個にまとめた。
リポジトリを clone し、Github の GITHUB_API_TOKEN
を取得し、パスの通ったディレクトリにシンボリックリンクを置くことで簡単に使える。
実際の動き
シェル上で任意の Github リポジトリのディレクトリに入って、 pt-tree
と打つことで以下のようにプルリク一覧がブランチのツリー構造で表示される。
現在誰がどのプルリクに着手しているか分かるし、ブラウザで見たければ command
を押しながら URL をクリックすると瞬時に開く(iTermの場合)。
大きめのフィーチャーブランチから子プルリクを生やしてる場合などもツリー構造を簡単に把握できてレビューが捗る。
最近追加した機能としては、親ブランチの最新から乖離がある子ブランチの先頭に *
を表示するようにして、出来るだけ素早くリベースするなりして親ブランチから置いて行かれないようにしている。
実運用で使ってみている
なんとなく欲しくなったものを ruby で Github API を使う練習なるなぁ、くらいの気持ちで作ってみたけどしばらく使っているうちに手放せないツールとなった。お仕事をいただいている現場のメンバーにも共有したら早速使ってくれてる人もいて嬉しい。
もしかしたらもっと便利なツールが世の中には転がっているかもしれないけど、自分で作れるものは自作するのも楽しくていいな。
Rails で migrate 時に実行される SQL を確認する
以前の記事 のように migrate 時に data メソッドを使ってデータ移行しようとした場合に、実際に db:migrate
した際に発行される SQL を確認したい。
なぜなら、spec で単体では上手く動いても、まとめて実行した際に意図した動作にならない時にデバッグしたいから。
やり方としては、ログを表示する rake ファイルを追加し、db:migrate
時に一緒に実行させてあげると、標準出力にSQLのログが出力される。
lib/task/log.rake
task log: :environment do ActiveRecord::Base.logger = Logger.new(STDOUT) end
コマンド
$ bin/rails log db:migrate
地味に便利。
Rails で DBカラム変更と同時に既存データを移行する
既に本番稼働中のサービスでDBのカラムを変更する場合に、デプロイと同時に既存データを移行しないと整合性が取れない場合がある。
rake タスクとしてデータ移行処理を記述しておいて、本番サーバで実行する方法などもあるけど、 migration_data
gem を使うとマイグレーションと同期してデータ移行処理を叩いてくれて便利だったので簡単な使い方とハマったポイントをメモしておく。
データ移行の処理を書く
データ移行の処理をどこに書くかというと、 migration ファイルの data
メソッドに書く。
公式サイトから。
class CreateUsers < ActiveRecord::Migration def change # Database schema changes as usual end def data User.create!(name: 'Andrey', email: 'ka8725@gmail.com') end def rollback User.find_by(name: 'Andrey', email: 'ka8725@gmail.com').destroy end end
data
メソッドにデータ移行の処理を書いておけば、 rails db:migrate
時に実行してくれるとのこと。
rollback
は rails db:rollback
時に実行してくれるのかな。
spec を書く
data
rollback
に書いた処理は spec でテストを書くことも出来る。
データ移行はとてもセンシティブな処理なのでテストが簡単に書けるのはとても心強い。
再び公式サイトから。
require 'spec_helper' require 'migration_data/testing' require_migration 'create_users' describe CreateUsers do describe '#data' do it 'works' do expect { described_class.new.data }.to_not raise_exception end end describe '#rollback' do before do described_class.new.data end it 'works' do expect { described_class.new.rollback }.to_not raise_exception end end end
ハマったポイント
ただ、spec が通ったからと言って本番環境で通るとは限らない。
特に複数の migration ファイルをまとめて実行する際に以下のような問題に遭遇した。
- model と カラムの不一致でデータの保存が出来ない
- 古いカラム情報が残って正しくデータの更新が行えない
1つずつ解説する。
model と カラムの不一致でデータの保存が出来ない
例えば1つ目の migration ファイルで Child
テーブルのカラム hoge
を追加して、2つ目の migration ファイルで同テーブルにカラム fuga
を追加しつつ model では fuga
の validates :fuga, presence: true
を行ってるとする。
そして、hoge
への更新処理を1つ目の migration ファイルの data
メソッドに activerecord-import
の gem を使って以下のように書いているとする。
以下のようなパターン
- migration ファイル1
- children テーブルにカラム
hoge
を追加 - data メソッドにデータ移行処理を記述
- children テーブルにカラム
def data children = Child.includes(:parent).map do |child| child.hoge = child.parent.old_hoge child end Child.import children, on_duplicate_key_update: [:hoge] end
- migration ファイル2
- children テーブルにカラム
fuga
を追加 - Child model で
validates :fuga, presence: true
を実装
- children テーブルにカラム
ここで、これら2つの migration ファイルをまとめて db:migrate
しようとした場合にエラーが発生する。
原因は、上記 data
が実行される際に後から追加されるはずの fuga
へのバリデーションのコードが実行されてしまうが、まだ fuga
はカラムとして追加されていないため未定義な変数へのアクセスエラーとなる。
この data
メソッドを書いた時点では後に同テーブルにどんなカラムが追加され、どんなバリデーションが追加されるか分からないため単体の spec が通ったとしても安心できない。
回避方法としては validate: false
オプションでバリデーションせずに更新する方法がある。
def data children = Child.includes(:parent).map do |child| child.hoge = child.parent.old_hoge child end Child.import children, on_duplicate_key_update: [:hoge], validate: false end
バリデーションを通さないので若干不安ではあるが・・・他に回避方法あるかなぁ。
古いカラム情報が残って正しくデータの更新が行えない
複数の migration ファイルで data
を定義すると、最初に実行されたメソッド内で where
などが呼ばれたテーブルのカラムの情報がキャッシュされてしまい、その後の migration ファイルで追加したカラムが data
メソッド内で認識できずに更新などが失敗する事がある。
以下のようなパターン
- migration ファイル1
- children テーブルにカラム
hoge
を追加 - data メソッドで Child.where(...) などの処理を行う
- children テーブルにカラム
def data Child.where(...) # ここでカラムの情報がキャッシュされる end
- migration ファイル2
- children テーブルにカラム
fuga
を追加 - data メソッドで同じように Child.where(...) などして、レコードに対して
update(fuga: 'hello')
を行う
- children テーブルにカラム
def data Child.where(...).update(fuga: 'hello') end
ここで fuga
の更新が無視される!!!!
こんな時は、2つ目以降の data メソッド内で、カラム情報をリセットする Class.reset_column_information
を呼んであげればいい
def data Child.reset_column_information # これを追加 Child.where(...).update(fuga: 'hello') end
fuga
が正常に更新されるようになる。
migration ファイルってデプロイのタイミングによってどんな風にまとめられて実行されるか分からないので、毎回上記のような対策をしておくのが良いのかも。
関連記事を見つけました。さすが onk さん。 blog.onk.ninja
毎日18時にPCが重くなる原因が分かった
時々PCが重くなる
毎日自宅でリモートワークしているのだけど、時々PC(MacBookPro 13インチ 2017)がめちゃくちゃ重くなる(ファンが回りっぱなしで、文字入力も遅くなるくらい)ことがある。 Docker とか立ち上げてるし外部ディスプレイにも繋いでいるし、Zoomとか繋ぐと重くなるので仕方がないかなーって思いつつ、どうしても我慢できない時はPCを再起動することでだましだまし凌いでいた。
決まった時間に重くなる
普段は18時には仕事を切り上げて子供を迎えに行ったりするのだけど、時々18時を過ぎても作業を続けているとだいたい決まってその時間からPCが重くなることに気づいた。またいつものアレかなと思ってPCを再起動するも再起動後も重い症状が続く。これはただ事ではないと思いアクティビティモニタを見てもTopで「WindowServer」が良い数字を叩き出しているくらいで原因はよく分からない。
ただずっと続くかと思えばそうでもなく、だいたい30分くらいで収まる感じ。18時を過ぎるとドアの向こうで子供達の暴れる声が聞こえてきて一刻も早く仕事を切り上げたい状況にも関わらずテキスト入力すらままならない状況にかなりイラつく。そんな状況がここ数ヶ月続いていて、ググってもそれっぽい情報に当たらないし、「そろそろPC買い替えかなー」とか思うようになってきた。
突然の解決
そんなある日に突然原因がわかってしまった。18時に重くなるという時間がヒントで、もしかして夕方になるとMacOSさんがブルーライトモードにして目の負担を和らげてくれる 「Night Shift」 が悪さしてるんじゃないかと!
早速設定を確かめると案の定「Night Shift」のスケジュールが「日の入りから日の出まで」になっていた。「オフ」に設定してしばらく様子を見ることにした。
- 設定変更は以下を参考にしました。
結果
数日経ったが例の現象は再現しない。 どうやら本当に「Night Shift」が原因だったようだ。ただPCを購入した当初は発生していなかったので、その後にインストールしたアプリとの副作用などが原因なのかもしれない。個人的には目の負担も減る気がしたので気に入っていた「Night Shift」モードだったが仕方がない。真の原因がわかるまでオフの運用を続けることにしよう。
もし定期的にPCが重くなるような症状に遭遇したら「Night Shift」モードを疑ってみるのも良いかもしれない。
まぁ、そもそも Zoom で 4Kディスプレイの画面共有をしながらペアプロとかやると重過ぎて作業がかなりツライのでそろそろマシンの買い替えを検討したいところ。eGPU とかも興味あるけど効果あるのかなー。
Rails で nested_form に ActiveModel::Base な Formオブジェクトを適用する
rails で nested_form をActiveModel::Base な Formオブジェクト(DBに紐づかない Model)に適用してみたのでメモしておく。
想定するケース
- 他サービスに可変個のデータを送るため、UIから
nested_form
を用いてデータを受け取りつつ、DBには保存したくないような場合 - ネストされる側の Model は validation 等の便利機能は使いたいため、
ActiveModel::Model
を mixin する
対応方針
単にネストしたフォームに ActiveModel::Base な Formオブジェクトを適用するだけであれば、こちら のような対応で可能だが、 nested_form
を用いたネストしたフォームの動的追加・削除を実現するにはさらなるメソッドの追加が必要だった。
そこで、 nested_form
が必要とする ActiveRecord::Base
が持つ一部の処理を擬似的に再現する module を定義し、 ネストする側とネストされる側の Model にそれぞれ mixin する方法で実現した。
- ネストする側が mixin する module :
ActiveModelNestedFormAcceptable
- ネストされる側が mixin する module :
ActiveModelNestable
module の定義
ネストする側の Model が mixin するモジュール
- app/models/concerns/active_model_nested_form_acceptable.rb
accepts_nested_attributes_for
の代わりに、 ActiveModel 向けの関連付けメソッドaccepts_active_model_nested_attributes_for
を定義する。- やってることは、フォームの動的追加の際に呼ばれる
reflect_on_association
でクラス名を取得可能にするのと、フォームのパラメータをネストする側の Model に設定する際に呼ばれる代入演算子のオーバーライド。
module ActiveModelNestedFormAcceptable extend ActiveSupport::Concern included do def self.accepts_active_model_nested_attributes_for(association, klass: nil) klass ||= association.to_s.classify.constantize add_nested_association(association, klass) define_method("#{association}_attributes=") do |attributes| self.instance_variable_set( "@#{association}", attributes .select { |_, attribute| attribute[:_destroy] == 'false' } .map { |_, attribute| klass.new(attribute) } ) end end def self.add_nested_association(association, klass) @nested_attributes ||= {} @nested_attributes[association] = klass end def self.reflect_on_association(association) if @nested_attributes[association] data = { klass: @nested_attributes[association] } OpenStruct.new data else super end end end end
ネストされる側の Model が mix in するモジュール
- app/models/concerns/active_model_nestable.rb
- フォーム削除時に必要なアトリビュート
_destroy
をダミーメソッドとして追加する。
- フォーム削除時に必要なアトリビュート
module ActiveModelNestable def _destroy # NOTE: dummy method for nested_form false end def _destroy=(value) # NOTE: dummy method for nested_form end end
具体的なクラスに適用してみる
では、具体的にネストする側 HogeWebService
クラス、ネストされる側 TemporaryObject
クラスとした場合の例を以下に示す。
ネストする側のクラスに ActiveModelNestedFormAcceptable
を適用する
- app/models/hoge_web_service.rb
accepts_active_model_nested_attributes_for
でフォームオブジェクトを関連付ける
class HogeWebService < ApplicationRecord include ActiveModelNestedFormAcceptable attr_accessor :temporary_objects accepts_active_model_nested_attributes_for :temporary_objects : # インスタンス生成時に temporary_objects に initialize で渡されたパラメータが設定される end
ネストされる側のクラスに ActiveModelNestable
を適用する
- app/models/additional_destination.rb
class TemporaryObject include ActiveModel::Model include ActiveModelNestable : # validate など普通に書く end
あとは通常通り nested_form
を使うコードを書けば動くはず。