【问题标题】:Decorator pattern for model functionality in Rails 5Rails 5 中模型功能的装饰器模式
【发布时间】:2018-11-09 20:54:13
【问题描述】:

我正在按照“胖模型/瘦控制器”模式开发 Rails 5 应用程序。当我添加诸如日志记录和验证之类的东西时,我发现我的模型变得有点太胖了。例如,这是订阅列表的方法的草图......

class SubscriberList < ApplicationRecord
  # relationships and validations

  def subscribe!(args)
    log that a subscription is attempted

    begin
      do the subscription
    rescue errors
      log the failure and reason
      rethrow
    end

    log successful subscription
    log other details about the subscription

    SubscriptionValidationJob.perform_later( new_subscriber )

    return new_subscriber
  end
end

它越来越多地妨碍日志记录和验证与订阅行为的结合。我知道我应该通过将日志记录和验证移动到decorators 来解决这个问题,可能使用draper

我对装饰师没有太多经验。我主要担心的是由于代码在应该使用装饰模型时使用未装饰模型而导致的错误。或相反亦然。接口是一样的,变化是副作用,所以很难察觉。

我很想大量使用 decorates_associationdecorates_finders 来避免这种情况,但 Draper 文档说要避免这种情况......

装饰器的行为应该与它们装饰的模型非常相似,因此很容易在控制器操作开始时装饰您的对象,然后在整个过程中使用装饰器。不要。

但是,Draper(以及我能找到的大多数 Rails + Decorator 文章)似乎都专注于视图功能......

因为装饰器被设计为被视图使用,所以你应该只在那里访问它们。操作您的模型以准备好东西,然后在渲染视图之前的最后一分钟进行装饰。这避免了在创建装饰器(特别是集合装饰器)之后尝试修改装饰器时出现的许多常见缺陷。

与视图功能不同,您有一个控制器来确保传递给视图的模型被装饰,我的装饰器用于模型功能。装饰器主要用于代码组织和易于测试,几乎所有东西都应该使用装饰模型。

使用装饰器添加模型功能的最佳做法是什么?总是使用装饰模型?更激进的方法,比如将订阅和取消订阅转移到另一个类?

【问题讨论】:

    标签: ruby-on-rails model-view-controller decorator


    【解决方案1】:

    我认为这不适合装饰者。在 Rails 中,装饰器主要用视图中使用的表示逻辑包装模型对象。它们作为单个对象的扩展工作,可让您在逻辑上分离对象的不同任务。

    例如:

    class User
      def born_on
        Date.new(1989, 9, 10)
      end
    end
    
    class UserDecorator < SimpleDelegator
      def birth_year
        born_on.year
      end
    end
    

    当涉及多个对象交互的操作等过程时,装饰器并不适合。

    您应该关注的是服务对象模式,您可以在其中创建执行单一任务的单一用途对象:

    class SubscriptionService
    
      attr_accessor :user, :list
    
      def initialize(user, list)
        @user = user
        @list = list
      end
    
      def self.perform(user, list)
        self.new(user, list).perform
      end
    
      def perform
         @subscription = Subscription.new(user: @user, list: @list)
         log_subscription_attempted
         if @subscription.create
           send_welcome_email
           # ...
         else
           log_failure_reason
           # ...
         end
    
         @subscription
      end
    
      private
    
        def send_welcome_email
          # ...
        end
    
        def log_subscription_attempted
          # ...
        end
    
        def log_failure_reason
          # ...
        end
    end
    

    但您还应该考虑是否正确组合模型。在此示例中,您希望三个模型像这样互连:

    class User
      has_many :subscriptions
      has_many :subscription_lists, through: :subscriptions
    end
    
    class Subscription
      belongs_to :user
      belongs_to :subscription_list
      validates_uniqueness_of :user_id, scope: :subscription_list_id
    end
    
    # or topic
    class SubscriptionList 
      has_many :subscriptions
      has_many :users, through: :subscriptions
    end
    

    每个模型都应在应用程序中处理一个单独的实体/资源。因此,例如SubscriptionList 模型不应该直接参与订阅单个用户。如果您的模型变胖了两个,则可能表明您在一组太小的业务逻辑对象中塞进了太多东西,或者数据库设计设置不当。

    【讨论】:

    • 谢谢,我会看看服务对象。这些模型的布局与您描述的相似。 SubscriptionList.subscribe! 真的是对Subscription.create! 的薄包装,加上所有额外的业务逻辑。它可能是最复杂的方法,只有 40 行。大多数型号只有 50 到 100 行。唔。由于订阅和取消订阅跨越多个模型并包含复杂的业务逻辑,因此将其分离到自己的类中是有意义的。这使模型保持简单。谢谢,我会睡着的。
    • 一般来说,您只想在种子文件、控制台等上下文中使用像 .create! 这样的 bang 方法,或者如果您需要在单个事务中包装多个操作,因为正常情况下不应使用异常控制流。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-24
    • 2016-12-14
    • 1970-01-01
    • 1970-01-01
    • 2018-07-30
    • 1970-01-01
    相关资源
    最近更新 更多