iT邦幫忙

2023 iThome 鐵人賽

DAY 19
1
自我挑戰組

富士大顆系列 第 19

vol. 19 Rails 裡的 helper, Service Object, Concern 到底怎麼用?

  • 分享至 

  • xImage
  •  

你好,我是富士大顆 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 結尾

helper

在 Ruby on Rails 中,helper 主要是用來輔助 View 的。它們是可重複使用的方法,而這些方法可以在 view 中被呼叫使用,以進行各種操作和格式化:

功能:

  1. 格式化資料
    例如,將日期和時間格式化為特定的格式。
def format_date(date)
  date.strftime("%Y-%m-%d")
end

view 會長這樣:

<%= format_date(Date.today) %>  
#<!-- 2023-10-04 -->
  1. HTML相關元素
    例如,產生表單、連結或其他 HTML 元素。
    產生連結:
def user_profile_link(user)
  link_to user.name, user_path(user)
end

view 會長這樣:

<%= user_profile_link(@user) %>  
#<!-- <a href="/users/1">Aiko</a> -->

常見的 Form Helpers

  1. text_field
    <input type="text">

    <%= f.text_field :name %>
    
  2. password_field
    <input type="password">

    <%= f.password_field :password %>
    
  3. text_area
    <textarea> 輸入框。

    <%= f.text_area :description %>
    
  4. check_box
    <input type="checkbox"> 打勾。

    <%= f.check_box :is_active %>
    
  5. radio_button
    <input type="radio"> 單選。

    <%= f.radio_button :gender, "male" %>
    <%= f.radio_button :gender, "female" %>
    
  6. select
    <select> 下拉選單。

    <%= f.select :category, ["Tech", "Life", "Travel"] %>
    
  7. file_field
    <input type="file">,文件上傳。

    <%= f.file_field :avatar %>
    
  8. submit
    <input type="submit">,送出按鈕。

    <%= f.submit "送出" %>
    
  9. 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> -->
  1. 邏輯分離
    將複雜的視圖邏輯移出 view,使其更容易維護。
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 -->

位置:

  • Helper 通常存放在 app/helpers 目錄下。
  • 每個 Controller 通常都有一個對應的 helper。

使用方式:

  1. 全域 Helper:定義在 ApplicationHelper 模組中的 helper 方法可以在所有 view 中使用。
  2. 專用 Helper:定義在特定 Controller 的 helper 模組中,只能在該 Controller 的 view 中使用。

注意事項:

  • 避免在 helper 中放入過多的業務邏輯。它們主要輔助 view 。
  • Helper 方法也可以在 Controller 中使用,但不建議。
  • 過度依賴 helper 可能會讓業務邏輯變得分散,難以管理。

Service Object

  • 當有一個會動到多個 models 、Controllers 或很多步驟的複雜操作。
  • 當需要一個可測試、可重複使用的獨立單元(unit)。
  • 不想讓 controller 越來越肥!

範例:

假設你有一個線上商店,需要在 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

Concern

  • 當多個 models 、Controllers 有共享的邏輯或行為。
  • 想把這些共享邏輯抽取出來,以避免重複的程式碼。

範例:

假設在你的應用中,不只 Order model 需要計算稅金,InvoiceQuote 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

這樣,OrderInvoiceQuote model 都有了 calculate_tax 方法,達到 DRY 程式碼的目的。


為什麼建議用小幫手來處理複雜邏輯?

1. Code Reusability 重複使用

例子:

假設你有一個電商網站,需要在多個頁面上顯示價格,並且都需要加上稅金和貨幣符號。

# 原始方式
<%= "#{product.price * 1.05} $" %>

# 使用 helper
<%= display_price_with_tax(product.price) %>

在 helper 中:

def display_price_with_tax(price)
  "#{price * 1.05} $"
end

2. Maintainability 好維護

例子:

假設你有一個部落格平台,文章的顯示日期格式可能會隨著時間而變更。

# 原始方式
<%= 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

3. Testability 好測試

例子:

假設你有一個需要計算折扣的功能。

# 原始方式
<%= 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 方法寫單元測試。

4. Readability 好閱讀

例子:

假設你需要在視圖中顯示一個使用者的全名。

# 原始方式
<%= "#{user.first_name} #{user.last_name}" %>

# 使用 helper 
<%= display_full_name(user) %>

在 helper 中:

def display_full_name(user)
  "#{user.first_name} #{user.last_name}"
end

在什麼情況下,會需要在 Controller 之外使用 helper?

1. Background Jobs 幕後的賈伯斯(誤)

例子:

假設你有一個 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

2. Mailers

例子:

在一封訂單確認信中,你可能需要使用 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

3. Shared Logic

例子:

假設你有一個計算折扣價格的邏輯,這個邏輯在 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) %>

4. API Responses

例子:

假設有一個 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

5. Testing

例子:

在測試中,你可能需要使用 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

6. Data Migration

例子:

在 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

7. Rake Tasks

例子:

在自訂的 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

Rake Tasks 是什麼?

大抵上,常用於執行各種自動化任務,包括 data migration、testing、資源清理(就是有些渣渣)等。

  • data migration:rake db:migrate
  • 測試:rake test
  • 資源清理:rake assets:clean
基本用法:
  1. 在 lib/tasks 目錄下,新增 .rake 檔案來設定任務。
# lib/tasks/example.rake
namespace :example do
  desc "This is an example task."
  task :hello do
    puts "Hello, World!"
  end
end
  1. 執行 Rake Task:在終端機中,使用 rake 命令來執行任務。
$rake example:hello

會輸出 "Hello, World!"。

  1. 可以使用 $rake -T 來查看所有可用的 Rake Tasks。

下一篇!談談 Callback 回呼是什麼吧


上一篇
vol. 18 Rails 的 render 和 redirect_to 有什麼差別?
下一篇
vol. 20 Rails 裡的 Life Cycle & Callback
系列文
富士大顆30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言