Help us understand the problem. What is going on with this article?

Railsでメタプログラミング(黒魔術)と呼ばれるsendメソッドを活用してみた

More than 1 year has passed since last update.

はじめに

この記事を書こうと思ったのはsendメソッドを活用するイメージが湧かない方に、僕はRailsでこんな感じで使う場面がありましたよー的な意味で紹介するために書きました。
sendメソッドを使うのが最善かどうかは置いといて...笑

メタプログラミングとは?

まずメタプログラミングの意味を軽く紹介します。
Railsチュートリアルではこのように説明されてます。

メタプログラミングを一言で言うと「プログラムでプログラムを作成する」ことです。メタプログラミングはRubyが有するきわめて強力な機能であり、Railsの一見魔法のような機能 (「黒魔術」とも呼ばれます) の多くは、Rubyのメタプログラミングによって実現されています。

プログラムでプログラムを作成する?ちょっとこの説明だけ読んでもよくわからないですね笑
Railsチュートリアルでは実際にsendメソッドというものを利用してメタプログラムを作成していますのでそちらを見るとよりイメージが湧くかも。
※この後実際に例を用いて説明します

sendメソッドとは?

Rubyにはsendというメソッドがあります。
どのように使うか簡単な例で説明します。

例えば、upcaseという文字列を大文字に変換して出力するメソッドを例に使います。

string = "ruby"  #=> "ruby"
string.upcase  #=> "RUBY"
string.send(:upcase)  #=> "RUBY"
string.send("upcase")  #=> "RUBY"

このようにsendメソッドの引数に呼び出したいメソッド名のシンボルか文字列を渡すと通常通りメソッドの呼び出しが起こります。

では次に引数を持つメソッドを呼び出したい時にどのようにするか、splitメソッド例を出しましょう。
Railsチュートリアルではここまでは説明されていませんが意外と簡単です。

splitメソッドは文字列などを引数に渡した区切りで分割して配列にしてくれます。

string = "aaaaaxbbbbbxccccc"  #=> "aaaaaxbbbbbxccccc"
string.split("x")  #=> ["aaaaa", "bbbbb", "ccccc"]
string.send(:split, "x")  #=> ["aaaaa", "bbbbb", "ccccc"]
string.send("split", "x")  #=> ["aaaaa", "bbbbb", "ccccc"]

引数をメソッドに渡したい場合sendメソッドの第二引数以降に渡します。

Railsでsendメソッド活用

では本題に入ります。
RailsでECサイトを作っていたのですが、3つのモデル(Artist, Label, Genre)のデータを一覧表示するviewがほぼ同じだったのでこれをパーシャル化してしまおうという場面で使いました。
どのようなレイアウトか一応簡単なモックアップを載せておきます。(かなり雑なのは気にしないでください笑)

モックアップ

pacto_orbis(admin)-アーティスト一覧.png
pacto_orbis(admin)-ジャンル一覧.png
pacto_orbis(admin)-レーベル一覧.png

うん、これはもう表示するデータしかほぼ変わらないですね。これをパーシャル化せずに何をする?ということでパーシャル化しました。

コントローラ

app/controllers/artists_controller.rb
# 中略
  def index
    @artists = Artist.page(params[:page])
    @artist = Artist.new
  end
# 中略
app/controllers/genres_controller.rb
# 中略
  def index
    @genres = Genre.page(params[:page])
    @genre = Genre.new
  end
# 中略
app/controllers/labels_controller.rb
# 中略
  def index
    @labels = Label.page(params[:page])
    @label = Label.new
  end
# 中略

ビュー

