何をしたか
クエリパラメータを付与したリンクを共通化することで
クエリパラメータのキーが変更された場合に、
49箇所 で変更が必要だったものを 1箇所 で管理できるようにしました。
どんなメリットがあるか
- リンクのクエリパラメータ変更時に 改修が一箇所 で済みます
- 重複箇所を減らせるため、コードの不必要なところまで読まなくて済み、 工数削減 につながります
具体的な内容
実務で下記のようなコードに出会いました。
データベースにあるデータが、ベタ書きされています。
= link_to 'A', search_path('search[tags]': ['1']), class: 'style-classes'
= link_to 'B', search_path('search[tags]': ['2']), class: 'style-classes'
= link_to 'C', search_path('search[tags]': ['3']), class: 'style-classes'
= link_to 'D', search_path('search[tags]': ['4']), class: 'style-classes'
= link_to 'E', search_path('search[tags]': ['5']), class: 'style-classes'
/ .
/ .
/ .
/ 同じコードが大量に続く
データベースのデータの構造的には下記のように、
Railsのモデルで、 name が link_toのテキスト であり、
クエリパラメータ として渡したい値が id となるイメージです。
= link_to language.name, search_path('search[tags]': [language.id]), class: 'style-classes'
class Skill
attr_accessor :id, :name
def initialize(id:, name:)
@id = id
@name = name
end
end
language = Skill.new(id: 1, name: 'Ruby')
language.id #=> 1
language.name #=> Ruby
また、下記のようなブロックメソッド呼び出しで link_to が利用され、
クエリパラメータを渡している箇所もありました。
= link_to(search_path(:'search[tags]' => "#{language.id}"), class: 'style-classes') do
button = language.name
これらを踏まえてやりたかったこと
- クエリパラメーター付きのリンク生成の箇所で改修箇所を少なくしたい
- 3パターン全てに対応したい
- nameとidを持ったオブジェクト(モデル)から参照する箇所
- 文字列と数値で渡している箇所
- リンクでボタンをラップしている箇所
どのようにしたか
Rubyにおける「ブロック」という概念自体理解していなかったため、
参考文献で学習していく中で、そういった方法があることを知り、活用してみました。
今回は、Helperで定義しました。
def serach_link(name, id, class_str, &block)
# search_pathはRailsのルーティングで定義したURLジェネレーターのものを利用
path = search_path('search[tags][]': id)
if block_given?
link_to(path, class: class_str, &block)
else
link_to(name, path, class: class_str)
end
end
第一引数には、リンクに表示したい 文字列
第二引数には、クエリパラメータで飛ばしたい id
第三引数には、 Bootstrapのクラス
第四引数には、 ブロック を受け取るようにしました。
ローカル変数path
には、idを値にしたクエリパラメータ付きのPathをRailsのURLジェネレーターを利用して格納しています。
そして、link_toを使ってリンクを生成する際の引数をブロックの有無で条件分岐しています。
こうすることで、意図した挙動となり、クエリパラメータのキーが変わったとしても変更しやすくなりました。
また、実際にはBootstrapの長いクラス名や、クエリパラメータのキーがもっと長いので、可読性が上がりました。
Railsのlink_toのソースコード
下記のような実装となっているため、
メソッド内の一行目で、ブロックが渡された場合多重代入によって
optionsがhtml_optionsへ、
nameがoptionsへ、
blockがnameへ、
それぞれ代入されるようになっているようです。
def link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options, name = options, name, block if block_given?
options ||= {}
html_options = convert_options_to_data_attributes(options, html_options)
url = url_target(name, options)
html_options["href"] ||= url
content_tag("a", name || url, html_options, &block)
end
ここまでが実際に適用したコードとなるのですが、今見返してみるとさらにより方法があるのではないかと思い、考えてみることにしました。
さらにリファクタリング
ブロック付きメソッド呼び出しの場合obj.nameは利用しないので、
引数を3つにできると思いました。
def serach_link(obj, class_str, &block)
path = search_path('search[tags][]': obj.id)
if block_given?
link_to(path, class: class_str, &block)
else
link_to(obj.name, path, class: class_str)
end
end
ただ、この方法だとデータベースのデータをベタ書きしている箇所の呼び出し方法はメソッド呼び出しの方法にする必要があるため、
データベースから呼び出す形にして、オブジェクトのメソッドで呼び出す形にする方がいいのかなと思いました。
データベースのデータベタ書きの箇所をHashで管理しようとすると、
別のHelperにする必要が出てきますが、おおよそ下記のようになるのかなと思いました。
あくまでも一例ですので、もっと良い方法が必ずあると思います。
- resources = [
- {id: 1, name: 'A'},
- {id: 2, name: 'B'},
- {id: 3, name: 'C'},
- {id: 4, name: 'D'},
- {id: 5, name: 'E'},
- .
- .
- .
- ]
- resources.each do |resource|
= serach_link(resource, 'style-classes')
def serach_link(obj, class_str)
path = search_path('search[tags][]': obj[:id])
link_to(obj[:name], path, class: class_str)
end
今後
実装時は「完璧じゃん、最高!」とか思っていても、時間が経って見返してみると
「え、なんでこんなことしてんの自分」ということが、よくあります。
成長している証ではあるのですが、振り返っても「よくできてるなぁ」と思えるように今後も学習を続けていきます。
参考文献
リファクタリングの目的やメリットについて理解が深まりました。
Rubyの基礎学習に利用しました。
ブロックを使ったメソッド定義を学ぶことで、
「同じパラメーターのキーで値を渡す必要があるが、構造が違う」
といったケースにも対応できました。
また、キーワード引数や多重代入などの知識もRailsのコードリーディングをする際に役立ちました。