RoR成就系统-多态协会与设计问题 [英] RoR Achievement System - Polymorphic Association & Design Issues

查看:69
本文介绍了RoR成就系统-多态协会与设计问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在Ruby on Rails中设计成就系统,并且遇到了我的设计/代码的麻烦.

I'm attempting to design an achievement system in Ruby on Rails and have run into a snag with my design/code.

尝试使用多态关联:

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

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

Then, when I try this following throw-away unit test, it fails because it says that the achievement is null.

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 ...

And my fixtures: achievements.yml...

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

weight_achievements.ym ...

weight_achievements.ym...

 BrickHouseCriteria:
     id: 1
     weight_required: 500
     exercises_id: 1

尽管如此,我还是无法运行它,也许在宏伟的计划中,这是一个糟糕的设计问题.我试图做的是拥有一张包含所有成就及其基本信息(名称和描述)的表格.使用该表格和多态关联,我想链接到其他表格,这些表格将包含完成该成就的标准,例如WeightAchievement表将具有所需的体重和运动ID.然后,用户的进度将存储在UserProgress模型中,并链接到实际的Achievement(与WeightAchievement相对).

Even though, I can't get this to run, maybe in the grand scheme of things, it's a bad design issue. What I'm attempting to do is have a single table with all the achievements and their base information (name and description). Using that table and polymorphic associations, I want to link to other tables that will contain the criteria for completing that achievement, e.g. the WeightAchievement table will have the weight required and exercise id. Then, a user's progress will be stored in a UserProgress model, where it links to the actual Achievement (as opposed to WeightAchievement).

之所以需要在单独的表格中使用该标准,是因为该标准在不同类型的成就之间会千差万别,并且以后会动态添加,这就是为什么我没有为每个成就创建单独的模型的原因.

The reason I need the criteria in separate tables is because the criteria will vary wildly between different types of achievements and will be added dynamically afterwards, which is why I'm not creating a separate model for each achievement.

这甚至有意义吗?我是否应该仅将成就表与特定的成就类型(例如WeightAchievement)合并(因此表是名称,描述,weight_required,exercise_id),然后当用户查询成就时,我只需在我的代码中搜索所有成就? (例如,体重成就,耐力成就,重复成就等)

Does this even make sense? Should I just merge the Achievement table with the specific type of achievement like WeightAchievement (so the table is name, description, weight_required, exercise_id), then when a user queries the achievements, in my code I simply search all the achievements? (e.g. WeightAchievement, EnduranceAchievement, RepAchievement, etc)

推荐答案

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

The way achievement systems generally work is that there are a large number of various achievements that can be triggered, and there's a set of triggers that can be used to test wether or not an achievement should be triggered.

使用多态关联可能不是一个好主意,因为载入所有成就以进行测试并对其进行测试可能最终是一项复杂的工作.还有一个事实,就是您必须弄清楚如何在某种表中表达成功或失败的条件,但是在许多情况下,您可能最终得到的定义并没有那么整齐地映射.您可能最终会有60个不同的表来代表所有不同种类的触发器,这听起来像是一场噩梦.

Using a polymorphic association is probably a bad idea because loading in all the achievements to run through and test them all could end up being a complicated exercise. There's also the fact that you'll have to figure out how to express the success or failure conditions in some kind of table, but in a lot of cases you might end up with a definition that does not map so neatly. You might end up having sixty different tables to represent all the different kinds of triggers and that sounds like a nightmare to maintain.

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

An alternative approach would be to define your achievements in terms of name, value and so on, and have a constant table which acts as a key/value store.

以下是示例迁移:

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代码.通常,将其装入,包装并最终成为您可以调用的实用程序方法:

The achievements.proc column contains the Ruby code you evaluate to determine if the achievement should be triggered or not. Typically this gets loaded in, wrapped, and ends up as a utility method you can call:

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类定义了可以调整的各种参数:

The TriggerConstant class defines various parameters you can tweak:

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代码意味着无需重新部署应用程序即可更容易地即时调整规则,但这可能会使测试变得更加困难.

Having the raw Ruby code in your DB means that it is easier to adjust the rules on the fly without having to redeploy the application, but this might make testing more difficult.

样本proc可能如下所示:

user.max_weight_lifted > TriggerConstant[:brickhouse_weight_required]

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

If you want to simplify your rules, you might create something that expands $brickhouse_weight_required into TriggerConstant[:brickhouse_weight_required] automatically. That would make it more readable by non-technical people.

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

To avoid putting the code in your DB, which some people may find to be in bad taste, you will have to define these procedures independently in some bulk procedure file, and pass in the various tuning parameters by some kind of definition. This approach would look like:

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

调整Achievement表,以便它存储有关传入的选项的信息:​​

Adjust the Achievement table so that it stores information on what options to pass in:

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

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

In this case trigger_options is a mapping table that is stored serialized. An example might be:

{ :weight_required => :brickhouse_weight_required }

与此相结合,您会得到一些简化的,不太令人满意的结果:

Combining this you get a somewhat simplified, less eval happy outcome:

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记录以查看是否已实现,除非您有一个映射表可以宽松地定义触发器测试的记录类型.该系统的更强大实现可让您定义每个成就要观察的特定类,但是这种基本方法至少应作为基础.

You'll often have to strobe through a whole pile of Achievement records to see if they've been achieved unless you have a mapping table that can define, in loose terms, what kind of records the triggers test. A more robust implementation of this system would allow you to define specific classes to observe for each Achievement, but this basic approach should at least serve as a foundation.

这篇关于RoR成就系统-多态协会与设计问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