RubyOnBasis[13] 新人Railsエンジニア向け講座

このシリーズについて

RubyOnBasisはRuby on Railsを業務に利用する新人Webエンジニアのための、Railsを使わないテキストだ。この講座ではWEBrickを使ってWebアプリを書きながらHTTPを始めとする基礎知識をキャッチアップしてRailsがどうやって作られ動くのかを理解していく。

このシリーズを始めから読む

責務の分離

このチャプターのコード: https://github.com/owlworks/ruby_on_basis/tree/13_change_routing_process_and_add_id_column_to_product

さてProductのGET(Read)、PUT(Update)、Deleteを実装する予定だが今日はその下準備だ。まずコードが少しややこしくなってきた。具体的にはmount_procメソッドのブロック内で処理を書きすぎている。そこでリクエストからレスポンスを作るまでの実処理をControllerオブジェクトへ任せよう。

class Controller
  def initialize
    @products = []
    @products << Product.new(name: 'ぺんぎんのぬいぐるみ', price: 2900, stock: 1)
    @products << Product.new(name: '食ぱんクッション', price: 5600, stock: 21)
  end

  def get_products(request)
    @products.map(&:inspect).join("\n")
  end

  def post_product(request)
    q = request.query
    new_product = Product.new(
      name: q['name'],
      price: q['price'].to_i,
      stock: q['stock'].to_i
    )
    @products << new_product
    new_product.inspect
  end
end

# リクエストのデータを処理してレスポンスを作成するControllerオブジェクト
controller = Controller.new

# Webickのサーバ設定と起動
server = WEBrick::HTTPServer.new(Port: 3000)

server.mount_proc('/products') do |req, res|
  case req.request_method
  when "GET"
    res.body = controller.get_products(req)
  end
end

server.mount_proc('/product') do |req, res|
  req.query.each { |key, value| req.query[key] = value.force_encoding('utf-8') }
  case req.request_method
  when "POST"
    res.body = controller.post_product(req)
  end
end

trap(:INT) { server.shutdown }
server.start

レールの上に乗る(on rails)

うーん。しかし、どこか冗長なコードに思えるね。やり過ぎかも知れないがもう少しだけ手を入れてみよう。

# Webickのサーバ設定と起動
server = WEBrick::HTTPServer.new(Port: 3000)

RESOURCES = ['products', 'product']

RESOURCES.each do |resource|
  server.mount_proc("/#{resource}") do |req, res|
    req.query.each { |key, value| req.query[key] = value.force_encoding('utf-8') }
    method_name = "#{req.request_method.downcase}_#{resource}"
    # sendメソッドを使うとメソッド名を動的に生成して呼び出せる
    # 'abc:efg'.send(:split, ':')と'abc:efg'.split(':')は同じ挙動になる
    res.body = controller.send(method_name.to_sym, req)
  end
end

trap(:INT) { server.shutdown }
server.start

わかるかな? Controllerで定義するメソッド名に規則性を持たせて自由に命名できなくなった代わりに使いたいリソースをRESOURCESで定義すればリクエストに対応したメソッドを呼んでくれるというワケ。例えば/productsにGETメソッドでリクエストを送ればControllerのget_productsメソッドが呼び出されるといった具合。

このように命名規則などの規約を決めることで処理を省略したり複雑で細かい関連処理を意識しなくても良くなる。たとえばreq.queryのエンコード問題とかね。Railsはまさにこういった規約(レール)の上に乗ることで手軽さを手に入れる。モデルとコントローラーの命名、viewファイルの名前まで規約によって決められているだろう?

また、このmount_procを自動生成するやり方はRailsのルーティングでresources :productsとするのに似ているのも気が付いたかな。実際、この部分の処理はRailsで言うところのルーティングだ。URLとメソッドの組合せでControllerのどのactionを実行すべきかを定義しているんだからね。

識別子くんのこと

それで、POST以外のメソッドを実装するんだけどその前にまだ少しだけ寄り道をする。今までの話で更新するだの削除するだのと言って来たが待ってほしい。どの商品を削除するかどうやって指定するんだ?そう。指定する方法がない。そこでどの商品か一意に特定するための属性を設定しよう。identifier(識別子)だから@idだ。そう、これもRailsActiveRecordでプラマリーキーとして使ったことがあるよね。これはDBじゃないけど理屈と必要性は同じだ。

# リソースのクラス定義
class Product
  @@id_num = 0
  attr_accessor :name, :price, :stock

  def initialize(attrs)
    @@id_num += 1
    @id = @@id_num
    attrs.each { |attr, value| instance_variable_set("@#{attr}", value) }
  end
end

Productクラスにクラス変数@@id_numを作り、新規にインスタンスが作られる度にインクリメントしてその値をインスタンスの@idとして持たせる。クラス変数はその名の通りクラス自体が保持出来る変数だ。話がややこしくなるからクラスとインスタンスについて説明したチャプターでは説明しなかったけどね。

これで一度http://localhost:3000/productsへアクセスしてみよう。

#<Product:0x007fc3650bc068 @id=1, @name="ぺんぎんのぬいぐるみ", @price=2900, @stock=1>
#<Product:0x007fc3650be638 @id=2, @name="食ぱんクッション", @price=5600, @stock=21>

Tallyho! 無事にインクリメントされた値が@idへ格納されているようだ。これで次からは@idを指定すれば更新や削除が出来るというワケ。実際のシステムでは、こうしたidよりも「絶対に被らないユニークな文字列」をcodeといった名前の属性として定義して、API上のプライマリーキーとして使うことも多いけれどね。

何故かって? 1つはその方が人間に優しいからだ。id: 1998931よりもcode: honsha-charactor-sanrio-kitty-021のほうが間違いや内容がわかりやすいだろう? もう1つはこの@idというのはあくまでもこのアプリケーションだけでの識別子だからだ。他のシステムでも商品データを扱っている場合は、両方で共通する値で指定できないと困る。それぞれが勝手なidを作ってしまうからね。これはURLの指定でIPアドレスではなくドメイン名が使われるのと同じだ。

<< 前のチャプターへ戻る | 次のチャプターへ進む >>