6

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

次に、この次の使い捨て単体テストを試すと、実績が 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

そして私の備品: achievements.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 のような特定の種類のアチーブメント (したがって、テーブルは name、description、weight_required、exercise_id) とマージする必要があります。その後、ユーザーがアチーブメントを照会すると、コードですべてのアチーブメントを検索するだけですか? (例: WeightAchievement、EnduranceAchievement、RepAchievement など)

4

1 に答える 1

13

アチーブメント システムが一般的に機能する方法は、トリガーできるさまざまなアチーブメントが多数あり、アチーブメントをトリガーする必要があるかどうかをテストするために使用できる一連のトリガーがあります。

ポリモーフィックな関連付けを使用することは、おそらく悪い考えです。すべての実績を読み込んで実行し、それらすべてをテストすることは、複雑な作業になる可能性があるためです。また、何らかの表で成功または失敗の条件を表現する方法を考え出さなければならないという事実もありますが、多くの場合、うまくマッピングされていない定義になってしまう可能性があります。さまざまな種類のすべてのトリガーを表すために 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

DB に未加工の Ruby コードがあるということは、アプリケーションを再デプロイしなくてもその場でルールを簡単に調整できることを意味しますが、これによりテストがより難しくなる可能性があります。

サンプルは次のprocようになります。

user.max_weight_lifted > TriggerConstant[:brickhouse_weight_required]

ルールを単純化したい場合は、自動的に展開$brickhouse_weight_requiredされるものを作成できTriggerConstant[:brickhouse_weight_required]ます。これにより、技術者以外の人が読みやすくなります。

DB にコードを入れるのを避けるには、これらのプロシージャをいくつかのバルク プロシージャ ファイルで個別に定義し、何らかの定義によってさまざまなチューニング パラメータを渡す必要があります。このアプローチは次のようになります。

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トリガーがテストするレコードの種類を大まかに定義できるマッピング テーブルがない限り、達成されたかどうかを確認するために、多くの場合、レコードの山全体を調べなければなりません。このシステムをより堅牢に実装すると、各アチーブメントを監視する特定のクラスを定義できるようになりますが、この基本的なアプローチは少なくとも基盤として機能するはずです。

于 2010-08-21T17:54:01.103 に答える