MogLog

メモというか日記というか備忘録というか

「既存のコードがそうなっているから」は逃げる理由になりがち

何らかの追加実装の作業をしているとき、往々にして既存のコードと類似した実装をする、あるいはそうできる場面に遭遇することがあると思う。

このような時、既存のコードと同じパターン・同じ構成で実装するというのは1つの選択肢になる。 なぜならそのように書いたコードは、現在のコードとの調和を保ちつつも「既に動いている」という実績を持ち合わせているからだ。 多くを考えること無く、短時間で動くコードを実装できることも開発者には嬉しい点だ。

一方このやり方は、考えることから逃げる理由になりがちであるとも感じる。 本来であれば要件に応じた最適なコードを自分の中で考えるフェーズがあるはずだが、既存実装があるとこのフェーズをすっ飛ばしてしまうことができる。

既に動いていたとしても、既存のコードが適切な実装であるとは限らない。仮に適切であったとしても、今から追加をする段階で適切ではなくなる可能性も高い。

既存のコードを踏襲するにあたっては、「自分だったらこの要件をどう実現するか」をゼロベースで考えることや、「本当にこの既存実装は適切か」、「より良い実装は無いだろうか」というような点をまずは考えることを意識したい。

体験としては既存の実装は少なからず改善点を抱えているので、調和なんて知ったことかくらいの勢いで、自分が考える最適実装に仕上げるのがいいんじゃないかと感じている。

【Rails】データベース名を明示せずにActiveRecordでMySQLに接続する

環境

状況

データベースはまだ存在しないが、ActiveRecordMySQLへの接続だけは確立したい。

何も考えずに次のようなコードを実行すると...

ApplicationRecord.connection.execute("<なんかSQL>")

次のようなエラーが起こる。

ActiveRecord::NoDatabaseError: Unknown database '<データベース名>'

どうするか

このような場合は、次のようにすると比較的スッキリ書ける。

config = ApplicationRecord.connection_config
ApplicationRecord.establish_connection(config.merge(database: nil)) # ★
ApplicationRecord.connection.execute("<なんかSQL>")

こんなんいつ役に立つんじゃい

自分は以下のような状況で使った。

  • 一時的に複数のデータベースを扱う必要があるRailsアプリケーションがある(そのためdatabase.ymlが2つある状況)
  • 2つ目のデータベースの作成と削除も、rakeタスクでRailsらしく実行したい
    • bin/rails 2nd_db:createとかそんな感じで
  • 2つ目のデータベース用の基底クラス(ActiveRecord::Baseを継承したクラス)があるので、この基底クラスから CREATE DATABASE <2つ目のデータベース> を発行したい。だが単純に実行しようとすると先述の通りエラーになる。
  • 実装の都合上、1つ目のデータベースに接続するApplicationRecordはできれば使いたくない
  • そこでコレ

状況がニッチすぎて誰得な感じは否めないが、自分はそれなりに悩んだのでインターネットの海に放流しておこうと思った次第である。その他、エレガントな実装方法があれば教えてほしい。

【Rails】app/以下に新規作成したディレクトリのファイルが読み込まれない件

結論:springの再起動が必要だった

あらまし

Ruby On Railsアプリケーションの開発中、app/以下に新しくvalidatorsというディレクトリを作ることになった。 最近のRailsではapp以下のファイルを自動で読み込んでくれるようになっているので、追加の設定は不要だ。

Railsガイドにも次のように書かれている。

ところで、Railsにはpost.rbのようなファイルを探索する$LOAD_PATHに似た、ディレクトリのコレクションがあります。このコレクションはautoload_pathsと呼ばれており、デフォルトで以下が含まれます。

アプリケーションとエンジンのappディレクトリ以下にあるすべてのサブディレクトリ。app/controllersなどが対象。app以下に置かれるapp/workersなどのカスタムディレクトリはすべてautoload_pathsに自動的に属するので、デフォルトのディレクトリとする必要はない。

