使用普通的舊 Ruby 對象構建時尚的 Rails 組件

已發表: 2022-03-11

您的網站正在獲得牽引力,並且您正在迅速發展。 Ruby/Rails 是您選擇的編程語言。 你的團隊更大了,你已經放棄了“胖模型,瘦控制器”作為 Rails 應用程序的設計風格。 但是,您仍然不想放棄使用 Rails。

沒問題。 今天,我們將討論如何使用 OOP 的最佳實踐來使您的代碼更清晰、更孤立、更解耦。

你的應用值得重構嗎?

讓我們首先看看您應該如何確定您的應用程序是否適合重構。

這是我通常會問自己以確定我的代碼是否需要重構的指標和問題列表。

  • 緩慢的單元測試。 PORO 單元測試通常使用隔離良好的代碼快速運行,因此運行緩慢的測試通常表明設計不佳和責任過度耦合。
  • FAT 模型或控制器。 具有超過 200 行代碼 (LOC) 的模型或控制器通常是重構的良好候選者。
  • 過大的代碼庫。 如果您有超過 30,000 個 LOC 的 ERB/H​​TML/HAML 或超過 50,000 個 LOC 的 Ruby 源代碼(沒有 GEM),那麼您很有可能應該重構。

嘗試使用這樣的東西來找出你有多少行 Ruby 源代碼:

find app -iname "*.rb" -type f -exec cat {} \;| wc -l

此命令將搜索 /app 文件夾中所有擴展名為 .rb 的文件(ruby 文件),並打印出行數。 請注意,這個數字只是近似值,因為註釋行將包含在這些總數中。

另一個更精確、信息更豐富的選項是使用 Rails rake 任務stats ,它輸出代碼行數、類數、方法數、方法與類的比率以及每個方法的代碼行數比率的快速摘要:

 bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
  • 我可以在我的代碼庫中提取循環模式嗎?

脫鉤在行動

讓我們從一個真實的例子開始。

假設我們要編寫一個跟踪慢跑者時間的應用程序。 在主頁上,用戶可以看到他們輸入的時間。

每個時間條目都有日期、距離、持續時間和附加的相關“狀態”信息(例如天氣、地形類型等),以及可以在需要時計算的平均速度。

我們需要一個顯示每周平均速度和距離的報告頁面。

如果條目的平均速度高於整體平均速度,我們將通過 SMS 通知用戶(在本示例中,我們將使用 Nexmo RESTful API 發送 SMS)。

主頁將允許您選擇慢跑的距離、日期和時間,以創建類似於以下內容的條目:

我們還有一個statistics頁面,它基本上是一份每週報告,其中包括每週的平均速度和距離。

  • 您可以在此處查看在線示例。

代碼

app目錄的結構類似於:

 ⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

我不會討論User模型,因為它沒有什麼特別之處,因為我們將它與 Devise 一起使用來實現身份驗證。

至於Entry模型,它包含我們應用程序的業務邏輯。

每個Entry都屬於一個User

我們驗證每個條目的distancetime_perioddate_timestatus屬性的存在。

每次創建條目時,我們將用戶的平均速度與系統中所有其他用戶的平均速度進行比較,並使用 Nexmo 通過短信通知用戶(我們不會討論如何使用 Nexmo 庫,雖然我想演示一個我們使用外部庫的案例)。

  • 要點樣本

請注意, Entry模型不僅僅包含業務邏輯。 它還處理一些驗證和回調。

entries_controller.rb具有主要的 CRUD 操作(雖然沒有更新)。 EntriesController#index獲取當前用戶的條目並按創建日期對記錄進行排序,而EntriesController#create創建一個新條目。 無需討論EntriesController#destroy的明顯職責和職責:

  • 要點樣本

statistics_controller.rb負責計算每週報告, StatisticsController#index獲取登錄用戶的條目並按週對它們進行分組,使用 Rails 中 Enumerable 類中包含的#group_by方法。 然後它嘗試使用一些私有方法來裝飾結果。

  • 要點樣本

我們在這裡不討論太多視圖,因為源代碼是不言自明的。

下面是列出登錄用戶 ( index.html.erb ) 條目的視圖。 這是將用於在條目控制器中顯示索引操作(方法)結果的模板:

  • 要點樣本

請注意,我們正在使用部分render @entries ,將共享代碼提取到部分模板_entry.html.erb ,以便我們可以保持代碼乾燥和可重用:

  • 要點樣本

_form部分也是如此。 我們創建一個可重用的部分錶單,而不是使用(新建和編輯)操作相同的代碼:

  • 要點樣本

