【问题标题】:How to create composable scopes for plain Ruby arrays如何为普通 Ruby 数组创建可组合范围
【发布时间】:2016-06-20 02:05:50
【问题描述】:

我真正喜欢 Active Record 的一件事是它的命名范围,并且能够将范围链接在一起以构建富有表现力的查询。

用普通的 Ruby Enumerables/Arrays 实现这一点的类似方法是什么,最好不要以任何危险的方式对 Enumerable 或 Array 进行猴子修补?

例如:

### ActiveRecord Model
class User < ActiveRecord::Base
  scope :customers, -> { where(:role => 'customer') }
  scope :speaking, ->(lang) { where(:language => lang) }
end

# querying
User.customers.language('English')  # all English customers

### Plain-Ruby Array
module User
  class << self
    def customers
      users.select { |u| u[:role] == 'customer' }
    end

    def speaking(lang)
      users.select { |u| u[:language] == lang }
    end

    private

    def users
      [
        {:name => 'John', :language => 'English', :role => 'customer'},
        {:name => 'Jean', :language => 'French', :role => 'customer'},
        {:name => 'Hans', :language => 'German', :role => 'user'},
        {:name => 'Max', :language => 'English', :role => 'user'}
      ]
    end
  end
end

User.customers  # all customers
User.language('English')  # all English speakers
# how do I achieve something similar to User.customers.language('English') ...?

我知道我可以在模块内构建一个方法customers_with_language,但我正在寻找一种通用方法来解决这个问题,它可以使用任意数量的“范围”。

【问题讨论】:

    标签: arrays ruby activerecord composition enumerable


    【解决方案1】:

    这是ScopableArray 的粗略实现,它继承了Array

    class ScopableArray < Array
      def method_missing(method_sym, *args)
        ScopableArray.new(select { |u| u[method_sym] == args[0] } )
      end
    end
    

    当这个类接收到一个它不能识别的方法时,它假设你想根据方法名称的字段和参数的值来过滤它:

    users = ScopableArray.new([
        {:name => 'John', :language => 'English', :role => 'customer'},
        {:name => 'Jean', :language => 'French', :role => 'customer'},
        {:name => 'Hans', :language => 'German', :role => 'user'},
        {:name => 'Max', :language => 'English', :role => 'user'}
    ])
    
    users.role('customer')
    # => [{:name=>"John", :language=>"English", :role=>"customer"}, {:name=>"Jean", :language=>"French", :role=>"customer"}]
    users.role('customer').language('English')
    # => [{:name=>"John", :language=>"English", :role=>"customer"}]
    

    您还可以查看ActiveRecord's implementation pattern 以获得更详细的方案,您可以通过传递名称和可调用块来定义范围,如下所示:

    class ScopableArray2 < Array
      class << self
        def scope(name, body)
          unless body.respond_to?(:call)
            raise ArgumentError, 'The scope body needs to be callable.'
          end
    
          define_method(name) do |*args|
            dup.select! { |x| body.call(x, *args) }
          end
        end
      end
    end
    

    然后你可以这样做:

    class Users < ScopableArray2
      scope :customers, ->(x) { x[:role] == 'customer' }
      scope :speaking, ->(x, lang) { x[:language] == lang }
    end
    
    users = Users.new([
            {:name => 'John', :language => 'English', :role => 'customer'},
            {:name => 'Jean', :language => 'French', :role => 'customer'},
            {:name => 'Hans', :language => 'German', :role => 'user'},
            {:name => 'Max', :language => 'English', :role => 'user'}
        ])
    
    users.customers.speaking('English')
    # => [{:name=>"John", :language=>"English", :role=>"customer"}]
    

    【讨论】:

    • 谢谢尤里。它看起来很整洁,但只处理属性选择。我正在寻找更通用的/可组合的/功能性的/富有表现力的东西。所以我可以像所有不会说英语的名字中带有“j”的客户一样做一些事情......(理想情况下,我可以用 ActiveRecord 范围做任何事情,只需对数组进行操作)。有意义吗?
    • @gingerlime - 添加了更精细的解决方案。您可以使用此模式使您的解决方案更加精细。
    • 看起来棒极了,乌里!非常感谢。
    猜你喜欢
    • 1970-01-01
    • 2017-07-03
    • 1970-01-01
    • 1970-01-01
    • 2011-12-17
    • 2012-05-28
    • 1970-01-01
    • 1970-01-01
    • 2012-03-15
    相关资源
    最近更新 更多