6

Я пытаюсь создать систему достижений в Ruby on Rails и столкнулся с проблемой с моим дизайном/кодом.Система достижений RoR - Полиморфная ассоциация и проблемы дизайна

Попытки использования полиморфных ассоциаций:

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 

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

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

Имеет ли это смысл? Должен ли я просто слить таблицу достижений с конкретным типом достижения, например WeightAchievement (так что таблица - это имя, описание, weight_required, exercise_id), а затем, когда пользователь запрашивает достижения, в моем коде я просто ищу все достижения? (например, WeightAchievement, EnduranceAchievement, RepAchievement и т. д.)

ответ

13

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

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

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

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

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 

Имея необработанный код на Ruby в вашем DB означает, что легче регулировать правила «на лету» без необходимости повторного развертывания приложения, но это может затруднить тестирование.

Образец 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 

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

+1

Спасибо - это, по сути, то, что я искал, но не мог окутать мою голову. – MunkiPhD

Смежные вопросы