定数の自動読み込みと再読み込み | Rails ガイド

そう、そのはずなのだが何故か読み込まれない。 bin/rails cでコンソールを立ち上げて、$LOAD_PATHActiveSupport::Dependencies.autoload_pathsを確認してもやはりapp/validatorsは含まれていない。

1時間くらいハマッて調べた結果、springがキャッシュを持っていることが原因だとわかった。

$ bin/spring stop

上のコマンドでspringを停止し、その後 bin/rails c でコンソールを立ち上げて確認したところ無事に読み込まれていた。 springは裏でひっそりと動くのであまり気にしなくて良い反面、存在自体を忘れがちである。

draperを使ってみた

draper というデコレーター(またはプレゼンター)の機能を持つGemを使ってみた記録。

draperとは

  • draper is ...
    • ViewModel for Rails
    • Railsアプリケーションにオブジェクト指向なプレゼンテーション層を追加する
    • Modelにメンテナンス性の高いプレゼンテーションロジックを包み込む

なぜdraperを使うのか

  • Viewにまつわるロジックの置き場としてHelperも存在する。しかし、Helperはグローバルスコープを持っているため全てのControllerとViewで利用できてしまう
  • Helperを使った場合、Modelに応じた微妙な差異を吸収するために新しいメソッドが追加される
    • 例:Articleモデルは日時をYYYY-MM-DD HH:mm:ssで表現したいが、BookモデルはYYYY-MM-DDで表現したい
  • Helperはオブジェクト指向に書きづらい

→ draperはこのような問題を解決する

使ってみる

セットアップ

gem 'draper'をGemfileに追記してインストールしたら、ジェネレートコマンドを使って一式生成する。
Userモデルがあるとして、それに対応するdecoratorを生成する。

>> bin/rails g decorator User
      create  app/decorators/user_decorator.rb
      invoke  test_unit
      create    test/decorators/user_decorator_test.rb

app以下にdecoratorsディレクトリが作成され、その下にuser_decorator.rbが追加された。

デコレートメソッドを追加する

早速Userモデルをデコレートしていく。
ここでは、Userが自己紹介をするためのデコレートメソッドを追加した。

# app/decorators/user_decorator.rb
def greeting
  "Hi! My name is #{user.name}. I am #{user.age} years old. Thank you."
end

decoratorクラス内で、対応するモデルにアクセスするには object か 対応するモデル名(Userモデルの場合は user)を呼び出す。

Controllerでモデルをデコレートする

Modelをデコレートする準備は整ったので、次はControllerで取得したModelをデコレートした上でViewに受け渡す。
次のように、Modelに対して decorate メソッドを呼び出すだけでOK。

# app/controllers/users_controller.rb
def show
  @user = User.find_by(id: params[:id]).decorate
end

Viewでデコレートメソッドを呼び出す

Viewではデコレートされていることを意識する必要はなく、普通にメソッドを呼び出すだけでいい。

<!-- app/views/users/show.html.erb -->
<h1><%= @user.greeting %></h1>

画面

f:id:mogulla3:20180210122304p:plain

これでdraperを使った一連の実装を体感できた。

アソシエーションに対応する

関連するモデルも含めてデコレートしたい場合は、次のようにデコレータクラスで decorates_association メソッドを呼び出す。

# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
  # ここでは、UserはN個のDeckを持つ関連が設定されているとする
  decorates_association :decks
end

これで、deck側もデコレートされたオブジェクトとして扱うことができる

