iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
自我挑戰組

富士大顆系列 第 13

vol. 13 Rails 的精髓 MVC 架構:不只是概念還有用法,專案開發不再害到人

  • 分享至 

  • xImage
  •  

你好,我是富士大顆 Aiko
今天來談面試題大魔王 MVC


Ruby on Rails 是一個高度生產力的 Web 框架,受到全端和後端開發人員的廣泛喜愛。Rails 專案是採用 Model、View、Controller(簡稱 MVC)的方式設計的,MVC 即模型(Model)、視圖(View)、控制器(Controller)。
MVC 模式是由 Trygve Reenskaug 在 1978 年所提出,是為程式語言 Smalltalk (聽說 Ruby 有借鏡其中的 message 特性)發明的一種軟體架構。

MVC 的特性讓專案在開發的過程中,各個模組權責分離,也較好維護及調整:

龍哥:
另一個好處,就是因為整個 Rails 專案都是遵循 MVC 的慣例結構,所以即使是不同程度的開發者寫出來的 Rails >專案,Controller 通常會放在 app/controllers 目錄裡,Model 應該也會放在 app/models 裡,不會有太大的差別。

Model 掌管資料,使用 ORM (Object Relationship Mapping)轉譯 SQL 跟資料庫溝通拿資料。 Controller 就是好多個樓層的好多櫃檯,來處理接收到的訊息往下去請 View 或是 Model 工作,算是滿重要的中控流程中心。View 則負責資料顯示,往往是 user 看到的畫面,可以是網頁、手機應用程式的介面,或者其他 user 互動的形式。View 不包含任何業務邏輯,它只是將 Model 中的資料呈現給 user,並接收來自 user 的輸入。


細談 M,Model

負責 (Responsibility)

  • 資料存取 (Data Access): 負責與資料庫互動,儲存或讀取。可以想像是資料庫是一個監獄,看守的警員就是 Model,要放人進來或出去,要經過看守的人。
  • 數據驗證 (Data Validation): 在儲存數據前確保其有效性。人要放進去監獄之前也要經過檢查,確定身份。
validates :name, presence: true
#存進去前,name 欄不能是空的
  • 業務邏輯 (Business Logic): 處理與應用特定業務邏輯相關的計算或判斷。(這邊先略過看守監獄的想像,因為我其實也沒待過,可能是一些嚴刑拷打?!)
    現在,假設你正在開發一個電商網站,以下可能是一些典型的業務邏輯:

-- 庫存管理(Inventory Management): 確保購買的商品數量不超過庫存數量。

def purchase_item
  if self.stock >= 1
    self.stock -= 1
    save
  else
    errors.add(:stock, "Out of stock")
  end
end

-- 價格計算(Price Calculation): 計算商品的最終價格,考慮促銷折扣、稅等。

def final_price
  self.base_price - self.discount + self.tax
end

-- 會員資格(Membership Eligibility): 根據 user 購買歷史或積分,決定他們是否有資格升級為 VIP 會員,等於 user 的某個欄位,例如 level 應該要被更新。 user.level # vip

def upgrade_to_vip
  if self.total_purchase >= 1000
    self.vip = true
    save
  end
end

-- 訂單處理(Order Processing): 在 user 下訂單後,進行一系列的檢查和操作,例如減少庫存、發送確認郵件等。

def process_order
  purchase_item
  send_confirmation_email if self.stock >= 1
end

以上都是業務邏輯的例子,它們通常會放在相對應的 model 中,以保持 controller 和 view 的簡單和清晰。這樣做還有助於改動和維護。可以輕易地在不同的 controller 或甚至不同的應用程式中重用清晰定義業務邏輯的 model。

在 Ruby on Rails 中,業務邏輯通常會以實例方法(instance methods)或是類方法(class methods)的形式呈現在模型(Model)中。這樣做可以保持業務邏輯和數據模型的緊密結合,並利於單元測試(unit testing)。

  • 資料關聯(Data Relationships): 模型通常會定義資料之間的關聯,多對多,一對多等。
    例如,一個訂單屬於一個使用者:
<!-- orders.rb -->
belongs_to :user
  • 資料邏輯(Data Logic): 模型中包含的業務邏輯通常與單一實體的行為有關。例如,計算訂單的總價。
def total_price
  order_items.sum(&:price)
end
#&: 是一種簡潔的語法糖衣,用於調用一個對象(obj)上的方法。它實際上是將一個 Symbol 對象轉換為一個 Proc 對象。這通常用於像 map、select、sum 等 Enumerable 方法中,當你想對每個元素調用同一個方法時。
#不使用會長這樣 order_items.sum { |item| item.price }
  • Callback: Model 通常會使用回呼(Callbacks)來在資料儲存之前或之後執行某些操作,用於在生命周期的不同階段執行特定操作。例如,想在一個 user 創建後自動發送一封歡迎郵件。
after_create :send_welcome_email

private

