APIのバージョニングは限局分岐でやるのが良い
ちょっと前にTwitterでAPIのバージョニングをどうやるかみたいな話をしていたのですが、そのへんもやもやしているので少し整理しておきたいなと。
APIのURLを/api/v1/*とかってやるの、やめたほうがいいとおもうんだけどなぁ。いざv2を作るとなったときに、大量のコピペが発生して後悔するよ、って伝えたい。
— Kenn Ejima (@kenn) February 28, 2014
さて、これについて色々と異論・反論も含めた意見が出たのですが、まずは、大昔にURL方式(=コントローラ分割)でやってきて後悔したぼくが、(5年ぐらい前から)現在はどうやってAPIのバージョンを管理しているか?について紹介します。
基本原理としては、コピペが多発する根っこで分岐(=コントローラ分割)じゃなくて、必要最小限のところで限局的に分岐するのがいい、という考え方に基づきます。
一言でいうと、「パラメータとしてAPI versionを渡し、それをリクエスト単位でスレッドローカル変数に保存し、必要に応じて分岐する」というやり方です。
API versionの渡し方
具体的なAPI versionの渡し方としては、おおまかに二種類あります。
まずは、ログインしてセッションを生成する段階でパラメータとして渡す方法。ものすごく簡素化していうと
/api/session/create?id=foo&password=bar&api_version=1
みたいな感じで、認証に成功したら短命なsession_tokenを受け取り、以降はサーバ側で保存されたAPI versionが適用されるので、クライアント側からはapi_versionのような付帯情報を毎回送る必要がなく、session_tokenだけをキーとして送ればよくなります。
このやり方は、グループチャットのLingrや対戦ゲームインフラのPankiaのような、オンライン・オフライン状態の識別がクリティカルなサービスの場合に有効です。
もう一つは、API Versioning - O'Reilly Broadcastでも紹介されていますが、毎回HTTP Headerにapi_versionを埋め込む方法です。
X-Api-Version: 1
このやり方は、データベースのリモートバックアップサービスDumperで採用していますが、毎回送るべき情報が少なく、オンライン・オフラインの区別がほとんどないサービスに有効です。OAuthベースのシステムとも親和性が高いでしょう。
リクエスト・コンテキスト
さて、なんらかの方法でAPI versionがリクエストにのってサーバに受け渡されたとして、それを使って分岐する可能性のある場所は、コントローラに限りません。データベースの構造が変わった場合や、JSONの構造が変わった場合など、モデルやその他のクラスでもAPI versionを参照する必要がでてきます。
そこで、リクエスト単位をライフサイクルとするスレッドローカルな変数を使い、コードのどこからでもAPI versionにアクセスできるようにします。
具体的にRailsのコードで見ていきましょう。まずは、「APIというコンテキスト」を扱うオブジェクトを定義します。ここではOpenStructを使って、ApiContext.api_version = 1
のような形でアサインすると、自動でスレッドローカルに保存してくれるようにします。以下のコードをlib/api_context.rb
に置きます。
require 'ostruct' class ApiContext class << self def method_missing(method_name, *args) Thread.current[:api_context] ||= OpenStruct.new Thread.current[:api_context].send(method_name, *args) end end
そして、app/controllers/application_controller.rb(あるいはapp/controllers/api/base_controller.rb)に
class ApplicationController < ActionController::Base before_action :clear_context_variables def clear_context_variables Thread.current[:api_context] = nil end end
のようにしてグローバルなbefore_actionを定義してやり、リクエスト毎に必ずスレッドローカル変数がクリアされるようにしておき、
ApiContext.api_version = request.headers['x-api-version'].to_i
のような形でアサインします。以降はこのApiContext.api_versionを参照して、コントローラやモデルなど、どこにでも分岐を記述できるようになり、
if ApiContext.api_version > 1 { result: [{ name: 'foo', age: 20 }] } else { result: { name: 'foo', age: 20 } } end
のように局所的に分岐できるようになります。
ところで余談ですが、スレッドローカル変数はいわゆる(スレッドセーフな)グローバル変数なので、条件反射的に「使うべきではない」という反応をする人たちがいます。しかしRails本体でも、たとえばi18nでリクエストごとの言語ロケールをセットするところなどはスレッドローカルで実装されており、リクエストをライフサイクルとする広域変数にスレッドローカル変数を使うのはむしろ定番です。
ただし、当然ながら副作用としてスコープリークが発生し、MVCの分離が甘くなるので、ApiContextの使用は必要最小限にとどめ、テストの記述にも注意が必要です。
議論
さて、ここまでの実装をふまえて、個々に議論していきたいと思います。
@kenn ああ、そういうことか。了解。まぁよほど変わらない限りv2は使わないよね。そもそも。コピペレベルじゃないときにv2を使うイメージ。
— Yuichiro MASUI (@masuidrive) February 28, 2014
まさしくその通り!
つまり、コントローラ分割だと、よほど根本的で大きな変更がないかぎり、v2を導入することができなくなります。言い換えると、99.9%のサービスでは、v2を導入する日は永遠にやってきません。
ぼくがやりたいのは逆で、もっと気軽にAPI versionを使って、APIの質をどんどん改善していくことです。非互換な変更を入れなければいけないときというのは、よっぽど慎重にデザインしていても、やっぱり発生します。そういうときに、互換性を壊さないために「改善しない」という選択をするのではなく、「古いクライアントはそのまま動く、新しいクライアントではより良く動く、時間とともに良いクライアントが増える、いつかは古いバージョンをディスコン」というオペレーションです。
.@kenn 僕は、古いAPI実装を壊したくないから、バージョンによる分岐よりも URI パス等で実装まるごと切り替えるアプローチのが好きですねー
— Kazuho Oku (@kazuho) February 28, 2014
これもわかります。というか、その昔にぼくがURLで丸ごと切り替えるという判断をした理由もこれでした。
とにかくコードは一行でもいじると壊れる可能性があるので、互換性を最大限に維持したいなら、一切いじらなくてすむのが一番。だから古いコードはさわらず、新しくv2として書き始める。
と考えたのですが、これには大きな罠があります。
それは、アプリが進化する限り、下回りで依存しているライブラリ(Rails本体、Gemなど)もアップデートし続ける必要があり、それにあわせて結局v1のコードも書き換え続ける必要がある、ということです。
v1, v2, v3と増えるごとに、書き換える対象が増え続ける、これぞまさしく「コピペが増える」という表現で伝えたかったことです。
これは結局、シングルソースの限局分岐をメンテするよりも多大な労力がかかるので、結果的に後方互換性を壊してしまうリスクは同等かそれ以上にある、ということになります。
それだったらば、ライブラリにあわせての変更は一箇所で済み、分岐が限局されているやり方のほうが、結果的に品質も高くなるのではないか、という考え方もあるかなと思います。
@kenn そうですね。さらにいうとコントローラーをコピペなどで分けても結局さらに DB などの層でバージョンニングを意識しないといけませんしね。
— Taro Minowa Higepon (@higepon) February 28, 2014
そうそう、極めつけはこれだと思います。
せっかくコントローラを分けても、モデル層やJSON生成層その他で発生した非互換な変更には無力なので、結局ぼくがやってるのと似たような仕組みでMVCを超えたバージョンの受け渡しが必要になります。コントローラだけを大局分岐してバージョン管理できた気になるというのは、やはり悪手だろうと思います。
@kenn 最近はRESTfulなエンドポイントは完全に後方互換なまま、クライアントごとにオーケストレーション層(radical versionin)を設けるという方向にシフトしようとしている。詳しくは http://t.co/zODm7mFr5B
— Tatsuhiko Miyagawa (@miyagawa) February 28, 2014
なんかすごそうなことをやってる。。。もうちょっとkwsk!というわけで、33の続報をRebuildできけることに期待。
いかがでしょうか。
ぼくなりの現時点でのテンポラリな結論としては以上なのですが、MVCがリークしているのでテストの書き方が難しいとか、まだまだ満足のいく水準にあるとはいえず、これという正解が見つかってない世界だと思います。
このやり方でうまくいってるという方法があれば、ぜひ教えてください。
ユーザとユーザを多対多で関連付けるモデルを共通化する
思いのほか前回のRailsプチ・デザインパターンの紹介に反応があったので、こういう小ネタも出していったほうがいいのかな、ということで第二弾。
ソーシャル系アプリだと、ユーザとユーザを関連付ける多対多のモデルがたくさんでてきます。たとえば、一般的なところではフォローとかブロックとか足あととか。さらにデーティングサイトになると、ウィンクだったり、Secret admirer(こっそりlikeするけど両思いだったらおめでとうって通知がくるってやつ)だったり、いろいろなモデルがこのパターンにあてはまります。
この場合、「AがBをフォローしている」「BがAをフォローしている」「AとBがお互いにフォローしている」という3つの状態があるわけですが、相互フォローの状態は「AがBをフォローし、かつBがAをフォローしている」と読み替えてSQLでも記述可能なので、以下ではシンプルに単方向のグラフで全てを扱うものとします。こうすることで、フレンドリクエストとそれへの承認というのも、同じ枠組みで扱えるようになります。
一般的にはこういう感じの記述になるでしょう。
class User < ActiveRecord::Base has_many :follows has_many :follows_as_target, class_name: 'User', foreign_key: 'target_user_id' end class Follow < ActiveRecord::Base belongs_to :user belongs_to :target_user, class_name: 'User', foreign_key: 'target_user_id' end
ここでUser側にhas_many :through
も定義したくなるかもしれませんが、user.follows.preload(:target_user).map(&:target_user)
などとすればFollowモデルの向こう側にいるユーザ一覧も簡単にとれるので、あえて定義しないことにします。
followsテーブルの構造はこんな感じ。
create_table :follows do |t| t.integer :user_id, null: false t.integer :target_user_id, null: false t.datetime :created_at, null: false t.index [:user_id, :target_user_id], unique: true t.index :target_user_id end
(user_id, target_user_id)の組み合わせにユニーク制約つきのインデックスをはると同時に、target_user_id単独のインデックスもはります。この理由は、組み合わせのインデックスの左側のプレフィックスは検索に使えるため、user_id側から検索する場合には(user_id, target_user_id)のインデックスが使えるけれども、target_user_id側から検索する場合にはそれが使えないので、単独のインデックスを用意してやります。
さらに、あるユーザを削除するときには、関連するfollowsレコードを、まずuser_id側から探して消し、またtarget_user_id側からも探して消してやる必要があります。なので両側にdependent: :delete_all
を定義して、ユーザと一緒にカスケード削除されるようにします。
has_many :follows , dependent: :delete_all has_many :follows_as_target, dependent: :delete_all, class_name: 'User', foreign_key: 'target_user_id'
さて、ここでBlockやVisitやWinkといったFollowにそっくりなモデルが増えてくると、User側のコードは上記のコピペでどんどん膨れ上がっていきます。
そこで、これらの共通処理をRails 4から正式な機能へと格上げされたActiveSupport::ConcernでDRYにすることを検討してみましょう。
module GraphicalHasMany extend ActiveSupport::Concern module ClassMethods def graphical_has_many(name) has_many name , dependent: :delete_all has_many :"#{name}_as_target", dependent: :delete_all, class_name: 'User', foreign_key: 'target_user_id' end end end
上記のコードを、app/models/concerns/graphical_has_many.rb
に保存し、
module GraphicalBelongsTo extend ActiveSupport::Concern included do belongs_to :user belongs_to :target_user, class_name: 'User', foreign_key: 'target_user_id' validates :target_user_id, uniqueness: { scope: :user_id } end end
をapp/models/concerns/graphical_belongs_to.rb
に保存します。
なお、Rails 3系の場合は、config/application.rb
で
config.eager_load_paths << "#{config.root}/app/models/concerns"
を追加しておけば、Rails 4と同様に動きます。
さて、上記の準備ができれば、モデルは以下のように書き換えられます。
class User < ActiveRecord::Base include GraphicalHasMany graphical_has_many :follows graphical_has_many :blocks graphical_has_many :visits ... end class Follow < ActiveRecord::Base include GraphicalBelongsTo end
さて、ここでいったん一区切りとし、冒頭に少し触れた「相互フォロー状態はSQLで記述可能」という部分にたちもどってみましょう。アイデアとしては、「AがBをフォローし、かつBがAをフォローしている」というAND集合を取り出せばよいので、followsテーブルをセルフジョインしてやるのがよさそうです。なので、ベタにJOIN句を文字列で書くならば
scope :reciprocal, -> { joins("INNER JOIN follows AS f ON follows.target_user_id = f.user_id AND f.target_user_id = follows.user_id") }
というスコープをFollowクラスに定義してやれば、user.follows.reciprocal
で相互フォローしているユーザ一覧がとれるようになります。しかし、これではテーブル名が固定になってしまうので、汎用化することができません。また、Rails標準の機能ではジョイン時にテーブルエイリアスを指定することができない(これができるようになるだけで数々の問題が解決するのですが。。。)ので、ジョインの記述を生のArelを使って組み立てることにし、reciprocalメソッドとしてGraphicalBelongsToに追加します。
module GraphicalBelongsTo extend ActiveSupport::Concern included do belongs_to :user belongs_to :target_user, class_name: 'User', foreign_key: 'target_user_id' validates :target_user_id, uniqueness: { scope: :user_id } end module ClassMethods def reciprocal t1 = arel_table t2 = arel_table.alias('alt') join_node = t1.join(t2).on(t1.create_and([t1[:target_user_id].eq(t2[:user_id]), t1[:user_id].eq(t2[:target_user_id])])).join_sources joins(join_node) end end end
ちょっと煩雑に見えますが、やってることは基本的に文字列のベタ書きと同じで、違いは、どのクラスでも動くということです。
こんな感じで、モデル間で共通する処理はどんどんくくりだしていくことができます。
このアプローチの何がいいかというと、共通部分を奥にしまいこんで隠せることで、各モデルに固有の処理をする部分がよりハイライトされる・それに専念できるということです。
ユーザの持ち物を扱うメソッドがUserに集中しすぎて、Userクラスが肥大化してしまうというのはよくあることですが、せっかくFollowやBlockといったクラスがあるのだから、各モデル固有の処理はそちら側に書いてやるほうがコンテキストに沿った分離がしやすいはずです。
そのために、has_many
にはextend
というオプションがあり、外部モジュールを取り込むことができます。これをうまく使ってやることで、拡張をUserクラスに直接書かずに済みます。というわけで、GraphicalHasManyを拡張して、オプションをとるようにしましょう。
module GraphicalHasMany extend ActiveSupport::Concern module ClassMethods def graphical_has_many(name, options = {}) has_many name , options.merge(dependent: :delete_all) has_many :"#{name}_as_target", options.merge(dependent: :delete_all, class_name: 'User', foreign_key: 'target_user_id') end end end
そして、モデルにはこう書きます。
class User < ActiveRecord::Base include GraphicalHasMany graphical_has_many :follows, extend: Follow::UserScopes end
class Follow < ActiveRecord::Base include GraphicalBelongsTo module UserScopes def since(time) where('follows.created_at > ?', time) end end end
こうすることで、user.follows.since(3.days.ago)
のようなFollowの拡張を、Followクラス自身に書いていくことが可能となり、Userクラスはスッキリしたまま、将来的にも変更がいらない状態になりました。
さて、今回のパターンは、あくまでコードを読みやすく整理する、という観点ではあるのですが、実際にBefore / Afterでコードを見比べてみると、その違いを体感できるでしょう。
ちなみに、今回のようなコード共通化をSingle Table Inheritanceを使って実現する方法もあって、そちらのほうがコード自体はシンプルになるのですが、多対多のテーブルはそれ自体がユーザ数の二乗で増えていくのに加えて、複数のモデルが同一テーブルに同居することにより、レコードの増加スピードはさらに加速されるのでボトルネックになりやすく、最初からテーブルを分けておくのが得策です。
さらに続きの話としては、default_scope { where(deleted_at: nil) }
のようなデフォルトスコープを使って論理削除を実現している場合にも対応するため、GraphicalHasManyやGraphicalBelongsToをもう一段深く拡張していくこともできるのですが、だいぶ話が長くなったので今回はここまで。
それにしても、ORMもここまで来ると次元が違いますね。もう生SQLの時代には戻れない。。。
プライマリキーを使った1:1関連でカラム数の多いテーブルを分割する
おそらく多くのソーシャル系アプリにあてはまるRailsのプチ・デザインパターン的な話。
ぼくが今やっているEast Meet Eastには、ユーザごとに数多くのプロフィール属性があります。名前、性別、生年月日、郵便番号、職業などなど、カラム数にしてざっと25個。これを、全部ひとつのusersテーブルに詰め込むのは、コードの見通しという観点からも性能の観点からも、あまりよろしくありません。
なぜならば、ユーザ関連の情報を扱う局面としては主に
- メールアドレスとパスワードなどを使ってログインする(アカウント情報)
- プロフィール情報で条件を指定してユーザを検索・推薦する(プロフィール情報)
という2つの独立性の高いユースケースにわかれるため、ログイン処理をやってるときにはプロフィール情報はいらないし、プロフィールを検索してるときにはメールアドレスやパスワードをロードするのは無駄です。また、開発やデバッグ時に関心のない情報が大量についてくるのは純粋に邪魔です。
そこで、このテーブルをusersとprofilesという2つのテーブルに分割することを考えます。これを普通にhas_one
でやると
class User < ActiveRecord::Base has_one :profile end class Profile < ActiveRecord::Base belongs_to :user end
としておいて、profilesテーブルにuser_idカラムを作って参照してやるのがrails的には普通だと思います。
しかし!
よく考えてみると、usersとprofilesは完全に1:1の関連であり、あたかも同一のテーブルをたまたまカラムのグループで2つに分割しただけの存在です。だとすると、プライマリキーさえ一致していれば、わざわざuser_idという外部キーを作ってやる必要はないはずです。
そこで、以下のようなパターンを使います。
class User < ActiveRecord::Base has_one :profile, foreign_key: 'id' end class Profile < ActiveRecord::Base belongs_to :user, foreign_key: 'id' end
これだけで、usersとprofilesのプライマリキーが完全に一致するようになり、自分のidを使って相手を探しに行くようになります。余計な外部キーがいらなくなるので、インデックスも不要です。あらゆる点で無駄がなく、すごくスッキリしますね。
また、UserとProfileの両方にまたがる検索条件をつけての探索も、ActiveRecordのパワフルなスコーピングを使えば、まったく問題ありません。以下のようにjoins
とmerge
メソッドを組み合わせたスコープを定義してやることで、User側に定義したスコープをProfile側からも使えるようになります。
class Profile < ActiveRecord::Base scope :active, -> { joins(:user).merge(User.active) } end Profile.active # SELECT `profiles`.* FROM `profiles` # INNER JOIN `users` ON `users`.`id` = `profiles`.`id` # WHERE `users`.`status` = 'active'
この考え方を応用すると、1:1でのテーブル分割が割と気軽にできるようになり、可能性が広がってきます。
たとえば、Rails 3.2から導入されたActiveRecord::Storeという、text型のカラムにyamlやjsonなどの塊を突っ込んでKVS的に使うという機能があるのですが、usersテーブルに直接このカラムを追加すると、User.findするたびにこのカラムの中身まで全部読むことになり、非常に無駄です。さらには、そのyamlやjsonの深い部分で変更があったかどうかをトラッキングする方法がないため、レコードの更新処理があるたびにyamlやjsonの全体を再ダンプして比較する検出処理が走ります。これは、textのサイズが大きくなってくると無視できないコストです。
ActiveRecord::Storeは、そもそもカラムに昇格させるほどではない「あまり重要ではないが参考のためにとっておく」程度の情報をまとめて突っ込んでおくための入れ物なので、普段は必要ないことのほうが多いでしょう。そうすると、このカラムだけを分離した別テーブルにすることも考えられます。
class User < ActiveRecord::Base has_one :user_store, foreign_key: 'id' end class UserStore < ActiveRecord::Base belongs_to :user, foreign_key: 'id' store :data end
さらには、DeNAのソーシャルゲームのためのMySQL入門その2で挙げられているテクニックですが、ソーシャルゲームなど更新頻度の高い用途の場合、更新パターンに応じたテーブル分割というのも有効です。参照中心のアカウント情報(メールアドレス、パスワード、トークンなど)とは別に、ユーザがログイン中にはひっきりなしに更新されるようなゲームデータ(ヒットポイント、経験値など)は、usersテーブルからは切り離して1行あたり数十バイト程度の小さい単独のテーブルにしておくと、同じページサイズにのる行数が増えるため、(適切なflush設定をしてあれば)更新処理をまとめて実行できる可能性が上がり、より少ないIOPSで大量の処理をさばけるようになります。
というわけで、has_one
でforeign_key: 'id'
を使う、というのは、非常に応用範囲の広い考え方なのでオススメです。
追記
いま一緒に仕事してる@onoさんに指摘されたのですが、上記のコード例は、常にUserとProfileを常に同時に挿入・削除する場合にのみ、両テーブルでauto incrementが連動するので有効です。途中からprofilesテーブルを追加するようなケースや、profiles側を削除→挿入することがあるようなケースでも問題なく動くようにするには、before_create
で明示的に親からidのコピーを行っておきます。
class Profile < ActiveRecord::Base belongs_to :user, foreign_key: 'id' before_create do self.id = user.id end end
もし不安なら、プライマリキーを使った1:1関連のテーブル分割で自動採番をしないようにする - かみぽわーるのように、idカラムのAUTO_INCREMENTを外しておくと、idが空のまま挿入しようとしたときに例外を投げてくれます。
テスト考2014
年々、ウェブアプリを開発するときにテストを書こうという機運が強くなっていると感じる。
これは、開発パラダイムの成熟を意味することであり、基本的に良いことだと思っている。
しかし同時に「テスト原理主義」とでもいうような極端な考え方もでてきていて、開発スタイルをめぐって摩擦が起こっている。
そして、この議論は「テストは、ないよりあったほうが良いよね」という、微視的には誰も反論できないロジックに押し通されがちで、「地獄への道は善意で舗装されている」の典型的な現象に見えて仕方がない。
テストを書かない、というと背景にどんな深い考えがあっても素人くさく聞こえ、逆にテストを書くというだけで良いプログラマーに見える、という非対称な化粧効果がある。ソフトウェア・コンサルティング会社がテスト好きなのは決して偶然ではない。
ソフトウェアというのは、結局のところ、動いてナンボ、使われてナンボである。
期待するものが作れて動きさえすればどんな言語でもいい、どんなに汚いコードでもいい、という考え方がある。ぼくはその考え方には反対だが、その対局にあるのが「テストカバレッジ100%」ならば、もっと強く反対する。
ぼくは会社だけでなく個人でもいくつかのウェブサービスを作ってきているけれど、新しめのプロジェクトではテストはほとんど書かなくなった。それどころか、コメントすらほとんど書かなくなった。そして、それと反比例するように、コード自体のクオリティは、ぐんぐん向上している。
なかなか面白い現象だと思う。
思うに、
- それなりにできるプログラマが一人で作っている場合には、テストやコメントはほとんど必要ない
- しかしチームで開発する場合には、他人にコードを壊される機会が増えるので自然とテストを書くようになる
- 一人でゼロからリリースまでやりぬいた経験のあるプログラマーは1%もおらず、あとの99%はチーム開発しか経験したことがない
- だからテストは絶対的なものとして君臨する
おおよそ、以上のような流れではないだろうか。
ポール・グレアムは、「頭の中にプログラムを入れる」という有名なエッセイで、「コードの同じ部分を複数の人にいじらせない」といった。「複数の人でプロジェクトをやろうと思うなら、それをコンポーネントに分けて、それぞれを一人が受け持つようにすることだ。」と。しかし、オープンソースやPull Requestベースのワークフローというのは、これの真逆をいくもので、同じコードを不特定多数でワッショイワッショイ編集しまくるので、誰にとっても全体像が見えにくくなっていく。しかし組織としては、ある重要なコンポーネントを一人の人間に依存するのは怖いので、それなりのコストを払ってでもリスクヘッジしたい。まさしく「組織というものを定義付ける資質の一つは、個々人を交換可能なパーツとして扱うということだ。」と上記のエッセイに書かれている通りのことだ。
つまり、テストやコメントは、「個々人を交換可能なパーツとして扱う」ための必要悪なのであって、保険みたいなものだ。
テストはリファクタリングを助ける?邪魔する?
良いコードを維持するためには内部構造をしょっちゅうリファクタリングする必要がある。テストの存在は、外部インターフェースを壊すことなく内部をリファクタリングすることを可能にしてくれるが、一番よく変化し、壊れやすいのは内部構造のほうであったりする。こちらは、残念ながらテストで解決するのには向かない領域だ。(とはいえ、そんなことさえ知らず内部構造に対するテストを書いて自分で自分の足を撃ってるプロジェクトが多いことを考えると、もっと事態は深刻かもしれない)
もし、あなたのアプリが公開されたAPIをもち、それを維持していく必要があるなら、もっとも特徴的なAPI(たとえば認証まわり)いくつかに対してテストを書くべきだ。しかし、すべてのAPIに対してテストを書いてカバレッジ100%を目指すのはやめたほうがよい。おそらく、追加で書かれるテストの存在価値は限りなくゼロに近いし、そうでなければ、そのことのほうが問題だ。
その追加で書かれるテストは、おそらくほとんどが他のAPIからのコピペになるだろう。コピペでコードを書くことは害悪とされているのに、テストならばコピペは許されるのだろうか。
一番よいコードは、何も書かないことである。というと禅問答のようだが、自分で車輪の再発明をするのではなく枯れたライブラリを使うというのも、この範疇である。これは、同じことを実現するコードなら短いほうがよい、と一般化できる。そうした場合に、テストコードというのは実現したいこと(=アプリケーションコードの価値)には影響しないが、書いた行数は増える、すなわちメンテナンスという意味ではアプリケーションコードと同等のコストを払うことになるので、同じことを実現するためのコード量が増え、冗長になったことと等価である。
だから、テストは必ず書くという考え方はばかげている。アプリケーションコードと全く同じで、コストとリターンを見極めながら必要に応じて書く、が正しい。
コメントが必要なコードは抽象度が甘い
これは、経験からいうと、コードのクオリティが上がったからコメントが必要なくなった、というほうが正しいかもしれない。とくにRubyのような自然言語に近い記述ができるパワフルな言語では、コメントに書くぐらいならコードにしてしまったほうが読みやすくなるケースも多い。自然言語で仕様を記述するのにくらべてコードが複雑でコメントが必要になるケースというのは、得てして対象の理解が曖昧で、したがってコードの抽象度が甘いだけのことが多い。
これは、実例を出さないとわかりにくいだろうけど、サクッと出せるシンプルな例題が手元にないのでちょっともどかしい。
それでも、コメントを書く機会はテストよりは多い。なぜならば、未来の自分は別人なので、過去の自分がどういう経緯で今のような実装にしたのか、書いておいてもらわないと理解できないからだ。そういう場合には、悩んで現在のような実装に妥協した理由を、簡潔にコメントとして残しておく。逆に、ダラダラと長くコメントを書く必要があるような甘い実装は許容しないというルールでもある。これは経験上、けっこう良い規律としてワークする。どうしても複雑な事情でワークアラウンドせざるをえない場合には、該当するGithub IssueやStack Overflowなどへのリンクをコメントに貼っておく。なければ自分で外部サイトに書く。とにかくソースコード内にはダラダラ書かない。
エラー通知システムの存在
最後になったけど、時代の進歩という意味で、これが一番大きいかもしれない。
ウェブアプリならAirbrakeやHoneybadgerのような例外通知機構を導入することで、ユーザが踏んだバグをもれなくリアルタイムに知ることができるようになった。これらのサービスは、同じ種類のバグレポートが何度も通知されないようになっており、影響範囲の大きいバグをデプロイしてしまったときにもメールボックスが溢れてしまうというようなことがない。
個人的にAirbrakeを使い始めたのは2008年頃だったが、以来、ユーザが目にした500 Internal Server Errorは、もれなく追跡できている。無料なので、あらゆるプロジェクトで使っている。
おかげで、たいていのバグは、デプロイ直後に見つかり、すぐ修正できる。テストなんかより、よっぽど日々の役に立っているという実感がある。
いまAirbrakeの履歴をざっとみて、「これ、テストがあれば防げたなぁ」と感じるバグの割合を探ってみると、5-6%程度もない。つまり、たとえテストのカバレッジ100%であっても防げなかったバグがほとんどだったということになる。ほとんどの場合は、そもそも想定外の手順でリクエストがきたり、壊れたデータがきたりという例外なので、想定した範囲のケースしか扱えないテストで検出できないのは当たり前である。
逆に、こうしてプロダクションにリリースしてから発覚した「想定外の手順」や「想定外のデータ」をレグレッション対策としてテストケースに追加していくのは、ものすごく有効だ。自分の頭で想定できないパターンなので、将来の役に立つ。つまり、予測にもとづいてテストケースを増やすのではなく、実際に起きた問題にもとづいてテストケースを増やしていく。これはYAGNI原則という意味で重要なアプローチの違いである。
その結果、最近では「エラーゼロ」が個人的な目標になっている。新しく開発を続ける限りバグはゼロにはならないけど、とにかくすぐになおす。慢性的に発生する問題をひとつも放置しない。
このゴールを追求すると、プログラミングのレイヤーだけではなく、ミドルウェアやOSやインフラに関する深い知識が必要になるけど、そういったものを学ぶ良いきっかけでもある。
ソフトウェアがある程度の規模になると、エラーゼロというのは現実的ではないと思っていたけれど、そんなのは言い訳にすぎないと思うようになった。
モダンなエラー通知システムを使ったことがないのにソフトウェア品質を語り、テストの有用性を説くというのは、老害以外の何ものでもない。使ったことのない人は2014年のやることリストに「Airbrakeの導入」を追加しておこう。
まとめ
まとめると、今でもテストを書いてるのは以下のようなケースである。
- 複雑な挙動をするクラスのユニットテスト、とくに境界条件まわり
- 複雑な正規表現を書くときはTDD的で、テスト環境で例を追加しながら作業する
- デプロイ後にプロダクションで発生した想定外の外部入力
- 一度発生したレグレッションの再発予防(先回りはしない)
- 複雑なステップを踏む特異的なフォーム(サインアップなど)
- 外部に公開するAPIのうち特徴的なもの(認証や課金など)
こう書くと、すごく大量にあるように思えるかもしれないが、実際のボリュームは極めて少ない。おかげでテストのS/N比は高く、実際に役に立つテストになっている。また、テストには習慣的な側面もあるので、書くのがおっくうにならない程度の周期で書くようにしている。
ウェブアプリ開発が1980年代のミドルウェア開発やゲーム開発などと同じ道のりをたどっていくとするならば、この次にくるのは専業のQAチームが開発チームと独立して存在してリリースのサインオフ権限を握り、想定外のパターンをテストしまくってリリース前にバグを出し切る、という体制になるのかもしれない。
このアプローチが素晴らしいのは、開発者が自分で想定したテストを書いて満足するというマスターベーション的なテストではなく、本当の意味で想定外のケースがテストできることだ。しかし、想像に難くないことだが、開発やリリースのサイクルは劇的に落ちる。しかし、成熟というのにはそういう面もあるのだろう。
まぁ、なんにせよ、現在のウェブアプリ開発におけるテストなんて一歩間違えれば「ままごと」みたいなレベルだから、そんなに原理主義的になるのはダサいよねって話です。
でも、テストを一行も書かないあなたはクビですので念のため。
出口が開いてないと腰を据えられない
はてなブログ、使いやすいので気楽にいろいろ書きたいのだけど、なぜか本腰で書く気になれない。
理想的なブログツールを求めて自分でブログツールを開発してみたり、その途中でGhostを見かけて、やりたいことがほぼ同じだとわかったのでもう自分で作らなくていいやと応援する側に回ってみたり、と右往左往している。
そんな風に逡巡してる理由を自分なりに考えてみたんだけど、一言でいうと「ここにはエクスポート機能がないから」に集約される気がしてきた。実際、Ghostのフォーラムでも同じ質問をしている。
TwitterやFacebookのようなフローで垂れ流しのサービスなら、あまりそういうのは気にならない。そもそも情報を整理する目的で使ってないし、どちらかというとチャットの延長上でS/N比が低いから、再利用性の高い情報をそこに貯めていこうという動機が、ハナからない。
でも、ブログは違う。ブログは、ふわふわと浮いているさまざまなアイデアを一本のコンテキストで串刺しにして、ひとつのアーティクルとして編集・整理することができる。その編集という工程に付加価値があるからこそ、いまでもブログは生き残っているし、今後も残り続けるのだと思う。
おそらくブログのコンテンツの寿命は平均的なウェブサービスの寿命よりも長い。だから、昔はあまり気にならなかった、データのポータビリティみたいなものが、より大きな比重をもって感じられるようになってきたのだとおもう。
どんなに大きな会社の有名なサービスでも、方針転換などによってある日突然なくなったりするということを、ぼくらは何度も何度も目にしてきた。そんな状況で、ある一つの会社の一つのサービスに、魂を込めて書いた自分の人生そのものであるような文書を、唯一無二の保存場所として預けることができるだろうか。たとえば、研究者が自分の論文をあるウェブサイトに掲載するとして、自分のマシン上に元本がない状態でも安心できるだろうか。そのような制約下で、本当に素晴らしいコンテンツを書き上げようというエネルギーは湧いてくるだろうか。
この構図には、変なたとえだが、社員をいろいろなルールで縛ってがんじがらめにして小さくまとまらせておきながら「うちの社員には突き抜けた行動力のある優秀なのがいなくて。。。」と愚痴をたれる上司に似た矛盾を感じなくもない。
現在のネットのトレンドとして、データの寿命を意図的に短く10秒まで縮めたsnapchatのような揮発性のコミュニケーションと、長く残り続けることを志向するブログのような文書志向のツールとの二極化がますます進行していると思う。つまり、「長く残り続ける」というのは、ブログのたくさんある特性の一つではなく、進化していくネットにおいて他のツールと差別化していく上で最も重要な特性になってきてるのではないだろうか。
サービス提供者の立場からすれば、エクスポート機能を排除してユーザとデータを囲い込みたいというのは順当な心理ではあると思う。しかし、その発想はあまりにも「器が小さい」ように思う。実際、フローにすぎないTwitterですらアーカイブのエクスポート機能を提供しているし、Googleに至ってはTakeoutという、エクスポートセンターのような場所さえある。資本主義の権化といわれるアメリカの、それも株式を公開している企業がこのような懐の深さを見せていながら、日本ではカッティング・エッジな存在として知られるはてながセコい囲い込みに終始しているように見えてしまうのは、なんとも残念でならない。
むしろ、ブログというツールが果たす役割を考えるとき、「コンテンツが長く残るという安心感」につながる一つの切り口として、エクスポートというのはむしろこれまで以上に強化していくべき側面ではないだろうか。
とってつけたようなエクスポートではなく、たとえば、リンクしたDropboxアカウントにMarkdownファイルを吐き出してくれるとか、逆にDropboxに置いたMarkdownファイルをインポートしてブログ記事化してくれるとか、記事を公開するたびに記事単位でダウンロードリンクが生成されるとか、ブログを真剣に書きたい人の深層心理に刺さるアドバンテージとして積極的に開拓していく余地は大きいように思う。
というわけで、はてながエクスポートを実装するのが先か、Ghostが使えるレベルになるのが先か。2014年にはブログを再開できることを願いたい。
East Meet Eastはじめます
以前から予告していた、新しくはじめるスタートアップの件について、いよいよご紹介します。
パートナーは時岡まりこ。オックスフォードのMBAで、ロンドンのスタートアップQuipperの共同創業者兼COOをやってきていた人です。
やることは、アメリカやイギリスなど、主に英語圏の欧米諸国に住むアジア人向けのデーティングサイト。名称はEast Meet East。
アジア諸国は経済の発展がめざましく、人口ボーナスもあって、ますます世界の移民動態における存在感が高まってきています。でも、西欧諸国に住むアジア人は、自分が育った環境と比べて文化的なギャップが大きいので、似たようなバックグラウンドの友人・恋人とまじわる傾向があります。もちろん、あえて故郷を振り返ることなく現地の人と結婚したりする人もいるのですが、全体の統計レベルでみるとやはり圧倒的に同一民族内での結びつきが多数派です。
いろいろな人と付き合ってみたけど、やっぱり最後に結婚する相手を探すとなると、小さな頃に同じようなことを経験し、深いところで価値観を共有できる相手がいい。でもそういう人が周囲に少ない。このような悩みを抱えている人々は意外なほど多く、こうした問題をインターネットの力で解決することには、大きなチャンスがあると考えました。
デーティングサイトといえば、日本語にすると「出会い系サイト」ということで、ややダークなイメージが残っているかもしれませんが、ここ米国では世代をこえて完全に一般化していて、Match.comやeHarmonyを使っていることを公言している人も珍しくありません。むしろ現在では、あまりに一般化したためニーズの細分化・ニッチ化がすすんで、ユダヤ人向けのJDate.comやクリスチャン向けのChristian Mingleなど、バーティカルな市場が立ち上がってきています。そんな市場環境の中で、西欧諸国に移住したアジア人向けという、自分たちの土地勘があるマーケットから攻めていこうというわけです。
ぼくは、渡米してからの8年間、いかにもシリコンバレー的な、エンジニア集団でものを作るというスタンスでやってきました。最初は、インフォテリアUSAの立ち上げでアメリカ人のエンジニアチームで消費者向けネットサービスを作り、その次はパンカクで日本にいるエンジニアチームと一緒にモバイルゲーム向けのプラットフォームを作り、という感じで色々やってきて、Cometを世界で初めて実用化したサービスLingrや、3年間で3000万ユーザまで急成長したプラットフォームPankiaなど、技術的な達成は大きかったのですが、それをビジネスに結びつけるところがうまくいきませんでした。
これらの経験を通じて、自分は何が得意で、何が苦手か、はっきりと痛感したのです。自分でそうありたいと願っていたほどビジネスのセンスがよくないだとか、マーケティングもからっきしダメだとか、認めたくないけれども認めざるをえない弱点が、長年の失敗の積み重ねで、ようやく見えてきたのです。一方で、エンジニアとしてのセンスやバランス感覚には、今でも確固たる自信を持っているということも。
だから、オラクル時代の共通の友人を通じて今回の話を紹介されたときには、すぐにピンときました。トッキーは、冒頭で紹介した経歴からわかるようにビジネスパーソンとして申し分のないキャリアを積んできていて、また実際に話をしていくうちに、事業の難しい局面でも折れることなくやり切ることのできる芯の強さのある人だと感じました。私情を挟むことなく徹底的にロジカルにものごとを考える習慣があり、速いスピードでガンガン意思決定していくという部分ではスタイルが共通していて、しかし片手間では絶対に追いつけないレベルでの専門性を分担するという、理想的なシナジーがあると確信できたのです。
何でも自分一人の力でやろうとして、うまくできなくて落ち込んで、というのは士気が下がるだけでなくて、人間としての成長という観点からもタイムロスです。向上心やガッツは誰よりもあると自負しているけれども、気ばかり焦って何をやっても改善しなくて、さらに焦って。。。という空回りの悪循環は苦しいものです。Paul Grahamも、The 18 Mistakes That Kill Startupsという記事で、スタートアップが失敗する一番目の理由として挙げたのが「Single Founder」でした。だから今回は、モノよりもヒトで決めたし、その直感を信じることにしたのです。
彼女はロンドン在住で、ぼくはラスベガス。地理的に離れているので、週三回のミーティングはskypeベースです。最終的には、メインのターゲット市場となるニューヨークなどのメトロエリアにHQを構えることになる予定です。
開発チームは、イギリス在住のビジュアル・デザイナーのRobと、ブルックリン在住のフロントエンド・デザイナーのScottとぼくの3名、3つのタイムゾーンでガッツリ開発してきており、そろそろベータリリースが見えてくるといった頃合いです。
人材・資金・コネクションなど、さまざまなものがゼロベースからの再出発ですが、なんとしてもこの米国市場・世界市場で存在価値のあるものをつくりあげたい。East Meet East成功のためになら、石にかじりついてでも、つべこべ言わず何でもやるつもりです。
応援メッセージや協業のご提案をいただける場合には kenn - at - eastmeeteast.com まで。各方面にこれから色々とご支援をお願いすることになるかと思いますが、どうかよろしくお願いいたします!
ラスベガス生活がはじまって
7月にラスベガスへ引っ越してきてから、あっという間に2ヶ月近くが経ちました。
思いがけず仕事の変化が重なって、忙しすぎてアップデートできなかったのですが、忘れないうちにラスベガス生活について書いておこうと思います。
意外に過ごしやすい気候
まず最初に、一番心配していた気候について。到着した当初が気温のピークで、華氏113度(摂氏45度)という超絶猛暑だったのですが、結論をいえば「意外と問題ない」。むしろ、先月に出張で行った東京のほうが蒸し暑くて不快感が強かったです。
というのも、ラスベガスは湿度がほぼゼロの砂漠気候。これだけ気温が高くても、汗ダラダラということがありません。汗かきの人間にとっては、全身が汗だくになって、それが冷房で冷やされるというのが究極の不快感なのですが、そういうことが起きないのです。汗かきでない人にとっての夏というのはこういう感じだったのか、とある意味で新鮮な経験です。
ただし、陽射しの強さは桁違いですから、自動車の窓に断熱フィルムを取り付けるのは必須です。この処置をしてからは、外出時にも快適に過ごせるようになりました。屋外の駐車場にしばらく停車するとハンドルが握れないぐらい熱くなりますが、これもサンシェードを使うようになってから解決。
サンマテオ・サンフランシスコ時代には、どんなに暖かい日でも夜になると冷えるので、Tシャツだけで一日過ごせるということが年中通してほぼなかったのですが、今ではシンプルにTシャツ・短パン・サンダルという軽装がちょうど良いので、いたって快適です。というか、むしろカリフォルニアこそがこういう気候だと思っていたのですが。。。アメリカ人がちょっと暑いと感じるぐらいの温度が日本人にとってはちょうどいい、という、エアコン設定温度をめぐる飽くなき戦いはよく知られていますが、その傾向はここでもあてはまりそうです。
ひんやり気持ち良い明け方も素晴らしく、エアコンが必要なのは午後の数時間だけだし(ただし、これは建物全体にエアコンが効いてて外部から熱を取り込むのは窓側の一面のみという高密度なコンドミニアムで、かつ北向きの部屋ゆえかも知れませんが)、年中暖房を入れっぱなしだったサンフランシスコ時代とくらべて、トータルで見れば生活空間の気温に関しては改善されたという印象です。
さまざまな出来事
想定外だったのは、夏はモンスーンの季節で、結構雨が降るということ。それも、洪水を引き起こすような激しい雷雨もあります。こんな感じで道路が完全に冠水して、車が流されたりしてました。正直これは怖かった。。。
この夜には大規模な停電も起きて、エレベーターが停止したため、30階以上という高層階にある自分の部屋に戻ることができずロビーで復旧を待つことになり、翌朝は海外出張のフライトなのに準備どうしよう、そもそも飛ぶのかな?などと心細い思いをしたりしました。
この他にも、引越し業者が荷物を配達してくる日を勝手に二週間も遅らせたり、やっと届いたと思ったら荷物が半分しかなかったり、家具を買ったらことごとく配送中に壊れてたり部品が足りなかったり、、、
というアメリカ生活でありがちな地雷をフルコースで踏んでエネルギーを吸い取られたりもしましたが、悪いことばかりではありません。
到着してすぐに独立記念日の花火を自宅の部屋から楽しんだり、
ジモティーの友人に歓迎してもらったり、
やたらクオリティが高くてしかも安い和食レストランがたくさんあることに驚いたり、
新居で無料の朝食とコーヒーが提供されたり(管理費に含まれているわけですが)、
毎朝ベッドからゴージャスな日の出を眺めたり、
ラスベガスのRubyユーザグループLVRUGに参加してみたり、
壮観なMount Charlestonの山火事を目撃したり、
いい感じのコワーキングスペースに通い始めたり、
こんな感じで色々な出来事を楽しんでいます。
いろいろ忙しくて、まだ「いかにもラスベガス」っていうストリップ界隈に繰り出す機会はぜんぜんないのですが、新しい生活の基盤はようやく整ってきました。
ラスベガスに足りないもの
逆に、ラスベガスに来てから不便を感じることとしては、
- ニジヤやミツワのような大手の日系スーパーがない
- サンフランシスコにあったようなオシャレ系のカフェがほとんどない
- 緑の草木がほとんどない(砂漠なので。。。)
の3つです。
小さな日系のスーパーはいくつかあるのですが、やはり品揃えがちょっと物足りないです。本国の勢いを反映してか、韓国系のスーパーは巨大なのがあって、そこに日本製の商品や日本食の食材も置いてあるので何とかなるのですが、かゆいところに手が届かない。和食レストランはサンフランシスコよりレベル高いのですが、どうやって食材調達してるんでしょうかね?ロサンゼルスから取り寄せてるのかな?
逆に、この3つさえ我慢できて、あと街中にあるあまり上品とはいえない看板類に慣れてくれば、物価は目に見えて安いし、町の歴史が浅いゆえにいろいろなものがモダンで新しくて便利だし、ポテンシャルの高さを実感できます。
住めば都
今回は勢いでラスベガス移住を決めてしまったので、住み始めてから知ったことですが、ラスベガス西部のサマリン、南東部のヘンダーソンは落ち着いた感じの街で、住民の所得水準や教育水準も高く「全米住みやすい街ランキング」のトップ常連です。いってみればベイエリアでいうクパチーノやフォスターシティみたいな雰囲気で、そういう郊外の選択肢もちゃんとある。
住めば都とはよくいったもので、今まで色々なところに住んできましたが、どの街にもそれぞれに良さがあります。最初はなじめなくても、住んでいるうちに大好きになっていく。引っ越す前は、どうしてもネガティブな面ばかりが気になって、これで正しかったのだろうかと選択を疑ったり不安になったりするのですが、エイヤで実行してしまえばたいてい杞憂で、むしろ良い方向に発展する。これ、仕事や結婚など、人生で大きな決断をするときには何にでもあてはまる気がしますね。一度決めたら、とことんポジティブにやりきることが大事。
こんな感じで、引き続きラスベガスでの生活をエンジョイしていきたいと思います。
そんななか、新しいスタートアップの起業準備のためロンドンに行ってきたりしたのですが、そのあたりについてはまた今度。