你好,我是富士大顆 Aiko
今天來談談這三個概念有點像的 Rails 元素:
helper, Service Object, Concern
特性/用途 | Helper | Service Object | Concern |
---|---|---|---|
主要目的 | 輔助 View | 封裝複雜的業務邏輯 | 重複使用的 model 或 controller 的邏輯 |
使用場景 | 主要在 View 和部分 Controller | 跨 Model 或 Controller 的複雜操作 | 多個 models 或 Controllers 都會重複用到 |
結構 | 模組(Module) | 類別(Class) | 模組(Module) |
可測試度 | 中等,如果依賴於某實體變數,那在測試中就要設定這個實例變數 | 容易,因為很獨立 | 中等至困難,通常與 model 或 Controller 關係緊密,例如有不同的生命週期階段,就會增加複雜度 |
重複使用性 | 一般,主要用於 View | 高 | 高 |
命名規則 | 通常是動詞 | 動作或任務描述 | 通常以 -able 結尾 |
在 Ruby on Rails 中,helper
主要是用來輔助 View 的。它們是可重複使用的方法,而這些方法可以在 view 中被呼叫使用,以進行各種操作和格式化:
def format_date(date)
date.strftime("%Y-%m-%d")
end
view 會長這樣:
<%= format_date(Date.today) %>
#<!-- 2023-10-04 -->
def user_profile_link(user)
link_to user.name, user_path(user)
end
view 會長這樣:
<%= user_profile_link(@user) %>
#<!-- <a href="/users/1">Aiko</a> -->
text_field<input type="text">
。
<%= f.text_field :name %>
password_field<input type="password">
。
<%= f.password_field :password %>
text_area<textarea>
輸入框。
<%= f.text_area :description %>
check_box<input type="checkbox">
打勾。
<%= f.check_box :is_active %>
radio_button<input type="radio">
單選。
<%= f.radio_button :gender, "male" %>
<%= f.radio_button :gender, "female" %>
select<select>
下拉選單。
<%= f.select :category, ["Tech", "Life", "Travel"] %>
file_field<input type="file">
,文件上傳。
<%= f.file_field :avatar %>
submit<input type="submit">
,送出按鈕。
<%= f.submit "送出" %>
DRY程式碼:
將重複使用的程式碼片段封裝成 helper 方法。
通知訊息:
def display_flash_messages
flash.map do |type, message|
content_tag(:div, message, class: "alert alert-#{type}")
end.join.html_safe
end
view 會長這樣:
<%= display_flash_messages %>
#<!-- <div class="alert alert-success">成功!</div> -->
def calculate_cart_total(cart_items)
cart_items.sum { |item| item.product.price * item.quantity }
end
view 會長這樣:
<%= number_to_currency(calculate_cart_total(@cart_items), unit: "NT$") %>
#<!-- NT$ 1,200.00 -->
app/helpers
目錄下。ApplicationHelper
模組中的 helper 方法可以在所有 view 中使用。假設你有一個線上商店,需要在 user 下單時計算訂單總金額,這涉及到商品價格、稅金、運費等多個因素。
class CalculateOrderTotalService
def initialize(order)
@order = order
end
def call
subtotal = calculate_subtotal
tax = calculate_tax(subtotal)
shipping = calculate_shipping
total = subtotal + tax + shipping
@order.update(total: total)
end
private
def calculate_subtotal
# 計算小計
end
def calculate_tax(subtotal)
# 計算稅金
end
def calculate_shipping
# 計算運費
end
end
在 Controller 中:
def create
# 建立訂單邏輯
CalculateOrderTotalService.new(@order).call
end
假設在你的應用中,不只 Order
model 需要計算稅金,Invoice
和 Quote
model 也需要。
module TaxCalculatable
extend ActiveSupport::Concern
def calculate_tax
self.total * 0.05 # 假設稅率是5%
end
end
然後在需要的 model 中引用它:
class Order < ApplicationRecord
include TaxCalculatable
end
class Invoice < ApplicationRecord
include TaxCalculatable
end
class Quote < ApplicationRecord
include TaxCalculatable
end
這樣,Order
、Invoice
和 Quote
model 都有了 calculate_tax
方法,達到 DRY 程式碼的目的。
假設你有一個電商網站,需要在多個頁面上顯示價格,並且都需要加上稅金和貨幣符號。
# 原始方式
<%= "#{product.price * 1.05} $" %>
# 使用 helper
<%= display_price_with_tax(product.price) %>
在 helper 中:
def display_price_with_tax(price)
"#{price * 1.05} $"
end
假設你有一個部落格平台,文章的顯示日期格式可能會隨著時間而變更。
# 原始方式
<%= post.created_at.strftime("%Y-%m-%d") %>
# 使用 helper
<%= format_post_date(post.created_at) %>
在 helper 中:
def format_post_date(date)
date.strftime("%Y-%m-%d")
end
假設你有一個需要計算折扣的功能。
# 原始方式
<%= product.price - (product.price * product.discount / 100) %>
# 使用 helper
<%= calculate_discounted_price(product.price, product.discount) %>
在 helper 中:
def calculate_discounted_price(price, discount)
price - (price * discount / 100)
end
這樣你就可以針對 calculate_discounted_price
方法寫單元測試。
假設你需要在視圖中顯示一個使用者的全名。
# 原始方式
<%= "#{user.first_name} #{user.last_name}" %>
# 使用 helper
<%= display_full_name(user) %>
在 helper 中:
def display_full_name(user)
"#{user.first_name} #{user.last_name}"
end
假設你有一個 Background Jobs 需要生成報告,報告中需要顯示格式化的日期。
# app/helpers/my_helper.rb
module MyHelper
def self.format_date(date)
date.strftime("%Y-%m-%d")
end
end
# app/jobs/report_job.rb
class ReportJob < ApplicationJob
def perform
formatted_date = MyHelper.format_date(Time.now)
# 生成報告的邏輯
end
end
在一封訂單確認信中,你可能需要使用 helper 方法來格式化價格。
# app/helpers/my_helper.rb
module MyHelper
def self.format_price(price)
"$#{'%.2f' % price}"
end
end
# app/mailers/order_mailer.rb
class OrderMailer < ApplicationMailer
def order_confirmation(order)
formatted_price = MyHelper.format_price(order.total)
# 發送郵件的邏輯
end
end
假設你有一個計算折扣價格的邏輯,這個邏輯在 Model 和 View 中都有用到。
# app/helpers/my_helper.rb
module MyHelper
def self.calculate_discount(price, discount)
price - (price * discount / 100)
end
end
# app/models/product.rb
class Product < ApplicationRecord
def discounted_price
MyHelper.calculate_discount(self.price, self.discount)
end
end
# app/views/products/show.html.erb
<%= MyHelper.calculate_discount(@product.price, @product.discount) %>
假設有一個 API 需要回傳格式化的日期。
# app/controllers/api/v1/dates_controller.rb
class Api::V1::DatesController < ApplicationController
def show
render json: { formatted_date: MyHelper.format_date(Time.now) }
end
end
在測試中,你可能需要使用 helper 方法來產生預期的輸出或模擬某些操作。
# test/models/my_helper_test.rb
class MyHelperTest < ActiveSupport::TestCase
test "should format date" do
expected_output = MyHelper.format_date(Time.now)
assert_equal expected_output, Time.now.strftime("%Y-%m-%d")
end
end
在 migration 中,你可能需要使用 helper 方法來轉換或格式化資料。
# db/migrate/20220101000000_format_old_data.rb
class FormatOldData < ActiveRecord::Migration[6.0]
def up
OldModel.all.each do |record|
formatted_data = MyHelper.format_data(record.old_data)
# 更新資料庫的邏輯
end
end
end
在自訂的 Rake 任務中,可能需要使用 helper 方法來執行某些操作。
# lib/tasks/my_task.rake
namespace :my_task do
task calculate: :environment do
result = MyHelper.some_calculation(10, 20)
puts "Result: #{result}"
end
end
大抵上,常用於執行各種自動化任務,包括 data migration、testing、資源清理(就是有些渣渣)等。
rake db:migrate
rake test
rake assets:clean
# lib/tasks/example.rake
namespace :example do
desc "This is an example task."
task :hello do
puts "Hello, World!"
end
end
$rake example:hello
會輸出 "Hello, World!"。
$rake -T
來查看所有可用的 Rake Tasks。下一篇!談談 Callback 回呼是什麼吧