def send_welcome_email
  UserMailer.welcome_email(self).deliver_now
end
  • Scopes: 編寫可重用的查詢。可以將常用的 ActiveRecord 查詢封裝起來。
scope :active, -> { where(active: true) }

然後你就可以像這樣使用這個範圍:

User.active

ORM (Object-Relational Mapping)

  • ActiveRecord: Rails 內建的 ORM。也就是看守的人跟內部溝通的工具,可以想像是對講機,會操作才能對話。
    使用範例 (Usage Example):
# 使用 ActiveRecord 查找 User
user = User.find(1)

# 使用 ActiveRecord 更新 User
user.update(username: "new_username")
  • 繼承和多型(Inheritance and Polymorphism):進階的 ORM 特性還包括模型繼承和多型,這對於更複雜的資料模型來說是非常有用的。
# 父類
class Animal < ApplicationRecord; end

# 子類
class Dog < Animal; end
class Cat < Animal; end
  • 枚舉(Enum): 使用 Enum 來定義一個屬性有固定幾種可能的狀態。可以用在下拉式選單
class Article < ApplicationRecord
  enum status: { draft: 0, published: 1, archived: 2 }
end
# Article model 裡面有個 status 欄位

Rails 會自動為你生成一系列的 methods,方便操作這個 Article model

article = Article.new

# 查詢
article.draft? # => true 或 false
article.published? # => true 或 false

# 設定
article.published! # 將狀態設定為 "published"

# 作為查詢條件
Article.published # 回傳所有狀態為 "published" 的文章


細談 V,View

負責 (Responsibility)

  • 資料呈現 (Data Presentation): 將資料轉換為用戶可見的格式。
  • 無邏輯 (Logic-less): 儘量避免處理邏輯,專注於展示。

Template Engine

ERB (Embedded Ruby): Rails Template Engine。
使用範例 (Usage Example):

<!-- 在 view 展示 username -->
<h1><%= @user.username %></h1>