その他のメモ

  • decoratorクラス内で、helperにアクセスするには hを呼び出す
    • 例:h.content_tag(:strong, user.name)
  • コレクションをdecorateするいくつかの方法があるようだ
    • UserDecorator.decorate_collection(User.all)
    • User.all.decorate
    • これは具体的に何が違うのかまで調べていない
  • ApplicationDecoratorなどとして、Decorator共通の処理を括りだすことができる。この場合、各デコレータクラスはDraper::DecoratorではなくApplicationDecoratorを継承するようにする
    • class ApplicationDecorator < Draper::Decorator
    • class UserDecorator < ApplicationDecorator
  • Decoratorクラスはデフォルトでdeltega_allが呼び出され、全てのメソッド呼び出しを委譲しているが、delgateメソッドで個別に指定することも可能
  • どのデコレータを使うかの指定も可能。デフォルトではファイル名から判定されている。

ActiveModel::Serializersを使ってみた

ActiveModel::Serializers(AMS) という、出力するJSONをいい感じにコントロールできるというライブラリを使ってみた記録。多分、RailsAPIを作るときなんかによく使われるライブラリ、くらいの認識からスタートした。

github.com

出鼻をくじかれる

「よーし使ってみるか」という気持ちで意気揚々と調べ始めたところ、2つの点で出鼻をくじかれた。

README.mdを読んだ

masterブランチの README.md を見ると、ざっくり次のようなことが書かれていた。

  • masterブランチへのPRはcloseやめて、0-10-stableブランチへ頼むよ
  • 0.10.xバージョンは巨大なメンテナンスバージョンになっているよ
  • 0.10.xバージョンは安定版だけど、積極的なメンテナンスはしていないよ
  • バージョン1.0に向けての開発はしているよ

要するに、現在の安定バージョンは 0.10.x であるが積極的なメンテナンスはされていないし、PRも基本的に受け付けていない。今は、色々あって1.0に向けて大規模な改修(開発)をしています...、といった感じ。

fast_jsonapiの登場

NetFlix社から、fast_jsonapiというライブラリを開発したというブログが公開された medium.com

これは ActiveModel::Serializers が提供する多くの機能を受け継ぎつつ、パフォーマンスを改善したライブラリらしい。AMS(ActiveModel::Serializersの略称)と比較して、パフォーマンスは25倍近く向上したと書かれている...。

...といった感じで出鼻をくじかれてしまった。
しかしAMSは有名なライブラリ(のよう)だし、fast_jsonapiはAMSに影響を受けているようなので、まぁやって無駄にはならないだろうと考えて当初の予定通り使ってみることにした。

Getting Started

まずは動かすところまでやってみる。ガイドを参考に進める。

まずは、DBスキーマ、モデル、コントローラを適当に一式用意する。

DBスキーマ

create_table "users", force: :cascade do |t|
  t.string "name"
  t.integer "age"
  t.string "sex"
  t.boolean "premium"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["name"], name: "index_users_on_name", unique: true
end

モデル

# app/models/user.rb
class User < ApplicationRecord
end

コントローラ

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    render json: User.all
  end

  def show
    render json: User.find_by(id: params[:user_id])
  end
end

ルーティング

# config/routes.rb
Rails.application.routes.draw do
  get 'users' => 'users#index'
  get 'users/:user_id' => 'users#show'
end

ここまでで、サーバを起動してリクエストを送るとJSON形式でuserデータを返してくれる状態になった(※ マイグレーションやテストデータの挿入は省略している)。

>> curl -s localhost:3000/users/1 | jq .
{
  "id": 1,
  "name": "user-0",
  "age": 1,
  "sex": "male",
  "premium": true,
  "created_at": "2018-02-03T01:59:54.243Z",
  "updated_at": "2018-02-03T01:59:54.243Z"
}

ここからAMSを使ってみる。まずはシリアライザクラスを作る。

bin/rails g serializers User

これで、app/serializers/user_serializers.rbが作られる。ファイルの中身は次のようになっている。

# app/serializers/user_serializers.rb
class UserSerializer < ActiveModel::Serializer
  attributes :id
end

attributesメソッドに指定した値だけをレスポンスするようになるので、この状態で再びリクエストを送ると次のような結果になる。

>> curl -s localhost:3000/users/1 | jq .
{
  "id": 1
}

attributesメソッドにidしか指定してないので、レスポンスに含まれるデータもidのみとなった。 attributesに複数の引数を指定することももちろん可能。

# app/serializers/user_serializers/user_serializers.rb
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :age
end

レスポンスも合わせて変化する。

>> curl -s localhost:3000/users/1 | jq .
{
  "id": 1,
  "name": "user-0",
  "age": 1
}

ここまでで、とりあえずAMSがどんなものかのイメージができた。

アソシエーションに対応させる

次は、モデルのアソシエーションに対応させてみる。 Userは0個以上のDeckを持てるものとして、データモデルを変更する。

DBスキーマ

create_table "decks", force: :cascade do |t|
  t.string "name"
  t.string "description"
  t.string "image_url"
  t.boolean "favorite"
  t.integer "user_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["user_id"], name: "index_decks_on_user_id"
end

モデル

# app/models/user.rb
class User < ApplicationRecord
  has_many :decks
end

# app/models/deck.rb
class Deck < ApplicationRecord
  belongs_to :user
end

リアライザ

# app/serializers/user_serializer.rb 
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :age
  has_many :decks # ★ ここ追加
end

# app/serializers/deck_serializer.rb 
class DeckSerializer < ActiveModel::Serializer
  attributes :id, :name
end

この状態でリクエストする。すると、次のように関連するモデルのデータもレスポンスにのってくる。

>> curl -s localhost:3000/users/1 | jq .
{
  "id": 1,
  "name": "user-0",
  "age": 1,
  "decks": [
    {
      "id": 1,
      "name": "deck-0"
    },
    {
      "id": 11,
      "name": "deck-10"
    }
  ]
}

同様に、 has_onebelongs_toというメソッドもある。ActiveRecordの規則に従っているようでわかりやすい。

アダプタを切り替える

標準で以下3種類のadapterが提供されていて、それらに切り替えることができる。

  • :attribuets (default)
  • :json
  • :json_api

rendering時のadapterパラメータに指定することで切り替え可能(※ グローバルな設定として持たせることも可能)。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    render json: User.all
  end

  def show
    render json: User.find_by(id: params[:user_id]), adapter: :json # ★ ここ
  end
end

アダプタごとに出力形式に特徴がある。 例えばJSONアダプタ(:json)の場合はレスポンスのroot要素が作られるといった違いがある。

>> curl -s localhost:3000/users/1 | jq .
{
  "user": {
    "id": 1,
    "name": "user-0",
    "age": 1,
    "decks": [
      {
        "id": 1,
        "name": "deck-0"
      },
      {
        "id": 11,
        "name": "deck-10"
      }
    ]
  }
}

JSON APIアダプタ(:json_api)は、jsonapiというJSON形式でデータを返すAPIの仕様を決めている団体?のようなものがあり、そこで作られたversion1.0の仕様に従っているようだ。
各アダプタの詳細は Adapters に書かれている。

※ 余談:JSONアダプタという名称にはちょっと違和感を感じる。デフォルトのattributesアダプタもJSON返してるし...。

独自の属性を追加する

メソッドを定義するなどして、Modelの属性としては存在しない独自の属性を追加できる。 以下は、fooというキー名でbarという値を返すよう、メソッドの定義とattributesメソッドの引数変更をしたときの例。

# app/models/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :age, :foo # ★ ココと..
  has_many :decks

  # ★ ココ
  def foo
    "bar"
  end
end

レスポンスは次のように変わる。

>> curl -s localhost:3000/users/1 | jq .
{
  "id": 1,
  "name": "user-0",
  "age": 1,
  "foo": "bar",  # ★ 増えた
  "decks": [
    {
      "id": 1,
      "name": "deck-0"
    },
    {
      "id": 11,
      "name": "deck-10"
    }
  ]
}