至於週報頁面視圖, statistics/index.html.erb顯示了一些統計數據,並通過對一些條目進行分組來報告用戶的周表現:

  • 要點樣本

最後,條目的幫助器entries_helper.rb包括兩個幫助器readable_time_periodreadable_speed ,它們應該使屬性更易於閱讀:

  • 要點樣本

到目前為止沒有什麼花哨的。

你們中的大多數人會爭辯說,重構這違反了 KISS 原則,並且會使系統更加複雜。

那麼這個應用程序真的需要重構嗎?

絕對不是,但我們將其僅用於演示目的。

畢竟,如果您查看上一節,並且表明應用程序需要重構的特徵,很明顯我們示例中的應用程序不是重構的有效候選者。

生命週期

因此,讓我們從解釋 Rails MVC 模式結構開始。

通常,它從瀏覽器發出請求開始,例如https://www.toptal.com/jogging/show/1

Web 服務器接收請求並使用routes來找出要使用的controller

控制器完成解析用戶請求、數據提交、cookies、會話等工作,然後要求model獲取數據。

這些models是 Ruby 類,它們與數據庫對話、存儲和驗證數據、執行業務邏輯以及完成繁重的工作。 視圖是用戶看到的:HTML、CSS、XML、Javascript、JSON。

如果我們想顯示 Rails 請求生命週期的順序,它看起來像這樣:

Rails 解耦 MVC 生命週期

我想要實現的是使用普通的舊 ruby​​ 對象 (PORO) 添加更多抽象,並使create/update操作的模式類似於以下內容:

Rails 圖創建表單

對於list/show操作,如下所示:

Rails 圖表列表查詢

通過添加 PORO 抽象,我們將確保 SRP 職責之間的完全分離,這是 Rails 不太擅長的。

指導方針

為了實現新設計,我將使用下面列出的指南,但請注意,這些不是您必須遵循的 T 規則。將它們視為使重構更容易的靈活指南。

  • ActiveRecord 模型可以包含關聯和常量,但僅此而已。 所以這意味著沒有回調(使用服務對象並在那裡添加回調)和驗證(使用 Form 對象來包括模型的命名和驗證)。
  • 將控制器保持為薄層並始終調用服務對象。 你們中的一些人會問為什麼要使用控制器,因為我們想繼續調用服務對象來包含邏輯? 好吧,控制器是進行 HTTP 路由、參數解析、身份驗證、內容協商、調用正確的服務或編輯器對象、異常捕獲、響應格式化和返回正確的 HTTP 狀態代碼的好地方。
  • 服務應該調用 Query 對象,並且不應該存儲狀態。 使用實例方法,而不是類方法。 與 SRP 保持一致的公共方法應該很少。
  • 查詢應該在查詢對像中完成。 查詢對象方法應該返回一個對象、一個散列或一個數組,而不是一個 ActiveRecord 關聯。
  • 避免使用 Helpers 並改用裝飾器。 為什麼? Rails 助手的一個常見缺陷是它們會變成一大堆非 OO 函數,它們都共享一個命名空間並相互踩踏。 但更糟糕的是,沒有很好的方法來使用 Rails 助手的任何類型的多態性——為不同的上下文或類型提供不同的實現,覆蓋或子類化助手。 我認為 Rails 輔助類通常應該用於實用方法,而不是用於特定用例,例如為任何類型的表示邏輯格式化模型屬性。 讓它們保持輕盈和微風。
  • 避免使用關注點,而是使用裝飾器/委託器。 為什麼? 畢竟,關注點似乎是 Rails 的核心部分,並且在多個模型之間共享時會導致代碼枯竭。 儘管如此,主要問題是關注點不會使模型對象更具凝聚力。 代碼組織得更好。 換句話說,模型的 API 沒有真正的變化。
  • 嘗試從模型中提取值對象,以保持您的代碼更簡潔並對相關屬性進行分組。
  • 始終為每個視圖傳遞一個實例變量。

重構

在我們開始之前,我想再討論一件事。 當你開始重構時,通常你會問自己: “那真的是很好的重構嗎?”

如果您覺得您在職責之間進行了更多分離或隔離(即使這意味著添加更多代碼和新文件),那麼這通常是一件好事。 畢竟,解耦應用程序是一種非常好的做法,並且使我們更容易進行適當的單元測試。

我不會討論諸如將邏輯從控制器轉移到模型之類的東西,因為我假設您已經這樣做了,並且您對使用 Rails(通常是 Skinny Controller 和 FAT 模型)感到滿意。

為了保持這篇文章的緊湊,我不會在這裡討論測試,但這並不意味著你不應該測試。

