volpe’s diary

フリーランスじゃなくなったプログラマ volpe が日々便利だなぁと感じたことを中心に綴るブログです

iPad でクラス図をサクサク描ける astah pad を久々に使ってみた

iPad Air を弄っていたら数年前にインストールした astah pad を見つけたので久しぶりに起動してみた。

リビングのソファーでクラス図を

このアプリは何と言ってもUXがとても使いやすくてタッチ操作でも直感的にサクサクとクラス図を描けてしまうのが良い。

特に、配置したクラスを指先で自由に動かせたりクラス間の関連線を簡単なドラッグ操作で繋げられたり出来るので、デスクトップアプリを起動して「クラス図を描くぞ!」と意気込まなくても、フリーハンドに近いラフな操作感でリビングのソファーでテレビを観ながらでも気軽に描き始められてしまう。

SpecDown の概念図を描いてみた

試しに マークダウンのリストから spec の雛形に変換するツールを作ってみた - volpe’s diary で作った SpecDown の概念図みたいなものを描いてみた。 クラス図として完全にはソースコードと一致しているわけではないけれど、「こんなオブジェクトがあってここは抽象化して関連があって具象オブジェクトがあって..」みたいな概念を表現している。

f:id:volpe0104:20200313085224p:plain
SpecDown の概念図

このくらいの図であれば 10分くらいでサクッと描ける。

趣味プロダクト向きかも

機能としては描いたクラス図を画像として出力することしか出来ないので、ドキュメント管理をしっかりやるチーム開発向きとは言えないけれど、趣味のプロダクトでセカンドディスプレイ代わりに iPad を手元に置いといて、ラフにモデリングしながらコード書いたりするにはぴったりかも。

実運用でもコードを書く前の事前設計だったり部分的なコードをヴィジュアル化して、画像を Github のプルリクエスト等に貼り付けたりして共有するのには使えそう。

iPad にとりあえず入れておくと良さそう

調べてみるとまだちゃんと提供されているっぽい。

astah* UML pad

astah* UML pad

  • Change Vision, Inc.
  • Business
  • Free
apps.apple.com

iPad があればとりあえず入れておいて、いつでもどこでもクラス図を気軽に描けるのって素晴らしいですね。

やっぱり素晴らしい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アプリを作ってみた。

f:id:volpe0104:20200312003441p:plain
画面イメージ

左側のテキストエリアにマークダウンを貼ると右側に実際の spec のコードテンプレートが表示される。 右上のセレクトボックスで言語の選択も可能で、今のところ良く使いそうな rspec と jest に対応してみた。

f:id:volpe0104:20200312003842p:plain
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インターフェースはいいですね。

github.com

実際に使えるデモページはこちら https://specdown.herokuapp.com/

Github の PullRequest をコマンドラインで表示出来るようにしてみた

モチベーション

フリーランスになって Github のプルリクエスト駆動で開発するようになって、今現在アクティブなプルリクエストのリストをいちいちブラウザを開かなくても手元のシェルで確認したい気持ちが高まってきた。

また、人のプルリクをレビューする際などにちょっと階層が深い複雑なブランチ構造のプルリクを開くと、自分は一体何の差分をレビューしているのか見失うことがあるので、プルリクのブランチ階層構造をサクッと把握したい。

コマンドラインツールを作ってみた

そこで、GithubAPIを使って pr-tree というコマンドラインツールを作ってみた。

なるべく簡単にインストール出来るように ruby の実行ファイル一個にまとめた。 リポジトリを clone し、GithubGITHUB_API_TOKEN を取得し、パスの通ったディレクトリにシンボリックリンクを置くことで簡単に使える。

実際の動き

シェル上で任意の Github リポジトリディレクトリに入って、 pt-tree と打つことで以下のようにプルリク一覧がブランチのツリー構造で表示される。

f:id:volpe0104:20191016003318p:plain

現在誰がどのプルリクに着手しているか分かるし、ブラウザで見たければ command を押しながら URL をクリックすると瞬時に開く(iTermの場合)。 大きめのフィーチャーブランチから子プルリクを生やしてる場合などもツリー構造を簡単に把握できてレビューが捗る。

最近追加した機能としては、親ブランチの最新から乖離がある子ブランチの先頭に * を表示するようにして、出来るだけ素早くリベースするなりして親ブランチから置いて行かれないようにしている。

実運用で使ってみている

なんとなく欲しくなったものを rubyGithub 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 時に実行してくれるとのこと。 rollbackrails 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 では fugavalidates :fuga, presence: true を行ってるとする。

そして、hoge への更新処理を1つ目の migration ファイルの data メソッドに activerecord-import の gem を使って以下のように書いているとする。

以下のようなパターン

  • migration ファイル1
    • children テーブルにカラム hoge を追加
    • data メソッドにデータ移行処理を記述
  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 を実装

ここで、これら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(...) などの処理を行う
  def data
    Child.where(...) # ここでカラムの情報がキャッシュされる
  end
  • migration ファイル2
    • children テーブルにカラム fuga を追加
    • data メソッドで同じように Child.where(...) などして、レコードに対して update(fuga: 'hello') を行う
  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が重くなる原因が分かった

f:id:volpe0104:20190912230749p:plain

時々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オブジェクトを適用する

railsnested_form をActiveModel::Base な Formオブジェクト(DBに紐づかない Model)に適用してみたのでメモしておく。

想定するケース

  • 他サービスに可変個のデータを送るため、UIから nested_form を用いてデータを受け取りつつ、DBには保存したくないような場合
  • ネストされる側の Model は validation 等の便利機能は使いたいため、 ActiveModel::Model を mixin する

f:id:volpe0104:20190826231852p:plain

対応方針

単にネストしたフォームに ActiveModel::Base な Formオブジェクトを適用するだけであれば、こちら のような対応で可能だが、 nested_form を用いたネストしたフォームの動的追加・削除を実現するにはさらなるメソッドの追加が必要だった。

そこで、 nested_form が必要とする ActiveRecord::Base が持つ一部の処理を擬似的に再現する module を定義し、 ネストする側とネストされる側の Model にそれぞれ mixin する方法で実現した。

  • ネストする側が mixin する module : ActiveModelNestedFormAcceptable
  • ネストされる側が mixin する module : ActiveModelNestable

f:id:volpe0104:20190826232414p:plain

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 を適用する

class TemporaryObject
  include ActiveModel::Model
  include ActiveModelNestable

              :

  # validate など普通に書く
end

あとは通常通り nested_form を使うコードを書けば動くはず。

参考