Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all 1836 articles
Browse latest View live

RailsでGraphQL APIをつくる: Part 2 – Railsで基本的なAPIを書く(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Part 1〜3の導入部は共通のため、2と3では省略しました。

RailsでGraphQL APIをつくる: Part 2 – Railsで基本的なAPIを書く(翻訳)

このブログ記事シリーズでは以下の3つのトピックについて扱います。

  1. GraphQLとは何か
  2. Railsで基本的なAPIを書く(本記事)
  3. ベストプラクティス集

Railsで基本的なAPIを書く

元ネタは私のslide#4です。

コードを書き始める前に最も大まかなユーザー要件を定義してから、順に実装を進めます。

トップレベルのユーザー要件
1. ユーザーはAPIを使うためのアカウントを必要としている。
2. ユーザーはAPIから自分のアカウント情報を取得できる。

要件1. 「ユーザーはAPIを使うためのアカウントを必要としている」

認証用gemのインストール

Railsアプリなら好みの認証用gemをインストールするだけなので、実に簡単です。DeviseClearanceあたりがよいでしょう。本記事の例では新しめのgemということでClearanceを使いますが、どれでも同じなので好みの認証gemを使えます。

Clearanceの詳しいインストール方法は省略します(ドキュメントをご覧ください)。インストール後、以下の3つのコマンドを実行します。

$ rails generate clearance:install
$ rails generate clearance:routes
$ rake db:migrate

実に簡単です。Rubyコミュニティにひたすら感謝。🙏

APIトークンとUserモデルを設定する

先に進む前に、API経由でユーザーを許可(authorize)するauth_tokenが必要です。今回はhas_secure_token(Rails 5以降ではgemのインストールは不要です)でUser#api_tokenを生成することにしました。generate後のUserモデルは次のような感じになります。

class User < ActiveRecord::Base
  include Clearance::User
  has_secure_token :api_token
end

# == Schema Information
#
# Table name: users
#
#  id                 :integer          not null, primary key
#  created_at         :datetime         not null
#  updated_at         :datetime         not null
#  email              :string           not null
#  encrypted_password :string(128)      not null
#  confirmation_token :string(128)
#  remember_token     :string(128)      not null
#  api_token          :string

要件1.を達成できました。おめでとうございます🎉

要件2. 「ユーザーはAPIから自分のアカウント情報を取得できる」

これからGraphQL APIを作成します。目標は、以下のようなクエリをやりとりできるAPIエンドポイントの作成です。

query {
  me: viewer {
    id
    email
    created_at
  }
}

上のようなクエリを受信し、以下のようなJSONを返します。

{
  "data": {
    "me": {
      "id": 1,
      "email": "wayne.5540@gmail.com",
      "created_at": 1477206061
    }
  }
}

必要なgemをインストールする

# コアとなるgem
gem 'graphql', '~> 1.0.0'

# GraphQL APIエクスプローラを作成できる便利なgem(必須ではない)
gem 'graphiql-rails'

# TDD
group :development, :test do
  gem 'rspec-rails', '~> 3.5'
  gem 'shoulda-matchers'
  gem 'factory_girl_rails'
  gem 'faker'
end

APIエンドポイントを作成する

Authorizationヘッダー{ "Authorization" => "Token #{user.api_token}" }を追加したPOST /graphql?query=graphql-queryリクエストを受信できるようにします。

そのために、まずリクエストの処理に必要なルーティングを書きます。

# config/routes.rb
Rails.application.routes.draw do
  post "graphql" => "graphqls#create"
end

続いてコントローラを書きます。

class GraphqlsController < ApplicationController
  before_action :authenticate_by_api_token!

  def create
    query_string = params[:query]
    query_variables = JSON.load(params[:variables]) || {}
    context = { current_user: current_user }
    result = Schema.execute(query_string, variables: query_variables, context: context)
    render json: result
  end
end

Schema.execute(query_string, variables: query_variables, context: context)行はgraphql gemの基本的な利用法を示しており、ここでクエリ文字列、グラフ変数、コンテキストをSchemaに渡します。Schemaはこの後で定義します。


GraphQLの型について

コントローラのアクションで行われることを理解するために、ここでGraphQLの型システムについて簡単に説明します。前述のクエリ例をもう一度引用します。

query {
  me: viewer {
    id
    email
    created_at
  }
}

実際には、このクエリにSchemaQueryTypeUserTypeという3つの異なるスコープが含まれています。

各々の型はAPI開発者が定義します。以下の画像で各スコープの概要をご覧ください。

Schemaスコープ: 最も外側のスコープであり、QueryおよびMutationという特殊型を含みます。QueryはHTTPのGETリクエストに相当し、MutationPOSTリクエストに相当すると考えるとわかりやすいでしょう。

QueryTypeスコープ:

UserTypeスコープ:


GraphQLで型を定義する

それでは型定義コードを書いてみましょう。GraphQL::ObjectType APIを利用すれば比較的簡単です。

UserTypeスコープ: User型を定義するために、各フィールドを定義します。レスポンスのカスタマイズにはresolveブロックを使います。カスタマイズしない場合はフィールド名のメソッドから取得します(例: フィールドidでuser.idがレスポンスとして使われる)。
今回の例ではupdated_atcreated_atIntegerに設定したいので、resolveブロックのupdated_atフィールドでobj.updated_at.to_iを呼び出します。

# app/graph/types/user_type.rb
UserType = GraphQL::ObjectType.define do
  name "User"
  description "1人のユーザー"

  field :id, types.Int
  field :email, types.String
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end

QueryTypeスコープ: UserTypeの場合と同じ要領でviewerフィールドを作成し、typeUserTypeを指定します。

# app/graph/types/query_type.rb
QueryType = GraphQL::ObjectType.define do
  name "Query"
  description "このスキーマのクエリのルート(root)"

  field :viewer do
    type UserType
    description "Current user"
    resolve ->(obj, args, ctx) {
      ctx[:current_user]
    }
  end
end

context[:current_user]Schema.executeメソッドに渡され、Schemaの下のすべてのフィールドで使えるようになります。

なお、UserType自身もcurrent_userを取得できるので、実際にはUserTypeにcurrent_userを渡す必要はありません。ここでは、QueryType#viewerresolveブロック内でcurrent_userが返すuserオブジェクトがUserTypeに渡される様子を例示するために、あえて使っています。

Schemaスコープ:

# app/graph/schema.rb

Schema = GraphQL::Schema.define do
  query QueryType
end


仕上げの設定

あとは設定をいくつか追加すれば完了です。

グラフや型のautoload:

# config/application.rb
module GraphBlog
  class Application < Rails::Application
    # ...
    config.autoload_paths << Rails.root.join('app', 'graph', 'types')
  end
end

GraphiQLの設定(Graph APIのインタラクティブUI):

最初に、GraphiQLエンジンをRailsルーティングにマウントします。

# config/routes.rb

Rails.application.routes.draw do
  # ...
  mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
  # ...
end

次にデフォルトのAuthorizationヘッダーを設定します。

# config/initializers/graphiql.rb

GraphiQL::Rails.config.headers['Authorization'] = -> (context) {
  "Token #{context.request.env[:clearance].current_user.try(:api_token)}"
}

あとはRailsサーバーを起動してhttp://localhost:3000/graphiqlを開けば、GraphQLで遊べるようになります。

サンプルクエリをいくつか掲載します。

query {
  viewer {
    id
    email
  }
}
query {
  me: viewer {
    ...userFields
  }
  alsoIsMe: viewer {
    ...userFields
  }
}

fragment userFields on User {
  user_id: id
  email
}

以上で、Railsプロジェクトで初めてのGraphQL APIエンドポイントが動くようになりました。🎉

今回のガイドではRailsで簡単なGraphQL APIを書くにとどめましたので、次回の「RailsでGraphQL APIをつくる: Part 3 – ベストプラクティス集」でベストプラクティスをいくつかご紹介します。



Vue.jsサンプルコード(14)「承認」ボタンを押したら解除できないようにする

$
0
0

14. 「承認」ボタンを一度押すと解除できないようにする

  • 画面を再読み込みするとチェックボックスはオフの状態に戻ります。

サンプルコード


ポイント: HTMLのinputで@change="a = true"を指定することで、常にtrueにしています。

<input type="checkbox" v-model="a" @change="a = true">
  承認
</input>

  • 前記事: 13. 「承認」チェックボックスをオンにしないと「送信」ボタンを押せないようにする
  • 次記事: 15. パスワードのマスク解除ボタン

Ruby: 8/27発表のRubyGems脆弱性と修正方法のまとめ

$
0
0

こんにちは、hachi8833です。先週はRailsウォッチをお休みしてしまったことをお詫びいたします。

表題のニュースは先週のものですが、それなりに重要性が高いため改めて記事にいたしました。gemspecファイルからの入力もユーザー入力である以上サニタイズが必要なんですね。

RubyGemsで複数の脆弱性が発見・修正された(抜粋・まとめ)

参照

要点

公式ブログRubyGemsの2.6.13がリリースされました。2.6.13では、従来のRubyGems gemで見つかった複数の脆弱性が修正されています。

影響を受けるRubyバージョン

  • Ruby 2.2系のうち、2.2.7以前のバージョン
  • Ruby 2.3系のうち、2.3.4以前のバージョン
  • Ruby 2.4系のうち、2.4.1以前のバージョン
  • trunkのリビジョンが59672より前のバージョン

公式ブログ公開時点(2017/08/29)では、修正済みRubyGemsを含むRubyはリリースされていません。
その代わり、RubyGemsのバージョンを単独で2.6.13以降にアップデートすることで問題を回避できます。

修正方法

コマンドプロンプトで以下を実行することで、RubyGems gemをアップデートできます。

gem update --system

事情があってRubyGemsをアップデートできない場合、Rubyのバージョンに応じて以下のパッチを適用できます。

補足: rbenvRVMなどのバージョン管理ソフトウェアで複数のRubyを使っている場合、念のため、修正可能なすべてのバージョンで上のコマンドを実行しておくのがよいでしょう。当然ながら、VM、またはDockerやsystemd nspawnなどのコンテナでRubyを使っている場合はそちらもチェックし、必要であればアップデートします。

[Rails 5] rbenvでRubyをインストールして新規Rails開発環境を準備する

RubyGems 2.6.13の主な修正内容

DNSリクエストがハイジャックされる脆弱性の修正(CVE-2017-0902

  • DNSへの名前参照に対して中間者攻撃(MITM)が可能になっていたのを修正(8d91516fb
# lib/rubygems/remote_fetcher.rb
-      if /\.#{Regexp.quote(host)}\z/ =~ target
+      if URI("http://" + target).host.end_with?(".#{host}")
          return URI.parse "#{uri.scheme}://#{target}#{uri.path}"
        end

ANSIエスケープシーケンスの脆弱性の修正(CVE-2017-0899

  • gem spec内文字列を取り出すときに非表示文字(いわゆる印刷できない文字)を除去するようになった(ef0aa611
# lib/rubygems/text.rb
+  # Remove any non-printable characters and make the text suitable for
+  # printing.
+  def clean_text(text)
+    text.gsub(/[\u0000-\u0008\u000b-\u000c\u000e-\u001F\u007f]/, ".".freeze)
+  end

gem queryコマンドでのDOS脆弱性の修正(CVE-2017-0900

  • summaryのサイズに制限を加えた(8a38a4fc
# lib/rubygems/commands/query_command.rb
   def spec_summary entry, spec
-    entry << "\n\n" << format_text(spec.summary, 68, 4)
+    summary = truncate_text(spec.summary, "the summary for #{spec.full_name}")
+    entry << "\n\n" << format_text(summary, 68, 4)
   end

spec内のgem名に..を含めて任意のファイルを上書きできる脆弱性の修正(CVE-2017-0901

  • rubygems.orgで受け付けない無効な文字をspec名から除去するようになった(44cc27cd, ad5c0a53
# lib/rubygems/specification.rb
+  VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc:

補足: Herokuでの対応について

HerokuではRuby 2.4.1についてホスト側で対応済みなので、ユーザーはローカルの環境について対応すればよいことになります。

本記事執筆時点のHerokuのドキュメントでは、GemfileでRubyバージョンを指定していない場合はデフォルトで1つ前のバージョン(現時点ではRuby 2.3.4)が使われると記載されています。アプリがデフォルトのRubyバージョンに依存しないよう、アプリのGemfileには以下のようにRubyバージョンを明示的に記載することが強く推奨されています

# Gemfile
source 'https://rubygems.org'

ruby '2.4.1'   # 👈

gem 'rails', '5.1.3'

gem 'bcrypt'
...

なお、以下のバージョンについてのChangelogはありますが、RubyGemsのバージョンは2.6.13にはなっておらず、パッチが適用済みかどうかについては確認できませんでした。

関連記事

そのパッチをRailsに当てるべきかを考える(翻訳)

Railsアプリの認証システムをセキュアにする4つの方法(翻訳)

[Rails 5] rbenvでRubyをインストールして新規Rails開発環境を準備する

【翻訳+解説】binstubをしっかり理解する: RubyGems、rbenv、bundlerの挙動

RailsでGraphQL APIをつくる: Part 3 –ベストプラクティス集(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

RailsでGraphQL APIをつくる: Part 3 – ベストプラクティス集(翻訳)

このブログ記事シリーズでは以下の3つのトピックについて扱います。

  1. GraphQLとは何か
  2. Railsで基本的なAPIを書く
  3. ベストプラクティス集(本記事)

1. インターフェイス

2つの型が多数のフィールドを共有している状況を考えてみましょう。たとえば、2つの型のどちらにもidupdated_atcreated_atといったActiveRecordの基本的なフィールドがあるとします。こうした型を整然と扱うにはどうしたらよいでしょうか。1つの方法は、次の「インターフェイス」を使うことです。

リファクタリング前のUserTypePostTypeは次のようになっています。

# app/graph/types/user_type.rb
UserType = GraphQL::ObjectType.define do
  name "User"
  description "1人のユーザー"

  field :id, types.Int
  field :email, types.String
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end
# app/graph/types/post_type.rb
PostType = GraphQL::ObjectType.define do
  name "Post"
  description "1件の投稿"

  field :id, types.Int
  field :title, types.String
  field :content, types.String
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end

そこで、active_record_interfaces.rbファイルを新たに作成して、idupdated_atcreated_atフィールドを定義します。

# app/graph/types/active_record_interfaces.rb

ActiveRecordInterface = GraphQL::InterfaceType.define do
  name "ActiveRecord"
  description "Active Recordインターフェイス"

  field :id, types.Int
  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end
  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end

これで、UserTypePostTypeを次のようにすっきりと書き直せます。

# app/graph/types/user_type.rb

UserType = GraphQL::ObjectType.define do
  interfaces [ActiveRecordInterface]
  name "User"
  description "1人のユーザー"

  field :email, types.String
end
# app/graph/types/post_type.rb

PostType = GraphQL::ObjectType.define do
  interfaces [ActiveRecordInterface]
  name "Post"
  description "1件の投稿"

  field :title, types.String
  field :content, types.String
end

2. Serviceオブジェクト

resolveブロックは各フィールドの振る舞いを扱いますが、resolveブロックにコードロジックを直接追加するとresolveブロックが不安定になり、テストも行いにくくなってしまいます。よりよい方法は、ロジックをresolveブロックから切り離してServiceオブジェクトに閉じ込めることです。なお、Serviceオブジェクトは抽象化のためのものなので、好みのデザインパターンがあればそれを使って抽象化してもかまいません。

以下のコード例をご覧ください。

CreatePostMutation = GraphQL::Relay::Mutation.define do
  # ...

  resolve -> (object, inputs, ctx) {
    post = ctx[:current_user].posts.create(title: inputs[:title], content: inputs[:content])

    {
      post: post
    }
  }
end

上のコードは以下のように書き換えられます。

CreatePostMutation = GraphQL::Relay::Mutation.define do
  # ...

  resolve -> (object, inputs, ctx) {
    Graph::CreatePostService.new(inputs, ctx).perform!
  }
end

こうしておけばGraph::CreatePostServiceだけをテストすれば済むので、テストやメンテナンスが楽になります。

3. Relayモジュールを使う

graphql-ruby gemRelay名前空間にはいくつかの便利なモジュールがあります。APIをGitHub GraphQL APIのようにしたい場合や、Relayに統合するだけなら、これらのモジュールを使うことで多くの時間を節約できます。

以下のモジュールの使い心地を試してみるとよいでしょう。

  • GraphQL::Relay::ConnectionType
  • GraphQL::Relay::Node
  • GraphQL::Relay::Edge

以上、私が学んできた経験をご紹介いたしました。この他にもGraphQLとRailsで何かよい手法を見つけたら、ぜひgistのコメントでお知らせください。
お読みいただきありがとうございました。

関連記事

RailsでGraphQL APIをつくる: Part 1 – GraphQLとは何か(翻訳)

RailsでGraphQL APIをつくる: Part 2 – Railsで基本的なAPIを書く(翻訳)

CSS/JS: 画像を不規則な形に切り取ってテープで貼り付けるテクニック(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

注: 英語記事執筆の時点からブラウザのバージョンが進んでいるため、本記事内の細かなCodePen埋め込み表示は元記事のCodePen表示と若干異なっている可能性があります。また、ブラウザによってもCodePen表示が若干異なる可能性があります。

CSS/JS: 画像を不規則な形に切り取ってテープで貼り付けるテクニック(翻訳)

この間、Vasilis van Gemert氏のサイト「Atlas of Makers」のことがふと頭をよぎりました。とても面白く風変わりなこのサイトに惹かれて調べてみたところ、ここはまさに学ぶ価値のあるサイトであると確信できました。このサイトには数年におよび記事やよもやま話がたくさん詰まっていますが、サイト制作に使われている機能が実にクールなのです。このサイトの技法にはCSS Grid、カスタムプロパティ、blendモードや、SVGまで動員されていますが、それにもかかわらずなぜかそれほど知られていません。

このサイトでは四角でない不定形の画像の作成にSVGを使っており、まるでページに蛍光テープやスコッチテープで貼り付けたかのように表示されています(スコッチテープエフェクト)。本記事では、ブラウザだけで作業できるシンプルな手法でこの技法を再現する方法について解説します。それでは始めましょう。

訳注: 以下、原文のscotch tapeは単に「テープ」と表記します。

1. 画像のポリゴン頂点座標の取得

まずは題材選びから。こちらのsnow leopard(ユキヒョウ)画像を例として使うことにします。

ふわふわのsnow leopardが好奇の目でこちらを見ている画像です

続いて、このsnow leopardの輪郭に合わせてざっくりポリゴンで囲みます。今回はClippy(作: Bennett Feely)というサービスを使いました。CSSのclip-pathは現時点ではクロスブラウザではないため、ここでは使いません(使いたい方はMicrosoft Edgeのフォーラムで投票してください: ログイン不要です)。Clippyはとても素晴らしいサービスで、うんざりするほどのボタンやらオプションやらを装備した画像レタッチソフトウェアを使わずに、ポリゴンの頂点のデータだけをきわめて高速に取り出せます。

次は画像のカスタムURLとカスタム寸法を設定します。Clippyではviewportのサイズに沿って寸法を制限できますが、ここでは実際の画像のサイズはまったく重要ではありません(特に、最終出力は%値だけで指定されるため)。この画像にはアスペクト比2:3だけを指定することにします。

ClippyでサイズとURLを指定する

Clippyでカスタム寸法とURLを指定する場所は上のスクリーンショットに印をつけてあります。カスタム寸法の縦横比は2:3になるようにする必要があります。幅は540 = 2*270、高さは810 = 3*270とします。

作業を説明しやすくするため、Show outside clip-pathオプションをオンにしておきます。

アニメーションGIF: Show outside clip-pathをオンにしておくと後で役に立ちます。猫の周りをポリゴンの頂点でおおまかに囲むときに、ポリゴンの外側が部分表示で見えるようになります。

続いて、カスタムポリゴンをクリッピングパスで使えるように、すべての頂点を選択し、パスをきちんと閉じてから頂点の位置を適当に調整します。


ネコの輪郭をごく大まかに外側からなぞるようにカスタムポリゴンの点を選択すると、CSSのclip-pathコードが生成されます。

2. ブラウザのdeveloper consoleでの作業

このポイント(%値)のリストを選択し、ブラウザのdeveloper consoleを開いて以下のJavaScriptコードを貼り付け、文字列部分にポイントリストを貼り付けます。

// JS(見やすさのため適当に改行を挿入しています)
let coords = '69% 89%, 84% 89%, 91% 54%,
              79% 22%, 56% 14%, 45% 16%,
              28% 0, 8% 0, 8% 10%,
              33% 33%, 33% 70%,
              47% 100%, 73% 100%';

次のコードをコンソールに貼り付けて、%記号をすべて取り除き、文字列を分割します。

// JS
coords = coords.replace(/%/g, '').split(', ').map(c => c.split(' '));

続いて画像の寸法を指定します。

// JS
let dims = [736, 1103];

これで、画像の寸法の座標を拡大縮小できるようになりました。得られた値は次のJavaScriptコードで四捨五入します。画像がかなり大きいので、ネコの輪郭をなぞるポリゴン座標の小数点部分は不要です。

// JS
coords = coords.map(c => c.map((c, i) => Math.round(.01*dims[i]*c)));

後は以下のコードを実行すればおしまいです。

// JS
`[${coords.map(c => `[${c.join(', ')}]`).join(', ')}]`;

得られた値をブラウザのdeveloper consoleからコピーしてフォームに貼り付けます。

ブラウザのdeveloper toolsでの作業

3. SVG画像の生成

今度はPugでSVG画像を生成しましょう。ここでは2.で取得した頂点座標の配列をそのまま使っています。

// pug
- var coords = [[508, 982], [618, 982], [670, 596], [581, 243], [412, 154], [331, 176], [206, 0], [59, 0], [59, 110], [243, 364], [243, 772], [346, 1103], [537, 1103]];
- var w = 736, h = 1103;

svg(viewBox=[0, 0, w, h].join(' '))
  clipPath#cp
    polygon(points=coords.join(' '))
  image(xlink:href='snow_derpard.jpg'
        width=w height=h
        clip-path='url(#cp)')

上のコードを実行すると、次のように不規則な形の画像を得られます。

See the Pen irregular shaped image by Ana Tudor (@thebabydino) on CodePen.

4. 不規則な画像にテープエフェクトを追加する

いよいよページの画像にテープを貼り付けます。テープを生成するために、先ほどと同じ座標の配列を使います。テープの長さを座標から読み出すコードを繰り返すために、まずループを書きます。

// pug
-// (上と同じなので省略)
- var n = coords.length;

svg(viewBox=[0, 0, w, h].join(' '))
  -// (上と同じなので省略)
  - for(var i = 0; i < n; i++) {

  - }

続いてこのループ内で、頂点から次の頂点にテープを貼るかどうかを乱数で決めます。

// pug
- for(var i = 0; i < n; i++) {
  - if(Math.random() > .5) {
    path(d=`M${coords[i]} ${coords[(i + 1)%n]}`)
  - }
- }

strokenoneになっているので、これだけではテープは表示されません(Codepen)。

次のようにhsl()にランダムなhue値を与えてstrokeを太くすると、テープが表示されるようになります。

// scss
stroke: hsl(random(360), 90%, 60%);
stroke-width: 5%;
mix-blend-mode: multiply

さらにmix-blend-mode: multiplyを指定することで、よりテープらしく見えるようにしています。

See the Pen irregular shaped image with tape #1 by Ana Tudor (@thebabydino) on CodePen.

5. クロスブラウザ化

だいぶよくなってきましたが、実はまだ問題がいくつか残されています。

最初にして最大の問題は、クロスブラウザになっていないということです。mix-blend-modeはまだEdgeで動きません(まだの方はぜひ投票をお願いします!)。Edgeでほぼ同等の効果を得るために、Edgeの場合のみstrokeを半透明にすることにします。

当初私が思いついた手法は、RGBコンポーネントでサポートされない非整数をcalc()値で整数に変換するというものでしたが、この方法は現時点ではEdgeでしかサポートされていません。今使っているのは、残念ながらrgb()値ではなくhsl()値です。今回は幸いScssを使っているので、これでRGBコンポーネントを抽出できます。

// scss
$c: hsl(random(360), 90%, 60%);
stroke: $c;
stroke: rgba(calc(#{red($c)} - .5), green($c), blue($c), .5)

最下部の行はEdgeには適用されますが、calc()の動作によってChromeとFirefoxでは無効になります(下図左がChrome、右がFirefox)。

2つ目のstrokeの指定がChrome(左)とFirefox(右)で無効になっている

ただし、今後他のブラウザがEdgeと同じ動作になればこの手は使えなくなります。

今後も使える方法としては、@supportsがよいでしょう(codepen)。

path {
  $c: hsl(random(360), 90%, 60%);
  stroke: rgba($c, .5);

  @supports (mix-blend-mode: multiply) { stroke: $c }
}

6. テープの端を重ね貼り表示する

次の問題は、テープの端をもう少し伸ばしてテープらしく表示する方法です。幸い、この問題はstroke-line-capsquareに設定するだけで簡単に修正できます。これを指定すれば、簡単にテープの両端をテープ幅の半分だけ長くできます。

See the Pen irregular shaped image with tape #3 by Ana Tudor (@thebabydino) on CodePen.

7. テープのはみ出しが切り落とされないようにする

最後の問題は、テープがSVG画像の端で切り落とされてしまうことです。SVGのoverflowプロパティをvisibleに設定したとしても、SVGの内容が切り落とされてしまったり、画像の次に来る要素で隠されてしまったりする可能性が残ります。

これについては、imageの周りのviewBoxスペースを増やせばどうにかできそうです。テープで隠れないようにするのに十分なスペース増加分を、ここではpと呼ぶことにします。

-// 同上
- var w1 = w + 2*p, h1 = h + 2*p;

svg(viewBox=[-p, -p, w1, h1].join(' '))
  -// 同上

このpの値をどうやって決めるかが問題です。

理論編

値を決めるときには、今使っているstroke-widthの値が%になっている点に注意しておく必要があります。SVGでは、stroke-widthなどの%値はSVG領域の対角線の長さから算出されます。今回の場合、SVG領域は幅w、高さhの四角形(rectangle)です。この四角形に対角線を引くと、ご存知ピタゴラスの定理によって以下の黄色い三角形の斜辺を計算できることがわかります。

SVGの四角形に斜辺を引くと、直角の隣にある2辺がSVG viewBoxの幅と高さになる

したがって斜辺の長さは以下のPugコードで得られます。

// pug
- var d = Math.sqrt(w*w + h*h);

ここからは、stroke-widthを対角線の長さの5%で計算することにします。これは対角線d.05をかけたものと同等です。

// pug
- var f = .05, sw = f*d;

ここで得た値は、%値(5%)からユーザー設定の単位(.05*d)に変換されていることにご注目ください。こうしておくと、viewBoxの寸法を増やしたときに対角線の長さも増え、対角線の5%の長さも得られるので便利です。

strokeを描画すると、あらゆるpathについて幅の半分はパスの線の外側に、半分はパスの線の内側にそれぞれはみ出ます。今やりたいのは、viewBoxのスペースをstroke-widthの半分の長さより大きく取ることです。前述のstroke-linecapによって、パスの両端からstroke-widthの半分だけはみ出す点にも注意しておく必要があります。


`stroke`はパスの線の両側面に半分ずつはみ出し、`square-linecap`によって`stroke-width`の半分だけ端点からはみ出す。

実践編

ここからは実践編として、クリッピング用ポリゴンの頂点の1つが元画像のちょうど端にかかっている状況を考えてみましょう。こうした点がいくつあっても方法は同じですが、話を簡単にするために、ここでは1つの点についてだけ考えることにします。

以下のようにポリゴンの端点Eが元画像の最上部の境界にかかっているとします。さらに、切り出したSVGでも同様であるとします。

元画像の最上部の境界にある点を示す画像。

今知りたいのは、作成したstrokestroke-linecapsquareが設定されている場合に、画像最上部の境界からどのぐらいはみ出すかです。はみ出す長さは境界との角度によって変わるので、はみ出し部分が切り落とされてしまわないよう、境界のさらに上に確保する最大の余白を知りたいのです。

この点をより深く理解するために、次のインタラクティブなデモを用意しました。デモを開き、スライダを左右に動かしてはみ出した部分を回転させると、stroke(およびsquare-linecap)の隅が境界の外側でどのぐらいはみ出すかを実感できます。

See the Pen rotation at the top edge by Ana Tudor (@thebabydino) on CodePen.

strokestroke-linecapの外側の隅が描く軌道をたどるとわかるように、画像の境界の外側に必要な余白は、境界上の端点Eと、strokeおよびstroke-linecapによってできた隅(図ではAまたはB、位置は角度によって異なる)との間の線分が境界と垂直になるときに最大になり、必要な余白の長さはこの線分AE(またはBE)の長さに等しくなります。

テープの端はstroke-widthの半分の長さだけ端点Eからはみ出します。このはみ出しは、余弦PBと通常の方向EPで同じ長さになります。したがって、三角形EBPは2辺の長さがそれぞれstroke-widthの半分に等しい直角二等辺三角形となります。線分EBの長さは、この直角二等辺三角形の斜辺の長さになります。

パスの端点Eと、strokeおよびstroke-linecapではみ出した隅(AまたはB)との間の線分の長さは、stroke-widthの半分の長さを2辺とする直角二等辺三角形の斜辺と等しくなることを示す

ふたたびピタゴラスの定理に登場いただくと、Pugコードは次のようになります。

// pug
- var hw = .5*sw;
- var p = Math.sqrt(hw*hw + hw*hw) = hw*Math.sqrt(2);

これまでの結果をすべて合わせると、次のPugコードになります。

// pug
/* 座標と初期サイズは同上 */
- var f = .05, d = Math.sqrt(w*w + h*h);
- var sw = f*d, hw = .5*sw;
- var p = +(hw*Math.sqrt(2)).toFixed(2);
- var w1 = w + 2*p, h1 = h + 2*p;

svg(viewBox=[-p, -p, w1, h1].join(' ')
    style=`--sw: ${+sw.toFixed(2)}px`)
  /* 同上 */

後はCSS側で、stroke-widthの値を与えてテープのはみ出しを調整するだけです。

// scss
stroke-width: var(--sw);

この--swは、stroke-widthを設定してcalc(var(--sw)*1px)を算出するのに使うため、単位のない値は使えない点にご注意ください。理論的にはこれでも動くはずですが、現実にはFirefoxとEdgeでstroke-*calc()値がまだサポートされていません。

CodePen上の最終的な表示は、次のようになります。

See the Pen irregular shaped image with tape #4 by Ana Tudor (@thebabydino) on CodePen.

関連記事

新版: やばい臭いのするCSSコード(翻訳)

Webデザイナーがこっそり教える4つのテクニック

CSSでの句読点ぶら下げ: hanging-punctuationプロパティ

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

$
0
0

こんにちは、hachi8833です。N+1問題の検出といえばbullet gemですが、BatchLoaderはより積極的かつ一般性の高い方法でN+1を解決するgemです。

概要

原著者の許諾を得て翻訳・公開いたします。

訳注: バッチング(batching)という語はここでは主に「(小分けにされたものを)1つにする」操作を指しています。
Unityなど3Dゲーム制作方面では、バッチングは「個別の物体のレンダリングを1つにまとめる」ことを指すようです。

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

本記事では、バッチングと呼ばれる技法でN+1クエリを回避する方法、HaskelのHaxlやJavaScriptのDataLoaderでのバッチング、およびRubyプログラムでできるアプローチについて解説します。

N+1クエリとは何か

最初に、N+1クエリとその呼び名の由来について説明します。userspostsという2つのSQLテーブルがあるとすると、ActiveRecordモデルを使って次のように書くことができます。

posts = Post.where(id: [1, 2, 3])
# SELECT * FROM posts WHERE id IN (1, 2, 3)
users = posts.map { |post| post.user }
# SELECT * FROM users WHERE id = 1
# SELECT * FROM users WHERE id = 2
# SELECT * FROM users WHERE id = 3

最初のSELECT * FROM postsクエリは1件、その後のSELECT * FROM users...クエリはN件になるので、このコードでは1+N件のクエリが生成されます。加算は順序を変えても合計が変わらないことから、一般には順序を変えたN+1クエリで呼ばれます。

N+1クエリのよくある解決法

Ruby界隈では一般に、N+1クエリ問題の解決に次の2つの方法がよく使われます。

  • モデルでのeager loading
posts = Post.where(id: [1, 2, 3]).includes(:user)
# SELECT * FROM posts WHERE id IN (1, 2, 3)
# SELECT * FROM users WHERE id IN (1, 2, 3)
users = posts.map { |post| post.user }

指定された関連付けをコードの内部でpreloadし、利用しやすくします。ただし、ORMではこの手が常に使えるとは限りません(別のデータベースがあるなど、データを異なる場所から読み込む必要がある場合)。

  • preloadしたデータを引数経由で渡す
class Post < ApplicationRecord
  def rating(like_count, angry_count)
    like_count * 2 - angry_count
  end
end
posts = Post.where(id: [1, 2, 3])
# SELECT * FROM posts WHERE id IN (1, 2, 3)
post_emoticons = Emoticon.where(post_id: posts.map(&:id))
like_count_by_post_id = post_emoticons.like.group(:post_id).count
angry_count_by_post_id = post_emoticons.angry.group(:post_id).count
# SELECT COUNT(*) FROM emoticons WHERE name = 'like' AND
# post_id IN (1, 2, 3) GROUP BY post_id
# SELECT COUNT(*) FROM emoticons WHERE name = 'angry' AND
# post_id IN (1, 2, 3) GROUP BY post_id
posts.map do |post|
  post.rating(
    like_count_by_post_id[post.id],
    angry_count_by_post_id[post.id]
  )
end

この手法は柔軟性が高く、#includesを使ったシンプルな方法が利用できない場合にも使えます。また、メモリ効率も高くなることがあります。上の例ではpostごとのレーティングを計算するためにすべてのemoticon(絵文字)を読み込むわけではありません。そうした面倒な部分はすべてデータベースが肩代わりし、各postのcountは引数として渡されます。ただし、特に下にいくつものレイヤがある(Emoticonsを読み込み、Users経由でPostsに渡すなど)場合に、引数経由でのデータ渡しが複雑になることがあります。

これら2種類のアプローチは、いずれもN+1クエリを回避するのに有用です。問題は、トップレベルでどのデータをpreloadする必要があるかを「事前に」見極めなければならない点です。さらに、preloadが不要な場合にもpreloadが行われてしまいます。たとえばGraphQLを使っていると、ユーザーからのクエリにどんなフィールドが含まれるかを事前に予測しきれないため、こうしたアプローチが使えません。このような場合にN+1クエリを回避する妙案はないものでしょうか。それがバッチングと呼ばれる手法です。

バッチングとは

バッチングは、N+1の解決手法として新しいものではありません。Facebookは2014年にHaskel Haxlライブラリをリリースしましたが、技法自体はずっと前から使われており、Monad(モナド)、Applicative、Functor(関手)といった概念が採り入れられています。これらの概念を説明しようとするとそれだけで個別の記事が必要になるので、ここではこれ以上踏み込みません(Rubyでの関数プログラミングについてもっと知りたいという方はいませんか?)。

訳注: これらの用語は主に数学の圏論に由来しています。Applicativeには今のところ定訳はないようです。

バッチングの概念は他のプログラミング言語にも実装されています。その中で最もよく知られているのはJavaScriptのDataLoaderライブラリで、GraphQLとともに非常に人気が高まっています。FacebookのエンジニアLee Byronによる素晴らしい動画とソースコードが公開されており、わずか300行であるにもかかわらず、かなり素直なコードです。

一般的なバッチングの手順

  • アプリで読み込む項目をバッチに渡す(読み込む場所はアプリ内のどこでもよい)
  • バッチ内で、受け取った項目の値の読み込みとキャッシュを行う
  • 読み込んだ値は、項目が渡された元の場所で取得される

この手法の主な利点は、バッチングが独立していることです。そのおかげで、アプリのどの場所でも必要なタイミングでデータを読み込めます。

JavaScript DataLoaderを使った基本的な使用例をご紹介します。

var batch = (userIds) => ...;
var loader = new DataLoader(userIds => batch(userIds));
// “load”は、Node.js “process.nextTick” でキューをdispatchするジョブを
// スケジューリングし、promiseを1つ返す
loader.load(userId1).then(user1 => console.log(user1));
loader.load(userId2).then(user2 => console.log(user2));
loader.load(userId3).then(user3 => console.log(user3));

最初にloaderを作成します。ここでは、userIdsをloadする項目のコレクションをすべて受け取る関数を1つloaderに渡します。これらの項目は、全ユーザーを一括で読み込むbatch関数に渡されます。次に、loader.load関数を呼びます。この関数は、読み込んだひとつの値(user)を含むpromiseを返します。なるほど。ところでRubyではどうやればよいのでしょうか。

Rubyでのバッチング

Universeでは毎月ハッカソンが開催されており、EthereumやElixir、プログレッシブWebアプリといったアイデアや技術を誰でも自由に体験できます。私は前回のハッカソンで指名を得ることができたのですが、ちょうどGraphQLでN+1クエリを回避する既存の手法や、GraphQLクエリをバッチングでMongoDBのAggregation Pipelineに変換する方法を学んでいたところでした。Aggregation Pipelineでさまざまなコレクションをjoinしたりfilterしたりserializeしたりする方法については、前回の記事をご覧ください。

また、その頃の私たちは、従来のRESTful APIのサポートを継続しながらGraphQLに統合する作業も進めていました。通常、私たちの開発しているアプリでは、プラットフォームのスケールアップを繰り返そうとすると、N+1のDBクエリやHTTPリクエストによるボトルネックが主な問題となっていました。そういうわけで、私たちは既存のRESTful APIとGraphQLの両方でN+1クエリを解決できるツールの開発を決めました。Ruby開発者なら誰でも内容を理解して利用できるシンプルなツール、その名もBatchLoaderです。

lazyな実装

class Post < ApplicationRecord
  belongs_to :user

  def user_lazy
    # BatchLoaderでクールなコードをここに書く
  end
end
posts = Post.where(id: [1, 2, 3])
# SELECT * FROM posts WHERE id IN (1, 2, 3)
users_lazy = posts.map { |post| post.user_lazy }
BatchLoader.sync!(users_lazy)
# SELECT * FROM users WHERE id IN (1, 2, 3)

BatchLoaderの実装は、他のプログラミング言語における非同期的な性質を持つ実装の猿真似を目指しておらず、Promiseのような余分なプリミティブを使っていません。EventMachineを使っているのでもなければ、Rubyでそうしたものを使う理由はありません。

それに代わるものとして、Rubyの標準ライブラリにある「lazyなオブジェクト」というアイデアを採用しています。たとえば「lazyな配列」では、要素の操作を、それが必要になる最終段階で解決することができます。

range = 1..Float::INFINITY
values_lazy = range.lazy.map { |i| i * i }.take(10)
values_lazy.force
# => [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

上から分かるように、2つのコードブロックでも次の同じパターンが使われています。

  • lazyなオブジェクトのコレクションを取る
  • コレクションは最後に解決する

バッチング

それでは、Post#user_lazyの部分を詳しく見てみましょう。このメソッドはlazyなBatchLoaderインスタンスを返します。

# app/models/post.rb
def user_lazy
  BatchLoader.for(user_id).batch do |user_ids|
    User.where(id: user_ids)
  end
end

BatchLoader.forは項目(user_id)を1つ受け取ります。この項目はコレクションされ、後でバッチングに使われます。次に、#batchメソッドを呼び出します。このメソッドに渡されるブロックは、コレクションされたすべての項目(user_ids)に適用されます。ブロックの内部では、項目を取得するバッチクエリ(User.where)を1つ実行します。

JavaScript DataLoaderでは、渡された項目や読み込まれた値を明示的にマップしていますが、この方法は次の2つの制限を守らないと使えません。

  • 渡された項目の配列(user_ids)の長さは、読み込まれた値の配列(user)の長さと同じでなければならない。通常、この条件を満たすために配列の値がない部分をnilで埋める必要があります。
  • 渡された項目の配列の各要素のインデックスは、読み込まれた値の配列の各要素の同じインデックスと対応していなければならない。通常、この条件を満たすために配列の値をソートする必要があります。

一方、BatchLoaderが提供する読み込み用メソッドは、渡された項目(user_ids)と読み込まれた値(user)を単純にマップします。

# app/models/post.rb
def user_lazy
  BatchLoader.for(user_id).batch do |user_ids, batch_loader|
    User.where(id: user_ids).each { |u| batch_loader.load(u.id, u) }
  end
end

RESTful APIの例

今度は、普通のRailsアプリがN+1 HTTPリクエストを行っている状況を考えてみましょう。

# app/models/post.rb
class Post < ApplicationRecord
  def rating
    HttpClient.request(:get, "https://example.com/ratings/#{id}")
  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    posts = Post.limit(10)
    serialized_posts = posts.map do |post|
      {id: post.id, rating: post.rating} # <== N+1 HTTP requests
     end
    render json: serialized_posts
  end
end

parallelというgemを使うと、すべてのHTTPリクエストをスレッド化して並列(concurrent)実行することでリクエストをバッチ化できます。幸いなことにMRIでは、スレッドがブロッキングI/O(ここではHTTPリクエスト)に遭遇するとGIL(global interpreter lock)を解放してくれます。

# app/models/post.rb
def rating_lazy
  BatchLoader.for(post).batch do |posts, batch_loader|
    Parallel.each(posts, in_threads: 10) do |post|
      batch_loader.load(post, post.rating)
    end
  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    posts = Post.limit(10)
    serialized_posts = posts.map do |post|
      {id: post.id, rating: post.lazy_rating}
    end
    render json: BatchLoader.sync!(serialized_posts)
  end
end

スレッドセーフ

HTTPリクエストをスレッド並列化した上のコード例は、HttpClientがスレッドセーフでないと正常に動作しません。BatchLoader#loadは最初からスレッドセーフなので、余分な依存性が発生しません。

GraphQLの例

バッチングが特に有用なのは、GraphQLです。データの事前読み込みのような手法でN+1クエリを回避しようとすると、どのフィールドもユーザーからのクエリに含まれる可能性があるため、コードが恐ろしく複雑になってしまうことがあります。graphql-rubyの簡単なスキーマでコード例を見てみましょう。

Schema = GraphQL::Schema.define do
  query QueryType
end

QueryType = GraphQL::ObjectType.define do
  name "Query"
  field :posts, !types[PostType], resolve: ->(obj, args, ctx) do
    Post.all
  end
end

PostType = GraphQL::ObjectType.define do
  name "Post"
  field :user, !UserType, resolve: ->(post, args, ctx) do
    post.user # <== N+1 queries
  end
end

UserType = GraphQL::ObjectType.define do
  name "User"
  field :name, !types.String
end

次のようなシンプルなクエリを実行すると、post.userを実行するたびにN+1クエリが発生します。

query = "
{
  posts {
    user {
      name
    }
  }
}
"
Schema.execute(query)
# SELECT * FROM posts WHERE id IN (1, 2, 3)
# SELECT * FROM users WHERE id = 1
# SELECT * FROM users WHERE id = 2
# SELECT * FROM users WHERE id = 3

この問題は、リゾルバを変更してBatchLoaderを使うだけで回避できます。

PostType = GraphQL::ObjectType.define do
  name "Post"
  field :user, !UserType, resolve: ->(post, args, ctx) do
    BatchLoader.for(post.user_id).batch do |ids, batch_loader|
      User.where(id: ids).each { |u| batch_loader.load(u.id, u) }
    end
  end
end

後は、GraphQLに組み込まれている#lazy_resolveメソッドを使ってセットアップします。

Schema = GraphQL::Schema.define do
  query QueryType
  lazy_resolve BatchLoader, :sync
end

これでおしまいです。GraphQLのlazy_resolveは、フィールドで基本的にresolve lambdaを呼び出します。lazyなBatchLoaderが返されると、後でBatchLoader#syncが呼び出され、実際に読み込まれた値を自動的に取得します。

GraphQLの作者にも認められたbatch-loader

キャッシュ

BatchLoaderには最初からキャッシュメカニズムが提供されているので、読み込み済みの値に対するクエリは発生しません。次の例をご覧ください。

def user_lazy(id)
  BatchLoader.for(id).batch do |ids, batch_loader|
    User.where(id: ids).each { |u| batch_loader.load(u.id, u) }
  end
end

user_lazy(1)      # リクエストは発生しない
# => <#BatchLoader>

user_lazy(1).sync # SELECT * FROM users WHERE id IN (1)
# => <#User>

user_lazy(1).sync # リクエストは発生しない
# => <#User>

まとめ

一言で言えば、バッチングはN+1クエリを回避するための強力な手法です。私は、GraphQLや他の言語を使っている開発者に限らず、すべてのRuby開発者がバッチングを知っておくべきであると確信しています。バッチングを使うことで、アプリから無関係な部分を切り離し、パフォーマンスを犠牲にせずにいつでもどこでもデータを読み込めるようになります。

Universeでは、本番のRESTful APIとGraphQLの両方でコードを共有するかたちでBatchLoaderを利用しています。詳しくはBatchLoaderのREADMEとソースコードをご覧ください。ソースはわずか150行です。

関連記事

Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README(翻訳)

Rails: render_async gemでレンダリングを高速化(翻訳)

週刊Railsウォッチ(20170908)Rails 5.1.4と5.0.6リリース、コード書換え支援gem「synvert」、遅いテストを分析するTestProfほか

$
0
0

追記: るびまのRubyKaigi 2017特集に本記事のリンクを掲載いただきました。
ありがとうございます!

こんにちは、hachi8833です。ジェットコースターみたいに上下する気候はこたえますね。

6日に発生した太陽表面での大規模爆発の影響が本日15:00から夜半にかけて地球規模で生じるそうで、高緯度を中心にオーロラ発生が見込まれるほか、通信機器やGPSに障害が出るかもしれないとのことです。この時期に大きなリリースや祝賀ミサイル発射などをご検討の方は十分ご注意ください。


https://www.nict.go.jp/press/2017/09/07-1.htmlより

臨時ニュース: Rails 5.1.4と5.0.6が正式リリース

本日出たてほやほや。バグ修正が中心です。

いつもの記念写真です。

RubyKaigi 2017@Hiroshima開催迫る

今年のRubyKaigiももう再来週ですね。9/18(月)〜9/20(水)、広島の国際会議場です。

RubyKaigiは、ビジネス志向寄りのRuby World Conferenceと対照的にギーク色が強く、Rubyエンジニアの祭典という側面が前面に出ています。

年号付きの大規模なRubyKaigiは、会場のご当地名物をシンボルに採り入れています。昨年は京都の風景でしたが、今年はやはり広島・厳島神社の鳥居が使われています。

私も昨年のRubyKaigi 2016@京都や今年の大江戸Ruby会議に参加してみましたが、いずれもいい年こいてギンギンに意識が高まってしまったのが思い出されます。

Ruby界隈の特徴として、うかつな質問とかしたら後ろからバッサリ斬られそうなコワイ人がとんと見当たらないのがとてもありがたい点です。社交辞令ではなく、ユーモアやジョークを真面目に愛するいい人たちばかりだったという思いです。今回気になる点があるとすれば、今年の会場ではどのぐらいスムーズにWiFiにつなげるかぐらいでしょうか。

皆さまも今年こそ思い切って参加してみてはいかがでしょう。得られるものは想像以上に大きいと思います。
弊社からはTechRachoでおなじみmorimorihogeと、不肖私めも参加いたします。

Ruby Kaigi 2016に全日参加しました!(hachi8833)

追記

Rails公式

Rails公式の更新情報がここ数週間怒涛のように増えていて、遠い昔の夏休みの宿題を思い出してしまいました。

RailsのSDocを更新

group :doc do
-  gem "sdoc", "> 1.0.0.rc1", "< 2.0"
+  gem "sdoc", github: "robin850/sdoc", branch: "upgrade"
   gem "redcarpet", "~> 3.2.3", platforms: :ruby
   gem "w3c_validators"
   gem "kindlerb", "~> 1.2.0"

コードの変更量は少ないですが、SDocのテーマ変更やSEOタグ追加などいろいろ変わるようです。

SDocドキュメントってどこに行ったら見られるんだろうかと一瞬考えてしまいましたが、Railsディレクトリでsdocを実行して生成するのでした。すっかり忘れていましたが、ドキュメントの生成はかなり時間がかかるので、.gemrcに以下を書いてスキップしている人がほとんどかと思います。

# ~/.gemrc
install: --no-rdoc --no-ri
update:  --no-rdoc --no-ri

今回の変更はまだmasterブランチにしかないので、今日の5.1.4にはまだ含まれていません。以下は更新前のSDoc動画です。そのうちhttp://api.rubyonrails.org/で使えるようになるのでしょうか。

ActiveStorageのドキュメント更新

# activestorage/README.md
...
-Image files can further more be transformed using on-demand variants for quality, aspect ratio, size, or any other
-MiniMagick supported transformation.
+Image files can furthermore be transformed using on-demand variants for quality, aspect ratio, size, or any other [MiniMagick](https://github.com/minimagick/minimagick) supported transformation.
...

つっつきボイス: 「最初はからっぽだったActiveStorageドキュメントも着々と整備されてますね」「エライエライ」

「ところで、Railsガイドにはもう反映されてるかしら」「うむ、ないでござる」「じゃEdgeGuidesはどうだろう: こっちに先に掲載されるのが普通なので」


edgeguides.rubyonrails.orgより

「こっちにもまだないか」「EdgeGuidesなんてのがあったのね: シラナカッター」

Rails: 先々週の改修

Hash#deep_mergeの改良

あまり見かけない1ファイルのみの変更です。

# activesupport/lib/active_support/core_ext/hash/deep_merge.rb
  # Same as +deep_merge+, but modifies +self+.
   def deep_merge!(other_hash, &block)
-    other_hash.each_pair do |current_key, other_value|
-      this_value = self[current_key]
-
-      self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
-        this_value.deep_merge(other_value, &block)
+    merge!(other_hash) do |key, this_val, other_val|
+      if this_val.is_a?(Hash) && other_val.is_a?(Hash)
+        this_val.deep_merge(other_val, &block)
+      elsif block_given?
+        block.call(key, this_val, other_val)
       else
-        if block_given? && key?(current_key)
-          block.call(current_key, this_value, other_value)
-        else
-          other_value
-        end
+        other_val
       end
     end
-
-    self
    end
  end

つっつきボイス: 「↑再帰やってるかなと思ったけどやってなさげ」「deepの付くコピーやマージはRuby言語側で直接サポートしていないこともあって実装もいろいろ、かつ改良が絶えない感じですね。」

「deepなんちゃらって、やっぱり言語でやらない方がいいんでしょうか?」「ワイはそう思う: deepなんちゃらは一般化しづらいし、言語で責任取りきれないっしょ」

Eager loadingで一発目のレスポンスタイムを短縮

次のPRとも関連しているようです。

# actionmailer/lib/action_mailer/railtie.rb
+    initializer "action_mailer.eager_load_actions" do
+      ActiveSupport.on_load(:after_initialize) do
+        ActionMailer::Base.descendants.each(&:action_methods) if config.eager_load
+      end
+    end
+
     config.after_initialize do |app|
       options = app.config.action_mailer

:action_controllerでのパラメータ設定読み込みを繰り返さないようにした

# actionpack/lib/action_controller/railtie.rb
-      ActiveSupport.on_load(:action_controller) do
+      ActiveSupport.on_load(:action_controller, run_once: true) do
         ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
         if app.config.action_controller[:always_permitted_parameters]
           ActionController::Parameters.always_permitted_parameters =
# activesupport/lib/active_support/lazy_load_hooks.rb
       base.class_eval do
         @load_hooks = Hash.new { |h, k| h[k] = [] }
         @loaded     = Hash.new { |h, k| h[k] = [] }
+        @run_once   = Hash.new { |h, k| h[k] = [] }
       end
     end

...

+    private
+
+      def with_execution_control(name, block, once)
+        unless @run_once[name].include?(block)
+          @run_once[name] << block if once
+
+          yield
+        end
+      end
+
+      def execute_hook(base, options, block)
+        with_execution_control(name, block, options[:run_once]) do

つっつきボイス: 「この2つのコミット、関連してそうですね」「実装上はたぶん独立してるけど、目的は関連してるかも」
「1つめのif config.eager_loadで、設定ファイルからでもコントロールできるようにしてるのか」

uniquenessバリデーションで:scopeが効かないことがあったのを修正

# activerecord/test/cases/validations/uniqueness_validation_test.rb
+  def test_validate_uniqueness_with_scope_invalid_syntax
+    error = assert_raises(ArgumentError) do
+      Reply.validates_uniqueness_of(:content, scope: { parent_id: false })
+    end
+    assert_match(/Pass a symbol or an array of symbols instead/, error.to_s)
+  end

つっつきボイス: 「おおー、uniquenessでは今まで:scope使えてなかったのか」「改修部分よりテストの方がわかりやすいかと思ってテストを引用してみました」


ソースのactive_record/validations/uniqueness.rbを見ると、scopeのドキュメントは前からあったようです。

Ruby trunkより

String#valid_encoding?の副作用

content = "\xE5".dup.force_encoding(Encoding::ASCII_8BIT)

content.encode(Encoding::UTF_8, Encoding::UTF_8, invalid: :replace, replace: '?')
=> "?"

content.valid_encoding?

content.encode(Encoding::UTF_8, Encoding::UTF_8, invalid: :replace, replace: '?')
=> "\xE5"

つっつきボイス:content.valid_encoding?って何じゃらほいと思ったら、これを実行したら結果が変わってたってことか: これはイヤなバグだ」「破壊的でないはずのメソッドが破壊的に振る舞ってたのか(怖」

「ところでEncoding::ASCII_8BITってエンコードひどいw: UTF-8とすら互換性なくなりますね」

ハッシュが空なのにHash#compact!nilを返す

irb(main):001:0> {}.compact!
=> nil

# For Comparison
irb(main):002:0> { foo: nil }.compact!
=> {}
irb(main):003:0> {}.compact
=> {}
irb(main):004:0> { foo: nil }.compact
=> {}

つっつきボイス: 「Rubyだと{x: 3}.compact!{x: 3}じゃなくてnil返してる? 一瞬意味わからんと思ったけど、配列だと[3].compact!nilになるから、それと揃えたってことか」
!系メソッドの戻り値ってあてにしない方がよさげですなー」
「どうやら、挙動はこのままが正しくて、ドキュメントの方が違ってたってことみたいですね↓」

compact! → hsh
Removes all nil values from the hash. Returns the hash.

Rubyだと、他に返すものがないからとりあえずnilを返す、みたいな破壊的メソッドがそこそこある気がしました。
nilを埋め草的に使うのはあんまりよくなさそうな気がします。

Rails

JOINすべきかどうか、それが問題だ(Ruby Weeklyより)

良記事です。


つっつきボイス: 「このタイトルも、記事の小見出しもシェークスピアのハムレットをもじりまくってるんですよw: 誰がうまいこと言えと」「何人わかるんかしらそれ: ドラクエで説明してくれー」「イギリス人かと思ったけど著者名ラテン系だし」

「N+1を回避するために、INNER JOINなのかOUTER JOINなのか迷うのに、さらにRailsで eager_load とか #includes とか色々あって、どれにしたらいいのか考えるのがつらいでござるよ」「この記事、まさにそのあたりを衝いてるみたい: 翻訳したろ」

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)


ついでながら、actの本来の意味は「演技する」や「(劇の)場面」のことだったりします。サブタイトルに「An act of #includes」とあるのもまさにそれです。第一幕、第二幕みたいなのをact I、act IIなどと表したりします。

英語だと「演技する」「演じる」「上演する」「演奏する」がplayやperformanceやactという基本的過ぎる言葉で表されることがとっても多いので、こういう言葉が短い見出しで使われていると意味を取り違えないよう緊張してしまいます。

⭐synvert: Rubyのコードをいろいろ変換するリファクタリングgem⭐(Ruby Weeklyより)


xinminlabs.github.io/synvertより

あまり追えていませんが、かなりよさげなコード変換ツールです。

gem install synvert  # これでインストールできる
synvert --sync       #最初に一回だけ実行: スニペットを取ってくる
synvert -l           #現在のスニペットのリストを表示↓

default
check_syntax
factory_girl
fix_deprecations
use_new_syntax
use_short_syntax
rails
convert_dynamic_finders
convert_mailers_2_3_to_3_0
convert_models_2_3_to_3_0
convert_rails_env
convert_rails_logger
convert_rails_root
convert_routes_2_3_to_3_0
convert_views_2_3_to_3_0
redirect_with_flash
strong_parameters
upgrade_2_3_to_3_0
upgrade_3_0_to_3_1
upgrade_3_1_to_3_2
upgrade_3_2_to_4_0
upgrade_4_0_to_4_1
upgrade_4_1_to_4_2
upgrade_4_2_to_5_0
upgrade_5_0_to_5_1
rspec
be_close_to_be_within
block_to_expect
boolean_matcher
collection_matcher
custom_matcher_new_syntax
explicit_spec_type
its_to_it
message_expectation
method_stub
negative_error_expectation
new_config_options
new_hook_scope
one_liner_expectation
pending_to_skip
remove_monkey_patches
should_to_expect
stub_and_mock_to_double
use_new_syntax
ruby
block_to_yield
fast_syntax
gsub_to_tr
iconv_to_encode
keys_each_to_each_key
map_and_flatten_to_flat_map
merge_to_square_brackets
new_hash_syntax
new_lambda_syntax
new_safe_navigation_operator
parallel_assignment_to_sequential_assignment
remove_debug_code
use_symbol_to_proc
shoulda
fix_deprecations
use_matcher_syntax
use_new_syntax
will_paginate
use_new_syntax

備え付けのスニペットもいろいろあり、upgrade_5_0_to_5_1のようなアップグレード向けの書き換えや、new_hash_syntaxのような非推奨文法を推奨文法に書き換えられるスニペットだけでも便利そうです。

convert_models_2_3_to_3_0のようなスニペットは、Rails 2系から3系へのアップグレードのような考えるだけでつらくなる作業を助けてくれそうです。DSLでスニペットを書くこともできます。

railsdiffと合わせて使えば、Railsのアップグレードがとってもはかどりそうな予感。


つっつきボイス: 「こやつはrubocopチャンと喧嘩するやつ?」「どっちかというと書き換えがメインなんで、住み分けできそうですね」


期待を込めて、今週の⭐を進呈いたします。おめでとうございます。

ドメインイベントでRailsの複雑なドメインを分割する(Ruby Weeklyより)


blog.carbonfive.comより


つっつきボイス: 「この場合のドメインは設計上の話でござるか」「ドメインって言葉、文脈でいろいろ変わってくるのでめんどいですね」
「Passengerとルーティングを例に取ってる: このぐらいだったらまとめて扱ってもいい気はするけど、分離してイベントでゴニョゴニョする方が幸せになれるっていう意図なのかな」「ビジネスロジックを分離するみたいな、理想だけど実践するのは大変なやつに見えますね: この間のウォッチ↓で扱ったTrailbrazerをちょっとだけ連想しました」

週刊Railsウォッチ(20170804)Rails 5.1.3と5.0.5が正式リリース、GitHubでローカライズ基盤サービス、正規表現で迷路を解くほか


trailblazer.toより

Rails + ActionCableで即応性の高いアプリを作る(RubyFlowより)


www.icicletech.comより


つっつきボイス: 「原文でReal Timeって言ってるけど、ガチでリアルタイム処理研究している人からツッコミ入りそうな気がしましたw: そのあたりよくわかってませんが」「記事は実用的でよさげ」

TestProf: Rubyのテストが遅くなる原因を診断するツール(Ruby Weeklyより)


evilmartians.comより

テストのどこで時間がかかっているのかをプロファイリングしてくれます。CIに組み込んでくれとツールが私の耳元でささやいているような気がしました。
記事もツールの紹介だけではなく、自分でプロファイルするうえでも役に立つことが書かれており、Tips to improve speed of your test suiteという記事を参考にリストアップしています。


つっつきボイス: 「このツールよさそう: アプリの改修が進むとテストも増えて時間かかるので、こういう問題は切実」「遅いテストは、開発者の意欲をザクザク削ってしまうのもデメリットですね」「そうなんでござるよ: デプロイのたびにCIで何十分も待たされるとか、耐え難い」

関数は小さい方がよいとは限らない(Awesome Rubyより)


edium.com/@copyconstructより

Rubyなどの言語ではコードをDRYに書くことが推奨されていますが、ルールを盲目的に守ればいいというものではないという主張です。ちょっと長いですが、良記事だと思います。


つっつきボイス: 「DRYに書けばいいってもんじゃない、と言ってますね」「確かに: メソッドを細かく割りすぎると今度は読みにくいんですよ、マジで」
「関数やメソッドを細かく分割しすぎると、名前つけるだけで大変だし、切れっ端みたいなコードがあちこちで再利用されると副作用が変なところに出てきたりする、とも言ってます」「まったくじゃわいw」


ところで、「considered Harmful」は最近流行りの煽り見出しのようですね。↓こういう記事の見出しなら内容に合ってると思いますが、何度も見かけるとちょっと鼻についてしまいます。

Railsの`CurrentAttributes`は有害である(翻訳)

コード品質ツールチェックの効果についてのアンケート(Awesome Rubyより)

さっと読める記事です。


rubyblog.proより


つっつきボイス: 「ツールで品質が良くなりましたか?というアンケート↑、2割近くが『なってない』って答えてるw」
「↓これも切実だなー: ツールにどこをチェックして欲しい?の回答みると、complexity(複雑さ)とかデザパタとかOOP準拠とかコードの臭いとかが上位」「ツールが苦手なところばっかりですねw: スタイルみたいな型にはめるチェックはだいぶできるようになりましたが」
「もちろんツールがないと困るし、コードレビューでスペースインデントみたいなどうでもいいところで時間取られたくないでござるよ」「まったく」


rubyblog.proより

FabricationとFactoryGirl、どっちがいいの?(Ruby Weeklyより)


ksylvest.comより

ざっくりベンチ取ってるだけの記事なので、すぐ読めます。結果はここには書きません。

factoryとfixtureを両方使いたいお(RubyFlowより)


evilmartians.comより


つっつきボイス: 「一つ前の記事と見出しがよく似てますw」
「あー、factoryとfixtureか: 自分の理解ではfixtureは↓こんなyamlとかの固定テストデータ、factoryは動的に生成されるテストデータ」「fixtureの本来の意味は『簡単に取り外せない(ホテルなんかの)備品』ですね」「まさしく、fixtureは備品のイメージだわ: 無理してどっちかに統一することはないでござるよ」

rubyonrails:
  id: 1
  name: Ruby on Rails
  url: http://www.rubyonrails.org

google:
  id: 2
  ;name: Google
  url: http://www.google.com

「記事の結論↓: factoryとfixtureは適材適所で使い分けよ、って」

Our conclusion is obvious: do not oppose factories and fixtures, bring them all and in the darkness bind them to rule your test suites mightily.


ところで、年がバレるのも構わずにIT業界の英語でやたら使われるのが、RPGの遠いご先祖とも言える「ロード・オブ・ザ・リング」の冒頭のナレーション↓のもじりです。上のは無理やり過ぎてだいぶ壊れてますが。

Three Rings for the Elven-kings under the sky,
Seven for the Dwarf-lords in their halls of stone,
Nine for Mortal Men doomed to die,
One for the Dark Lord on his dark throne
In the Land of Mordor where the Shadows lie.

One Ring to rule them all, One Ring to find them,
One Ring to bring them all and in the darkness bind them
In the Land of Mordor where the Shadows lie.
https://en.wikipedia.org/wiki/One_Ringより

「Oneなんとか、Oneなんとか、Oneなんとか」が出てきたら、「ああアレね」と生暖かい目で華麗にスルーしてあげてください。ついでながら、Martian Chroniclesというブログサイト名にも年を感じてしまいました

Railsのポリモーフィック関連付けを学ぶ(Ruby Weeklyより)


semaphoreci.comより

目新しい内容ではなさそうですが、シンプルなのですぐ読めそうです。

Visual Studio CodeでRailsしてみたった


medium.com/@PaulWritesCodeより

言われてみればVS Studioでもできそうですね。やってる人をまだ見たことがありません。

Rubyで混乱しやすい機能6つ(RubyFlowより)

著者はどことなくヴェトナム人っぽい名前ですね。rsyncを6歳児にもわかるように説明するという記事なども面白そうです。

  1. []メソッド
  2. %演算子
  3. Integer#zero?メソッド
  4. $[数字]グローバル変数
  5. 万能すぎてつらいTime.parse
  6. delegatorの挙動がRubyドキュメント↓と違う

Equality — At the Object level, == returns true only if obj and other are the same object. – Ruby documentation.


つっつきボイス: 「↓6.はちょっと言い過ぎな気がするかも: equalityとは何かみたいなのは文脈にもよるんだし、obj idが違うから同じではいけないというものでもないんじゃなかろうか」

# https://hqc.io/posts/six-confusing-features-in-ruby より
# 6.のコード
class Foo < Delegator
  def initialize(the_obj)
    @the_obj = the_obj
  end

  def __getobj__
    @the_obj
  end
end

foo = Foo.new(1)
foo.inspect # => 1
foo == 1 # => true

「そういえばRubyのトリプルイコール===case文の条件以外では使わないこと、というのがRubyスタイルガイドにありました」「そうそう」「RubyとJavaScriptで===の挙動が全然違うのってつらいですね、あれw」

Rubyの===演算子についてまとめてみた

Gemを書くときのチェックリスト(RubyFlowより)


gemcheck.evilmartians.ioより

ブラウザ上でそのままチェックリストをオンオフできます。
こういう形式の記事、結構便利かもです。

RubyでSlackコマンドAPIを書く

Railsでの画像アップロードをShrine.rbとDropzone.jsで実現(RubyFlowより)


codyeatworld.comより

4回シリーズの記事です。


つっつきボイス: 「Amazon S3に置くのが前提みたいです: ところでRailsの定番アップローダーgemって何でしたっけ」「carrierwave

JavaScript

deeplearn.jsをGoogleが発表

元記事: Harness the Power of Machine Learning in Your Browser with Deeplearn.js
公式サイト: https://pair-code.github.io/deeplearnjs/


research.googleblog.comより

機械学習も着々とパッケージ化されてきてますね。実は日本語記事も出ています。

Upterm: Electronベースのターミナルソフトウェア

★16000超えの快挙です。


つっつきボイス: 「自分のMacbook Proに入れてみました: ほれほれ」「おおお、凄いでないのコレ!: htopの後に自動で画面キャプチャまでしてくれた」
.bashrcとかまったく読み込まれなかったので、ほとんど素っ裸のシェルですが、それでもこれだけ使えるので、とにかく見せ方がうまいですね」

Android

flexbox-android


github.com/google/flexbox-layoutより

こちらもGoogle公式のライブラリです。こちらも★9000超えです。


つっつきボイス: 「これイイナー: Androidじゃなくても使いたい」

その他

homebrew-cask-upgrade: caskのバージョンチェックとアップグレードツール

個人的にとても便利だったので。合言葉はbrew cu
むしろ、これなしでbrew cask使いたくないくらいです。

プログラマーが健康と引き換えに失ったもの(RubyFlowより)


hackernoon.comより

急な激しい運動は身体を痛める元になるので、皆さまもご注意。

bettercap: 侵入テスト向けの中間者攻撃ツール(Ruby Weeklyより)

いわゆる『人に向けてはいけない』ツールです。

番外

これ欲しい

神経線維が光ファイバーになる日は来るか

あれは何だったのか


今週は以上です。

バックナンバー(2017年度)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Rein: RailsのActiveRecordでDB制約やデータベースビューを使えるgem(README翻訳)

$
0
0

こんにちは、hachi8833です。今回はRailsウォッチでもご紹介したRein gemのREADME翻訳をお送りします。
多くの機能がPostgreSQL寄りなので、PostgreSQLで使うとさらに幸せになれるかもしれません。

概要

README末尾のMITライセンスおよびリポジトリのライセンスに基づき翻訳・公開いたします。

Rein: RailsのActiveRecordにDB制約を追加するgem(README翻訳)

データ完全性(data integrity)はよいものです。
値の制約は、アプリのレベルよりもデータベースのレベルでかける方が、データを正しく保つ方法としてより強力になります。

残念なことに、SQLを手書きせずにデータ完全性を実現しようとしても、ActiveRecordではそうしたサポートについて冷淡であり、許してすらくれません。Rein(発音はrainと同じ)は、データベース上のデータを正しく保つのに役立つさまざまなメソッドをActiveRecordのマイグレーションに追加するgemです。

ReinのDSLで使えるメソッドはすべて逆操作が可能なので、Railsのマイグレーションで可逆的な操作を利用できます。

クイックスタート

  • 1: gemをインストールします。
gem install rein
  • 2: マイグレーションに制約(constraint)を追加します。
class CreateAuthorsTable < ActiveRecord::Migration
  def change
    create_table :authors do |t|
      t.string :name, null: false
    end

    # authorには必ずnameがあること
    add_presence_constraint :authors, :name
  end
end

利用できる制約

外部キー制約

外部キー制約は、カラム内の値が別のテーブル内の行(row)の値を一致しなければならないことを指定します。

たとえば、「booksテーブルのauthor_idは、authorsテーブルのidにある値に限定したい」場合は、次のように書きます。

add_foreign_key_constraint :books, :authors

外部キー制約を追加しても、参照されるカラムにインデックスが自動で追加されるわけではありません。一般に、インデックスを追加することで外部キーのJOINを高速化できます。インデックスを作成するには、indexオプションを使います。

add_foreign_key_constraint :books, :authors, index: true

Reinは、テーブルに対応するカラム名を自動で推測します。明示的に指定したい場合は、referencedオプションやreferencingオプションを利用できます。

add_foreign_key_constraint :books, :authors, referencing: :author_id, referenced: :id

参照先の行のひとつが更新または削除されたときの動作も指定できます。

add_foreign_key_constraint :books, :authors, on_delete: :cascade, on_update: :cascade

DELETEやUPDATEで指定できる動作の全オプションを以下に示します。

  • no_action: 制約チェック時に、参照元の行がまだ存在している場合にはエラーを出力します。オプションを指定しない場合はデフォルトでこの動作になります。
  • cascade: 参照先の行が削除されたときに、参照元の行も同時に削除されなければならないことを指定します。
  • set_null: 参照先の行が削除されたときに、参照元のカラムにNULLを設定します。
  • set_default: 参照先の行が削除されたときに、参照元のカラムにデフォルト値を設定します。
  • restrict: 参照先の行の削除を禁止します。

外部キー制約を削除するには、次を使います。

remove_foreign_key_constraint :books, :authors

inclusion制約

inclusion制約は、カラムに設定できる値のリストを指定します。

たとえば、「stateカラムの値はavailableon_loanの2つのみを取れる」ようにするには、次のようにします。

add_inclusion_constraint :books, :state, in: %w[available on_loan]

inclusion制約を削除するには、次を使います。

remove_inclusion_constraint :books, :state

ifオプションも併用すると、次のように特定の条件を満たす場合にのみ制約をかけることもできます。

add_inclusion_constraint :books, :state,
  in: %w[available on_loan],
  if: "deleted_at IS NULL"

nameオプションで名前をカスタマイズすることもできます。

add_inclusion_constraint :books, :state,
  in: %w[available on_loan],
  name: "books_state_is_valid"

長さ制約

長さ制約は、文字列カラムの値が取れる長さの範囲を指定します。

たとえば、「call_numberの長さを1から255の間にする」には次のようにします。

add_length_constraint :books, :call_number,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 255

長さ制約の全オプションを以下に示します。

  • equal_to
  • not_equal_to
  • less_than
  • less_than_or_equal_to
  • greater_than
  • greater_than_or_equal_to

ifオプションも併用すると、次のように特定の条件を満たす場合にのみ制約をかけることもできます。

add_length_constraint :books, :call_number,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  if: "status = 'published'"

nameオプションで名前をカスタマイズすることもできます。

add_length_constraint :books, :call_number,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  name: "books_call_number_is_valid"

長さ制約を削除するには、次を使います。

remove_length_constraint :books, :call_number

数値制約

数値制約は、数値カラムが取れる値の範囲を指定します。

たとえば、「publication_monthの値は1から12の間だけを取れる」ようにするには、次のようにします。

add_numericality_constraint :books, :publication_month,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12

数値制約の全オプションを以下に示します。

  • equal_to
  • not_equal_to
  • less_than
  • less_than_or_equal_to
  • greater_than
  • greater_than_or_equal_to

ifオプションも併用すると、次のように特定の条件を満たす場合にのみ制約をかけることもできます。

add_numericality_constraint :books, :publication_month,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  if: "status = 'published'"

nameオプションで名前をカスタマイズすることもできます。

add_numericality_constraint :books, :publication_month,
  greater_than_or_equal_to: 1,
  less_than_or_equal_to: 12,
  name: "books_publication_month_is_valid"

数値制約を削除するには次のようにします。

remove_numericality_constraint :books, :publication_month

presence制約

presence(存在)制約は、文字列カラムの値が空にならないよう指定します。

いわゆるNOT NULL制約は空文字列でも満たされますが、文字列に(NULLでない)何らかの値があることを保証するには、次のようにします。

add_presence_constraint :books, :title

特定の条件が満たされる場合にのみ制約をかけたい場合は、ifオプションを渡します。

add_presence_constraint :books, :isbn, if: "status = 'published'"

nameオプションで名前をカスタマイズすることもできます。

add_presence_constraint :books, :isbn, name: "books_isbn_is_valid"

presence制約を削除するには、次のようにします。

remove_presence_constraint :books, :title

NULL制約

NULL制約は、カラムにNULL値が含まれていないことを保証する制約です。これはカラムにNOT NULL制約を加えることと同じですが、条件を指定できる点が異なります。

たとえば、「本がon_loan(貸出中)の場合のみdue_dateが必ずあるようにする」には、次のようにします。

add_null_constraint :books, :due_date, if: "state = 'on_loan'"

NULL制約を削除するには、次のようにします。

remove_null_constraint :books, :due_date

データ型

列挙型

列挙型(enum)は、静的かつ順序の変わらない値のセットを表します。

create_enum_type :book_type, %w[paperback hardcover]

データベースから列挙型を削除するには、次のようにします。

drop_enum_type :book_type

ビュー

データベース・ビュー(以下単にビュー)は、通常のテーブルと同じように参照できる「名前付きクエリ(named query)」です。Reinを使うと、データベースでビューをサポートするActiveRecordモデルも作成できるようになります。

たとえば、「現在貸出可能な本のリストを返すavailable_booksというビューを定義する」には、次のようにします。

create_view :available_books, "SELECT * FROM books WHERE state = 'available'"

ビューをデータベースから削除するには、次のようにします。

drop_view :available_books

スキーマ

ひとつのデータベースには、名前付きスキーマ(複数可)を含めることができます。この名前スキーマにはテーブルが含まれることになります。データベースを複数のスキーマに分割して、複数のテーブルを論理的にグループ化すると便利な場合があります。

create_schema :archive

スキーマをデータベースから削除するには、次のようにします。

drop_schema :archive

簡単な図書館貸出アプリを用いて、データベースの値に制限を追加するマイグレーションの実例をいくつか見てみましょう。

class CreateAuthorsTable < ActiveRecord::Migration
  def change
    # The authors table contains all the authors of the books in the library.
    create_table :authors do |t|
      t.string :name, null: false
      t.timestamps, null: false
    end

    # 制約: authorには必ずnameがあること
    add_presence_constraint :authors, :name
  end
end

class CreateBooksTable < ActiveRecord::Migration
  def change
    # booksテーブルには図書館のすべての本や状態(貸出中・貸出可など)が収録されている
    create_table :books do |t|
      t.belongs_to :author, null: false
      t.string :title, null: false
      t.string :state, null: false
      t.integer :published_year, null: false
      t.integer :published_month, null: false
      t.date :due_date
      t.timestamps, null: false
    end

    # book 1冊につき、authorが1人いること
    # authorを1人削除すると、authorのbookもデータベースから自動削除されること
    add_foreign_key_constraint :books, :authors, on_delete: :cascade

    # book 1冊につき、空でないtitleが1つあること
    add_presence_constraint :books, :title

    # stateは"available"(貸出可)、"on_loan"(貸出中)、"on_hold"(保留)のいずれかだけを取ること
    add_inclusion_constraint :books, :state, in: %w[available on_loan on_hold]

    # 古典はこの図書館の対象外
    add_numericality_constraint :books, :published_year,
      greater_than_or_equal_to: 1980

    # 月は常に1〜12であること
    add_numericality_constraint :books, :published_month,
      greater_than_or_equal_to: 1,
      less_than_or_equal_to: 12

    # 貸出中の本1冊には、due_date(返却期限)が1つあること
    add_null_constraint :books, :due_date, if: "state = 'on_loan'"
  end
end

class CreateArchivedBooksTable < ActiveRecord::Migration
  def change
    # archiveスキーマは、(非公開の)書庫に関するデータをすべて含む
    # このスキーマは、一般公開用スキーマと別にしておきたい
    create_schema :archive

    # archive.booksテーブルは、非公開書庫にあるすべての本を含む
    create_table "archive.books" do |t|
      t.belongs_to :author, null: false
      t.string :title, null: false
    end

    # book 1冊につき、authorが必ず1人いること
    # このデータベースでは、著書があるauthorの削除を禁止すること
    add_foreign_key_constraint "archive.books", :authors, on_delete: :restrict

    # book 1冊につき、空でないtitleが1つあること
    add_presence_constraint "archive.books", :title
  end
end

ライセンス

ReinはMIT Licenseに基いて公開しています。

関連記事

Rails開発者のためのPostgreSQLの便利技(翻訳)

週刊Railsウォッチ(20170331)PostgreSQLの制約機能を使えるRein gemはビューも使えるほか

PostgreSQLを使う理由(更新5年目)(翻訳)


JavaScriptスタイルガイド 1〜8: 型、参照、オブジェクト、配列、関数ほか (翻訳)

$
0
0

  • 1〜8: 型、参照、オブジェクト、配列、関数ほか — 本記事
  • 9〜14: クラス、モジュール、イテレータ、プロパティ、変数、ホイスティング
  • 15〜26: 比較演算子、ブロック、制御文、型変換、命名規則ほか

概要

AirbnbによるJavaScriptスタイルガイドです。
MITライセンスに基いて翻訳・公開いたします。


github.com/airbnbより

凡例

原文にはありませんが、利便性のため項目ごとに目安となる分類を【】で示しました。

  • 【必須】【禁止】:従わないと技術的な悪影響が生じる
  • 【推奨】【非推奨】:技術上の理由から強く推奨される、または推奨されない
  • 【選択】:採用してもしなくてもよいスタイル
  • 【スタイル】:読みやすさのためのスタイル統一指示
  • 【知識】:指示に該当しない基礎知識

なお、Translationに日本語を含む各国の既存訳へのリンク一覧があります。

JavaScriptスタイルガイド 1〜8: 型、参照、オブジェクト、配列、関数ほか (翻訳)

メモ: 本ガイドではBabelの利用を前提とします。また、babel-preset-airbnbあるいは同等のライブラリが必要です。他に、airbnb-browser-shimsまたは同等のライブラリでshims/polyfillsをアプリにインストールしていることも前提とします。

1. 型(type)

1.1 【知識】プリミティブ型: プリミティブ型にアクセスすると、その値を直接操作する。

  • string
  • number
  • boolean
  • null
  • undefined
const foo = 1;
let bar = foo;

bar = 9;

console.log(foo, bar); // => 1, 9

1.2 【知識】 複合型: 複合型にアクセスすると、(値そのものではなく)値への参照で操作する。

  • object
  • array
  • function
const foo = [1, 2];
const bar = foo;

bar[0] = 9;

console.log(foo[0], bar[0]); // => 9, 9

2. 参照(reference)

2.1 【推奨】参照は常に constを使い、varは避けること。

理由: constを使うことで、参照への再代入を防止できる。参照への再代入はバグを誘発し、コードを読みづらくする。

// Bad: 定数をvarで宣言している
var a = 1;
var b = 2;

// Good: 定数をconstで宣言している
const a = 1;
const b = 2;

2.2 【必須】参照の再代入にはvarではなくletを使うこと。

理由: letはブロックスコープであり、varなどのような関数スコープではない。

// Bad
var count = 1;
if (true) {
  count += 1;
}

// Good: letを使うべき
let count = 1;
if (true) {
  count += 1;
}

2.3 【知識】letconstはどちらもブロックスコープである点に注意すること。

// constとletはどちらもそれらが宣言されたブロック内でのみ有効
{
  let a = 1;
  const b = 1;
}
console.log(a); // ReferenceError
console.log(b); // ReferenceError

3. オブジェクト(object)

3.1 【推奨】オブジェクトの作成にはリテラル文法を使うこと(newを使わないこと)。

// Bad
const item = new Object();

// Good
const item = {};

3.2 【推奨】オブジェクト作成時にプロパティ名を動的に与える場合は、computedプロパティ名(訳注: ECMAScript 6の機能)を使うこと。

理由: computedプロパティ名を使うと、オブジェクトの全プロパティを1箇所で定義できる。


function getKey(k) { return `a key named ${k}`; } // Bad const obj = { id: 5, name: 'San Francisco', }; obj[getKey('enabled')] = true; // Good const obj = { id: 5, name: 'San Francisco', [getKey('enabled')]: true, };

3.3 【推奨】オブジェクトメソッドのショートハンド表記(訳注: ES6の機能)を使うこと。

// Bad
const atom = {
  value: 1,

  addValue: function (value) {
    return atom.value + value;
  },
};

// Good: 「addValue(value)」ショートハンド形式にする
const atom = {
  value: 1,

  addValue(value) {
    return atom.value + value;
  },
};

3.4 【推奨】プロパティ値のショートハンドを使うこと。

理由: 短くて書きやすく、内容を端的に表せる。

const lukeSkywalker = 'Luke Skywalker';

// Bad
const obj = {
  lukeSkywalker: lukeSkywalker,
};

// Good
const obj = {
  lukeSkywalker,
};

3.5 【スタイル】ショートハンドプロパティは他のプロパティと混ぜず、オブジェクト宣言の冒頭にまとめる。

理由: ショートハンド形式のプロパティであることをわかりやすくするため。

const anakinSkywalker = 'Anakin Skywalker';
const lukeSkywalker = 'Luke Skywalker';

// Bad
const obj = {
  episodeOne: 1,
  twoJediWalkIntoACantina: 2,
  lukeSkywalker,
  episodeThree: 3,
  mayTheFourth: 4,
  anakinSkywalker,
};

// Good
const obj = {
  lukeSkywalker,
  anakinSkywalker,
  episodeOne: 1,
  twoJediWalkIntoACantina: 2,
  episodeThree: 3,
  mayTheFourth: 4,
};

3.6 【推奨】プロパティを引用符で囲む場合は、識別子として無効な名前だけを引用符で囲むこと。

理由: 主観的にはこの方が一般に読みやすいと考えられる。さらにシンタックスハイライトにもよい影響があり、さまざまなJSエンジンで最適化が効きやすくなる。

// Bad
const bad = {
  'foo': 3,
  'bar': 4,
  'data-blah': 5,
};

// Good: 'data-blah'だけ引用符で囲む
const good = {
  foo: 3,
  bar: 4,
  'data-blah': 5,
};

3.7 【禁止】Object.prototypeのメソッド(hasOwnPropertypropertyIsEnumerableisPrototypeOfなど)を(.prototypeを介さずに)直接呼んではならない。

理由: これらのメソッドは、そのオブジェクトのプロパティによって隠蔽される可能性がある。{ hasOwnProperty: false }の場合もあれば、オブジェクトが実際にはnullオブジェクト(Object.create(null))のこともある。

// Bad
console.log(object.hasOwnProperty(key));

// Good
console.log(Object.prototype.hasOwnProperty.call(object, key));

// ベスト
const has = Object.prototype.hasOwnProperty; // 探索はモジュールスコープ内で1度キャッシュされる。
/* 以下でもよい */
import has from 'has';
// ...
console.log(has.call(object, key));

3.8 【禁止】オブジェクトの「浅いコピー(shallow-copy)」ではObject.assign を使わないこと。
【推奨】spread演算子(訳注: ドット演算子とも呼ばれる)...の利用が望ましい。
【推奨】特定のプロパティが省略されるオブジェクトを取得する場合は...演算子で受けること。

// 非常に悪い
const original = { a: 1, b: 2 };
const copy = Object.assign(original, { c: 3 }); // `original`が変更されてしまう! ಠ_ಠ
delete copy.a; // これも`original`が消される

// Bad
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 });
// copy => { a: 1, b: 2, c: 3 }

// Good
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 };
// copy => { a: 1, b: 2, c: 3 }

const { a, ...noA } = copy;
// noA => { b: 2, c: 3 }

4. 配列(array)

4.1 【推奨】配列はリテラル文法で作成すること。

// Bad
const items = new Array();

// Good
const items = [];

4.2 【推奨】配列の項目はArray#push で作成すること。配列に項目を直接代入してはならない。

const someStack = [];

// Bad
someStack[someStack.length] = 'abracadabra';

// Good
someStack.push('abracadabra');

4.3 【必須】配列のコピーには配列のspread演算子...を使うこと。

// Bad
const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i < len; i += 1) {
  itemsCopy[i] = items[i];
}

// Good
const itemsCopy = [...items];

4.4 【必須】配列に近いオブジェクトを配列に変換する場合は、Array.from を使うこと。

const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

4.5 【推奨】配列メソッドのコールバックではreturn文を使うこと。ただし、関数の内容が1文だけで、かつ副作用のない式を返す場合は8.2に基いてreturnを省略してもよい。

// Good
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});

// Good
[1, 2, 3].map(x => x + 1);

// Bad
const flat = {};
[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {
  const flatten = memo.concat(item);
  flat[index] = flatten;
});

// Good
const flat = {};
[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {
  const flatten = memo.concat(item);
  flat[index] = flatten;
  return flatten;
});

// Bad
inbox.filter((msg) => {
  const { subject, author } = msg;
  if (subject === 'Mockingbird') {
    return author === 'Harper Lee';
  } else {
    return false;
  }
});

// Good
inbox.filter((msg) => {
  const { subject, author } = msg;
  if (subject === 'Mockingbird') {
    return author === 'Harper Lee';
  }

  return false;
});

4.6 【スタイル】複数行に渡る配列では、開き角かっこ[の直後と閉じ角かっこ]の直前を改行すること。

// Bad
const arr = [
[0, 1], [2, 3], [4, 5],
];

const objectInArray = [{
id: 1,
}, {
id: 2,
}];

const numberInArray = [
1, 2,
];

// Good
const arr = [[0, 1], [2, 3], [4, 5]];

const objectInArray = [
{
  id: 1,
},
{
  id: 2,
},
];

const numberInArray = [
1,
2,
];

5. 分割代入(Destructuring)

5.1 【推奨】1つのオブジェクトで複数のプロパティにアクセスする場合は「オブジェクトの分割代入」を使うこと。

理由: プロパティの一時的な参照が作成されずに済むため。

// Bad
function getFullName(user) {
  const firstName = user.firstName;
  const lastName = user.lastName;

  return `${firstName} ${lastName}`;
}

// Good
function getFullName(user) {
  const { firstName, lastName } = user;
  return `${firstName} ${lastName}`;
}

// ベスト
function getFullName({ firstName, lastName }) {
  return `${firstName} ${lastName}`;
}

5.2 【推奨】「配列の分割代入」を使うこと。

const arr = [1, 2, 3, 4];

// Bad
const first = arr[0];
const second = arr[1];

// Good
const [first, second] = arr;

5.3 【推奨】複数の値を返す場合は「配列の分割代入」ではなく「オブジェクトの分割代入」を使うこと。

理由: 今後プロパティを追加したり順序を変更したりしても呼び出し側に影響を与えずに済むため。

// Bad
function processInput(input) {
  // この後えらいことになる
  return [left, right, top, bottom];
}

// 呼び出し元は戻り値の順序を常に気にしなければならない
const [left, __, top] = processInput(input);

// Good
function processInput(input) {
  // この後うれしいことが起きる
  return { left, right, top, bottom };
}

// 呼び出し側は欲しいデータだけを使えばよい
const { left, top } = processInput(input);

6. 文字列(string)

6.1 【スタイル】文字列は常に一重引用符' 'で囲むこと。

// Bad
const name = "Capt. Janeway";

// Bad - テンプレートリテラルは式展開か改行を含む場合に使うべき
const name = `Capt. Janeway`;

// Good
const name = 'Capt. Janeway';

6.2 【スタイル】100文字を超えるような複数行にわたる長い文字列を文字列結合などで分割しないこと。

理由: 分割された文字列は扱いづらく、コードの検索性も落ちる。

// Bad
const errorMessage = 'このウルトラスーパー長いエラーメッセージがスローされたのは\
バットマンのせい。バットマンがそんなことをする理由をいくら考えたところでさっぱり\
先に進まない。';

// Bad
const errorMessage = 'このウルトラスーパー長いエラーメッセージがスローされたのは' +
  'バットマンのせい。バットマンがそんなことをする理由をいくら考えたところでさっぱり' +
  '先に進まない。';

// Good
const errorMessage = 'このウルトラスーパー長いエラーメッセージがスローされたのはバットマンのせい。バットマンがそんなことをする理由をいくら考えたところでさっぱり先に進まない。';

6.3 【推奨】文字列をプログラムで組み立てる場合は+の文字列結合ではなく、テンプレート文字列を使うこと。

理由: テンプレート文字列は読みやすく、改行や文字列の式展開(interpolation)を正しく使える簡潔な文法であるため。

// Bad
function sayHi(name) {
  return 'How are you, ' + name + '?';
}

// Bad
function sayHi(name) {
  return ['How are you, ', name, '?'].join();
}

// Bad
function sayHi(name) {
  return `How are you, ${ name }?`;
}

// Good: ${name}にはスペースを含めないこと
function sayHi(name) {
  return `How are you, ${name}?`;
}

6.4 【禁止】eval()は決して文字列に対して使用してはならない。eval()は無数の脆弱性を引き起こす。

6.5 【推奨】文字列内の文字を不必要に\でエスケープしないこと。

理由: バックスラッシュ\は可読性を損なうため、本当に必要な場合にのみ使うこと。

// Bad
const foo = '\'this\' \i\s \"quoted\"';

// Good
const foo = '\'this\' is "quoted"';
const foo = `my name is '${name}'`;

7. 関数

7.1 【禁止】関数宣言(function declaration)は使わず、代わりに名前付き関数式(named function expression)を使うこと。

理由: 関数宣言はホイスティング(hoisting)されるため、ファイル内でその関数宣言の定義より上の位置から簡単に(あまりに簡単に)参照できてしまうため、可読性もメンテナンス性も損なわれる。ある関数定義が、ファイルの他の部分をすべて理解しなければならないほど巨大で複雑になっているのであれば、そろそろ関数を独自のモジュールに切り出してもよいだろう。その際、式には必ず名前をつけること。無名関数を使うとエラーのコールスタックで問題を特定するのが困難になるかもしれない(議論 )。

// Bad: 関数宣言
function foo() {
  // ...
}

// Bad: 無名関数
const foo = function () {
  // ...
};

// Good: 名前付き関数式
const foo = function bar() {
  // ...
};

7.2 【推奨】即時関数式(IIFE: immediately-invoked function expression)は丸かっこ( )で囲むこと。

理由: 即時関数式は単一のユニットであり、即時関数式とその呼出の丸かっこ()の両方を丸かっこ()で囲むことで明確に表現できるようになる。ただし、あらゆる場所でモジュールが使われているような世界では、即時関数式が必要になることはほぼまったくありえない。

// 即時関数式(IIFE)
(function () {
  console.log('インターネットへようこそ。フォローしてね。');
}());

7.3 【禁止】関数でないブロック(ifwhileなど)の中では決して関数宣言を行ってはならない。代わりに関数を変数に代入すること。ブラウザではそのようなコードでも動いてしまうが、コードはまったく違う形で解釈され、痛い目を見ることになる。

7.4 【知識】注: ECMA-262ではblockを文(statement)のリストと定義している。そして関数宣言は文ではないECMA-262の注記 を参照)。

// Bad
if (currentUser) {
  function test() {
    console.log('Nope.');
  }
}

// Good
let test;
if (currentUser) {
  test = () => {
    console.log('Yup.');
  };
}

7.5【禁止】 パラメータに決してargumentsという名前をつけてはならない。もし使うと、あらゆる関数スコープで与えられるargumentsオブジェクトが隠蔽されてしまう。

// Bad
function foo(name, options, arguments) {
  // ...
}

// Good
function foo(name, options, args) {
  // ...
}

7.6 【禁止】argumentsは決して使ってはならない。代わりに...記法を使うこと。

理由: ...を使うことで、どの引数を取得したいかが明確になる。さらに...記法の引数は本物の配列である。argumentsのような配列もどきではない。

// Bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments);
  return args.join('');
}

// Good
function concatenateAll(...args) {
  return args.join('');
}

7.7 【禁止】引数を関数で変更してはならない。代わりにデフォルトパラメータ文法を使うこと。

// 非常に悪い
function handleThings(opts) {
  // 関数でこのように引数を変更するべきではない!
  // さらなる副作用: optsがfalsyの場合、必要なものがそのオブジェクトに
  // 運よく設定されるかもしれないが、微妙なバグの原因になるかもしれない
  opts = opts || {};
  // ...
}

// これでも悪い
function handleThings(opts) {
  if (opts === void 0) {
    opts = {};
  }
  // ...
}

// Good
function handleThings(opts = {}) {
  // ...
}

7.8 【推奨】デフォルトパラメータでは副作用の発生を避けること

理由: 動作の理解で混乱を招く。

var b = 1;
// Bad
function count(a = b++) {
  console.log(a);
}
count();  // 1
count();  // 2
count(3); // 3
count();  // 3

7.9 【必須】デフォルトパラメータは常に末尾に置くこと。

// Bad
function handleThings(opts = {}, name) {
  // ...
}

// Good
function handleThings(name, opts = {}) {
  // ...
}

7.10 【禁止】関数を決して関数コンストラクタで作成してはならない。

理由: 関数コンストラクタを使うと文字列がeval()と似た方法で評価され、脆弱性が発生する。

// Bad
var add = new Function('a', 'b', 'return a + b');

// still bad
var subtract = Function('a', 'b', 'return a - b');

7.11 【スタイル】関数シグネチャにはスペースを置くこと。

理由: スタイル統一のため。名前の追加や削除の際にこのスペースを増やしたり減らしたりすべきではない。

// Bad
const f = function(){};
const g = function (){};
const h = function() {};

// Good
const x = function () {};
const y = function a() {};

7.12 【禁止】受け取ったパラメータを変更してはならない。

理由: パラメータとして渡されたオブジェクトを改変すると、呼び出し元で意図に反して変数の副作用が発生する可能性がある。

// Bad
function f1(obj) {
  obj.key = 1;
}

// Good
function f2(obj) {
  const key = Object.prototype.hasOwnProperty.call(obj, 'key') ? obj.key : 1;
}

7.13 【禁止】パラメータに決して再代入してはならない。

理由: パラメータに再代入すると、特にargumentsオブジェクトにアクセスするする場合に思わぬ挙動が発生する可能性がある。さらに、特にV8エンジンで最適化に問題が発生する可能性もある。

// Bad
function f1(a) {
  a = 1;
  // ...
}

function f2(a) {
  if (!a) { a = 1; }
  // ...
}

// Good
function f3(a) {
  const b = a || 1;
  // ...
}

function f4(a = 1) {
  // ...
}

7.14 【推奨】variadic関数の呼び出しより、spread演算子...が望ましい。

理由: ...の方が簡潔であり、コンテキストを与える必要がない。applyを使うと(意味のない)newが簡単にはできなくなる。

// Bad
const x = [1, 2, 3, 4, 5];
console.log.apply(console, x);

// Good
const x = [1, 2, 3, 4, 5];
console.log(...x);

// Bad
new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]));

// Good
new Date(...[2016, 8, 5]);

7.15 【スタイル】シグネチャや呼び出しが複数行に渡る関数は、本スタイルガイドの他でも使われている方法でインデントすること。各項目は単独で1行ずつ記述し、最終項目末尾のカンマは省略しないこと。

// Bad
function foo(bar,
             baz,
             quux) {
  // ...
}

// Good
function foo(
  bar,
  baz,
  quux,
) {
  // ...
}

// Bad
console.log(foo,
  bar,
  baz);

// Good
console.log(
  foo,
  bar,
  baz,
);

8. アロー関数(arrow function)

8.1 【推奨】関数式を無名関数として渡さなければならない場合は、アロー関数記法を使うこと。

理由: アロー関数を使うと、thisのコンテキストで実行される関数が与えられる。これは多くの場合において適切であり、文法も簡潔になる。

使わない場合の理由: 関数がかなり複雑な場合は、ロジックを独自の関数宣言に切り出すことを検討すること。

// Bad
[1, 2, 3].map(function (x) {
  const y = x + 1;
  return x * y;
});

// Good
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});

8.2 【スタイル】関数の内容が1つのを副作用なしで返す単一の文の場合、波かっこ {}returnを省略するか、波かっこ {}returnを明示的に書くか、どちらかにする。

理由: シンタックスシュガーであり、複数の関数を連鎖したときに読みやすくなる。

// Bad
[1, 2, 3].map(number => {
  const nextNumber = number + 1;
  `A string containing the ${nextNumber}.`;
});

// Good
[1, 2, 3].map(number => `A string containing the ${number}.`);

// Good
[1, 2, 3].map((number) => {
  const nextNumber = number + 1;
  return `A string containing the ${nextNumber}.`;
});

// Good
[1, 2, 3].map((number, index) => ({
  [index]: number,
}));

// 副作用ありの場合のreturnの省略
function foo(callback) {
  const val = callback();
  if (val === true) {
    // コールバックがtrueの場合に何かする
  }
}

let bool = false;

// Bad
foo(() => bool = true);

// Good
foo(() => {
  bool = true;
});

8.3 【スタイル】式が複数行に渡る場合、丸かっこ( )で囲んで読みやすくする。

理由: 関数の開始と終了がわかりやすくなる。

// Bad
['get', 'post', 'put'].map(httpMethod => Object.prototype.hasOwnProperty.call(
    httpMagicObjectWithAVeryLongName,
    httpMethod,
  )
);

// Good
['get', 'post', 'put'].map(httpMethod => (
  Object.prototype.hasOwnProperty.call(
    httpMagicObjectWithAVeryLongName,
    httpMethod,
  )
));

8.4 【スタイル】引数を1つだけ取り、波かっこ{ }を使わない関数では、引数の丸かっこ( )を省略するか、読みやすさと一貫性のために引数を常に丸かっこ( )で囲むか、どちらかにする。
注: 常に丸かっこ( )で囲むスタイルも許容される。eslintでは“always” オプションをオンにし、jslintの場合はdisallowParenthesesAroundArrowParamをすることで設定できる。

理由: 可読性の向上。

// Bad
[1, 2, 3].map((x) => x * x);

// Good
[1, 2, 3].map(x => x * x);

// Good
[1, 2, 3].map(number => (
  `${number}を含むとても長い文字列。あんまり長いので.map行で場所を取りたくない!`
));

// Bad
[1, 2, 3].map(x => {
  const y = x + 1;
  return x * y;
});

// Good
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});

8.5 【スタイル】比較演算子(<=>=)と紛らわしい場合はアロー関数記法(=>)の利用を避ける。

// Bad
const itemHeight = item => item.height > 256 ? item.largeSize : item.smallSize;

// Bad
const itemHeight = (item) => item.height > 256 ? item.largeSize : item.smallSize;

// Good
const itemHeight = item => (item.height > 256 ? item.largeSize : item.smallSize);

// Good
const itemHeight = (item) => {
  const { height, largeSize, smallSize } = item;
  return height > 256 ? largeSize : smallSize;
};

  • 1〜8: 型、参照、オブジェクト、配列、関数ほか — 本記事
  • 9〜14: クラス、モジュール、イテレータ、プロパティ、変数、ホイスティング
  • 15〜26: 比較演算子、ブロック、制御文、型変換、命名規則ほか

関連記事(JavaScript)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

JavaScript、jQuery入門ーフォーム作成で実際に使った例を振り返りながら

JavaScriptでElement.styleがnullになって焦った

HTML + CSS + JavaScript で簡単に導入できるdatetimepicker の比較

Rails アプリケーション開発で役に立ったJavaScript デバッグの小技

JSの非同期処理を初めてES6のPromiseを使ったものに書き換えてみた

JavaScriptでモーダルウィンドウを出すなら

新機能: Googleスプレッドシートなどの履歴に「名前」を付けられる

$
0
0

こんにちは、hachi8833です。

小ネタですが、GoogleスプレッドシートやGoogleドキュメントの履歴が強化されたので簡単にご紹介したいと思います。

なお、今のところモバイル版アプリでは履歴機能そのものがないようです。以下はすべてWeb版についてです。

関連情報

履歴に名前を付けられるようになった

使い方は簡単です。Web版のGoogleアプリ(ここではスプレッドシート)で [ファイル] > [版の履歴] を開くと、[最新の版に名前をつける]が追加されています。

[最新の版に名前をつける] をクリックしてその場で名前を付けてもよいのですが、普通に [変更履歴を表示] をクリックすれば履歴の版の名前をインラインで変更できますので、その方が楽だと思います。

おそらく以前からの機能だと思いますが、ある程度のマイナーな変更履歴は [▶] アイコンで自由に展開したり折りたたんだりできます。

Gitの履歴感覚で、履歴に名前を付けてがっつり管理できるようになります。Gitだとマイナーな変更履歴はsquashgit rebase -i)で行うのが普通ですが、折りたたみ/展開できるところがありがたいです。

長く使いそうなシートの履歴に折に触れて軽く名前を付けておくと、後で履歴を簡単に追えるようになるのでおすすめです。

履歴の名前付けが使えるGoogleアプリ(調べた範囲内)

  • Googleスプレッドシート(上図)
  • Googleドキュメント(下図、上)
  • Googleスライド(下図、中)
  • Google図形描画(下図、下)

いずれも操作が一貫しているので助かります。

GoogleマイマップやGoogleサイトにはなさそうです。

Google App Scriptには少し昔っぽいUIの履歴機能があります。もちろん名前も付けられます。

なお、ExcelやWordにもファイル共有機能と一体化した履歴機能はありますが、ファイルサーバー上で共有すると不意にファイルが壊れたり(同じセルに複数ユーザーが触ると起きやすいらしい)、履歴やユーザーが増えるに連れて動作がどんどん重くなったりするので、私は使っていません。むしろ受け取ったExcelブックの共有は速攻オフにするタイプです。

SharePointかOneDriveかOffice Onlineで共有すればよいのかもしれませんが…

関連記事

Googleスプレッドシートのセル内に画像をぴったり表示する方法

ExcelやGoogleスプレッドシートにデータをためるときに気をつけていること

HTMLでテーブルのヘッダーを固定する”FixedMidashi”

Slackbot活用術 — URLの呼び出しとGoogle Spreadsheetでのクエリ

米国から見た日本のRuby事情(翻訳)

