Система достижений RoR - Проблемы полиморфной ассоциации и дизайна

Я пытаюсь разработать систему достижений в 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

И мои фиксаторы: достижений.имл ...

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 будет иметь требуемый вес и идентификатор упражнения. Затем пользовательский прогресс будет сохранен в модели UserProgress, где он связан с фактическим достижением (в отличие от WeightAchievement).

Причина, по которой мне нужны критерии в отдельных таблицах, заключается в том, что критерии будут сильно отличаться между различными типами достижения и будут добавлены динамически после этого, поэтому я Я не создаю отдельную модель для каждого достижения. 12230 Это имеет смысл? Должен ли я просто объединить таблицу достижений с определенным типом достижений, таким как WeightAchievement (таким образом, таблица называется name, description, weight_required, exerc_id), тогда когда пользователь запрашивает достижения, в моем коде я просто ищу все достижения? (например, WeightAchievement, EnduranceAchievement, RepAchievement и т. д.)

6
задан MunkiPhD 21 August 2010 в 17:06
поделиться

1 ответ

Обычно системы достижений работают так: существует большое количество различных достижений, которые можно активировать, и есть набор триггеров, которые можно использовать для проверки того, должно ли достижение быть срабатывает.

Использование полиморфной ассоциации, вероятно, плохая идея, потому что загрузка всех достижений для прохождения и их проверки может оказаться сложным упражнением. Также существует тот факт, что вам придется выяснить, как выразить условия успеха или неудачи в какой-то таблице, но во многих случаях вы можете получить определение, которое не отображается так точно. В итоге у вас может получиться шестьдесят разных таблиц для представления всех видов триггеров, и поддерживать это будет кошмаром.

Альтернативный подход заключался бы в том, чтобы определить ваши достижения в терминах имени, ценности и так далее, и иметь таблицу констант, которая действует как хранилище ключей / значений.

Вот пример миграции:

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

Столбец achievement.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

Настройте таблицу Достижения так, чтобы в ней хранилась информация о том, какие параметры передавать:

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

Вам часто придется пробежать через целую кучу достижений записей чтобы увидеть, были ли они достигнуты, если у вас нет таблицы сопоставления, которая может в общих чертах определить, какие записи проверяются триггерами.Более надежная реализация этой системы позволила бы вам определять определенные классы для наблюдения за каждым достижением, но этот базовый подход должен, по крайней мере, служить в качестве основы.

13
ответ дан 8 December 2019 в 18:29
поделиться
Другие вопросы по тегам:

Похожие вопросы: