Я пытаюсь разработать систему достижений в 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 и т. д.)
Обычно системы достижений работают так: существует большое количество различных достижений, которые можно активировать, и есть набор триггеров, которые можно использовать для проверки того, должно ли достижение быть срабатывает.
Использование полиморфной ассоциации, вероятно, плохая идея, потому что загрузка всех достижений для прохождения и их проверки может оказаться сложным упражнением. Также существует тот факт, что вам придется выяснить, как выразить условия успеха или неудачи в какой-то таблице, но во многих случаях вы можете получить определение, которое не отображается так точно. В итоге у вас может получиться шестьдесят разных таблиц для представления всех видов триггеров, и поддерживать это будет кошмаром.
Альтернативный подход заключался бы в том, чтобы определить ваши достижения в терминах имени, ценности и так далее, и иметь таблицу констант, которая действует как хранилище ключей / значений.
Вот пример миграции:
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
Вам часто придется пробежать через целую кучу достижений
записей чтобы увидеть, были ли они достигнуты, если у вас нет таблицы сопоставления, которая может в общих чертах определить, какие записи проверяются триггерами.Более надежная реализация этой системы позволила бы вам определять определенные классы для наблюдения за каждым достижением, но этот базовый подход должен, по крайней мере, служить в качестве основы.