相反,您應該始終從測試開始,以確保一切正常,然後再繼續。 這是必須的,尤其是在重構時。

然後我們可以實施更改並確保代碼的相關部分的測試全部通過。

提取值對象

首先,什麼是值對象?

馬丁福勒解釋說:

值對像是一個小對象,例如貨幣或日期範圍對象。 它們的關鍵特性是它們遵循值語義而不是引用語義。

有時您可能會遇到這樣一種情況,即一個概念值得自己抽象,其平等不是基於價值,而是基於身份。 示例包括 Ruby 的日期、URI 和路徑名。 提取到值對象(或域模型)非常方便。

何必?

Value 對象的最大優勢之一是它們有助於在您的代碼中實現的表現力。 您的代碼往往會更加清晰,或者至少在您有良好的命名習慣的情況下可以這樣。 由於值對像是一種抽象,它可以使代碼更簡潔,錯誤更少。

另一個重大勝利是不變性。 對象的不變性非常重要。 當我們存儲某些可以在值對像中使用的數據集時,我通常不希望這些數據被操縱。

這什麼時候有用?

沒有單一的、一刀切的答案。 做最適合你的事情,在任何特定情況下做有意義的事情。

不過,除此之外,還有一些指導方針可以幫助我做出決定。

如果您認為一組方法是相關的,那麼使用 Value 對象它們更具表現力。 這種表現力意味著一個 Value 對象應該代表一組不同的數據,普通開發人員可以通過查看對象的名稱簡單地推斷出這些數據。

這是怎麼做到的?

值對象應遵循一些基本規則:

  • 值對象應該有多個屬性。
  • 屬性在對象的整個生命週期中應該是不可變的。
  • 平等是由對象的屬性決定的。

在我們的示例中,我將創建一個EntryStatus值對像以將Entry#status_weatherEntry#status_landform屬性抽像到它們自己的類中,如下所示:

  • 要點樣本

注意:這只是一個不繼承自ActiveRecord::Base的普通舊 Ruby 對象 (PORO)。 我們已經為我們的屬性定義了讀取器方法,並在初始化時分配它們。 我們還使用了一個可比較的 mixin 來使用 (<=>) 方法比較對象。

我們可以修改Entry模型以使用我們創建的值對象:

  • 要點樣本

我們還可以修改EntryController#create方法以相應地使用新的值對象:

  • 要點樣本

提取服務對象

那麼什麼是服務對象呢?

Service 對象的工作是保存特定業務邏輯的代碼。 與“胖模型”風格不同,其中少量對象包含許多用於所有必要邏輯的方法,使用服務對象會產生許多類,每個類都有一個目的。

為什麼? 有什麼好處?

  • 脫鉤。 服務對象可幫助您實現對象之間的更多隔離。
  • 能見度。 服務對象(如果命名良好)顯示了應用程序的功能。 我可以瀏覽一下服務目錄,看看應用程序提供了哪些功能。
  • 清理模型和控制器。 控制器將請求(參數、會話、cookie)轉換為參數,將它們傳遞給服務並根據服務響應重定向或呈現。 而模型只處理關聯和持久性。 從控制器/模型中提取代碼到服務對象將支持 SRP 並使代碼更加解耦。 模型的職責將僅是處理關聯和保存/刪除記錄,而服務對象將具有單一職責(SRP)。 這會帶來更好的設計和更好的單元測試。
  • 乾燥並擁抱變化。 我使服務對象盡可能簡單和小。 我將服務對象與其他服務對象組合在一起,並重用它們。
  • 清理並加速您的測試套件。 服務易於快速測試,因為它們是具有一個入口點(調用方法)的小型 Rub​​y 對象。 複雜服務由其他服務組成,因此您可以輕鬆拆分測試。 此外,使用服務對象可以更輕鬆地模擬/存根相關對象,而無需加載整個 rails 環境。
  • 可從任何地方調用。 服務對象可能會從控制器以及其他服務對象、DelayedJob / Rescue / Sidekiq Jobs、Rake 任務、控制台等調用。

另一方面,沒有什麼是完美的。 Service 對象的一個缺點是它們對於一個非常簡單的操作來說可能是多餘的。 在這種情況下,您很可能最終使代碼複雜化,而不是簡化。

什麼時候應該提取服務對象?

這裡也沒有硬性規定。

通常,Service 對象更適合大中型系統; 那些具有超出標準 CRUD 操作的大量邏輯的人。

因此,每當您認為代碼片段可能不屬於您要添加它的目錄時,重新考慮並查看它是否應該轉到服務對象可能是個好主意。

