在Grouper,我們是Rails的長期使用者,像其他在紐約的敏捷團隊RapGenius和Kickstarter一樣。它容易使用並且可以有效提高開發人員的效率。
然而,人們開始注意到它的缺點–一旦程式碼量超過幾千行,測試套件會變的緩慢並且框架載入時間會顯著增加。
一些沒有幫助的Rails特性鼓勵使用者少用設計模式,這通常會導致高度耦合的程式碼,以及緩慢不可維護的測試套件。我們意識到沒有必要非得這樣做。
Rails或許是敏捷開發的最完美的工具,但是對於中型或大型程式它並不是最有效的。我們通過三條準則解決了這些問題,我們相信這些概念可以成為任何高階rails開發的一部分,它們是:Interactors,Policies,Presents。
第一部分 — Interactors
現在流行的趨勢是在rails程式碼中,在ActiveRecord的models
資料夾中,將大部分業務邏輯程式碼放在一個巨大的類中。這通常會暗示一個類擁有太多的職責,一個巨大的公共API,以及僅僅為了實現複雜的關聯關係的物件功能的方法。
一個重要的因素是ActiveRecord callbacks;before_save
鉤子函式在一個類中修改其他物件的狀態。這是一個嚴重的問題,因為在處理我們關心的物件之前必須獲取所有這些其他物件。在測試的時候,將會變得相當緩慢(因為這些物件通常要從資料庫中獲取),回撥函式和長長的方法呼叫鏈的執行也會讓這個過程變得艱難。
解決的方法是”Interactors”的準則(通常被叫做”Service Objects”)。Interactors要求應用的核心儲存在一系列純粹的ruby物件中,儲存主要的應用邏輯,使ActiveRecord類作為資料持久化的介面層。通過檢視Interactors的名字,你可以清楚的明白應用的功能;例如,SignUp
,BookGrouper
,AssignBarForGrouper
等等。其他的類,像Member
和Bar
僅僅是為了驗證和儲存屬性,例如name,location和date。
(作為背景 — 組織者組織名為”Groupers”的活動,在酒吧中進行3對3暢飲,這是一個很有趣的活動。)
Interactor是一個輕量級的gem,提供了一系列方便的方法,例如success?
,failure?
,因此你的控制器看起來像下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class GroupersController < ApplicationController::Base … def create interactor = ConfirmGrouper.perform(leader: current_member) if interactor.success? redirect_to home_path else flash[:error] = interactor.message render :new end end … end |
你的Interactor看起來像這樣:
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 |
# Responsible for creating a Grouper, email # # class ConfirmGrouper include Interactor def perform grouper = Grouper.new(leader: member) fail!(grouper.errors.full_messages) unless grouper.save send_emails_for(grouper) assign_bar_for(grouper) end private def send_emails_for(grouper) LeaderMailer.grouper_confirmed(member: grouper.leader.id).deliver WingMailer.grouper_confirmed(wings: grouper.wings.map(&:id)).deliver AdminMailer.grouper_confirmed(grouper: grouper.admin.id).deliver end def assign_bar_for(grouper) # Asynchronous job because it’s a little slow AssignBarForGrouper.enqueue(grouper.id) end end |
這種方式優點是很多的,同時在程式碼複雜性和測試速度方面。舉例來說,你的測試現在會明顯加快,因為你幾乎不需要去讀取資料庫,同時你可以獨立的去測試每個ActiveRecord模型而不需要擔心它的關聯。Interactors可以使用虛擬物件(doubles)而不去使用ActiveRecord模型(models),同時,測試套件甚至沒有必要去載入Rails。
你的控制器測試看起來像下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
describe GroupersController do … describe “#create” do subject { post :create } before { ConfirmGrouper.stub(:perform).and_return(interactor) } let(:interactor) { double(“Interactor”, success?: success, message: “foo”) } context “when the interactor is a success” do let(:success) { true } it { should redirect_to home_path } end context “when the interactor fails” do let(:success) { false } it { should render :new } end end end |
通過所有的ActiveRecord資料庫呼叫都在stub中進行,測試執行時間從之前的4,5秒左右降到了0.15秒左右。
另外,這會使你的應用變得更加簡單明瞭,因為每一個類將具有單獨的作用,例如,Bar
類知道酒吧的位置和它開放的時間。它不需要知道Grouper
,有關於將哪一個Bar
分配哪一個Grouper
的複雜邏輯安全的儲存在AssignBarForGrouper
Interactor中。
最後,如果你保持Interactors短小並且具有單個目的性,你可以組合多個Interactors完成更加複雜的邏輯。這種混合匹配的方式可以有效降低你的程式碼的耦合度,並且可以讓你在必要的時候複用操作。
總結
很明顯,你需要去選擇合適的方式去解決你所遇到的問題,沒有一種萬能的模式去解決所有的問題,上面提到的準則或許在一個15分鐘快速建立部落格的應用中並不合適。我們非常反對Rails建它作為初學者的入門應用,並且建議對所有的情況採用相同的方式。一旦程式碼開始變得複雜,你將對你解決問題的方式沒有把握。我們建議使用Interactors作為解決問題的方式。
這個系列的下一部分, Policy Objects可以有效簡化控制器。
如果你對Rails設計模式和最佳實踐感興趣,請關注我們的職位,我們正在招聘。
你也可以在hacker news上得到一些啟發。