【问题标题】:RoR Achievement System - Polymorphic Association & Design IssuesRoR 成就系统 - 多态关联和设计问题
【发布时间】:2011-04-02 02:00:46
【问题描述】:

我正在尝试在 Ruby on Rails 中设计一个成就系统,但我的设计/代码遇到了障碍。

尝试使用多态关联:

class Achievement < ActiveRecord::Base
  belongs_to :achievable, :polymorphic => true
end

class WeightAchievement < ActiveRecord::Base
  has_one :achievement, :as => :achievable
end

迁移:

class CreateAchievements < ActiveRecord::Migration
... #code
    create_table :achievements do |t|
      t.string :name
      t.text :description
      t.references :achievable, :polymorphic => true

      t.timestamps
    end

     create_table :weight_achievements do |t|
      t.integer :weight_required
      t.references :exercises, :null => false

      t.timestamps
    end
 ... #code
end

然后,当我尝试以下一次性单元测试时,它失败了,因为它说成就为空。

test "parent achievement exists" do
   weightAchievement = WeightAchievement.find(1)
   achievement = weightAchievement.achievement 

    assert_not_nil achievement
    assert_equal 500, weightAchievement.weight_required
    assert_equal achievement.name, "Brick House Baby!"
    assert_equal achievement.description, "Squat 500 lbs"
  end

还有我的固定装置: 成就.yml...

BrickHouse:
 id: 1
 name: Brick House
 description: Squat 500 lbs
 achievable: BrickHouseCriteria (WeightAchievement)

weight_achievements.ym...

 BrickHouseCriteria:
     id: 1
     weight_required: 500
     exercises_id: 1

尽管如此,我无法让它运行,也许从总体上看,这是一个糟糕的设计问题。我试图做的是有一个包含所有成就及其基本信息(名称和描述)的表。使用该表和多态关联,我想链接到其他包含完成该成就的标准的表,例如WeightAchievement 表将包含所需的体重和锻炼 ID。然后,用户的进度将存储在 UserProgress 模型中,并在其中链接到实际的成就(而不是 WeightAchievement)。

我需要单独表格中的标准的原因是因为标准在不同类型的成就之间会有很大差异,并且会在之后动态添加,这就是为什么我没有为每个成就创建单独的模型的原因。

这有意义吗?我是否应该将成就表与特定类型的成就(如 WeightAchievement)合并(所以该表是名称、描述、weight_required、exact_id),然后当用户查询成就时,我只需在我的代码中搜索所有成就? (例如,WeightAchievement、EnduranceAchievement、RepAchievement 等)

【问题讨论】:

    标签: ruby-on-rails polymorphic-associations achievements


    【解决方案1】:

    成就系统通常的工作方式是可以触发大量的各种成就,并且有一组触发器可以用来测试是否应该触发成就。

    使用多态关联可能不是一个好主意,因为加载所有成就以运行并测试它们最终可能会成为一项复杂的练习。还有事实上,您必须弄清楚如何在某种表中表达成功或失败条件,但在很多情况下,您可能最终有一个没有如此整洁地映射的定义。您最终可能会拥有 60 个不同的表来表示所有不同类型的触发器,这听起来像是一场噩梦。

    另一种方法是根据名称、价值等来定义您的成就,并拥有一个充当键/值存储的常量表。

    这是一个示例迁移:

    create_table :achievements do |t|
      t.string :name
      t.integer :points
      t.text :proc
    end
    
    create_table :trigger_constants do |t|
      t.string :key
      t.integer :val
    end
    
    create_table :user_achievements do |t|
      t.integer :user_id
      t.integer :achievement_id
    end
    

    achievements.proc 列包含您评估的 Ruby 代码,以确定是否应该触发成就。通常,它会被加载、包装并最终成为您可以调用的实用方法:

    class Achievement < ActiveRecord::Base
      def proc
        @proc ||= eval("Proc.new { |user| #{read_attribute(:proc)} }")
      rescue
        nil # You might want to raise here, rescue in ApplicationController
      end
    
      def triggered_for_user?(user)
        # Double-negation returns true/false only, not nil
        proc and !!proc.call(user)
      rescue
        nil # You might want to raise here, rescue in ApplicationController
      end
    end
    

    TriggerConstant 类定义了您可以调整的各种参数:

    class TriggerConstant < ActiveRecord::Base
      def self.[](key)
        # Make a direct SQL call here to avoid the overhead of a model
        # that will be immediately discarded anyway. You can use
        # ActiveSupport::Memoizable.memoize to cache this if desired.
        connection.select_value(sanitize_sql(["SELECT val FROM `#{table_name}` WHERE key=?", key.to_s ]))
      end
    end
    

    在您的数据库中具有原始Ruby代码意味着在不必重新部署应用程序的情况下更容易调整规则,但这可能会使测试更加困难。

    proc 的示例可能如下所示:

    user.max_weight_lifted > TriggerConstant[:brickhouse_weight_required]
    

    如果您想简化您的规则,您可以创建一些将$brickhouse_weight_required 自动扩展为TriggerConstant[:brickhouse_weight_required] 的内容。这会让非技术人员更容易阅读。

    为了避免将代码放入您的数据库中,有些人可能会觉得这很糟糕,您必须在一些批量过程文件中独立定义这些过程,并通过某种定义传递各种调整参数。这种方法看起来像:

    module TriggerConditions
      def max_weight_lifted(user, options)
        user.max_weight_lifted > options[:weight_required]
      end
    end
    

    调整Achievement 表,使其存储有关要传入哪些选项的信息:​​

    create_table :achievements do |t|
      t.string :name
      t.integer :points
      t.string :trigger_type
      t.text :trigger_options
    end
    

    在这种情况下,trigger_options 是一个以序列化方式存储的映射表。一个例子可能是:

    { :weight_required => :brickhouse_weight_required }
    

    结合这些,您会得到一个稍微简化的、更少eval 快乐的结果:

    class Achievement < ActiveRecord::Base
      serialize :trigger_options
    
      # Import the conditions which are defined in a separate module
      # to avoid cluttering up this file.
      include TriggerConditions
    
      def triggered_for_user?(user)
        # Convert the options into actual values by converting
        # the values into the equivalent values from `TriggerConstant`
        options = trigger_options.inject({ }) do |h, (k, v)|
          h[k] = TriggerConstant[v]
          h
        end
    
        # Return the result of the evaluation with these options
        !!send(trigger_type, user, options)
      rescue
        nil # You might want to raise here, rescue in ApplicationController
      end
    end
    

    您通常必须通过整个堆Achievement recks看他们是否已实现,除非您有一个可以定义的映射表,以松散的术语,触发器测试是什么样的记录。这种系统的更强大的实现将允许您定义特定类来观察每个成就,但这种基本方法至少应作为基础。

    【讨论】:

    • 谢谢 - 这基本上是我一直在寻找的东西,但我无法理解。
    猜你喜欢
    • 1970-01-01
    • 2019-05-20
    • 1970-01-01
    • 2020-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-07
    • 1970-01-01
    相关资源
    最近更新 更多