以下是何時使用 Service 對象的一些指標:

  • 動作很複雜。
  • 該操作涉及多個模型。
  • 該操作與外部服務交互。
  • 該操作不是底層模型的核心關注點。
  • 有多種方法可以執行該操作。

你應該如何設計服務對象?

為服務對象設計類相對簡單,因為您不需要特殊的 gem,不必學習新的 DSL,並且可以或多或少地依賴您已經擁有的軟件設計技能。

我通常使用以下準則和約定來設計服務對象:

  • 不要存儲對象的狀態。
  • 使用實例方法,而不是類方法。
  • 應該有很少的公共方法(最好是支持 SRP 的一種。
  • 方法應該返回豐富的結果對象,而不是布爾值。
  • 服務位於app/services目錄下。 我鼓勵您將子目錄用於業務邏輯繁重的域。 例如,文件app/services/report/generate_weekly.rb將定義Report::GenerateWeeklyapp/services/report/publish_monthly.rb將定義Report::PublishMonthly
  • 服務以動詞開頭(不以 Service 結尾): ApproveTransactionSendTestNewsletterImportUsersFromCsv
  • 服務響應調用方法。 我發現使用另一個動詞有點多餘:ApproveTransaction.approve() 讀起來不好。 此外,調用方法是 lambda、procs 和方法對象的實際方法。

如果您查看StatisticsController#index ,您會注意到與控制器耦合的一組方法( weeks_to_date_fromweeks_to_date_toavg_distance等)。 那不是很好。 如果要在statistics_controller之外生成每週報告,請考慮後果。

在我們的例子中,讓我們創建Report::GenerateWeekly並從StatisticsController中提取報告邏輯:

  • 要點樣本

所以StatisticsController#index現在看起來更乾淨了:

  • 要點樣本

通過應用服務對像模式,我們將代碼捆綁在特定、複雜的操作周圍,並促進創建更小、更清晰的方法。

作業:考慮為WeeklyReport使用 Value 對象而不是Struct

從控制器中提取查詢對象

什麼是查詢對象?

Query 對像是代表數據庫查詢的 PORO。 它可以在應用程序的不同位置重用,同時隱藏查詢邏輯。 它還提供了一個很好的隔離單元來測試。

您應該將復雜的 SQL/NoSQL 查詢提取到它們自己的類中。

每個 Query 對象負責根據條件/業務規則返回一個結果集。

在這個例子中,我們沒有任何復雜的查詢,所以使用 Query 對象效率不高。 但是,出於演示目的,讓我們在Report::GenerateWeekly#call中提取查詢並創建generate_entries_query.rb

  • 要點樣本

Report::GenerateWeekly#call中,讓我們替換:

 def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end

和:

 def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end

查詢對像模式有助於使您的模型邏輯與類的行為嚴格相關,同時也使您的控制器保持精簡。 由於它們只不過是普通的舊 Ruby 類,查詢對像不需要從ActiveRecord::Base繼承,並且應該只負責執行查詢。

提取服務對象的創建條目

現在,讓我們提取為新服務對象創建新條目的邏輯。 讓我們使用約定並創建CreateEntry

  • 要點樣本

現在我們的EntriesController#create如下:

 def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end

將驗證移動到表單對像中

現在,這裡的事情開始變得更有趣了。

請記住,在我們的指南中,我們同意我們希望模型包含關聯和常量,但沒有其他內容(沒有驗證和回調)。 因此,讓我們從刪除回調開始,並改用 Form 對象。

Form 對像是一個普通的舊 Ruby 對象 (PORO)。 它在需要與數據庫通信的任何地方從控制器/服務對象接管。

為什麼要使用 Form 對象?

在尋求重構您的應用程序時,牢記單一職責原則 (SRP) 始終是一個好主意。

SRP 可幫助您圍繞類應負責的內容做出更好的設計決策。

例如,您的數據庫表模型(Rails 上下文中的 ActiveRecord 模型)表示代碼中的單個數據庫記錄,因此它沒有理由關心您的用戶正在執行的任何操作。

這就是 Form 對象的用武之地。

Form 對象負責在您的應用程序中表示一個表單。 所以每個輸入字段都可以被視為類中的一個屬性。 它可以驗證這些屬性是否滿足某些驗證規則,並且可以將“乾淨”的數據傳遞到它需要去的地方(例如,您的數據庫模型或您的搜索查詢構建器)。

什麼時候應該使用 Form 對象?

  • 當您想從 Rails 模型中提取驗證時。
  • 當單個表單提交可以更新多個模型時,您可能需要創建一個 Form 對象。

這使您可以將所有表單邏輯(命名約定、驗證等)放在一個位置。

如何創建 Form 對象?

  • 創建一個普通的 Ruby 類。
  • 包含ActiveModel::Model (在 Rails 3 中,您必須包含命名、轉換和驗證)
  • 開始使用你的新表單類,就好像它是一個常規的 ActiveRecord 模型一樣,最大的區別是你不能持久化存儲在這個對像中的數據。

請注意,您可以使用改革 gem,但堅持使用 PORO,我們將創建如下所示的entry_form.rb

  • 要點樣本

我們將修改CreateEntry以開始使用 Form 對象EntryForm

 class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end

注意:有些人會說不需要從 Service 對象訪問 Form 對象,我們可以直接從控制器調用 Form 對象,這是一個有效的參數。 但是,我希望有清晰的流程,這就是為什麼我總是從 Service 對象調用 Form 對象。

將回調移動到服務對象

正如我們之前所同意的,我們不希望我們的模型包含驗證和回調。 我們使用 Form 對象提取了驗證。 但我們仍在使用一些回調( Entry模型compare_speed_and_notify_user中的after_create )。

為什麼我們要從模型中移除回調?

Rails 開發人員通常會在測試期間開始注意到回調的痛苦。 如果您沒有測試您的 ActiveRecord 模型,那麼隨著應用程序的增長以及需要更多邏輯來調用或避免回調,您將開始注意到痛苦。

after_*回調主要用於保存或持久化對象。

一旦對像被保存,對象的目的(即責任)就已經完成。 因此,如果我們仍然看到在保存對像後調用回調,我們可能會看到回調到達對象的責任範圍之外,這就是我們遇到問題的時候。

在我們的例子中,我們在保存條目後向用戶發送短信,這與條目的域沒有真正的關係。

解決問題的一個簡單方法是將回調移動到相關的服務對​​象。 畢竟,為最終用戶發送 SMS 與CreateEntry服務對像有關,而不是與 Entry 模型本身有關。

這樣做,我們不再需要在我們的測試中存根compare_speed_and_notify_user方法。 我們使創建條目變得簡單,無需發送 SMS,並且我們通過確保我們的類具有單一職責 (SRP) 來遵循良好的面向對象設計。

所以現在我們的CreateEntry看起來像:

  • 要點樣本

使用裝飾器而不是助手

雖然我們可以輕鬆地使用 Draper 的視圖模型和裝飾器集合,但為了這篇文章,我將堅持使用 PORO,就像我到目前為止所做的那樣。

我需要的是一個將調用裝飾對像上的方法的類。

我可以使用method_missing來實現它,但我將使用 Ruby 的標準庫SimpleDelegator

下面的代碼展示瞭如何使用SimpleDelegator來實現我們的基礎裝飾器:

 % app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end

那麼為什麼使用_h方法呢?

此方法充當視圖上下文的代理。 默認情況下,視圖上下文是視圖類的實例,默認視圖類是ActionView::Base 。 您可以按如下方式訪問視圖助手:

 _h.content_tag :div, 'my-div', class: 'my-class'

為了更方便,我們在ApplicationHelper中添加了一個decorate方法:

 module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end

現在,我們可以將EntriesHelper助手移動到裝飾器:

 # app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end

我們可以像這樣使用readable_time_periodreadable_speed

 # app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
 - <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>

重構後的結構

我們最終得到了更多文件,但這並不一定是壞事(請記住,從一開始,我們就承認這個示例僅用於演示目的,不一定是重構的好用例):

 app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

結論

儘管我們在這篇博文中專注於 Rails,但 RoR 並不是所描述的服務對象和其他 PORO 的依賴項。 您可以將此方法用於任何 Web 框架、移動設備或控制台應用程序。

通過使用 MVC 作為 Web 應用程序的架構,一切都保持耦合併讓您運行得更慢,因為大多數更改都會影響應用程序的其他部分。 此外,它還迫使您考慮將一些業務邏輯放在哪裡——應該放在模型、控制器還是視圖中?

通過使用簡單的 PORO,我們將業務邏輯轉移到不從ActiveRecord繼承的模型或服務中,這已經是一個巨大的勝利,更不用說我們有更簡潔的代碼,它支持 SRP 和更快的單元測試。

乾淨的架構旨在將用例放在結構的中心/頂部,以便您可以輕鬆查看應用程序的功能。 它還使採用更改變得更容易,因為它更加模塊化和隔離。

我希望我演示瞭如何使用普通舊 Ruby 對象和更多抽象來解耦關注點、簡化測試並幫助生成乾淨、可維護的代碼。

相關: Ruby on Rails 有什麼好處? 經過兩個十年的編程,我使用 Rails