app/views/artists/index.html.erb
<h2>アーティスト一覧</h2>
<%= render 'layouts/object_list', objects: @artists, object: @artist %>
app/views/genres/index.html.erb
<h2>ジャンル一覧</h2>
<%= render 'layouts/object_list', objects: @genres, object: @genre %>
app/views/labels/index.html.erb
<h2>レーベル一覧</h2>
<%= render 'layouts/object_list', objects: @labels, object: @label %>
app/views/layouts/_object_list.html.erb
<div class="row">
  <div class="col-sm-5">
    <%= render 'layouts/error', object: object %>
    <%= form_with model: object, local: true do |f|  %>
      <%= f.label :name %>
      <%= f.text_field :name %>
      <%= f.submit "登録" %>
    <% end %>
  </div>
  <div class="col-sm-7">
    <div class="index-wrapper">
      <% object_name = object.class.to_s.downcase  # それぞれのオブジェクトのクラス名を変数に代入 %>
      <% objects.each do |data| %>
        <% edit_path = self.send("edit_#{object_name}_path", data)  ### ここで使用!!### %>
        <% destroy_path = self.send("#{object_name}_path", data)  ### ここで使用!!### %>
        <%= data.name %>
        <%= link_to "編集", edit_path %>
        <%= link_to "削除", destroy_path, method: :delete, data: { "confirm" => "本当に削除しますか?" } %>
      <% end %>
    </div>
    <%= paginate objects, class: "pagination" %>
  </div>
</div>

それぞれのindex.html.erbで書かれているパーシャルテンプレートの呼び出しは大丈夫だと思います。(わからない方はググってください)
_object_list.html.erbのコードで今回テーマのsendメソッドに関係するコードだけ説明していきます。

解説

まずそれぞれ渡されたオブジェクトのクラス名(全部小文字)を変数に代入します。
何故かは後々わかります。

<% object_name = object.class.to_s.downcase  # それぞれのオブジェクトのクラス名を変数に代入 %>

一つずつ説明すると、
.class → 呼び出したオブジェクトのクラスを取得
.to_s → 文字列に変換
.downcase → 文字列を小文字に変換

なのでまとめると

# 渡ってきた元のインスタンス変数 → object_nameに入る文字列
@artist  "artist"
@genre  "genre"
@label  "label"

となります。

次に本題のsendメソッドのコード

<% edit_path = self.send("edit_#{object_name}_path", data)  ### ここで使用!!### %>
<% destroy_path = self.send("#{object_name}_path", data)  ### ここで使用!!### %>

ここでしたいことは、edit_path, destroy_pathそれぞれにeachメソッドで取り出されるオブジェクト+パーシャルの呼び出しで渡ってきたオブジェクトの種類(artistかgenreかlabel)によって動的にパスを生成し、代入するということです。
つまり、呼び出すメソッドを動的に決めるということ。これがRubyのメタプログラムです。

sendメソッドの実行結果は以下のようになります。

# artist
edit_path = edit_artist_path(data)
destroy_path = artist_path(data)
# genre
edit_path = edit_genre_path(data)
destroy_path = genre_path(data)
# label
edit_path = edit_label_path(data)
destroy_path = label_path(data)

ここで、ん?self.ってなに?てか、railsの~_pathってメソッドなの?メソッドって***.メソッドって感じで何かオブジェクトに対して呼び出すものじゃないの?
って思うかもしれません。特に他の言語(JavaScriptやPHPなど)をやったことがありrubyやrailsを始めたばかりの方なら~_pathは関数じゃないの?って思うかもしれませんが、メソッドです!!
というかRubyには関数がありません。

この話をすると長くなってしまうので少し省略しますが、Rubyは全てがオブジェクトであり~_pathもなんらかのオブジェクトのメソッドになるのです。そして自身のメソッド呼び出す際には
self.メソッドという感じで書きます。しかしこれはself.を省略できてメソッド名だけで呼び出すことが可能です。

つまり、~_pathは普段はself.が省略されていて、実際にはself.~_pathでも呼び出せます。

じゃあsendメソッドを呼び出すときもself.を省略できるのかと。
その通りです。上で紹介したコードはさらに短くこのようにできます。

<% edit_path = send("edit_#{object_name}_path", data)  ### ここで使用!!### %>
<% destroy_path = send("#{object_name}_path", data)  ### ここで使用!!### %>

今回説明しやすいようにはじめはself.をつけた状態にしておきましたが、これが短く書くとしたら完成系になります。

まとめ

実際sendメソッドなんて使わなくてももっと簡単にできます。
prefix(~_path)を使わずに直接相対パスを書き、その相対パスに変数を入れ込めばそこまで難しいことではないでしょう。
しかし、ただただこの黒魔術と呼ばれるメソッドを使いたかったのです。笑
皆さんも遊び感覚であえて難しい実装方法を選んでみても面白いかもしれませんよ〜。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away