RoR成就系统 – 多态协会和设计问题

我试图在Ruby on Rails中设计一个成就系统,并且遇到了我的设计/代码。

试图使用多态关联:

class Achievement  true end class WeightAchievement  :achievable end 

迁移:

 class CreateAchievements  true t.timestamps end create_table :weight_achievements do |t| t.integer :weight_required t.references :exercises, :null => false t.timestamps end ... #code end 

然后,当我尝试下面的抛弃unit testing时,它会失败,因为它表示该成就为空。

 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 

我的灯具:achievement.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模型中,在该模型中,它链接到实际的Achievement(而不是WeightAchievement)。

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

这甚至有意义吗? 我是否应该将Achievement表与WeightAchievement等特定类型的成就合并(因此表格是name,description,weight_required,exercise_id),然后当用户查询成就时,在我的代码中我只搜索所有成就? (例如WeightAchievement,EnduranceAchievement,RepAchievement等)

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

使用多态关联可能是一个坏主意,因为加载所有成就并测试它们都可能最终成为一项复杂的练习。 还有一个事实是你必须弄清楚如何在某种表格中表达成功或失败的条件,但在很多情况下,你最终可能会得到一个没有如此整齐地映射的定义。 你最终可能会有六十个不同的表来表示所有不同类型的触发器,这听起来像是一个需要维护的噩梦。

另一种方法是根据名称,值等来定义您的成就,并使用一个常量表作为键/值存储。

这是一个示例迁移:

 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 } 

结合这一点,你会得到一个简化的,更少的快乐结果:

 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记录来查看它们是否已经实现,除非你有一个映射表,可以用松散的术语定义触发器测试的记录类型。 更加强大的系统实现将允许您定义要为每个成就观察的特定类,但这种基本方法至少应该作为基础。