Partials

  • DRY (Don't Repeat Yourself): 部件可用於將重複的 HTML 片段抽象化。"抽象化"(Abstraction)在這裡指的是將重複出現的 HTML 片段或程式碼封裝起來,以減少重複和提高程式碼的可維護性。這是一個常用的軟體設計原則,通常用以簡化複雜的系統。

例如,假設你有一個網站,其多個頁面上都有相同的 Navbar。不使用部件的情況下,可能需要在每一個相關的視圖文件中重複這個Navbar的 HTML 程式碼。這樣做會導致以下問題:

-- 程式碼冗餘(Code Redundancy): 要在多個地方維護相同的代碼。
-- 維護困難(Difficulty in Maintenance): 如果 Navbar 需要修改,你必須找到並更新所有重複的部分。
使用部件(Partials)能解決這些問題。你可以將 Navbar 的 HTML 代碼放到到一個名為_navbar.html.erb 的部件檔案中,然後在需要的視圖中引用它。

<!-- _navbar.html.erb -->
<nav>
  <%= link_to 'Home', root_path %>
  <%= link_to 'About', about_path %>
  <!-- More links here -->
</nav>

引用方式:

<%= render 'navbar' %>

參數化的 partial

你也可以傳遞變數或參數到部件中,進一步提高它的重複使用性和抽象程度。

<!-- _product.html.erb -->
<div>
  <h2><%= product.name %></h2>
  <p><%= product.description %></p>
</div>

在要使用的 product 的 view 檔案裡

<%= render partial: 'product', locals: { product: @product } %>
//'product'會找到 _product.html.erb;需要的 product 會是 @product 而這個會寫在 controller 裡

(locals 在 Ruby on Rails 的視圍(View)和部件(Partials)中用來傳遞變數或資料。這個選項允許你將控制器(Controller)中的資料或主視圖(Main View)中的變數傳遞到部件(Partial)裡面去。)

進階使用

  • View Helpers(視圖小幫手): 它們用於封裝複雜的邏輯,使 View 檔案更為簡潔。
# helper method
def formatted_date(date)
  date.strftime("%Y-%m-%d")
end

其他的 view 就可以:

<%= formatted_date(@user.created_at) %>
  • I18n(國際化): Internationality(娃,我以為是這個字結果數起來沒有 18)。結果是 Internationalization, 這個縮寫的命名方式是將這個詞中間的 18 個字母省略,只保留首尾的 i 和 n。在軟體開發中,i18n 涉及到將應用程式設計成可以輕易地適應不同語言、區域和文化。在 View 中顯示多語言,這在多國服務的應用是非常實用的。

使用 YAML 或其他格式的檔案,可以定義各種語言下相同詞語或句子的翻譯,然後在 View 或其他地方使用 ttranslate 方法來顯示對應語言的文字。

# config/locales/en.yml
en:
  hello: "Hello"
# config/locales/zh.yml
zh:
  hello: "你好"
<%= t('hello') %>

在 View 中:

<%= t('hello') %>

根據用戶設定的語言,將會顯示 "Hello" 或 "你好"。

--

細談 C,Controller

負責 (Responsibility):

  • 請求處理 (Request Handling): 解析客戶端發送的請求,包含所有的 http 請求:GET, POST, PATCH, DELETE..等。
def show
  @user = User.find(params[:id])
end
  • 資料準備 (Data Preparation): 從 model 中取得或準備 data,以供視圖(View)使用。
def index
  @users = User.all
end
  • 響應 (Response): 使用視圖渲染資料或重定向至其他頁面。判斷 user 的操作,進行下一步動作。
def create
  @user = User.new(user_params)
  if @user.save
    redirect_to @user
  else
    render 'new'
  end
end
  • 視圖選擇(View Selection): 決定應該呈現哪個 view。
def show
  render :show
end

動作 (Actions)

  • CRUD (Create, Read, Update, Delete): 標準 RESTful 控制器動作。

    • 首先,CRUD 是 "Create, Read, Update, Delete" 的縮寫,代表了數據持久層最基本的四種操作。在 Rails 的模型(Model)中,你會看到對應這四種操作的方法,例如 create, find, update, destroy 等。
      RESTful 則是一種軟體架構風格,其核心思想是將所有事物抽象為資源,並通過 HTTP 方法(GET、POST、PUT、DELETE 等)對這些資源進行操作。

    • Rails 將 CRUD 操作映射到 RESTful 架構中,也就是說,你可以用 HTTP 的方法來執行 CRUD 操作:
      Create 對應 POST
      Read 對應 GET
      Update 對應 PUT 或 PATCH
      Delete 對應 DELETE

    • 在 Rails 的路由(Routing)設定中,使用 resources 方法可以自動生成一套 RESTful 風格的路由,這些路由會對應到控制器(Controller)中相應的 CRUD 操作方法。
      GET /articles(對應 index 方法,展示所有文章,對應 CRUD 的 Read)
      GET /articles/:id(對應 show 方法,展示單一文章,對應 CRUD 的 Read)
      POST /articles(對應 create 方法,新增文章,對應 CRUD 的 Create)
      PUT/PATCH /articles/:id(對應 update 方法,更新文章,對應 CRUD 的 Update)
      DELETE /articles/:id(對應 destroy 方法,刪除文章,對應 CRUD 的 Delete)

    • 通常,在 Rails 的控制器中,你會看到名為 index, show, new, create, edit, update, destroy 等的方法,這些就是對應到 CRUD 操作。

    • 因為 Rails 採用了 RESTful 架構,開發者可以更容易地預測和理解各種不同資源(模型)的 CRUD 操作方式,這大大提高了開發效率和維護性。總結來說,在 Rails 中,CRUD 和 RESTful 是相互緊密關聯的。Rails 採用 RESTful 架構,使得 CRUD 操作更加標準化和直觀。

  • 過濾器 (Filters): 在執行動作之前或之後運行的方法(例如:before_action)。
    比較常用的,像是驗證使用者是否登入:

before_action :authenticate_user!, only: [:edit, :update]
class UsersController < ApplicationController
  before_action :find_user, only: [:show, :edit, :update, :destroy]

  def show; end

  private

  def find_user
    @user = User.find(params[:id])
  end
end
  • Strong Parameters: 為了安全性,Rails 鼓勵使用 Strong Parameters 來過濾不安全的輸入
def user_params
  params.require(:user).permit(:username, :email, :password)
end
  • 錯誤處理(Error Handling): 當 user 請求不存在的資源時,可能就要回傳一個 404 錯誤。
def show
  @user = User.find_by(id: params[:id])
  render '404' unless @user
end
  • Flash通知(Flash Messages): 訊息會在動作執行後(或前)被顯示,通常用於顯示操作成功或失敗的訊息。
def create
  @user = User.new(user_params)
  if @user.save
    flash[:notice] = 'User created successfully.'
    redirect_to @user
  else
    flash.now[:alert] = 'User creation failed.'
    render 'new'
  end
end
  • API 支援:
def index
  @users = User.all
  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @users }
  end
end
  • Nested Resources(巢狀資源):複雜的應用中,有時候會有巢狀資源的操作。控制器需要更複雜的邏輯來處理這些巢狀關係。
# config/routes.rb
resources :authors do
  resources :books
end

這樣,在 BooksController 裡,就需要找到對應的 Author:

def index
  @author = Author.find(params[:author_id])
  @books = author.books
end

如果後面有想到什麼會再更新,謝謝收看
我們明天見!
(本來以為明天再講 CRUD 結果一寫完全不能自己)(????)


上一篇
vol. 12 Rails 裡的設計原則 CoC:9大慣例提升你的開發速度!
下一篇
vol. 14 Rails Super DRY :程式界的 YOLO 態度、OAOO(到底多愛縮寫這群人!)
系列文
富士大顆30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言