代码之家  ›  专栏  ›  技术社区  ›  MunkiPhD

RoR成就系统-多态关联与设计问题

  •  6
  • MunkiPhD  · 技术社区  · 14 年前

    我正试图用rubyonrails设计一个成就系统,但我的设计/代码遇到了障碍。

    尝试使用多态关联:

    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
    

    还有我的设备:

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

    重量_成就.ym...

     BrickHouseCriteria:
         id: 1
         weight_required: 500
         exercises_id: 1
    

    尽管如此,我不能让它运行,也许从总体上来说,这是一个糟糕的设计问题。我试图做的是用一个表来显示所有的成就和它们的基本信息(名称和描述)。使用该表和多态关联,我想链接到包含完成该成就标准的其他表,例如,WeightAchievement表将具有所需的权重和练习id。然后,用户的进度将存储在UserProgress模型中,它与实际成就相联系的地方(与实际成就相对)。

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

    1 回复  |  直到 14 年前
        1
  •  13
  •   tadman    14 年前

    成就系统通常的工作方式是,有大量不同的成就可以被触发,有一组触发器可以用来测试一项成就是否应该被触发。

    使用多态关联可能是一个坏主意,因为加载所有的成果来运行和测试它们可能最终是一个复杂的练习。还有一个事实是,您必须弄清楚如何在某种表中表示成功或失败的条件,但在很多情况下,您可能会得到一个映射不那么整齐的定义。您可能会有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

    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
    

    你经常要在一堆的 记录以查看它们是否已经实现,除非您有一个映射表,可以松散地定义触发器测试的记录类型。这个系统的更健壮的实现将允许你定义特定的类来观察每一个成就,但是这个基本的方法至少应该作为基础。