その他気になった機能

  • 関連先のモデルは includeオプションでも指定できる
    • render json: User.all, include: ['decks']
  • includeの値として * を渡すと、関連先のモデルを1階層分だけ出力する
    • render json: User.all, include: "*"
  • 同様に、** を渡すと、関連先のモデルを全て辿って出力する
    • render json: User.all, include: "**"
  • ApplicationControllerのように、各シリアライザ共通の処理をApplicationSerializerなどとして括り出せる(クラスの名称は何でも良さそう)
  • cache機能がある
  • key_transform optionsオプションで、出力するキー名のフォーマットを変更できる
    • camel, camel_lower, dash, unaltered, underscore, nil などが指定できる
    • 例:render json: User.all, key_transform: :camel

使ってみての雑感

直感的な設計になっていて、大体こんなかんじで動いてくれるだろうな〜っていうのがその通りに動いてくれる感じが良かった。
v1.0に向けて開発中のようだが、この辺の使い勝手の良さはそのまま維持されると嬉しい。

SOP(Same Origin Policy)とCORS(Cross Origin Resource Sharing)について調べた

SOP(Same Origin Policy)の理解が怪しいなぁと思ったのでちゃんとした仕様を調べた。
同時にCORS(Cross Origin Resource Sharing)についても調べることになったのでまとめて整理した。

SOP(Same Origin Policy: 同一生成元ポリシー)

  • Webブラウザなどのセキュリティ機能の1つ
  • ある オリジン から読み込まれた文書やスクリプトについて、そのリソースから他の オリジン のリソースにアクセスできないように制限するもの

※ 参考:https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy

オリジンとは

スキーム、ポート、ホストがそれぞれ等しいものが、同じオリジンと見なされる http://domain-a.com/dir/page.html をベースとした場合、次のようになる。

(o) http://domain-a.com/dir2/other.html
(o) http://domain-a.com/dir/inner/another.html
(x) https://domain-a.com/secure.html
-> スキームが異なる
(x) http://domain-a.com:81/dir/etc.html
-> ポートが異なる
(x) http://domain-b.com/dir/other.html
-> ホストが異なる

※ 補足

  • IPアドレスは見ていない
  • パスが異なるだけのものは問題ない

※ 参考:https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy#Definition_of_an_origin

制限されるもの

制限されないもの

SOP(同一生成元ポリシー)には許されているものがいくつかある。 以下、異なるオリジンに埋め込むことができるリソースの例。

  • <script src="..."></script> によるJavaScript
  • <link rel="stylesheet" href="..."> によるCSS
  • <img>による画像
  • <video>, <audio>によるメディアファイル
  • <object>, <embed>, <applet> によるプラグイン
  • @font-face で指定されたフォント
  • <frame>, <iframe>に関連する様々なもの
  • <form>からの送信

異なるオリジンへのアクセスを許可する方法

  • かつてはJSONPという手法が使われていた(JSONPについては省略)
  • 現在では CORS を使用することが推奨されているようだ

CORS (Cross Origin Resource Sharing)

  • SOPはセキュリティ上重要な仕組みだが、まっとうなWebアプリケーション開発者にとってはオリジンをまたいだアクセスが必要になるケースが多かった。そこでCORSが登場した
  • Webサーバがドメインをまたぐアクセスを制御する方法を規定することで、ドメイン間の安全な通信を保証する
  • つまり、CORSの仕様に則れば、ドメインをまたいだアクセス(クロスオリジンHTTPリクエスト)ができるようになる

※ 参考:https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control#Overview

CORSのパターン

大きく2つのパターンがある

  • (1) simple cross-origin request
  • (2) actual request(プリフライトがある)

simple cross-origin requestになる条件は以下の通り

  • HTTPメソッドがGET,POST,HEADのいずれかである
  • HeaderがAccept,Accept-Language, Content-Language, Content-Type以外を含まない
  • Content-Typeを含む場合、その値がapplication/x-www-form-urlencoded, multipart/form-data, text-plainのいずれかである

上記の条件に合致しない場合はactual requestというプリフライトが必要なパターンになる

