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 应用程序的不断增长,您发现了哪些组织它们的模式?您是否尝试过类似的方法?请在评论中告诉我!