$
0
0

こんにちは、hachi8833です。来週のRubyKaigi 2017@広島にちなんで、米国から見た日本のRuby事情の翻訳記事をお送りいたします。

RubyKaigi 2017

概要

原著者の許諾を得て翻訳・公開いたします。

通常はリンクを日本語版に置換えますが、本記事では米国人が参照したリンクをたどりやすいよう、原則として英語リンクはそのままにしてあります。

米国から見た日本のRuby事情(翻訳)

最近のやりとりの中で「日本のRubyコミュニティにおけるRubyの使いみちは、米国とはだいぶ違うよね」という話題に触れたところ、ポカーンという顔をされてしまいました。

特に、「米国でRubyと言えばほぼRailsやWebアプリと相場が決まっているのにね」(英語圏の皆さまには言うまでもないことですが)と続けたところ「え、じゃ日本でRubyをRails以外の方面に使ってる人たちって、どんなことやってんの?」と一斉に聞き返されてしまいました。

皆がその点に関心を抱く理由を知りたくなったので、そのまま会話を続けました。米国流のRails利用法は、問題に対してメモリやconcurrencyをがっつり割り当てる物量作戦ですが、日本流ではRubyをそういう風に扱いません。こう話すと、決まって「RubyでJITが使えさえすればいいものを…どうしてJITがないんだ?」という話になります。JITは複雑かつメモリ集約的(memory-intensive: 大量のメモリと引き換えに速度や並列性などを向上させる)なので、Webサーバーを実行させれば素晴らしいのですが、◯◯◯に使うにはちょっと…。それはともかく、日本のRubyistたちのRuby活用法を探ってみたいと思います。

(MRIとJITについては、本記事へのツイート返信↓でも話題になりました: JITに関心のある方はどうぞ)

Zachary Scott、島根県松江市の「**Ruby City MATSUE**」看板前でキメるの図

Zachary Scott、島根県松江市の「Ruby City MATSUE」看板前でキメるの図

日本のRubyコミュニティはどこが違うか

米国のRubyコミュニティは、ほぼRailsをきっかけに立ち上げられました。え、「Rubyはもっともっと前からあるゾ!」って?、はいもちろん存じておりますし、実際そのとおりです。しかし米国でRubyが知られるようになったきっかけはやはりRailsだったのです。

そのため、米国におけるRubyの命運はRailsの動向に大きく左右されます。Railsが盛り上がればRubyも盛り上がるというわけです。Railsはほぼピークを迎えて現在ゆるやかに逓減気味ですが、Rubyについても同じことが言えます。もちろん、RubyにはRails以外の使いみちもあることは間違いありません。しかしながら米国や英語圏では、両者は栄枯盛衰をともにしています。

その点日本では少々趣が異なります。日本ではRails登場前からRubyが普及していましたし、日本でのRailsの普及は、Railsが米国で爆発的に広まったときほどに急速ではありませんでした。(米国における)Railsは引き潮気味であり、Rubyカンファレンスで見かける米国人も以前より減りつつあるようです。

一方日本ではあらゆる地域にRubyコミュニティが根付いていて、コミュニティは増加の一途をたどっています。Ruby Centralの日本版であるRuby-no-Kaiは、今年既に6つ以上もの地域RubyKaigi(Rubyカンファレンス)を開催しています。日本国内だけでこれほどの開催数です。新興のカンファレンスもあれば、数年目を迎えたカンファレンスや今年で11年目(!)という古参のカンファレンスまであります。そして、言わずと知れた国際的なRubyKaigiも開催されています。さらに、エンタープライズ向けカンファレンスであるRubyWorld Conferenceもあります。他にも、RubyBizFukuoka Ruby Awardsといった表彰カンファレンスがいくつも開催され、先のRuby World内でもRuby Prizeの授与式を行っています。Rubyは日本では今も勢いよく成長しています。

面白いことに、Ruby-no-Kaiではカンファレンスの運営をbug trackerで行っているんですね。運営の様子は誰でもここで見ることができます。

違いは他にもあります: 日本政府によるスポンサーシップです。日本では、Rubyが日本で発明されたことが大変誇りに思われており、そのことが今も日本のRubyを支えています。福岡県の下部組織であるFCOCAは、多くの米国Rubyツアー、多くの組み込みRuby関連、さまざまなRuby関連コンテンストや賞のスポンサーを務めています。島根県もRuby関連のスポンサーであり、県庁所在地の松江市も「The Ruby City」を名乗っています。各地域はミニチュア版シリコンバレーとして使われる特区を設置し、地方自治体がRubyによる町おこしに取り組んでいます。取り組みの多くは組み込みRubyやRuby IoT機器を用いています(すべてではありませんが)。

mrubyで日本政府によるスポンサーシップを非常によく見かける理由のひとつがこれです。聴衆の米国人から「なぜ組み込みRubyが必要なのですか?」とよく質問されています。しかし日本人にとっては、日本で既にRubyがそのように使われているから、という面が大きいのです。実際、Rubyはメモリ消費が大きい割にうまく組み込めていますし、mrubyは本当にうまく組み込めています。しかし英語圏のRuby界隈では、組み込みRubyやmrubyはそれほど大きな部分を占めていません。

他にも、日本のRubyコミュニティに(米国との)大きな違いがひとつあります。それはコミュニティの一極集中です。Koichi SasadaShyouhei UrabeYui NaruseZachary ScottAkira Matsudaといったコアコントリビューターの多くが、10分から15分もあればお互いに行き来できるほどの近所に固まって住んでおり、頻繁に顔を合わせています。言うまでもなく、Matzもコミッターの定例会などでこうしたコアコミッターたちと定期的に会っています。地域カンファレンスの運営組織は1つだけのことが多く、カンファレンスのスポンサーも同じ顔ぶれの少数スポンサーが務めることが多いのです。

もうひとつ、RubyのコアコミッターのRubyに対する技術的な視点に影響を与えているポイントがあります。MatzはHerokuの正社員であり、Koichi(現在のRuby VMの作者であり、Rubyアソシエーションの理事でもあります)も最近までHerokuに在籍していました。Herokuは米国企業であり、親会社はSalesForceです。Herokuはホスティングサービスを提供する企業でもありますが、メモリ使用量(最もコストが高い)とCPU(アイドリングすることが多く、VM間での移動が容易)の折り合いの付け方について、Railsを生のEC2インスタンスにホスティングする米国企業とはいささか考えを異にしています。Herokuは、最小限のHerokuインスタンスでRubyが正しく振る舞うようにすることも真剣に望んでおり、実際にその意向はあらゆる面で十分納得のいくものになっています。

日本のエンタープライズ向けRubyカンファレンス

この他の違いについても見てみたいので、日本の島根県松江市で開催された2016年のRubyWorld Conferenceのプログラムを眺めることにしましょう。

Ruby Worldの最初のスピーチは、車載電子機器制御ユニットのテスト機にRubyを使った話です。2番目は組み込み用ハードウェアのアプリ開発に組み込みのmrubyを使う話でした。そう、組み込みでやってるんです!

3番目はEnechangeという電気料金比較サービスの話題です。既にWebサイトはあるようですが、これも米国で一般的に見かけるRubyベースのスタートアップ企業とはおもむきが違っています。

続いて日立と永和システムマネジメントによるスポンサートークです。永和システムマネジメントの企業ページには「自動車の車載システム開発」とあり、ここでもきっと自動車での組み込みRubyに触れていたのではないかと思われます。

続く2つのスピーチは科学技術系の話題と、機械学習インフラについての話題です。どちらも実用的なトピックであり、かつ英語圏のRuby Worldにもそれぞれ同じテーマのスピーチがありましたが、これらについては日本のRubyコミュニティの方が活発です。日本語データ管理についてはTreasure Dataも見逃せません。同社は日本のRubyコミュニティへの影響力も大きく、著名なRuby開発者も擁しています。

続いてはLego MindStormでmrubyを学ぶという話題です。いかにも英語圏のRubyコンベンションで見かけそうなテーマですが、ここでも組み込みを使っています。R-learning社の「Rubyで企業を成長させる」スピーチの後は、「小さな町で子供向けのプログラミング講座をはじめてみて」で、こちらもカリフォルニアやニューヨークのRubyコンベンションで見かけそうなトピックです。

全般に、Rubyの応用よりも、ビジネスの話題や、アジャイル、DevOps、開発者の仕事の見つけ方など開発手法を話題にしたスピーチが多く目につきます。IoTセンサー企業によるスポンサートークでは酒造への応用を話題にし、続くスポンサートークではRailsコンサルタントが登壇しました。こうしてみると、Rubyの利用法が米国と日本でまったく違うかのように考えるのは正しくありません。

同じだけど違う、違うけど同じ

日本のコミュニティでRuby on Railsを見かけることもあると思いますし、実際使われています。しかしRailsの使われ方が一味違っていることにも気づくでしょう。たとえばクックパッドでは世界最大級の壮大なRailsモノリスを運営していますが、Railsを基本的にCMSとして使っており、コンセプトとしてはTwitterよりもむしろWordPressに近いでしょう。

Rubyアソシエーション近影(Googleストリートビューより)

Rubyアソシエーション近影(Googleストリートビューより)

言うまでもないことですが、英語圏のRubyもRails一色で塗りつぶされているわけではありません。米国のRubyカンファレンスでは機械学習やIoTの話題も取り上げられていますし、もしかしたら米国のどこかを走る自動車でとっくにRubyが動いているかもしれません。RubyとRailsの両方で活躍するAaron PattersonAkira MatsudaRichard Schneemanのように、Ruby世界とRails世界は互いに強くつながっていることは間違いありませんが、両者が見つめる世界は同じではありません。

いつの日か「どうしてRubyはRuby on Railsのみに完全に最適化しようとしないのだろうか」と疑問に思うことがあったら、日本のコミュニティを思い出すとよいでしょう。そこはRubyのふるさとであり、Rubyの開発はそこで行われているのですから。もちろん日本でもRailsは使われていますが、日本でメジャーになるのはまだ先のことです。

私よりもずっと日本のRubyコミュニティ事情に通じているZachary Scottに深く感謝いたします。Zacharyが本記事のドラフトをレビューしてくれたおかげで多くの視点に気づくことができ、いくつかの大きな誤りも見つかりました。「日本のRubyコミュニティはどこが違うか」セクションの多くは、Zacharyが親切に教えてくれるまで私がまったく知らなかった情報です。

そしてRubyとmrubyの生みの親であり、本記事でmrubyやHerokuの記述の修正に協力いただいたMatzにも深く感謝いたします。

関連記事

RubyWorld Conference2016 初日レポート

RubyWorld Conference2016 二日目&まとめレポート

RubyWorld Conference 2016に今年もスポンサー登録しました&紹介

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

[インタビュー] Aaron Patterson(後編): Rack 2、HTTP/2、セキュリティ、WebAssembly、後進へのアドバイス(翻訳)

『ふくおか経済』に弊社関連会社ウィングドア代表の福原のインタビューが掲載されました。

Ruby: delegate.rb標準ライブラリの動作を解明する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

以下のRailsの#delegate_missing_toメソッドの記事も参考にどうぞ。

[Rails 5.1] 新機能: delegate_missing_toメソッド(翻訳)

Ruby: delegate.rb標準ライブラリの動作を解明する(翻訳)

CC BY-SA 3.0 Nick Youngson

オブジェクト指向(OO)プログラミングについてよく言われるのは「オブジェクト間でのメッセージのやりとり」であるということです。オブジェクト指向によって、問題解決に必要な正しい名詞や動詞が見つけやすくなるということもよく言われます。私は、ひとつのプログラムを劇の舞台に見立てることを何かと好みます。出演者はその舞台で演じ、互いに会話を交わすわけです。しかし、あるキャラが別のキャラに話すときに、間に別の出演者をはさむこともあります。この場合、間に立つ俳優は、相手のキャラへのメッセージを託される(委譲される)ことになります。それではプログラミングの委譲(delegate)の話に戻りましょう。

委譲とは、あるオブジェクト(レシーバ)のメンバ(プロパティやメソッド)を、送信元オブジェクトとは別のコンテキストで評価することを指す。
Wikipedia英語版より

この定義は、先の演劇的なアナロジーとかなり似ています。つまり委譲を、オブジェクトが単にメッセージを右から左に転送するという形で、メッセージを相手のオブジェクトに渡すことであると定義しています。なぜ委譲が必要になるのでしょうか?

私は前回の記事で、Rubyの標準ライブラリの中からSimpleDelegatorに触れたとき、これ自体を記事にする価値があると思いました。最初に、delegate.rbで強力かつ柔軟なインターフェイスを設計する方法をご紹介します。次にソースコードを読んで、舞台裏で行われているマジックについて少し説明します。

おすすめ映画を表示する

ここではおすすめ映画エンジンの一部を開発しているとしましょう。この単純化された世界でMovieについて知っておくべきことは、iMDbRotten Tomatoesからエンジンが取得した(数値化された)レーティング(スコア)であるということだけです。2つのサイトのスコアは同じスケールに正規化されていると仮定し、外部スコアの平均値に基づくaverage_scoreという1つの数値だけを返すと仮定します。これをコードで表すと次のようになります(gist: movie.rb)。

class Movie
  attr_reader :imdb_score, :rotten_tomatoes_score

  def initialize(name, imdb_score, rotten_tomatoes_score)
    @name = name
    @imdb_score = imdb_score
    @rotten_tomatoes_score = rotten_tomatoes_score
  end

  def average_score
    (@imdb_score + @rotten_tomatoes_score) / 2
  end
end

次に、(おそらく何らかの回帰モデリングの後に)Moviesのarrayを保存するクラスが必要です。このクラスをRecommendedMoviesとし、ここで関連するクエリを実行できるようにします(gist: recommended_movie.rb)。

class RecommendedMovies
  def initialize(movies)
    @movies = movies
  end

  def best_by_imdb
    @movies.max_by(&:imdb_score)
  end

  def best_by_rotten_tomatoes
    @movies.max_by(&:rotten_tomatoes_score)
  end

  def best
    @movies.max_by(&:average_score)
  end
end

かなり素直なコードです。arrayを生のまま使わないなど、明確で使いやすいインターフェースを備える専用オブジェクトを作成したのは我ながら上出来です。それでは適当なデータを渡して値を取り出してみましょう。

north_by_northwest = Movie.new('North by Northwest', 85, 100)
inception = Movie.new('Inception', 88, 86)
the_dark_knight = Movie.new('The Dark Knight', 90, 94)

recommended_movies = RecommendedMovies.new([north_by_northwest, inception, the_dark_knight])

recommended_moviesへのクエリはシンプルです。

recommended_movies.best
 => #<Movie:0x007fbcf7048948 @name="North by Northwest", @imdb_score=85, @rotten_tomatoes_score=100>

責務を限定する

RecommendedMoviesは期待どおり動作しましたが、ひとつ大きな欠点があります。arrayを与えて初期化したにもかかわらず、元のArrayの振舞いが完全に失われてしまっているからです。たとえばrecommended_movies.countを実行すると、NoMethodErrorが返ります。RecommendedMoviesArray(とEnumerable)の素晴らしい機能をすべて使えればメリットははかりしれないだけに、この制約は残念です。このクラスにmethod_missingを実装すれば、Rubyの標準ライブラリであるdelegate.rbで見つけたエレガントな解決法が使えるでしょう。

このライブラリによって2つの具体的な解決を得ることができます。なお、どちらも継承によって実装されます。DelegateClassそのものも詳しく調べる価値がありますが、簡易版のSimpleDelegatorでも十分この2つの解決を得られます。使い方は次のような感じになります(recommended_movies_with_delegation.rb)。

require 'delegate'

class RecommendedMovies < SimpleDelegator
  def best_by_imdb
    max_by(&:imdb_score)
  end

  def best_by_rotten_tomatoes
    max_by(&:rotten_tomatoes_score)
  end

  def best
    max_by(&:average_score)
  end
end

これだけで十分です。以前の機能はそのまま動作するうえ、元のarrayのメソッドもすべて使えるようになりました。ここではarrayに対してDecoratorパターンを適用しました。さっきと同じデータを与えると、今度はrecommended_movies.count3を返すようになりました。しかもインスタンス変数moviesの宣言と参照が不要になったので、initializeメソッドを省略できたことにご注目ください。まるで、RecommendedMoviesを初期化するとRecommendedMoviesインスタンスのselfがarrayになったかのようです。

訳注: Decoratorパターンについては以下の記事もご覧ください。

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

舞台裏の仕掛け

delegate.rbのソースコードはここで参照できます。このソースの行番号を示しながら解説しますので、別タブで開きながらお読みください。lキーを押すと特定行にジャンプします。このファイルには興味深いコメントがいくつか書かれていますので、皆さんがコードを掘るときに読んでみることをおすすめします。それでは、SimpleDelegatorの使われ方をボトムアップで分析します。

