APIへのアクセスをモデルで包みたい

コントローラーで API のアクセス・エラーハンドリングなどやっていると、どうしてもコントローラが厚くなります。
今回は、API へのアクセスをモデルに包んで、なるべくコントローラを薄くするように努めてみます。

目次

作るもの

郵便番号から、住所の検索を行う画面を作ってみます。
zipcloud - 郵便番号検索 APIを使用します。

実装

コントローラにベタ書きしてみる

app/controllers/testa_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class TestaController < ApplicationController

def search

@zipcode = params[:zipcode]

valid = true

## 数字列では無い場合、エラーで返す
unless @zipcode.match( /^[0-9]+$/ )
flash[:alert] = "入力に数字以外が使用されています。"
valid = false
end

## 7桁では無い場合、エラーで返す
unless @zipcode.length == 7
flash[:alert] = "入力桁数が7桁ではありません。"
valid = false
end

if valid
begin
res = RestClient.get("https://zipcloud.ibsnet.co.jp/api/search", {params:{zipcode: @zipcode}})
@addresses = JSON.parse(res)
rescue => e
logger.error "ERROR:#{e.inspect}"
end
end

render "index.html"
end
end

コントローラにべた書きしているので、バリデーションもだらだら書き下す様子になりました。

この時の view の実装は次の通りです。

app/views/testa/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<div>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
</div>

<%= form_with(url: "/testa/search" , local: true) do |form| %>

<div class="field">
<%= form.label :zipcode %>
<%= form.text_field :zipcode,value: @zipcode %>
</div>

<div class="actions">
<%= form.submit "Serche" %>
</div>
<% end %>

<% if @addresses.present? %>
<% if @addresses["status"] == 200 && @addresses["results"].present? %>
<table>
<tbody>
<tr>
<th>住所1</th>
<th>住所2</th>
<th>住所3</th>
</tr>
<% @addresses["results"].each do | address|%>
<tr>
<td>
<%= address["address1"] %>
</td>
<td>
<%= address["address2"] %>
</td>
<td>
<%= address["address3"] %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% elsif @addresses["status"] == 200 && @addresses["message"].present? %>
<%= @addresses["message"] %>
<% else%>
該当の住所はありません
<% end %>
<% end%>

エラーメッセージは、Flash メッセージを使用して表示するようにしています。

モデルに包んでみる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TestbController < ApplicationController
def index
@zipcode = Zipcode.new()
end

def search
@zipcode = Zipcode.new(zipcode_params)

@addresses = @zipcode.call

render "index.html"
end

private
def zipcode_params
params.require(:zipcode).permit(:code)
end
end

モデルに包んだ結果、Form オブジェクトのように使用できたので、ストロングパラメータも使用しています。
というかやってみたら、Form オブジェクトの実装とほとんど同じだったわけですが。

search メソッドの実装がかなりシンプルになりました。

この時の Zipcode クラスの実装は以下の通りです。

app/models/zipcode.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Zipcode
include ActiveModel::Model

attr_accessor :code

# 空文字禁止
validates :code, presence: true

# 7桁に制限
validates :code, length: { is: 7 }

# 数字のみに制限
validates :code, numericality: { only_integer: true }

def call

# 値が適切でなければ、falseを返す
return nil if invalid?

# APIアクセス処理
begin
res = RestClient.get("https://zipcloud.ibsnet.co.jp/api/search",{params:{zipcode: code}})
addresses = JSON.parse(res)
return addresses
rescue => e
logger.error "ERROR:#{e.inspect}"
return nil
end
end
end

バリデーションは、Zipcode クラスに実装があります。
validatesを使用してinvalid?が使えるのは便利です。
callメソッドの中でinvalid?を使用し問題なければ、API にアクセスします。

この時の、view の実装は以下の通りです。

app/views/testb/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<ul>
<% @zipcode.errors.full_messages.each do |msg| %>
<li>
<%= msg %>
</li>
<% end %>
</ul>

<%= form_with model: @zipcode, url: "/testb/search" , local: true do |form| %>

<div class="field">
<%= form.label :code %>
<%= form.text_field :code %>
</div>

<div class="actions">
<%= form.submit "Serche" %>
</div>
<% end %>

<% if @addresses.present? %>
<% if @addresses["status"] == 200 && @addresses["results"].present? %>
<table>
<tbody>
<tr>
<th>住所1</th>
<th>住所2</th>
<th>住所3</th>
</tr>
<% @addresses["results"].each do | address|%>
<tr>
<td>
<%= address["address1"] %>
</td>
<td>
<%= address["address2"] %>
</td>
<td>
<%= address["address3"] %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% elsif @addresses["status"] == 200 && @addresses["message"].present? %>
<%= @addresses["message"] %>
<% else%>
該当の住所はありません
<% end %>
<% end%>

エラーメッセージを Flash メッセージではなく、@zipcode.errors.full_messagesを使って実装できるのが便利です。


今回は、WebAPI へのアクセスをモデルに包んで実装してみました。

  • コントローラーをシンプルに実装できる
  • バリデーションをきれいに実装できる

この 2 つがとてもよかったです。
積極的にこのパターンを使ってゆきたいと感じました。

ではでは。