※ 参考:『Real World HTTP』10章

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

プリフライトリクエストとは

  • 実際の通信の前に、権限確認目的で送信するリクエストのこと
  • プリフライトリクエストはHTTPメソッドのOPTIONSを使って送信される
  • リクエストには以下のヘッダを含む
    • Access-Control-Request-Method: 通信を許可してもらいたいメソッド
    • Access-Control-Request-Headers: 許可して欲しいヘッダー(カンマ区切り)
    • Origin: 通信元のウェブページのドメイン
  • プリフライトリクエストは、リクエスト前に毎回送信されるわけではない。通信内容を一定期間キャッシュすることができる。キャッシュ期間を制御するには以下のヘッダを使う

※ 参考:『Real World HTTP』10章

属人化の排除と責任感のトレードオフについて

スクラムでプロダクト開発をするようになって、約1年が経過した。

スクラムは実によくできたフレームワークであり、スクラムから得られたものはたくさんある。 例えば..

  • 今自分がやっていることに対して納得感を持って取り組むことができるようになった
  • 発生した問題に対して、その場しのぎではなく仕組みで解決することができるようになった
  • 同じゴールを共有するようになったことで、"チームでやっている"という感覚が強くなった。そのおかげで心理的負担が減り、同時にメンバー間も仲良くなった(と思う)
  • 優先順位に基いて業務を進めるようになったことで、属人化が減った

などである。

その一方で、デメリットとして最近感じているのは属人化を排除することで同時に個々人の責任感も削り落としてしてしまったのではないかという点である。

スクラム導入以前は、AさんはサービスAを担当し、BさんはサービスBを担当、というように個人が持つ領域が明確に分けて開発・運用をやっていた。今考えても、限られた人数でそこそこの規模のプロダクトを効率良く運営するにはこれ以外に良い方法が思い浮かばない。

当然この運営スタイルは属人化を強めてしまい、AさんはサービスAしか見れないし、また担当外のサービスBについてはほとんど関心が無いという状態になってしまった(少なくとも自分はそうなった)。 一方でメリットもあった。それは担当しているサービスに対する責任感が人一倍強くなるという点だ。障害が起きれば真っ先に対応するし、ユーザや関係者からの問い合わせがあればそれについても真っ先に対応する。サービスの短期的な面だけでなく、将来的な面についても検討することができていた。

スクラム開発では、スプリントバックログという形で1スプリントで取り組むタスクを優先順位で並べて可視化すると思う。優先順位でタスクを消化していくため、誰がどのタスクを取ることになるかはわからない。言い方を変えれば、どのタスクを取っても大丈夫という状態になっていなければならない(と理解している)。 このようなスタイルで開発を進めていくため、属人化された業務が減っていくわけだ。 そして、みんながサービスに対してある程度の知識を持つようになったことで、"ここは自分がやるんだ"という責任感も弱くなった。

心理学的に言えば 社会的怠惰 という現象に当てはまると思っている。「誰かがやってくれる」「自分が前に出なくても誰かが..」といった意識が無意識に働いているような気がする。 また「自分は1人ではなくチームでいる」という安心感が、危機感すらも鈍らせてしまったように思う。これまでは「何が何でも解決しなければならない」という気持ちだったものが、「今やれることをやっていこう。無理をしても仕方がない。」という方向になりやすくなった。

エンジニアの中で 属人化は悪 というのは常識のように語られ、自分もそれを信じていた。 しかし、もちろん自分の弱さもあるが、実際にそのデメリットを肌で感じることでこの通説に疑問を抱くようになった。今でも属人化が良いとは思っていないが。

何か、良い感じにバランスをとっていけるやり方は無いかなぁ。

...といったことを考えつつ、初心に帰ってスクラムの書籍でも読んでみようと思った2017年の秋だった。

SCRUM BOOT CAMP THE BOOK

SCRUM BOOT CAMP THE BOOK

とりあえずこれ買ってみた