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 约定。

所以我借鉴了:

  • 布拉德·弗罗斯特的原子设计(将复杂系统分解为基本单元)
  • Sandi Metz 教授关于小而聚焦的物体
  • 领域驱动设计的有界上下文
  • Rails 的约定优于配置
  • SOLID 设计原则
  • 核心思想很简单:使用 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 保持一致

  • 单一职责原则每个元素处理域的一个方面 Reports::DateRange 仅处理与日期相关的概念
  • 开放/封闭原则 可以通过创建新元素来添加新行为 现有元素保持不变
  • 里氏替换原则元素使用组合而不是继承完全避免继承层次结构
  • 接口隔离元素仅公开相关方法客户端仅依赖于它们需要的内容
  • 依赖倒置元素依赖于抽象实现细节保持私密
  • 何时使用元素

    元素的良好候选者:

  • 复杂模型(用户、账户、项目)
  • 围绕业务概念的代码
  • 共同成长的功能
  • 不要创建以下元素:

  • 简单模型(标签、类别)
  • 单一方法
  • 技术问题
  • 入门

  • 在模型中寻找相关方法的集群
  • 识别控制器中的业务概念
  • 为每个概念创建命名空间类
  • 逐步移动逻辑
  • 让测试指导你
  • 结论

    Elemental Ruby 并非万灵药,但它提供了一条围绕业务概念组织 Rails 应用程序的清晰途径。它以 Rails 惯例为基础,同时添加了有助于应用程序可持续增长的结构。

    下次打开控制器并感到熟悉的恐惧时,请记住:有更好的方法来组织代码。将其分解为元素,并让您的业务领域指导您。

    这是探索 Elemental Ruby 系列的第一篇。下次我们将研究如何识别遗留代码中的元素。

    随着 Rails 应用程序的不断增长,您发现了哪些组织它们的模式?您是否尝试过类似的方法?请在评论中告诉我!