Elemental Ruby:组织 Rails 应用程序的更好方法
每个 Rails 开发人员都经历过这样的时刻。你打开一个控制器,它就在那里:数百行错综复杂的业务逻辑摆在你面前。就我而言,它是一个报告控制器,已经增长到 300 多行,将日期解析、过滤、业务逻辑和导出处理混合在一起,形成一团难以理解的混乱。它没有测试,而且前任开发人员已经离开了公司——而我,新员工,被要求更改功能。
让我向您展示一些类似的东西,但实际上不展示旧代码:
def index # 300 lines of mixed concerns: @date = params[:date] ? Date.parse(params[:date]) : Date.current @report_type = params[:type] || default_report_type # Complex date logic if @view_type == "Day" # date calculations elsif @view_type == "Week" # more date calculations # ... several more conditions end # Business logic mixed with filtering @records = current_account.records @records = @records.where(complex_conditions) # Complex business rules if @report_type == "Summary" # 50 lines of summary logic elsif @report_type == "Detail" # 50 more lines of detail logic end # Export logic mixed in respond_to do |format| format.html format.csv { # complex CSV generation } end end
说我害怕改变它是轻描淡写。此外,该方法嵌套很深,并且由于它的逻辑分支无穷无尽,为它编写测试对我来说是一项艰巨的任务。我需要另一种模式。
传统方法
因此,我尝试了标准的 Rails 模式:
胖模型
class Report < ApplicationRecord def self.generate_summary # Move logic to model # Now model is huge instead of controller - not a solution end end
担忧
module ReportGeneration extend ActiveSupport::Concern # Move logic to concern # Now complexity is just hidden - not a solution end
服务对象
class GenerateReportService def call # Move logic to service # End up with explosion of services, # and functional programming methods in an OO language/app # just never felt right end end
虽然每种方法都有一定帮助,但均未能完全解决根本问题:代码并未围绕其所代表的实际业务概念进行组织。
进入元素红宝石
元素红宝石是几种强大思想交汇的结果。
我对解决方案有很多要求 — — 它必须具有可扩展性、遵循良好的设计原则、易于测试、易于维护,并且尽可能遵循 Rails 约定。
所以我借鉴了:
核心思想很简单:使用 Ruby 的自然命名空间功能,围绕业务领域的基本元素组织代码。
让我们看看我们的报告控制器如何演变:
第一次进化:服务对象
def index @date = DateParser.new(params[:date]).parse @report_type = ReportTypeSelector.new(current_account, params[:type]).select @date_range = DateRangeCalculator.new(@date, params[:view_type]).calculate @records = RecordFilter.new(current_account.records, params).filter @data = ReportGenerator.new( type: @report_type, records: @records, date_range: @date_range ).generate respond_with_report(@data) end
情况有所改善,但我们的业务概念仍然分散在多个服务对象中。我们用一种形式的复杂性换取了另一种形式的复杂性。
最终进化:元素红宝石
def index @date_range = Reports::DateRange.new( date: params[:date], view_type: params[:view_type] ) @report = Reports::Type.new( account: current_account, type: params[:type] ).build @data = @report.generate( records: Reports::Records.new(current_account, params), date_range: @date_range ) respond_with_report(@data) end
业务概念现已清晰且井然有序:
# app/models/reports/date_range.rb class Reports::DateRange def initialize(date:, view_type:) @date = parse_date(date) @view_type = view_type end def start_date case @view_type when "Day" @date when "Week" @date.beginning_of_week # etc end end def header case @view_type when "Day" I18n.l(@date, format: :long) when "Week" "#{start_date.strftime('%B %e')} - #{end_date.strftime('%B %e')}" # etc end end private def parse_date(date) Date.parse(date) rescue Date.current end end
为什么这种方法效果更好
如何与 SOLID 保持一致
何时使用元素
元素的良好候选者:
不要创建以下元素:
入门
结论
Elemental Ruby 并非万灵药,但它提供了一条围绕业务概念组织 Rails 应用程序的清晰途径。它以 Rails 惯例为基础,同时添加了有助于应用程序可持续增长的结构。
下次打开控制器并感到熟悉的恐惧时,请记住:有更好的方法来组织代码。将其分解为元素,并让您的业务领域指导您。
这是探索 Elemental Ruby 系列的第一篇。下次我们将研究如何识别遗留代码中的元素。
随着 Rails 应用程序的不断增长,您发现了哪些组织它们的模式?您是否尝试过类似的方法?请在评论中告诉我!