6

Ruby on Rails에서 업적 시스템을 설계하려고 시도 중이고 내 디자인/코드로 인해 문제가 발생했습니다. 다형성 연결을 사용하려고 RoR Achievement System - 다형성 연관 및 디자인 문제

:

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 테이블에는 필요한 가중치와 운동 ID가 있습니다. 그런 다음 사용자 진행률이 UserProgress 모델에 저장되며 실제 진행률 (WeightAchievement와 반대)으로 연결됩니다.

기준이 다른 테이블에서 기준을 필요로하는 이유는 다양한 업적 유형간에 기준이 크게 달라질 수 있으며 나중에 동적으로 추가되므로 각 업적에 대해 별도의 모델을 만들지 않기 때문입니다.

이 말이 맞습니까? 나는 성취도 테이블을 WeightAchievement와 같은 특정 유형의 성취도와 병합해야합니까 (테이블이 이름, 설명, weight_required, exercise_id). 그러면 사용자가 업적을 쿼리하면 코드에서 모든 업적을 간단히 검색합니까? (예 : WeightAchievement, EnduranceAchievement, RepAchievement 등)

답변

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 열은 달성이 트리거 여부를해야하는지 여부를 결정하기 위해 평가 루비 코드가 포함되어 있습니다 :

다음은 샘플 이동합니다.일반적으로 이것은,로드 포장, 당신이 호출 할 수있는 유틸리티 방법으로 끝됩니다 : 당신이 조정할 수

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는 응용 프로그램을 재배포하지 않고 즉시 규칙을 조정하는 것이 더 쉽다는 것을 의미하지만 테스트가 더 어려워 질 수 있습니다.

샘플 proc이 보일 수 있습니다 같은 :

user.max_weight_lifted > TriggerConstant[:brickhouse_weight_required] 

당신이, 당신은 자동으로 TriggerConstant[:brickhouse_weight_required]$brickhouse_weight_required을 확장 무언가를 만들 수 있습니다 규칙을 단순화하려는 경우

. 그러면 비 기술적 인 사람들이 더 쉽게 읽을 수 있습니다.

DB에 코드를 두는 것을 피하려면 일부 사용자가 불편을 겪을 수 있으므로이 절차를 일부 대량 프로 시저 파일에서 독립적으로 정의하고 다양한 종류의 정의 매개 변수를 전달해야합니다 . 이 경우 trigger_options에서

create_table :achievements do |t| 
    t.string :name 
    t.integer :points 
    t.string :trigger_type 
    t.text :trigger_options 
end 

직렬화 저장되어있는 매핑 테이블이다 : 그것은 전달하는 것을 옵션에 대한 정보를 저장하도록

module TriggerConditions 
    def max_weight_lifted(user, options) 
    user.max_weight_lifted > options[:weight_required] 
    end 
end 

Achievement 테이블을 조정 :처럼이 방법은 보일 것이다. 예는 다음과 같을 수 있습니다

{ :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