SimpleDelegatorの継承によって、RecommendedMoviesのancestorsチェインは次のようになります。

recommended_movies.class.ancestors
 => [RecommendedMovies, SimpleDelegator, Delegator,
#<Module:0x007fed5005fc90>, BasicObject]

継承しないバージョンのancestorsチェイン [RecommendedMovies, Object, Kernel, BasicObject]から大きく変わっているのがわかります。その理由は、SimpleDelegatorDelegatorという別のクラスを継承しており(line 316)、それがさらにBasicObjectを継承しているからです(line 39)。ObjectKernelがチェインに含まれていない理由がこれでわかります。#<Module:0x007fed5005fc90>(自分のPCで実行した場合の表示は少し異なります)という見慣れないものがありますが、これは無名モジュールで、Delegatorクラスで定義およびインクルードされます(line 53)。この無名モジュールはKernelモジュールの縮小版として振舞うもので、Kernelを複製して一時変数に保存し(line 40)、いくつかのメソッドをundefineする操作(line 44line 50)をその変数のクラスレベルで実行します(line 41)。こうした変更の後、最終的に更新されたKernelDelegateにインクルードされます。これで先ほどのancestorsチェインを理解できました。

「透明な」初期化

前述のとおり、更新したRecommendedMoviesクラスではinitializeメソッドを省略しています。Rubyでは新しいオブジェクトで自動的にinitializeを呼び出します(クラスでnewを呼んだ後など)が、私はinitializeメソッドを実装しなかったので、initializeメソッド呼び出しは期待どおりancestorsチェインの上の階層に上昇します。initializeメソッドはSimpleDelegatorには実装されていませんが、Delegatorには実装されています(line 71)。ここではobjという引数が1つ渡されることが期待されていますが、これはRecommendedMoviesインスタンス作成時に与えられた引数(ここではMovieオブジェクトのArray)であり、メッセージの委譲先オブジェクトです。

内部では、Delegator#initializeは単に__setobj__メソッドを呼び出し、同じobjをもう一度引数として渡します。しかしDelegator__setobj__を実装していないので、そのような呼び出しを受信するとエラーがraiseされるでしょう(line 176)。その理由は、Delegateが抽象クラスとして使われるためです。Delegateのサブクラスは__setobj__を実装すべきであり、実際SimpleDelegatorには実装されています(line 340)。SimpleDelegator#__setobj__objを単にdelegate_sd_objというインスタンス変数に保存します(sdはSimpleDelegatorを表します)。
本記事の例で、selfrecommended_moviesのまま変わらなかったことを思い出しましょう。

委譲完了!

先ほどお見せしたとおり、一度生まれたrecommended_moviesオブジェクトはarrayとしても扱うことができます。このオブジェクトでbestメソッドを呼び出すと、RubyはそのメソッドをオブジェクトのクラスRecommendedMoviesから探し、私たちの代わりにそれを実行します。しかしcountを呼び出してもクラスに見当たらないので、Rubyはancestorsチェインを上昇してメソッドを探索しますが、残念ながらcountはどのancestorsクラスにも定義されていません。

ここでmethod_missingの出番となります。Rubyは、通常のメソッド探索でメソッドを見つけられずに終了しても、すぐにはNoMethodErrorをスローせず、method_missingで探索を再開します。ancestorsチェインにあるいずれかのクラスでメソッドが定義されていれば、そのメソッドが呼び出され、そうでない場合は、チェインのトップレベルで探索を終了してNoMethodErrorをスローします。

このコンテキストでは、Delegatorクラスでmethod_missingが定義されています(line 78)。ここではまず__getobj__を呼び、委譲の対象となるオブジェクトをフェッチします(line 80)。そして__getobj__SimpleDelegatorで実装されています(line 318)。このメソッドは本質的に、@delegate_sd_objに保存されている対象オブジェクトを返し、次に、実行したいメソッドを対象オブジェクトで呼び出そうとします(line 83)。対象オブジェクトがメソッドに応答しない場合、Delegate#method_missingKernelが応答できるかどうかをチェックして呼び出します(line 85)。どちらも応答しない場合はsuperを呼びます(line 87)。ここまで来てやっとNoMethodErrorになります。実に長い旅でした。

Delegate#method_missingには他にもコードが含まれていますが、ここが動作のコアです。Paolo Perrotta『メタプログラミングRuby 第2版』p66(訳注: 英語版のページ)では、Blank Slateを「最小限のメソッドのみを備えた薄いクラス」と定義しています。RubyのDelegateクラスはこのテクニックを使うときにBasicObjectを継承することで、予想外の挙動による驚きを排除しています。しかしそれと同時に、method_missingの実装が賢いおかげで委譲先オブジェクトが特定のメソッドに応答するかどうかを確認できること、そして委譲先オブジェクトは(多くのRubyオブジェクトと同様)Objectを継承することも覚えてきましょう。やりとりは複雑ですが、最終的に得られるインターフェイス(例のRecommendedMoviesクラスなど)は非常にシンプルかつ直感的です。自分のコードをじっくり読んで、このパターンを適用できる場所を探してみれば、意外にたくさんあることに気づくでしょう。そして多くの場合、楽しくリファクタリングできることでしょう。


本記事についてお気づきの点やご質問がありましたら、ぜひ(元記事の)コメント欄までどうぞ。この記事が面白かった/役に立った場合は、元記事の下にある👏 をクリックして応援をお願いします。

関連記事

[Rails 5.1] 新機能: delegate_missing_toメソッド(翻訳)

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

[保存版]人間が読んで理解できるデザインパターン解説#2: 構造系(翻訳)

Railsのトランザクションと原子性のバグ(翻訳)

$
0
0

注記: 本記事のコード例ではhas_and_belongs_to_many関連付け(通称HABTM)が使われていますが、Railsでこれを使ったリレーションは悪手とされています。現在のRailsでは代わりにhas_many :through関連付けを使うのが一般的です。

概要

原著者の許諾を得て翻訳・公開いたします。

Railsのトランザクションと原子性のバグ(翻訳)

Ruby on Railsのトランザクションは、熟練Rails開発者もつまづくことがあるほど扱いが微妙です。次のいくつかの例では、単純なトランザクションすら意図と違う動きをする可能性があることと、それによって気づかないうちに原子性(訳注: atomicity、不可分性とも)が損なわれることを示します。

設定

本記事では、SurveyとQuestionというモデルを持つシンプルなアプリを使って調べます。Surveyには名前が1つ必要で、Questionには何らかのテキストが必要だとします。例では、SurveyとQuestionの間には多対多(has_and_belongs_to_many)のリレーションが設定されています。

rails new sample
cd sample
rails generate model survey name:string
rails generate model question text:string
rails generate migration CreateJoinTableSurveyQuestion survey question
rake db:create
rake db:migrate
  • app/models/survey.rb
class Survey < ApplicationRecord
  validates :name, presence: true
  has_and_belongs_to_many :questions
  accepts_nested_attributes_for :questions
end
  • app/models/question.rb
class Question < ApplicationRecord
  validates :text, presence: true
  has_and_belongs_to_many :questions
end

コード例A

has_manyhas_and_belongs_to_many関連付けによって提供されるヘルパーメソッドは興味深い挙動を示します。次のスニペットをご覧ください。

survey = Survey.create(name: "Shapes")
question = Question.create(text: "九角形の辺はいくつあるか?")

survey.attributes = { name: "", question_ids: [question.id] }
survey.save
BEGIN
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
COMMIT
BEGIN
ROLLBACK

上のとおり、attributesquestion_ids=(またはquestions=)の代入が行われると、バリデーションの外部でCOMMITされるINSERT文がただちに実行され、その後ROLLBACKします。

以下のようにattributes=saveupdateに差し替えると、期待どおりに原子性が保たれます。updateは内部でwith_transaction_returning_statusのすべての変更をラップしています(with_transaction_returning_statusは、トランザクションにラップされたブロックを1つ取るメソッドで、ブロックが「真らしい」と評価された場合はCOMMITを実行し、ブロックが「偽らしい」と評価された場合はROLLBACKします)。

survey = Survey.create(name: "Shapes")
question = Question.create(text: "九角形の辺はいくつあるか?")

survey.update({ name: "", question_ids: [question.id] })
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" IN (...)
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK

次のように書くこともできます。

survey = Survey.create(name: "Shapes")
question = Question.create(text: "九角形の辺はいくつあるか?")

survey.with_transaction_returning_status do
  survey.attributes = { name: "", question_ids: [question.id] }
  survey.save
end
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" IN (...)
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK

コード例B

transactionメソッドは期待どおりに動作しないことがあります。次のスニペットをご覧ください。

survey = Survey.create(name: "Numbers")
question = Question.create(text: "プランク定数の値はいくつか?")

Survey.transaction do
  survey.update({ name: "", question_ids: [question.id] })
end
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" = ...
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
COMMIT

興味深いことに、コード例Aの修正方法はここでは効きません。デフォルトでネストするトランザクションでは、親のトランザクションだけが使われます。

以下のようにActiveRecord::Rollback例外をraiseするか、親のトランザクションでjoinable: falseを指定することで、半端な変更が保存されないようになります。親のトランザクションでjoinable: falseを指定すると、多くのリレーショナルデータベースが備えるメカニズムとしての保存ポイントが内部で使われます。

survey = Survey.create(name: "Numbers")
question = Question.create(text: "プランク定数の値はいくつか?")

Survey.transaction do
  unless survey.update({ name: "", question_ids: [question.id] })
    raise ActiveRecord::Rollback
  end
end
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" = ...
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK

次のように書くこともできます。

survey = Survey.create(name: "Numbers")
question = Question.create(text: "プランク定数の値はいくつか?")

Survey.transaction(joinable: false) do
  survey.update({ name: "", question_ids: [question.id] })
end
BEGIN
SAVEPOINT active_record_...
SELECT "questions".* FROM "questions" WHERE "questions"."id" = ...
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK TO SAVEPOINT active_record_...
COMMIT

まとめ

上の例から、いくつかの規則が得られます。

  1. 代入可能な関連付けを扱うときは、attributes APIをじかに使うことを避け、updatecreateを使うこと。
  2. (トランザクションの)ネストを扱う場合は、ROLLBACKを伝搬させる例外を使うか、親トランザクションをJOINできないようにすること。

関連記事

Ruby on Rails のhas_many 関連付けのフィルタテクニック4種(翻訳)

Railsのdefault_scopeは使うな、絶対(翻訳)

Rails: JOINすべきかどうか、それが問題だ — #includesの振舞いを理解する(翻訳)

JavaScriptの正規表現のコンセプトを理解する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

JavaScriptの正規表現のコンセプトを理解する(翻訳)

本チュートリアルでは、JavaScriptのコンセプト全体を明らかにし、正規表現の深淵に迫ります。

JavaScriptの正規表現

JavaScriptの正規表現は、パターンといくつかのフラグによってできています。フラグはすべて、結果にどのように影響を与えるかを指定するシンプルな論理値になっており、必要に応じてオンまたはオフにします。興味深いできことが起きるのは、もちろんパターンの中であり、実に複雑かつ強力な文字列検索を定義できます。しかしパターンを解説する前に、JavaScriptで正規表現が動作するコンテキストのあたりを説明することにします。

JavaScriptではすべてがオブジェクトになっているので、正規表現もRegExpというオブジェクトです。このオブジェクトの作成方法には次の2とおりがあります。


  • 正規表現リテラル

var query = /パターン/フラグ; 2つの/ではさまれた内容はすべてパターンとして扱われ、フラグはその後ろに置かれます。このように正規表現リテラルを直接変数に代入する方法はパフォーマンスに優れていますが、最終的に正規表現がハードコードされることになるので、パターンを組み立てて使いたい場合にはあまり向いていません。

  • 正規表現コンストラクタ

var query = new RegExp('パターン', 'フラグ'); 値を渡すコンストラクタ関数であり、引数の「パターン」や「フラグ」に変数が使えます。これは(正規表現リテラルより)遅いのですが、ユーザー入力からパターンを動的に生成するのに向いています。


ここではRegExpオブジェクトを使います。これはメインのObject「クラス」の拡張であり、デフォルトのプロパティをすべて含むほか、独自のプロパティも持っています。


  • RegExp.source

(string): 検索に使うパターンであり、実際に使われるコンテキストに置かれるとわけがわからなくなるやつです。

  • RegExp.lastIndex

(int): 次の検索の開始位置を示す数値です。文字列の2文字目から検索を開始したい場合は1に設定します(数字は0から開始されるため)。デフォルトは0です。検索で最初のマッチが得られると、このインデックスは、メインの文字列でマッチした部分より後の最初の文字を指します。たとえば(長さが)3文字の語を検索し、2度目の文字から始まる文字列にパターンがマッチすると、lastIndexは7に設定されます。このコンセプトについては後で明らかにします。


RegExpのその他の独自プロパティには、渡したフラグによって設定された値が保存されます。

  • RegExp.ignoreCase

i(論理値): 読んで字のごとく、検索または表示される文字列の大文字小文字の違いを無視します。

  • RegExp.global

g(論理値): このフラグを指定しないと、何度実行しても最初の結果だけを返します。gフラグを指定すると、文字列全体をパターンで検索し、すべての結果を返します。

  • RegExp.multiline

m(論理値): 複数行を個別の文字列として扱い、^(文字列の冒頭)や$(文字列の末尾)が各行に適用されます。mフラグを指定しない場合、これらは文字列全体の冒頭と末尾のみにマッチします。

  • RegExp.sticky

y(論理値): lastIndexより後のマッチのみを検索します。現時点ではFirefoxでしかサポートされていませんが、ES6の仕様に含まれたので、他のブラウザでも実装されるかもしれません。


それでは簡単な例をご覧に入れましょう。このパターンは「Geeks Trick」に完全一致するマッチを検索し、複数ブラウザと互換性のある3つのフラグ(igm)が設定されています。フラグの順序に制限はありません。

  var myRegExp = /Geeks Trick/igm;
  alert("Source: " + myRegExp.source +
        "\n Ignore Case : " + myRegExp.ignoreCase +
        "\n Global : " + myRegExp.global +
        "\n Multiline : " + myRegExp.multiline +
        "\n Last Index: " + myRegExp.lastIndex)

/Geeks Trick/ig は‘Geeks Trick‘の完全一致をすべて検索(g)するが、大文字小文字は区別せず(i)、改行を文字列の境界として扱う(m

RegExp関数(exec()test()match()search()replace()split()

これらの関数はRegExpオブジェクトを受け取って、文字列に対して何らかの処理を行って結果を得るのに使われます。検索、存在チェック、新規文字列の作成、新規配列のビルドができるので、私にとってこれらの関数ですべて用は足ります。このセクションの各例では/\a\w+/(aで始まるすべての単語にマッチする)というシンプルなパターンを使うことにします。対象の文字列には「aa ab ac」を使い、それぞれの関数の働きをわかりやすく示します。各サンプルコードはコンソールに貼って動かすこともできますが、その際には先に以下の値をコンソールに貼っておいてください。

var stringToSearch = 'aa ab ac';
var myRegExp = /a\w+/g;

/a\w+/g は、aで始まる1文字以上(+)の単語(\w)をすべて返す(gフラグ)

exec()

検索が複数の部分文字列にマッチする可能性がある場合、この関数を使うことですべての結果を列挙できます。この関数はループ内で使うのがベストですが、以下の例では(ループせずに)明示的に実行しています。この関数はグローバルマッチのgフラグをRegExpに設定したときだけ機能します。gフラグが設定されていないとlastIndexプロパティが更新されず、最初の結果のところで止まってしまいますので、ループの際は設定を忘れずに。
最後の結果に到達するとexec()nullを返し、最初に戻って検索を開始します。

console.log( myRegExp.lastIndex); // 0
console.log( myRegExp.exec(stringToSearch) ); //["aa", index: 0, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 2
console.log( myRegExp.exec(stringToSearch) ); //["ab", index: 3, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 5
console.log( myRegExp.exec(stringToSearch) ); //["ac", index: 6, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 8
console.log( myRegExp.exec(stringToSearch) ); // null
console.log( myRegExp.lastIndex); // 0
console.log( myRegExp.exec(stringToSearch) ); // ["aa", index: 0, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 2
//and so on
  • index : 検索対象の文字列から得た結果の最初の文字を指します。上の例で言うと、最初は元の文字列の冒頭がaaなのでindexは0になり、次は元の文字列のabの最初の文字が3文字目なので3になります。
  • input: 元の文字列です。

test()

パターンが文字列に実際にマッチするかどうかをチェックしたいとします。この関数は、マッチする場合にtrue、マッチしない場合にfalseを単に返します。実にシンプルです。注意として、RegExpにグローバル検索のgフラグが設定されている場合は文字列全体を探索し、exec()の場合とまったく同じようにlastIndexを更新しますので、gフラグのグローバルパターンが使われた後でtest()を行う場合はRegExp.lastIndex = 0を設定しておくのがよいでしょう。こうすることで、うっかり元の文字列の末尾からチェックを開始して、マッチするはずのものがマッチしないという事態を防ぐことができます。

次の例もコンソールに貼って試すことができます(先の2つの変数を貼ってあれば、再度貼る必要はありません)。何回か動かしてみて、動作を確認してみましょう。

console.log( myRegExp.test(stringToSearch) ); //true
console.log( myRegExp.lastIndex); //n

グローバル設定を外したvar myRegExp = /\a\w+/;パターンも貼って同じ例を実行し、動作がどのように変わるかを確かめてみましょう。

match()

この関数は複数の結果を配列で返すので、結果の個数をカウントしたい場合に便利です。ただし、これもグローバルフラグgが前提です。このフラグが設定されていないと、最初の結果しか取れませんのでご注意ください。exec()test()は、RegExpオブジェクトのプロパティ(RegExp.function('ここに文字列を貼って渡す');)を呼ぶ)ですが、この後の4つの関数(matchsearchreplacesplit)はStringオブジェクトのプロパティなので呼び出しの形式(訳注: レシーバと引数)が逆になる点にご注意ください。

stringToSearch.match(myRegExp); // ["aa","ab","ac"]

search()

これはtest()に似た関数であり、文字列がパターンとマッチするかどうかをチェックできますが、最初のマッチのインデックスを返す点が異なります。しかも、グローバルフラグgを設定したりlastIndexを最初のマッチより後に設定したりしても動作は変わりません。また、マッチしない場合には-1を返してそのことを示します。

myRegExp.lastIndex = 5;          // インデックスを末尾に設定
stringToSearch.search(myRegExp); // それでもマッチするので0が返る
stringToSearch.search(/ad/);     // 見つからないので-1を返す

replace()

検索と置換を行います。RegExpは文字列の一部へのマッチに使われ、定義した別の文字列に置き換えられます。replace()関数は2つの引数を取ります。ここでは1つ目にRegExpオブジェクト、2つ目に置き換え後の文字列を渡します(stringToSearch.replace(myRegExp, 'replacement!');)。

var newString = stringToSearch.replace(myRegExp, 'replacement!');
console.log(stringToSearch);    // "aa ab ac"
console.log(newString);         // "replacement! replacement! replacement!"

注意: 置き換え文字列を指定しない場合、「undefined」という文字列が使われます。これに気づいたのは私だけでしょうか?置き換えで部分文字列を削除したい場合は、2番目の引数に空文字列''を指定してください。

訳注: JavaScriptの仕様として、引数を明示しない場合は暗黙的にundefinedが渡されます。

split()

1つの文字列を分割して部分文字列の配列にしたい場合に使います。分割箇所はマッチで定義します。コンマのように簡単なものから、複雑な文字列向けの複雑な変数まで使えます。

myRegExp = / /;                     // スペースで分割
var spaceSeparatedArray = stringToSearch.split(myRegExp);
console.log(spaceSeparatedArray);   //["aa", "ab", "ac"]

split()は上のように、グローバルフラグgを指定していなくてもグローバルに機能する点にご注意ください。

関連記事

Ruby 2.4.1新機能: Onigmo正規表現の非包含演算子(?~ )をチェック

正規表現の先読み・後読み(look ahead、look behind)を活用しよう

正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

原文タイトルは、よくあるヨーダのセリフのもじりです。

RailsのObject#tryがダメな理由と効果的な代替手段(翻訳)

Object#tryは、Railsアプリでnil値を扱う可能性がある場合をカバーするときや、与えられたメソッドがオブジェクトで必ずしも実装されていないといった場合に柔軟なインターフェイスを提供するときに、かなりよく使われています。#tryのおかげでNoMethodErrorを回避できるのですから、これで問題はなくなったように見えます。NoMethodError例外が起きなくなったからといって、本当に問題がなくなったと言えるのでしょうか?

実際にはそうとは言えません。Object#tryにはいくつかの重大な問題がつきまといますし、たいていの場合もっとよいソリューションをかなり簡単に実装できるのです。

Object#tryのしくみ

Object#tryの基本となるアイデアはシンプルです。nilオブジェクトにメソッド呼び出しを行った場合や、そのオブジェクトにメソッドが実装されていないnilでないオブジェクトにメソッド呼び出しを行った場合に、NoMethodError例外をraiseせず、単にnilを返すというものです。

たとえば、最初のユーザーのメールアドレスを取りたいとします。ユーザーが1人もいない場合に失敗しないようにするために、次のように書くことができます。

user.first.try(:email)

このとき、さまざまな種類のオブジェクトを渡せる一般的なサービスを実装していたとしたらどうでしょう。たとえば、オブジェクトを保存した後、オブジェクトがたまたまそのための正しいメソッドを実装した場合に通知を送信するにはどうしたらよいでしょうか。Object#tryを使うと次のように書けます。

class MyService
  def call(object)
    object.save!
    object.try(:send_success_notification, "MyServiceから保存されました")
  end
end

コードを見れば、このメソッドに引数を与えられることもわかります。

途中でnilを受け取る可能性のあるステップごとに、何らかのメソッドをチェインする必要があるときにはどうすればよいでしょうか。ご心配なく、Object#tryでできます。

payment.client.try(:addresses).try(:first).try(:country).try(:name)

Object#tryの何が問題なのか

一見するとObject#tryはさまざまなケースを扱えそうですが、使うとどんな問題が起きるのでしょうか。

その答えは「たくさん起きる」です。多くの場合、Object#tryの最大の問題は、問題を完全に握りつぶしてしまうことであり、nilが問題である場合にも問題を「解決」してしまいます。他にも、Object#tryを使うと意図がはっきりしなくなる点も挙げられます。次のコードが何をしようとしているかおわかりでしょうか。

payment.client.try(:address)

見た目どおり、支払い(payment)にクライアントがいない場合にnilになるケースを扱っているのでしょうか。それとも、単にクライアントがたまたまnilになったときにNoMethodErrorで失敗したくないから「念のため」使っているだけなのでしょうか。もっと悪い場合を考えると、クライアントがたまたまポリモーフィック関連付けになっていて、しかもaddressesメソッドがモデルに実装されているとは限らないとしたらどうでしょう。あるいは、データ完全性にある問題が生じ、何らかの理由でクライアントが削除された支払いがいくつか発生してしまい、しかも残っていないとしたらどうでしょう。

Object#tryの使いみちの可能性があまりに多いため、上のコードを見ただけでは真の意図を知ることは不可能です。

幸いなことに、Object#tryを取り除いて明確で表現力の高いコードにできる別のさまざまなソリューションがあります。そうしたソリューションを使えば、コードのメンテナンス性と読みやすさがより高まり、バグが発生しにくくなり、二度と意図があいまいにならないようにできます。

Object#tryを使わないソリューション

Object#tryの利用状況に応じた「パターン」をいくつかご紹介します。

1. デメテルの法則を尊重する

デメテルの法則は、構造的な結合を回避するのに便利な規則です(個人的には「法則」というほどではない気がします)。要するに、仮想のオブジェクトAは自分に直接関連することにのみ関心を持つべきであり、協同または関連する相手の内部構造に立ち入るべきではないという規則です。この規則は「メソッド呼び出しのドット.は1つだけにする」と解釈されることが多いのですが、デメテルの法則は本来ドットの数の規則ではなく、オブジェクト間の結合についての規則なので、操作や変換のチェインについてはまったく問題にはなりません。たとえば次の例は法則に違反しません。

input.to_s.strip.split(" ").map(&:capitalize).join(" ")

しかし次の例は違反です。

payment.client.address

デメテルの法則を尊重することで、多くの場合明確でメンテナンス性の高いコードを得ることができます。法則に違反する十分な理由がない限り、法則を守って密結合を回避するようにすべきです。

先のpayment/client/addressを使ったコード例に戻ります。次のコードはどのようにリファクタリングできるでしょうか。

payment.client.try(:address)

最初に行うのは、構造的な結合を減らしてPayment#client_addressメソッドを実装することです。

class Payment
  def client_address
    client.try(:address)
  end
end

これでさっきよりずっとよくなりました。payment.client.try(:address)で無理やりaddressを参照するのではなく、payment.client_addressを実行するだけで済みます。Object#tryが1箇所だけになったので既に1つ改善されました。リファクタリングを続けましょう。

ここから先は2つの選択肢があります。clientがnilになるのが正当か、そうでないかです。clientがnilになるのが正しいのであれば、自信を持って明示的にnilを早期に返します(訳注: いわゆるguard構文です)。こうすることで、clientが1つもないのは有効なユースケースであることがはっきりします。

class Payment
  def client_address
    return nil if client.nil?

    client.address
  end
end

clientは決してnilになってはならないのであれば、先のguard構文をスキップできます。

class Payment
  def client_address
    client.address
  end
end

このような委譲はかなり一般的に行われます。Railsではこういう場合にうまい解決法があるでしょうか?答えは「イエス」です。ActiveSupportは、まさにこういう場合にうってつけのActiveSupport#delegateマクロを提供しています。このマクロを使えば、nilをさっきとまったく同じように扱える委譲を定義できます。

最初の例では、nilになってもよいユースケースを次のように書き換えます。

class Payment
  delegate :address, to: :client, prefix: true, allow_nil: true
end

nilになってはならない場合は次のように書き換えます。

class Payment
  delegate :address, to: :client, prefix: true
end

これで先ほどよりもずっとコードが明確になり、結合も弱まりました。Object#tryをまったく使わずにエレガントに解決するという目的を達成できたのです。

しかし、他の場所ではpaymentにclientがないことを予測しきれていない可能性がまだ残されています(完了していないトランザクションのpaymentなど)。たとえば、トランザクションが完了したpaymentのデータを表示するときになぜかNoMethodError例外でつまづいてしまうことがあります。このような場合に、必ずしもdelegateマクロでallow_nil: trueオプションが必要になるとは限りません。もちろん、Object#tryを使わなければならないということでもありません。この場合の解決法はいくつか考えられます。

2. スコープ付きデータを操作する

完了したトランザクションのpaymentを扱うときにclientが存在することを保証するなら、単に正しいデータセットを扱えるようにするのが手です。Railsアプリではこういう場合に、PaymentコレクションにActiveRecordの何らかのスコープ(with_completed_transactionsなど)を適用します。

Payment.with_completed_transactions.find_each do |payment|
  do_something_with_address(payment.client_address)
end

完了していないトランザクションのpaymentでclientのaddressを使って何かする計画はまったくないので、ここでnilを明示的に取り扱う必要はありません。

にもかかわらず、paymentの作成にclientが常に必須になっているとしても、このコードでNoMethodErrorが発生する可能性は残されています(関連付けられたclientレコードが誤って削除されてしまった場合など)。この場合は修正が必要になるでしょう。

3. データ完全性

特にPostgreSQLなどのRDBMSを使っている場合、データの完全性を確実にする方法はかなりシンプルです。ここで押さえておくべきは、テーブルの新規作成時に適切な制約を追加することです。これはデータベースレベルで行う必要があることを忘れてはなりません。モデルでのバリデーションは簡単にバイパスされてしまうことがあるため、まったく不十分です。clientnilになってしまう問題を回避するには、paymentsテーブル作成時にNOT NULL制約とFOREIGN KEY制約を追加して、clientがまったく割り当てられない状況や、関連付けが一部のpaymentに残っているclientレコードが削除されるような状況を防ぐべきです。

create_table :payments do |t|
  t.references :client, index: true, foreign_key: true, null: false
end

以上で制約の追加はオシマイです。制約の追加を忘れないようにすることで、nilで起きる予想外のユースケースの多くを回避できます。

4. 明示的な変換で型を確定する

私は次のようなかなり風変わりなObject#tryの使い方を何度か目にしたことがあります。

params[:name].try(:upcase)

このコードから、params内のnameキーから何らかの文字列が取り出せることを期待しているのが読み取れます。それならto_sメソッドで明示的に変換することで文字列型を確定させればよいのではないでしょうか。

params[:name].to_s.upcase

これで意図がわかりやすくなりました。

ただし、上の2つのコードは同等ではありません。前者はparams[:name]が文字列であれば文字列を返しますが、nilの場合にはnilを返します。後者は常に文字列を返します。場合によってnilが戻ることが期待されるかどうかは元のコードからははっきりしないので(これはObject#tryのあからさまな問題ですが)、ここでは2つの選択肢が考えられます。

  • params[:name]nilならnilを返すことが期待される場合: 文字列の代わりにnilを扱うのはかなり面倒になるので、あまりよいアイデアとはいえませんが、nilを扱う必然性がどうしても生じることもあるかもしれません。そのような場合はguard構文を追加してparams[:name]nilになる可能性があることを明示的に示す方法が考えられます。
return if params[:name].nil?

params[:name].to_s.upcase
  • 文字列を返すことが期待される場合: この場合はguard構文は不要です。先の明示的な変換をそのまま使いましょう。
params[:name].to_s.upcase

もっと複雑な状況では、Form Objectを使うか、dry-rbなどのもっと安全な型管理を導入する(あるいは両方)のがよいかもしれません。ただしこれらは明示的な型変換と本質的に同じなので、設計を損なわない限りは有用だと思います。

5. 正しいメソッドを使う

ネストしたハッシュの取り扱いは、API開発やユーザー提供のペイロードを扱うときにかなりよく見かけるユースケースです。JSONAPI互換のAPIを扱っていて、更新時にclientの名前を取得したいとしましょう。この場合は次のようなペイロードが考えられます。

{
  data: {
    id: 1,
    type: "clients",
    attributes: {
      name: "some name"
    }
  }
}

しかしAPIのユーザーが提供するペイロードが正しいかどうかがどうしてもわからない場合、ペイロードの構造が正しくないという仮定が成り立つことがあります。

こういう場合の残念な対応方法といえば、もうおわかりですね。Object#tryです。

params[:data].try(:[], :attributes).try(:[], :name)

お世辞にも美しいとは言い難いコードです。しかし面白いことに、このコードは実に簡単にきれいに書き直すことができるのです。

1つの方法は、途中のステップに明示的な変換を適用することです。

params[:data].to_h[:attributes].to_h[:name]

さっきよりよくなりましたが、もう少し表現力が欲しいところです。理想的な方法は、こういう場合のための専用メソッドを使うことです。そうした専用メソッドはいくつかありますが、たとえばHash#fetchは、指定のキーがハッシュにない場合の値も指定できます。

params.fetch(:data).fetch(:attributes, {}).fetch(:name)

これでずっとよくなりましたが、ネストしたハッシュを掘ることにもう少し特化したメソッドがあればさらによいでしょう。幸いなことに、Ruby 2.3.0からまさにこのためのHash#digメソッドが使えるようになりました。このメソッドはネストしたハッシュをくまなくチェックし、中間のキーがない場合にも例外をraiseしません。

params.dig(:data, :attributes, :name)

6. 正しいインターフェイスかダックタイピングを使う

最初に使った、必要な場合に通知を送信する例に立ち戻ります。

class MyService
  def call(object)
    object.save!
    object.try(:send_success_notification, "saved from MyService")
  end
end

このコードの改善方法は2とおりあります。

  • サービスを2つ実装する: 1つのサービスは通知を送信し、もう1つは送信しません。
class MyServiceA
  def call(object)
    object.save!
  end
end

class MyServiceB
  def call(object)
    object.save!
    object.send_success_notification("saved from MyService")
  end
end

リファクタリングしたことでコードがずっと明確になり、Object#tryも取り除けました。しかし今度は、MyServiceAを使う必要があるオブジェクトの種類とMyServiceBを使う必要があるオブジェクトの種類を知る方法が必要になります。これはこれで理解できますが、別の問題となる可能性もあります。この場合は2番目の方法がよいでしょう。

  • ダックタイピングを使う: MyServiceに渡されるすべてのオブジェクトに単にsend_success_notificationメソッドを追加します。このメソッドは何もせず、メソッドの内容は空のままにします。
class MyService
  def call(object)
    object.save!
    object.send_success_notification("saved from MyService")
  end
end

この方法なら、オブジェクトで共通する振舞いを明示的に示せるので、そうした振舞いを認識しやすくなるというメリットも得られます。元のObject#tryには多数のドメイン概念が潜んでいるため、コードの意図がわかりにくくなるという問題があります。そうしたドメイン概念が存在しないのではなく、ちゃんと認識されていないということです。Object#tryを使うとドメイン(概念)も損なわれてしまいます。これもぜひ覚えておきたい重要なポイントです。

7.「Null Object」パターン

上の通知送信の例をもう一度使うことにします。モデルの形はある程度残しつつ、少し変更しました。メソッドの引数をmailerにし、それに対してsend_success_notificationを呼びます。

class MyService
  def call(object, mailer: SomeMailer)
    object.save!
    mailer.send_success_notification(object, "saved from MyService")
  end
end

これで、必要に応じていつでも通知を送信できるようになりました。さて、通知を送信したくないときはどうすればよいでしょうか。そんなときの残念な方法といえば、mailernilを渡してObject#tryを使うことです。

class MyService
  def call(object, mailer: SomeMailer)
    object.save!
    mailer.try(:send_success_notification, object, "saved from MyService")
  end
end

Service.new.call(object, mailer: nil)

ここまでお読みになった方は、この方法を使うべきでないことがおわかりいただけると思います。ありがたいことに、Null Objectパターンを適用すれば、何もしないsend_success_notificationメソッドを実装する何らかのNullMailerのインスタンスを渡すことができます。

class NullMailer
  def send_success_notification(*)
  end
end

class MyService
  def call(object, mailer: SomeMailer)
    object.save!
    mailer.send_success_notification(object, "saved from MyService")
  end
end


MyService.new.call(object, mailer: NullMailer.new)

これでObject#tryよりずっとよいコードになりました。

ぼっち演算子&.とは何か

Ruby 2.3.0で新しく導入された&.は「ぼっち演算子」や「safe navigation operator」などと呼ばれます(訳注: 以下ぼっち演算子で統一)。ぼっち演算子は一見Object#tryとよく似ていますが、Object#tryほどあいまいではありません。nil以外のオブジェクトに対してメソッド呼び出しを行い、かつオブジェクトにそのメソッドが実装されていない場合はNoMethodErrorがraiseされます(Object#tryはそうではありません)。次の例をご覧ください。

User.first.try(:unknown_method)  # `user`がnilであるとする
=> nil

User.first&.unknown_method
=> nil

User.first.try(:unknown_method!) # `user`はnilでないとする
=> nil

User.first&.unknown_method
=> NoMethodError: undefined method `unknown_method' for #<User:0x007fb10c0fd498>

ということは、ぼっち演算子なら安全に使えるからよいのでしょうか?そうでもありません。Object#tryの重大な問題が1つ減っただけで、他の問題はそのまま変わらないからです。

しかしながら、私はぼっち演算子を使ってもよいケースが1つあると考えています。次のコード例をご覧ください。

Comment.create!(
  content: content,
  author: current_user,
  group_id: current_user&.group_id,
)

ここでは、current_userに属するコメントを1つ作成したいと考えています。current_userは作者(author)になることがあり、current_userからgroup_idを代入しますが、このgroup_idnilになる可能性があるとします。

上のコードは次のように書き直せます。

Comment.create!(content: content, author: current_user) do |c|
  c.group_id = current_user&.group_id if current_user
end

次のように書き直すこともできます。

comment_params = {
  content: content,
  author: current_user,
}

comment_params[:group_id] = current_user.group_id if current_user

Comment.create!(comment_params)

しかし、書き直したコードが、元のぼっち演算子&.を使ったサンプルより読みやすくなったとは思えません。このように、意図があいまいになるのと引き換えに読みやすさを優先したい場合には、ぼっち演算子&.が有用なこともあります。

まとめ

私は次の理由から、Object#tryの有効なユースケースはひとつもないと信じています。Object#tryを使うと意図があいまいになってしまい、ドメインモデルに負の影響が生じます。Object#tryは問題を美しくない方法で「解決」してしまいますが、同じ問題をもっとスマートに解決できる方法が「デメテルの法則の尊重と委譲」から、「スコープが正しく設定されたデータを扱う」、「データベースに正しい制約を適用する」、「明示的な変換で型を確定させる」、「正しいメソッドを使う」、「ダックタイピングを利用する」「Null Objectパターン」に至るまで数多く存在するという単純な事実があります。ぼっち演算子&.すら、用途を限定すればずっと安全に使うことができます。

訳注: 本記事では言及されていませんが、!付きのObject#try!は実質ぼっち演算子と同じに使えます。
ぼっち演算子が#try!と少し異なるのは、引数付きだとnilのときに引数が評価されないという点です。
参考: Safe Navigation Operator で呼ばれるメソッドの引数はレシーバが nilなら評価されない

関連記事

Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど

Railsの`CurrentAttributes`は有害である(翻訳)


Vue.jsサンプルコード(21)[はい][いいえ]ボタンを押すと表示を変える

$
0
0

21. [はい][いいえ]ボタンを押すと表示を変える

  • Vue.jsバージョン: 2.5.2
  • [はい]または[いいえ]ボタンを押すと「ありがとうございました!」に変わります。
  • 画面をリロードすると最初の状態に戻ります。

サンプルコード


ポイント: HTMLコード中のdata-vはHTML 5のカスタムデータ属性です。

    <div @click="a" data-v="yes">
      はい
    </div>
    <div @click="a" data-v="no">
      いいえ
    </div>

カスタムデータ属性は、JavaScriptのHTMLElement.datasetでリードオンリーで読み取れます。

   this.answer = e.target.dataset.v

バックナンバー(Vue.jsサンプルコード)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

文中で言及されている記事「7 Gems Which Will Make Your Rails Code Look Awesome」の日本語版は以下をどうぞ。同記事からの引用もこの日本語版を使いました。

Railsコードを改善する7つの素敵なGem(翻訳)

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

最近「7 Gems Which Will Make Your Rails Code Look Awesome」というよく書けた良記事を読んでいて、decent_exposure gemのところで次の文言がふと目に留まりました。

マジックがあまり好きでない方はこのライブラリを使わなくてもよいでしょう
(強調は原著者)

この「mで始まる単語」が指していたのはメタプログラミングを使うライブラリです。シンプルなCRUDで十分な場合にexposeメソッドをRailsコントローラクラスに追加して、重要度の低い#index/#show/#newを生成します。

訳注: 「mで始まる単語(m-word)」という英語の言い回しは、ある品の良くない単語を暗に指すことがあります(参考)。

私はこの記事の著者に苦言を呈するつもりはまったくありませんのでご了承ください。単にいくつかの重要な点に言及するきっかけとなったに過ぎません。最近RubyやRailsのコミュニティでは「マジック」と「メタプログラミング」を一種の邪悪な同義語とみなして使うことがかなり広まっているようです。そこで私はあくまで品位を保ちつつ、こうきっぱり申し上げたいのです。

「マジック」と呼ぶのをやめよう

理由についてはこの先をお読みください。

免責事項: 説明に使っているコード例は手作りであり、いかなる特定の既存ライブラリにも基づいていません。このブログで以前起きたような「ワイらの%(ここにライブラリ名が入る)%をコケにするのか」などという反応で論点が見失われないようにするためです。

そもそも、

マジックとは何か?

Wikipediaを引用します。

マジック: 儀式、象徴、振舞い、身振り、言語を用いて、超自然的な力を振るう目的で使われる
Wikipedia: Magic (paranormal)より

次は

超自然的: 自然界の法則では説明できないあらゆるものを含む
Wikipedia: Supernaturalより

上をプログラミングに当てはめると、次のように定式化できます。

コードにおけるマジックとは、何らかの振る舞いが引き起こされるとき、その言語の熟練開発者ですらその原因を追うのが難しいか不可能な状況を指す。

これは定義として完全ではないので、やや理論的な例でこの点を明らかにしてみましょう。

class LandCruiserJ200 < Car
  def type
    :suv
  end

  def engine
    '4.5 L'
  end
end

LandCruiserJ200.new.drive! # "ブルン! ブロロロロロ!"

これはマジックでしょうか?#driveメソッドは上のコードのどこにも見当らないのに、魔法のように動作している???

まともな開発者ならここで苦笑するところでしょう。

馬鹿馬鹿しい、もちろんマジックなんかではありません。単なる継承です。#driveメソッドが親のCarクラスで定義されていることは見当がつきますし、クラスやメソッドのコードを探るのも簡単です(何らかの外部gem由来などの場合は、ドキュメントを探してもよいでしょう)。

もうひとつ例を出しましょう。

class LandCruiserJ200
  include Drivable

  def type
    :suv
  end

  def engine
    '4.5 L'
  end
end

LandCruiserJ200.new.drive! # "ブルン! ブロロロロロ!"

奇っ怪な!継承していないのに、どこから#drive!メソッドが来たのか?マジックだ!

…というか単なる普通のmixinです。Drivableのドキュメントを調べれば、これが使われる理由や詳細はもちろん、#drive!メソッドが何なのかもわかるでしょう。

もうひとつ。

class LandCruiserJ200
  drivable type: :suv, engine: '4.5 L'
end

LandCruiserJ200.new.drive! # "ブルン! ブロロロロロ!"

むむ、今度こそ説明不能な禁断の魔術に違いない!凡人には理解不能ではないか?

…というかたぶんdrivableが実はメソッドで、Classに直接定義されてるか、でなければ何らかのモジュールで拡張されているのでしょう(これはベストプラクティスではありませんが、それでもジュニアを卒業したRubyistなら簡単に見当がつくでしょう)。

LandCruiserJ200.method(:drivable) # => #<Method: Class(Drivable)#drivable>
LandCruiserJ200.method(:drivable).source_location # => /some/gem/drivable/lib/drivable.rb:123

ではどんなものがマジックなのか?

マジックをお見せしましょう。

class LandCruiserJ200Car
end

car = LandCruiserJ200Car.new
# => #<Car model="Land Cruiser J200">
car.drive! # "ブルン! ブロロロロロ!"

上のコードが動くとお考えください。なぜ動くのでしょうか。

私がこの事実と取り組まなければならなくなったら、こんなふうに推測するかもしれません。おそらく現在の何らかのスコープが規約を提供している、それは…うーん、名前がCarになっているものをすべて特別扱いするとか?それともapp/carsフォルダにあるものすべてを特別扱いしている?でなければ、carのクラスの全リストを持っているYAML設定か何かを使っている?

この振る舞いを追加する責務を持つのは一体何だろう、必要になったらドキュメントから探せるのだろうか、期待どおりに動作しない場合はどうやってデバッグすればいいんだろうか。

これは「超自然的な」振る舞いの例になっています(言語の自然な力とは明らかに無関係に何かが起きているなど)。そしてマジックは、「自然なツールや直感が通用しないので、魔導書を丸暗記しなければ魔法のコードを扱えない」というまさにその理由によって、邪悪なものです。

マジックでないものとは何か

ジュニア開発者が理解できないものはマジックに該当しない

「これはマジックが強すぎて経験の浅い開発者には理解できない」という言い方をよく見かけます。(私の記憶ですが)Matzの言うとおり「Rubyは、シンプルに開発するための複雑な言語」であって、その逆ではありません。

自動車のエンジンもコンピュータのCPUも相当複雑ですが、だからといってマジックだということにはなりません。これらを使うべきではないということにはなりませんし、マジックをなるべく使わないようにすべきということにもなりません。

モンキーパッチはマジックに該当しない

そう、モンキーパッチにはいろいろと疑問の余地があります。コアクラスへのモンキーパッチは重大な罪であると考えている人もいるほどです。

しかし別の言語からRubyにやってきた人の中には5.daysを見て「マジックだ」と呼ぶ人もいたりしますが、なぜこの人たちの言説を復唱しなければならないのでしょうか。

これは他の言語の開発者にとっては直感に反することもありますが、Rubyistにとってはそこで何が起こっているか、メソッドの由来の理解やドキュメントや実装の調べ方はおおよそ明らかです。

追記になりますが、Rubyの「オープンクラス」は(コアがオープンであることも含めて)Rubyを進化させるための重要な機能です。それによって新しい概念を実験することもできますし、必要に応じてbackportspolyfillといったgemを使って後方互換性を提供することもできます。

メタプログラミングはマジックに該当しない

そろそろ本記事の冒頭に立ち返ることにしましょう:)

「メタプログラミング」(コード実行中にコードオブジェクトを生成するなど)はRubyの非常に強力な機能であり、Rubyという言語にとって自然なものだからこそ強力たりえます。この「Rubyにとっての自然さ」によって、多くのコード設計で最初にして最善のものとして検討されます。

単に内部の値に委譲するいくつかの数学演算子をクラスに与える必要がある場合、次のようにするでしょう。

%i[+ - * /].each { |op| define_method(op) { ...

他の似たようないくつかのクラスでもこうした振る舞いを宣言的に定義する必要がある場合は、クラスメソッドのシンプルなDSLを追加するでしょう。こんなふうにいろんなことができます。

そして後になって、パフォーマンス上の理由やドキュメントを書きにくいという理由で(最近はメタプログラミングコードのドキュメント化に使えるなかなかいいツールがありますが)、あるいは以前は同じだったコードオブジェクトが同じでなくなってしまったという理由で、決定を変更するかもしれません。

しかしメタプログラミングは、いつでも引き出しから取り出して便利に使える、そうしたツールのひとつに過ぎません。

誰かが「メタプログラミングは不自然だ」と100回繰り返したからといって、メタプログラミングが不自然になることはありません

まとめ

本記事を書いた理由は次のとおりです。

(訳注: マジックに限らず)「mで始まる言葉」はコミュニティにとって有害です。この言葉はRuby世界の内外で自由に使われすぎたために悪いレッテルと化し、「単に好きでないから」とか「単に理解できないから」という理由でさまざまな手法や機能にデタラメに貼り付けられています。こんな状況では、設計を完璧に練り上げた自然なライブラリや言語機能がほとんど「使用禁止」になってしまい、多くのアイデアが立ち腐れてしまいます。

だからこそ、単刀直入に申し上げます。

Rubyistの皆様、「メタプログラミング」の同義語として「マジック」という言葉を使うのをやめましょう。

関連記事

Rubyスタイルガイドを読む: 正規表現、%リテラル、メタプログラミング(最終回)

Ruby: delegate.rb標準ライブラリの動作を解明する(翻訳)

[Rails5] Active Support Core ExtensionsのString#inquiryでメタプログラミング

CSSを迷いなく書く極意: 単一ファイルコンポーネント(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

SFCについてはVue.js: 単一ファイルコンポーネントもどうぞ。

CSSを迷いなく書く極意: 単一ファイルコンポーネント(翻訳)

これは未来の話だが、私たちは既にそれを実践しているのだ。

CSSを嫌うのが流行っています。CSSを嫌う理由はいくらでもありえますが、突き詰めれば「CSSが予測不可能である」ということです。スタイルのルールをこねくり回しているうちに、まったく関係ない別のレイアウトが不意に壊れてしまう(しかもたいてい納品間近に起きる)という経験をしたことがない人がいるとすれば、新人か、さもなければよほどプログラミングが上手いかのどちらかでしょう。

JavaScriptコミュニティはこの問題に取り組み続けてきました。ここ数年もの間、CSSがちゃんと振る舞うようにするための「CSS-in-JS」(CIJ)と呼ばれるライブラリが、カンブリア爆発のごとく大量に出現しました。

もしかすると誰も気づいてないのかもしれませんが、CSS-in-JSを使わなくてもCSSの大きな問題はすべて解決できるのです。これらの問題が片付きさえすれば、CSSを書く苦痛がなくなるだけでなく、むしろ書くのが楽しくなります。それに、CSS-in-JSを導入したために新たに起こる問題の解決方法を探して回る必要もありません。

この記事は、CSS-in-JSコミュニティの苦心の成果を批判するつもりはまったくありませんのでご了承ください。CSS-in-JSコミュニティは非常に活動のさかんなJSエコシステムであり、毎週のように新しいアイデアが生まれています。本記事の意図は、CSS-in-JS以外のアプローチ、つまり現実のCSSを使った単一ファイルコンポーネントに基づくアプローチの素晴らしさを示すことです。

CSSの最大の問題

CSSの中では、あらゆるものがグローバルです。それが原因で、ごく小さなマークアップに適用するつもりだったスタイルがしばしば他の部分にまで適用されてしまいます。それが原因で、開発者が最後の手段としてそこらで見つけてきた名前空間の命名慣習(強制は困難なので「ルール」とは呼べません)に頼らざるを得なくなることがしばしばあり、しかもその命名慣習は多くの場合RSIのリスクを増やすだけに終わってしまいがちです。

チームで作業しているとさらに深刻です。他の人が作ったスタイルは、意図もマークアップがどこに適用されるかもわかりにくく、それを外すとどんな災いが起きるか読めないことが多いので、あえて手を加えようとする人はいないでしょう。

これらすべてが原因で、スタイルシートはひたすら追加を繰り返すだけの代物になってしまいます。どのコードなら安全に削除できるのかを知る方法がないため、既存のスタイルを別のもっと詳細なスタイルで打ち消すことがかなり小さなプロジェクトですら当たり前になっています。

SFCがすべてを変える

SFC(単一ファイルコンポーネント: Single File Components)の基本アイデアは「1つのHTMLファイルに独自のコンポーネントを書き、スタイルや振舞いを記述する<style>属性や<script>属性を必要に応じてコンポーネントに含める」というシンプルなものです。SvelteRactiveVue.jsPolymerはどれもこの基本パターンに従っています。

原注: Svelteが初めての方はこちらの紹介記事推薦ツイートをご覧ください。

本記事では以後Svelteを使いますが、テンプレート言語を使うのがためらわれる方はVue.jsをお使いください。Vue.jsはSFCでJSXを使えます。テンプレート言語については心配無用とだけ申し上げておきますが、これはまた別のトピックとなります。

SFCを使うことで以下のような素晴らしい結果を得られます。

  • スタイルのスコープがそのコンポーネントに限定される: 他のコンポーネントに影響することもなければ、カスケードが予測不可能になることもありません。クラス名を恐ろしく長くしてコンフリクトを避ける設計も不要です(訳注: これはBEMなどを指しているようです)。
  • フォルダ構造をさんざん探した挙句、画面を壊すようなスタイルを見つけ出すことがなくなる
  • コンパイラ(Svelteの場合)は不要なスタイルを見つけて削除できる: 追加しかできないようなスタイルシートとおさらばできます。

実際の方法を以下の動画でご覧ください。

動画: https://svelte-technology-assets.surge.sh/just-write-css.mp4

これは「このプラットフォームを使え」ということですか?

コードエディタはCSSを理解できるので、オートコンプリート、Lint、シンタックスハイライトといった機能をすべて使えますし、こちらを消耗させるJS注入ツールも一切不要です。

しかもこれはキャメルケースをあちこちの引用符に紛れ込ませるどこかのまがい物と違う「本物のCSS」なので、開発ツール上で細かく調整してからソースコードに貼り直すというワークフローが使えます。私はもうこれなしではもう生きていけません。さらに、CSSで探している行を(エディタの)ソースマップで即座に見つけられます。これがどれほど重要であるか、いくら強調してもよいくらいです。WYSIWYGモードで作業していてもコンポーネントのツリーを気にかける必要がないので、スタイルを決めているのがどのコードかを確実に特定できます。そのコンポーネントを作成したのが他のメンバーであればなおさらです(私はこれこそがCSSワークフローの生産性を向上する唯一にして最大の方法であると断言します。私もかつてそうでしたが、今やソースマップ機能なしでCSSを書くのは本当に時間のムダです)。

Svelteはセレクタを変換することでスコープを実現します(そのために該当の要素にも適用される属性を1つ使いますが、正確な仕組みは重要ではなく、今後変わるかもしれません)。未使用のルールを見つけると警告して削除し、続いて結果を最小化して.cssファイルに書き出せる形にします。他にも、Webコンポーネントにコンパイルする新しい実験的オプションもあり、お望みならShadow DOMを用いてスタイルをカプセル化できます。

これらをすべて実現できるのは、CSSがcsstreeで解析され、自分のマークアップのコンテキスト内で統計的に分析されるからです。統計的分析によって、スマート最適化やa11y(accessibility)ヒントといった素晴らしい可能性への扉が一斉に開かれました。スタイルが実行時に動的に算出されていたらこうはいきません。これはまだ始まったばかりなのです。

「それ、このツール追加すればできるよ」について

先の動画を見て「なかなかいいね、でも自分たちはTypeScriptにするよ。エディタごとにプラグインを書けばオートコンプリートやシンタックスハイライトもできるようになるし」といった感想をお持ちの方、つまりCSSの一貫性を実現するためにコードやドキュメントをどっさり書いてチーム内で普及に努め、サブプロジェクトを山ほどメンテすることになっても一向に構わないという方は、残念ながら私と意見が合わないということになります。

まだすべての解を得られたわけではない

既に申し上げたように、CSS-in-JSは長年続く疑問のいくつかについて答えを示しています。

  • npmでスタイルをどうやってインストールするか
  • 1か所で定義された定数をどうやって再利用するか
  • 宣言をどうやって組み立てるか

個人的には、これらの問題があっても、先に示したアプローチから得られるメリットは帳消しにならないと考えます。優先順位は人それぞれ違うので、十分な理由があればCSSを捨てる人がいるかもしれません。

しかし最終的には、CSSの理解は避けて通れません。CSSが好きであろうと嫌いであろうと、CSSを学ばなければなりません。Webメンテナーにとっての選択肢は2つあります。何らかの抽象化を行って、Web開発の学習曲線を急上昇させる方法を選ぶか、皆で力を合わせてCSSの良くない点を修正するかです。私はもうどちらにするか決めています。

関連記事

新版: やばい臭いのするCSSコード(翻訳)

Webデザイナーがこっそり教える4つのテクニック

CSS/JS: 画像を不規則な形に切り取ってテープで貼り付けるテクニック(翻訳)

Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)

パフォーマンスに気を配っている開発者なら、#includes#preload#eager_loadなどの読み込みメソッドでN+1クエリを回避する方法をご存知でしょう。しかし自分の仕事が正しかったかどうか、期待する関連付けが本当にpreloadされているかどうかをチェックする方法はあるのでしょうか。そもそもどうやってテストすればよいのでしょうか。方法は2つあります。

Railsアプリに次の2つのクラスがあるとします。1つのorderは複数のorder line(order_lines)を持つことができます。

class Order < ActiveRecord::Base
  has_many :order_lines

  def self.last_ten
    limit(10).preload(:order_lines)
  end
end
class OrderLine < ActiveRecord::Base
  belongs_to :order
end

ここでOrder.last_tenというメソッドを実装しました。これはeager-loadingする関連付けを1つ使って、最新のorderを10件返します。このコードを呼び出した後でちゃんとpreloadされるかどうかを確認してみましょう。

1. association(:name).loaded?

require 'test_helper'

class OrderTest < ActiveSupport::TestCase
  test "#last_ten eager loading" do
    o = Order.new()
    o.order_lines.build
    o.order_lines.build
    o.save!

    orders = Order.last_ten
    assert orders[0].association(:order_lines).loaded?
  end
end

preload(:order_lines)を行ったので、order_linesが読み込まれているのかどうかを知りたいと思います。orders[0]などのOrderオブジェクトを1つ取得する必要があることをチェックするには、オブジェクトの照合を行います。ordersコレクションをチェックしても関連付けが読み込まれているかどうかはわからないため、コレクションのチェックは不要です。

RSpecでのテストは以下のような感じになります。

require 'rails_helper'

RSpec.describe Order, type: :model do
  specify "#last_ten eager loading" do
    o = Order.new()
    o.order_lines.build
    o.order_lines.build
    o.save!

    orders = Order.last_ten
    expect(orders[0].association(:order_lines).loaded?).to eq(true)
    # 次でもよい
    expect(orders[0].association(:order_lines)).to be_loaded
  end
end

2. ActiveSupport::Notificationsでクエリをカウントする

ActiveRecordライブラリにはassert_queriesという便利なヘルパーメソッドがあり、ActiveRecord::TestCaseに含まれているのですが、惜しいことに、ActiveRecord::TestCaseはActiveRecordに含まれていません。これはRailsの内部テストで振舞いをチェックする目的にのみ利用できます。しかし今回の目的に合わせてassert_queriesをエミュレートするのは意外に簡単です。

いくつかのActiveRecordオブジェクトのグラフを操作するが、オブジェクトを返さずに計算値だけを返すという状況を考えてみましょう。このときにN+1問題が発生していないことをどうやって確認すればよいでしょうか。副作用は見当たらず、loaded?かどうかをチェックできるレコードも返されません。何か方法はないものでしょうか。

class Order < ActiveRecord::Base
  has_many :order_lines

  def self.average_line_gross_price_today
    lines = where("created_at > ?", Time.current.beginning_of_day).
      preload(:order_lines).
      flat_map do |order|
        order.order_lines.map(&:gross_price)
      end
    lines.sum / lines.size
  end
end

class OrderLine < ActiveRecord::Base
  belongs_to :order

  def gross_price
    # ...
  end
end

上の状況で、Order.average_line_gross_price_todayがN+1クエリ問題を抱えていないかどうかをどのように確認すればよいでしょうか。order_lines?を読み取るときにorder.order_lines.map(&:gross_price)がSQLクエリをトリガしないことをどのように確認すればよいでしょうか(実はN+1問題が起きています)。

ActiveSupport::Notificationsを使えば、SQL文が実行されるたびに通知を受け取ることができます。

require 'rails_helper'

RSpec.describe Order, type: :model do
  specify "#average_line_gross_price_today eager loading" do
    o = Order.new()
    o.order_lines.build
    o.order_lines.build
    o.save!

    count = count_queries{ Order.average_line_gross_price_today }
    expect(count).to eq(2)
  end

  private

  def count_queries &block
    count = 0

    counter_f = ->(name, started, finished, unique_id, payload) {
      unless %w[ CACHE SCHEMA ].include?(payload[:name])
        count += 1
      end
    }

    ActiveSupport::Notifications.subscribed(
      counter_f,
      "sql.active_record",
      &block
    )

    count
  end
end

上のようにする場合、eager loadingの問題を検出するのに十分な数のレコードを作成しておいてください。order 1件とline 1件だけでは、eager loadingが発生するかどうかにかかわらずクエリの数が同じになってしまうので不十分です。今回はorderのlineが2の場合にのみ、preloadingでのクエリ数(2件、1つはすべてのorder、もう1つはすべてのline)とpreloadingされない場合のクエリ数(3件、1つはすべてのorder、残りは個別のline)に違いが生じることがわかります。修正する前にはテストが失敗することを必ず確認しましょう :)

この方法はもちろん有効ですが、責務を2つの小さなメソッドに分割できたらなおよいでしょう。責務の1つはデータベースから正しいレコードを抽出すること(IOが発生する)、もう1つはデータを変換して計算することです(IOも副作用もなし)。

この種のテストで役に立つRSpecマッチャーとして、db-query-matchers gemをチェックしてみてください。

もっと知りたい方に

本記事を気に入っていただけた方は、日々のRailsプログラミングに役立つ知識をいつも最初に知ることができる弊社のニュースレターをぜひご購読ください。コンテンツはRuby、Rails、Web開発、リファクタリングが中心ですが、その他の話題も扱っています。

大規模で複雑なRailsアプリを手がけている方は、弊社の最新刊『Domain-Driven Rails』もぜひチェックしてみてください。

関連記事

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

テストを不安定にする5つの残念な書き方(翻訳)

Goby: Rubyライクな言語(1)Gobyを動かしてみる

$
0
0

こんにちは、hachi8833です。
今回から不定期で、Go言語だけで書かれたRubyライクな言語「Goby」について書きます。おそらく日本語で書かれた最初のGoby記事になると思います。

Railsへのコミット経験もある@st0012さんが作ったGobyは現在バージョン0.1.3で、first commitからまだ1年も経過していませんが、st0012さんの驚異的な実装の速さのおかげでかなり早くから基本的な部分をひととおり動かすことができ、HTTP serverやDBアダプタといった基本的なライブラリも装備していて、簡単なWebアプリ(https://sample.goby-lang.org/)やAPIを実際に書くことができます。コミット数は現時点で1800を超えています。

View post on imgur.com

Gobyはその名のとおりRubyから強く影響を受けていて、Rubyと同じ感覚で使えます。Goby実行系はGo言語だけで書かれている(CGOなどのC言語のコードは今のところ含まれていない)ので、C言語を知らないけど気軽に言語系をいじって遊んでみたい方にはぴったりだと思います。Gobyの最適化はこれからですが、その分Goby実行系のソースコードが読みやすいのもありがたい点です。

また、Gobyを知ることでRubyを知るのにも役に立つと思いますし、少なくとも私はそう感じています。実際Gobyのcontributorの中には、Gobyで遊んだ後にRuby本体にパッチを投げた方もいます。

Gobyの特徴については追って順にご紹介しますので、今回はまずGobyの動かし方をご紹介します。

Gobyをどうとらえるか

たとえとしてはとても大ざっぱで恐縮ですが、RubyがサッカーだとすればGobyはさしずめフットサルのようなものと自分は考えています(それならmrubyだよね?というツッコミがありそう…)。フットサルはオフサイドやスローインがないなどルールが若干異なっていますが、同じ感覚でプレーでき、さらに屋内でもプレーできます。何より、サッカーをやる人はいつでもフットサルも楽しむことができます。

Gobyの目的の一つに「マイクロサービスやWebアプリを楽に書けるようにする」というのがあり、Gobyの仕様や標準ライブラリもその点を優先して整備が進められています。Gobyは、Rubyと完全に同じものにする予定は今のところないそうです。実際、明確な意図のもとに少し仕様を変えているところもあり、そうした点については今後の記事でご紹介します。

いろいろ書きましたが、要はGoby実行系のソースをいじるのは私にとって楽しいということです。

Gobyのインストール方法

Gobyのインストールには、Homebrewを使う方法、Go言語ソースをコンパイルする方法、Docker imageを使う方法があります。

Gobyインストール上のポイントは1つ、$GOBY_ROOTの設定です。この環境変数は、Gobyの標準ライブラリをrequireするときなどに必要です。

1. Mac+Homebrew

単に動かすのであれば、Macユーザーはhomebrewでインストールできます。この場合、$GOBY_ROOT環境変数も自動でセットアップされます。インストールの際は最新バージョンをご確認ください。

brew tap goby-lang/goby
brew install goby

gobyを実行して以下が表示されればインストール成功です。

$ goby
Usage of goby:
  -e    Generate reporting format
  -i    Run interactive goby
  -p    Profile program execution
  -v    Show current Goby version

2. ソースからインストール

GobyのGoソースをいじって楽しみたい方、Windows/Linuxの方は、Go言語の環境をセットアップしたうえでGobyのソースからコンパイルします。私はWindowsでGoを動かしたことがないので、ここではMacとLinuxで説明します。ご了承ください。

1. Go言語のセットアップ

Go言語が利用可能な方は1.をスキップできます。

Macの場合、brew install goでGo言語をインストールするのが楽です。
Linuxの場合は以下でGo言語をインストールします。

$ wget https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz -O /tmp/golang-1.9.tar.gz
$ sudo tar xv -C /usr/lib -f /tmp/golang-1.9.tar.gz
$ sudo mv /usr/lib/go /usr/lib/go-1.9

ここからはMac/Linux共通です。以下で$GOPATH$GOROOT環境変数をセットアップし、$PATHも設定します。ここでは.bashrcに設定する前提ですが、必要があれば.bash_profileの方に設定します。

Homebrewの場合を除き、$GOPATHは好きな場所に設定できます。~/go~/.goに置かれることが多いようです。$GOPATHは後から変更すると問題が起きやすいので、一度設定したら変えないようにしましょう。

$ echo 'export GOPATH=$HOME/go' >> $HOME/.bashrc
$ echo 'export GOBY_ROOT=$GOPATH/src/github.com/goby-lang/goby' >> $HOME/.bashrc
$ echo 'export PATH=$PATH:$GOPATH/bin' >> $HOME/.bashrc
$ source ~/.bashrc #設定をリロード

go versionを実行して以下のように表示されればGo言語のインストールは完了です。

go version go1.9.2 darwin/amd64

2. Gobyソースのインストールとコンパイル

以下を実行してGobyソースをインストールします。実行はどのディレクトリにいても構いません。

$ go get github.com/goby-lang/goby

以下を実行してGobyの$GOBY_ROOT環境変数を設定します。

$ echo 'export GOBY_ROOT=$GOPATH/src/github.com/goby-lang/goby' >> $HOME/.bashrc
$ source ~/.bashrc #設定をリロード

Gobyのディレクトリに移動してmake installを実行します。

$ cd $GOPATH/src/github.com/goby-lang/goby
$ make install

gobyを実行して以下が表示されればインストール成功です。お疲れさま!

$ goby
Usage of goby:
  -e    Generate reporting format
  -i    Run interactive goby
  -p    Profile program execution
  -v    Show current Goby version

3. Docker imageを取得して環境まるごとインストール

Gobyには公式のDocker imageもあります。
私のMacbookではなぜかDocker for Mac自体がどうしても動いてくれない(泣)ので、Homebrewで素のDockerをインストールして動かしました。Dockerのインストール方法は省略します。

# bash
$ docker pull gobylang/goby
$ docker run -it gobylang/goby

GobyのREPLやサンプルコード

goby -iでインタラクティブモード(REPL)でGobyを使えます。実はREPLの多くは私が実装しました(repl.go)。

  • resetでREPLをリセット、exitで終了できます。
  • Goby REPLでは、キーで履歴をさかのぼったりCtrl-Rで履歴マッチしたりもできます。いわゆるreadline的なことはひととおりできます。
  • お遊びですがREPLを起動すると絵文字のfortuneが3つ表示されます。
    • 素のWindowsだと絵文字が化けたので、何とあのmattnさんがパッチを当ててWindowでfortuneを非表示にしてくれました。感謝!

  • Goby REPLのプロンプトは»、インデント中は¤、出力はが使われていますが、ターミナルからコピーして貼り付けると行冒頭の»¤は自動で削除されるので、コードを楽にREPL上でコピペできます(下)。
# REPLのプロンプト: このままターミナルにコピペできます。
» def foo
¤   42
» end
#»

駆け足紹介

  • クラス名#methodsでメソッド一覧を表示、クラス名#ancestorsで継承パスを表示できます。
  • クラス名#singleton_classで特異クラスを表示できます。
  • moduleキーワードでモジュールを作成し、includeextendできます。
  • 次の特殊定数が使えます: ARGVSTDINSTDOUTSTDERRENV

Gobyのネイティブクラス

Gobyには現時点で以下のネイティブクラスがあります。Rubyでもお馴染みのクラスの他、Goby固有のクラスもあります。なお、ネイティブクラスはほとんどがnewできない仕様です(ユーザーのクラスはnewできます)。

Gobyの標準ライブラリ

Gobyには、上の他にrequireで導入できる標準ライブラリもあります。

  • Concurrent::Arrayrequire 'concurrent/array'): スレッドセーフなArray(新機能)
  • Concurrent::Hashrequire 'concurrent/hash'): スレッドセーフなHash(新機能)
  • DBrequire "db"): データベースアダプタ(現時点ではPostgreSQLのみ対応)
  • Net::HTTPNet::HTTP::Clientrequire "net/http"
  • Net::SimpleServerrequire "net/simple_server"
  • URLrequire "uri"
  • Jsonrequire 'json'
  • Pluginrequire "plugin"): Go言語の多くのパッケージをプラグイン化して利用できます(現時点ではLinux環境のみ)(サンプルコード

Gobyスクリプトの実行

goby ファイル名.gbでGobyスクリプトを実行できます。

# Goby
def f(from)
  i = 0
  while i < 3 do
    puts(from + ": " + i.to_s)
    i += 1
  end
end

f("direct")

c = Channel.new


thread do
  puts(c.receive)
  f("thread")
end

thread do
  puts("going")
  c.deliver(10)
end

sleep(2) # This is to prevent main program finished before goroutine.
# bash
$ goby channel.gb
direct: 0
direct: 1
direct: 2
going
10
thread: 0
thread: 1
thread: 2

Gobyスクリプトのサンプルとして、上のサンプルWebアプリのコードからmodel.gbも以下に転記してみました。こうしてみるとRubyとほぼ同じ感覚ですね。実際、Rubyのsyntax highlightingがそのまま使えます。

# Goby
require_relative "plugin"

PluginPG.run("create table if not exists list_items (
  id      serial primary key,
  title   varchar(40),
  checked boolean
)")

class ListItem
  attr_reader :id, :title, :checked, :error

  def initialize(params)
    @id      ||= params[:id]
    @title   ||= params[:title]
    @checked ||= params[:checked]
    @error   ||= params[:error]
  end

  def check
    self.class.plugin_db.exec('UPDATE list_items SET checked = true WHERE id = $1', @id)
    @checked = true
  end

  def uncheck
    self.class.plugin_db.exec('UPDATE list_items SET checked = false WHERE id = $1', @id)
    @checked = false
  end

  def update_title(title)
    self.class.plugin_db.exec('UPDATE list_items SET title = $1 WHERE id = $2', title, @id)
  end

  def destroy
    self.class.plugin_db.exec('DELETE FROM list_items WHERE id = $1', @id)
  end

  def valid?
    @error.nil?
  end


  def self.plugin_db
    PluginPG
  end

  def self.all
    plugin_db.query("SELECT * FROM list_items ORDER BY id DESC")
  end

  def self.find(id)
    result = plugin_db.query("SELECT * FROM list_items WHERE id = $1", id).first
    if result
      new({ id: result[:id], title: result[:title], checked: result[:checked] })
    end
  end

  def self.create(params = {})
    validates(params) do |result|
      if result[:error].nil?
        title   = params[:title]
        checked = params[:checked].to_i == 1
        resultID = self.plugin_db.exec("INSERT INTO list_items (title, checked) VALUES ($1, $2)", title, checked)
        new({ id: resultID, title: title, checked: checked })
      else
        new({ error: result[:error] })
      end
    end
  end

  def self.validates(params)
    if params.nil? || params[:title].nil?
      yield({ error: 'Title cannot be empty' })
    elsif params[:title].empty?
      yield({ error: 'Title cannot be empty' })
    else
      if params[:title].length > 40
        yield({ error: 'Title too long (should less than 40 characters)' })
      else
        yield({})
      end
    end
  end
end

GobyのSlackチャンネルとContribution Guideline

以下からGobyのSlackチャンネルにアクセスできます(英語のみ)。知りたいことなどや議論はこちらでどうぞ。

Gobyのバグを見つけたらissueまでお願いします。

Gobyにプルリクしたい方は以下のガイドラインをご覧ください。GitHubでGobyをforkしてローカルにcloneし、ブランチを切って修正したらgit pushしてプルリクするという、通常のプルリク手順です。

誰かGoのtestingパッケージでGobyにベンチマークを追加してくれないかな…

参考: Gobyのテストについて

Gobyディレクトリでmake testを実行するとテストが走ります。このときPostgreSQLアダプタのテストも行われるので、ローカル環境でPostgreSQLをセットアップしておかないとテストを完了できません。なお、Docker imageにもPostgreSQLが入っていないことに今気づいたので、Docker内でapt-get update; apt-get install postgresql-9.6でインストールしてください。

PostgreSQLのインストール方法については省略します。

テストを実行するには、postgresユーザーの権限でデータベースを作成できるようになっている必要があります。事情があって権限を与えられないといった場合は、事前にpsqlコマンドでgoby_testデータベースを作成しておきます。

$ psql
create database goby_test;
Viewing all 1836 articles
Browse latest View live