【转载】一步一步DSL

作者 @baya

原文

地址

12年的10月份,我准备为公司的彩票和话费充值等业务写一套DSL(Domain Specific Languages),顺路研究了一下Ruby特色的DSL技术,有些收获,于是写些东西作为资料备份。

Ruby中常见的DSL风格及其技术实现

我将通过一些例子来分析各个风格DSL的常见的技术实现,这些例子主要来自一些我们常用的gem的源代码,为了方便分析,我会对其中的代码做一些精简,后面 就不做一一说明了。

嵌套风格

嵌套风格的DSL给人的印象大概是这样,

something *args do |*items|
  ...
end
  • yield

通过yield obj或者block.call(obj)抛出待操作的对象

我们看一个来自rails_admin的例子:

RailsAdmin.config do |config|
  config.main_app_name = ["Cool app", "BackOffice"]
  config.authorize_with :cancan
end

下面的代码来自RailsAdmin

  # 为了分析, 省略了一些代码
  module RailsAdmin
	def self.config(&block)
	  block.call(RailsAdmin::Config)
	end
  end

代码中block.call(RailsAdmin::Config)的作用和yield RailsAdmin::Config一样,统一称为yield。 可以看出, RailsAdmin.config抛出的config对象就是RailsAdmin::Config,

  config.main_app_name = ["Cool app", "BackOffice"]

等同于

  RailsAdmin::Config.main_app_name = ["Cool app", "BackOffice"]
  • instance_eval

    将&block嵌入到待操作对象的上下文环境中

    下面的代码来自Rack::Builder, 注意Rack::Builder#initialize方法的实现,

app = Rack::Builder.new do
  use Rack::CommonLogger
  use Rack::ShowExceptions
	map "/lobster" do
	  use Rack::Lint
	  run Rack::Lobster.new
	end
  end
  class Rack::Builder

    def initialize(default_app = nil, &block)
      @use, @map, @run = [], nil, default_app
	  instance_eval(&block) if block_given?
	end

    def use(middleware, *args, &block)
       ...
    end

    def map(path, &block)
	  ...
    end

  end

instance_eval会将&block嵌入到方法调用者的上下文环境中,所以方法use 和 map 是在Rack::Builder.new创建的实例对象里执行的,效果类似于,

  app = Rack::Builder.new
  app.use Rack::CommonLogger
  app.use Rack::ShowExceptions
  app.map "/lobster" do
    use Rack::Lint
	run Rack::Lobster.new
  end
  • 转储&block

    将&block转化为一个对象存起来,并在适当的时候使用。

我们看看来自rake的例子,

  task :clobber => [:clean] do
     rm_rf "html"
  end

task 方法定义在dsl_definition.rb文件中,

  module Rake
    module DSL
	  def task(*args, &block)
	    Rake::Task.define_task(*args, &block)
	  end
	end
  end
  self.extend Rake::DSL

Rake::Task.define_task(*args, &block)最终在Rake::Task#enhance里实现, 而Rake::Task#execute将触发&block执行。 self.extend Rake::DSL 为当前的main对象扩展出task方法,

  module Rake
    class Task
	  def enhance(deps=nil, &block)
        @actions << block if block_given?
        self
      end

     def execute(args=nil)
        @actions.each do |act|
          case act.arity
          when 1
            act.call(self)
          else
            act.call(self, args)
          end
        end
      end

	end
  end

我们可以看到&block在Rake::Task#enhance方法里被转换为Proc对象后追加到了@actions这个数组的后面。 将&block转化为Proc对象存起来是件很容易的事情,&block去掉&就获得了一个Proc对象,接下来我们还可以看到使用其他手段转储&block。 我们熟悉的rspec中的describe和it等方法是通过使用module_eval转储&block来实现DSL的,

   describe Bowling, "#score" do
     it "returns 0 for all gutter game" do
       bowling = Bowling.new
       20.times { bowling.hit(0) }
       bowling.score.should eq(0)
     end
   end

现在我们来分析下describe和it的大概实现过程。 describe方法是在Rspec::Core::DSL中定义的,然后通过extend RSpec::Core::DSL变成了main对象的一个方法:

   module RSpec
     module Core
      module DSL
        def describe(*args, &example_group_block)
         RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register
        end
      end
    end
   end

   extend RSpec::Core::DSL
   Module.send(:include, RSpec::Core::DSL)

&example_group_block这个block被传递到RSpec::Core::ExampleGroup.describe方法里,我们看看在RSpec::Core::ExampleGroup.describe方法里能否找到我们想要的东西,

   module Rspec
	 module Core
	   class ExampleGroup

		 def self.describe(*args, &example_group_block)
		   child = const_set(
			 "Nested_#{@_subclass_count}",
			 subclass(self, args, &example_group_block)
		   )
		   children << child
		   child
		 end

		 def self.subclass(parent, args, &example_group_block)
		   subclass = Class.new(parent)
		   subclass.module_eval(&example_group_block) if example_group_block
		   subclass
		 end

	   end
	 end
   end

在subclass方法中&example_group_block被转储到了Rspec::Core::ExampleGroup的一个匿名子类中去了。describe方法执行后将会生成一个 Rspec::Core::ExampleGroup的子类并返回,并且此子类包含&example_group_block。

再看看it的实现,

  module Rspec
    module Core
	  class ExampleGroup

	    class << self

		  def self.define_example_method(name, extra_options={})
			module_eval(<<-END_RUBY, __FILE__, __LINE__)
			  def #{name}(desc=nil, *args, &block)
				examples << RSpec::Core::Example.new(self, desc, options, block)
				examples.last
			  end
			END_RUBY
		  end

		  define_example_method :it

		end
	  end
	end
  end

  module Rspec
    module Core
	  class Example
		def initialize(example_group_class, description, metadata, example_block=nil)
		  @example_group_class, @options, @example_block = example_group_class, metadata, example_block
		end
	  end
	end
  end

通过 define_example_method :it, it成为了Rspec::Core::ExampleGroup的一个实例方法,it方法执行后会生成一个Rsepc::Core::Example实例对象, it后面挂的&block会被转储到此实例对象里。

  • method_missing

    调用不存在的方法其实不可怕

jbuilder是使用method_missing实现嵌套风格DSL的典型例子。 不过Jbuilder#method_missing是一个比较复杂的实现, 为了更容易的理解method_missing的作用机制, 我参照jbuilder的源码写了一个简化版: jbuilder_mini,

  require 'multi_json'

  class JbuilderMini

	def initialize
	  @attributes = {}
	  yield self
	end

	def target!
	  ::MultiJson.dump @attributes
	end

	private

	def method_missing(key, value=nil, *args, &block)
	  result = if block
				 _scope { yield }
			   else
				 value
			   end
	  @attributes[key] = result
	end

	def _scope
	  parent_attributes = @attributes
	  @attributes = {}
	  yield
	  @attributes
	ensure
	  @attributes = parent_attributes
	end

  end

jbuilder_mini只能实现一些很简单的json字符串,

  json = JbuilderMini.new do |json|
	json.author do
	  json.name 'David'
	  json.age  32
	  json.book do
		json.name 'ruby'
		json.price 100.0
	  end
	end
  end.target!

  # {"author":{"name":"David","age":32,"book":{"name":"ruby","price":100.0}}}

JbuilderMini.new会抛出一个JbuilderMini的实例对象json,因为author不是JbuilderMini的实例方法, 所以json.author执行时会调用method_missing。 通过method_missing我们能够拿到未定义的方法名:key,方法的参数:value, *args和块: &block,然后在method_missing里实现我们想要的东西。

链式风格

链条,鞭子,蜡烛,你懂的

在rails2.x时代,写一个稍微复杂的查询是一件让人比较头疼的事情,


Person.find(:all, :conditions => [ "category IN (?) and name = ?", categories,  parmas[:name]], :limit => 50, :order => "created_at DESC")

而如今我们可以优雅灵活地构造查询语句,


  Person.where(category: categories, name: params[:name]).order('created_at DESC').limit(50)
  

这种便利正是链式风格的DSL带来的。

ActiveRecord::Base的子类使用的各种可以链式调用的方法比如where, select, group, order, limit, joins等都是通过delegate被代理到了ActiveRecord::Relation的实例上去了。我们做一个简化版的ActiveRecord::Relation,

  
  module ActiveRecord
    class Relation

	  def where(opts=:chain, *rest)
	    ...
		self
	  end

      def select(*fields)
	    ...
	    self
	  end

      def group(*args)
	    ...
		self
	  end

      def order(*args)
	    ...
		self
	  end

      def limit(value)
	    ...
		self
	  end

      def joins(*args)
	    ...
		self
	  end

	end
  end
  

可以看到这些能够链式调用的方法都有一个共同点就是它们最后都会返回self,返回self是构造链式DSL的一个比较方便自然的手段。

类宏风格

类里面不加self调用类方法

在Ruby中类宏风格的DSL常见且重要,比如我们经常用到的has_many, validate_presence_of, attr_reader等等就是类宏风格的DSL,

  
class Contest < ActivieRecord::Base

  has_many :votes
  has_many :entites

  validate_presence_of :title
  
end

class Book
  attr_reader :name
end

  • ClassMethods 和 InstanceMethods

类宏风格DSL的经典实现是写一个module, 然后在这个module里定义两个module: ClassMethods, InstanceMethods, 以及一个included钩子,

  
    module MaRo
	  def self.included(base)
	    base.extend ClassMethods
		base.send(:include, InstanceMethods)
	  end

      module ClassMethods
	    def act_as_something
		  ...
		end
	  end

      module InstanceMethods
	    def im
		  ...
		end
	  end
   end

   class A
     include MaRo
	 act_as_something
   end

在ActiveRecord::Base中与associations这一块相关的DSL,比如has_many, has_one, belongs_to, has_and_belongs_to_many实现的方式和上面类似, 但是它引入了ActiveSupport::Concern机制从而使得这一过程简便和标准化了,

   module ActiveRecord
     module Associations
	   extend ActiveSupport::Concern

       module ClassMethods

	    def has_many(name, scope = nil, options = {}, &extension)
          Builder::HasMany.build(self, name, scope, options, &extension)
        end

        def has_one(name, scope = nil, options = {})
          Builder::HasOne.build(self, name, scope, options)
        end

        def belongs_to(name, scope = nil, options = {})
          Builder::BelongsTo.build(self, name, scope, options)
        end

		def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
          Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension)
        end

	   end
	 end
   end

   module ActiveRecord
     class Base
	   include Associations
	 end
   end
  • define_method

用 define_method 实现一个类似attr_reader的宏,

  class A
    def self.aattr_reader *attrs
	  attrs.each {|attr|
	    define_method attr do
		  instance_variable_get "@#{attr}"
		end
	  }
	end
  end

  class B < A
    aattr_reader :foo, :bar

    def initialize
	  @foo = "foo"
	  @bar = "bar"
	end

  end

  b = B.new
  b.foo #=> foo
  b.bar #=> bar

补丁风格

子类通过特定的实例方法实现自己的特性

ActiveRecord::Migration是补丁风格DSL的一个很好的例子,

 
class CreateCategories < ActiveRecord::Migration
  def change
    create_table :categories do |t|
      t.string      :name
      t.timestamps
    end
  end
end

class AddRoleToUsers < ActiveRecord::Migration
  def change
    add_column :users, :role, :string, default: 'user' # 账号角色, user 普通用户, admin 管理员, sponsor 赞助商
  end
end

CreateCategories和AddRoleToUsers这个两个migration class继承自ActiveRecord::Migration,但是它们各自通过change这个补丁实现了 不同的功能,CreateCategories可以创建和销毁categories表,AddRoleToUsers可以增加和移除users表的role字段。通过打补丁,每个子类都可以 拥有一份自留地,种白菜,种萝卜,种点草都行。现在我们分析下ActiveRecord::Migration中补丁风格DSL实现的大致手段,


 module ActiveRecord
   class Migration

     # direction 为 :up或者:down, 其中:down表示回滚
     def migrate(direction)
       ActiveRecord::Base.connection_pool.with_connection do |conn|
		   exec_migration(conn, direction)
		 end
	   end

      def exec_migration(conn, direction)
        @connection = conn
        if direction == :down
          revert { change }
        else
          change
        end
      end

      def connection
        @connection || ActiveRecord::Base.connection
      end

      def method_missing(method, *arguments, &block)
        connection.send(method, *arguments, &block)
      end

	 end
   end

change方法执行的入口在exec_migration这个方法中,在exec_migration方法里,如果direction是:down即回滚,则以revert的形式执行change,否则直接执行change。

我们要知道诸如create_table, add_column, change_column这些方法并没有直接定义在ActivieRecord::Migration中,而是定义在module ActivieRecord::ConnectionAdapeters::SchemaStatements中的, ActiveRecord::ConnectionAdapters::AbstractAdapter include了模块 ActivieRecord::ConnectionAdapeters::SchemaStatements,

  
  module ActiveRecord
    module ConnectionAdapters
	  module SchemaStatements

         def create_table(table_name, options = {})
		   ...
		 end

         def add_column(table_name, options = {})
		   ...
		 end

         def change_cloumn(table_name, options = {})
		   ...
		 end

	  end
	end
  end
  module ActiveRecord
    module ConnectionAdapters
	  class AbstractAdapter
	    include SchemaStatements
	  end
	end
  end

最后, ActiveRecord::Base.connection.class.supperclass 就是 ActiveRecord::ConnectionAdapters::AbstractAdapter, 所以我们不难明白为什么ActiveRecord::Migration的实例调用的create_table, add_column等方法是通过method_missing代理到 ActiveRecord::Base.connection这个实例上去的。


DSL实践

ActiveRecord::Base是一个非常大的class,它所包含的那一套DSL内容非常丰富,包括了表间关系,字段验证,回调,事务等很多东西,我们暂且把这种 体积大的DSL叫做Big DSL,接下来我们要来实践一种小DSL,即Small DSL。

  • (D)omain,

    实现Small DSL首先应该确保Domain足够的准确和足够的具体,这要求Domain不能太大并且保有一定的复杂性。 拿彩票来说,彩票这个域有点大,我们可以对它进行细化,比如细化为注码的生成注码的投注等等。 其中注码的生成这个域也很大,可以继续细化为双色球注码生成福彩3D注码生成等等, 注码的生成即使细化到了彩种可能还是不够,还可以针对渠道做进一步细化,比如说渠道A的双色球的注码生成, 此时这个域就比较具体,可以写代码实现了。

  • (S)pecific,

    只做一件事情并且把这件事情做好。

  • (L)anguages,

    有可以重复使用的语法和词汇。

Samll DSL的Domain在ruby中是什么样子?

  • 是一个class
  • 可以接收外部数据

根据上面的定义我写了一个叫Dun::Land的class,可以用Dun::Land对Domain进行封装。 Dun::Land来自于一个叫dun的gem,可以通过gem install dun安装。 dun的详细用法可以看看README


class SomeDomain < Dun::Land
  data_reader :image, :name

  def call
    puts "#{image} #{name}"
  end
end

SomeDoamin image: 'fly.jpg', name: '秋天的味道'

data_reader 的作用是把外部数据变成域的实例方法,方便读取数据, 例如data_reader :image, :name 把image和name变成了SomeDomain的两个实例方法。 继承自Dun::Land的子类会自动拥有一个和类同名的全局方法,比如,

class Foo < Dun::Land
end

会生成一个叫Foo的全局方法。

简单例子: 用户验证

用户登录时,我们需要判断用户的邮箱和密码是否匹配,如何实现这个功能呢? 传统的实现方法如下,

class User < ActiveRecord::Base
  def self.authenticate(email, password)
    user = find_by_email(email)
    if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
      user
    else
      nil
    end
  end
end

user = User.authenticate "jim@126.com", "secret"

现在我们用另一种方式去实现这个功能,首先将用户验证抽象为一个域: AuthenticateUser,

class AuthenticateUser < Dun::Land
    data_reader :email, :password

    def call
      user = User.find_by_email(email)
      if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
        user
      else
        nil
      end

    end

  end

user = AuthenticateUser email: 'jim@126.com',password: 'secret'

看起来和传统实现方法没有太大的区别。现在我们有了(D)omain,那么(L)anguages在哪呢? 语法和词汇在哪呢? 为了让这个例子能够进行下去,我们想像数据库里还有一个叫admins的表,admins表里存放着系统管理员的帐号, 同样当这些管理员登录后台时,我们需要对其进行验证,按传统的方法,我们又写了下面的代码,

class Admin < ActiveRecord::Base
  def self.authenticate(name, password)
    admin = find_by_name(name)
    if admin && admin.password_hash == BCrypt::Engine.hash_secret(password, admin.password_salt)
      admin
    else
      nil
    end
  end
end

admin = Admin.authenticate "admin01", "secret"

如果又出现其他的模型需要认证,那么我们需要按照同样的步骤去添加代码,这些代码不完全相同,但是逻辑和控制结构几乎完全相同, 我们应该怎样去消除代码在逻辑和结构上的重复呢?在传统的方法上面去解决这类问题比较麻烦,原因在于User, Admin这些类相对于验证这个域体积过大了。 那我们用第二种方法试试,依葫芦画瓢, 我们封装域: AuthenticateAdmin,

class AuthenticateAdmin < Dun::Land
    data_reader :name, :password

    def call
      admin = Admin.find_by_name(name)
      if admin && admin.password_hash == BCrypt::Engine.hash_secret(password, admin.password_salt)
        admin
      else
        nil
      end

    end

  end

admin = AuthenticateAdmin name: 'admin01', password: 'secret'

用户认证这个域很小,很快我们就成为了其中的砖家,我们开始为其造些语法和词汇吧,

class AuthenticateUser

  auth :user, with: 'email'

end
class AuthenticateAdmin

  auth :admin, with: 'name'

end

auth model_name, with: col_name,读起来还算通顺。 现在有了语法和词汇,接下来我们开始写解释器! 由于只有一条语法,两个词(auth和with),这个解释器应该不会很复杂。 但是不管怎么说,我们要写的东西是一个解释器,好像很牛逼的样子,可是我对解释器一窍不通,有时连字节和位的关系都拈不清, 能写出解释器吗? 不过不要顾虑太多,因为我们马上要写的解释器是一个很普通,很简单的class。

定义Authenticate域,它将作为AuthenticateUser和AuthenticateAdmin的父类,对,Authenticate就是我们 要写的解释器。

class Authenticate < Dun::Land
  data_reader :password

  class << self

    def auth model_name, opts = {}
      col_name = opts[:with]
      data_reader col_name

      define_method :model do
        @model ||= Object.const_get model_name.capitalize
      end

      define_method :auth_obj do
        @auth_obj ||= model.send("find_by_#{col_name}", send(col_name))
      end
    end
  end

  def call
    if auth_obj && password_match?
      auth_obj
    else
      nil
    end
  end

  private

  def password_match?
    auth_obj.password_hash == BCrypt::Engine.hash_secret(password, auth_obj.password_salt)
  end

end

不超过30行代码,我们就拥有了一个用户验证方面的解释器,看来Small DSL不难实现嘛。虽然这个解释器只能解释一条语法,两个词汇,但是它功能强健, 可以想象,如果系统加入了一个新的模型Account需要验证,我们只需要增加一个AuthenticateAccount域就可以了,

class AuthenticateAccount < Authenticate
  auth :account, with: 'login'
end
AuthenticateAccount login: 'liliy20', password: 'secret'

有点复杂的例子: 积分分配

这个例子来自一个自线上产品。为了鼓励用户踊跃创建比赛,我们的比赛系统需要实现一个奖励积分的功能, 也就是当比赛结束后,如果用户创建的比赛的浏览人数达到一定数值后, 系统会给创建者奖励积分。

比赛的浏览人数与系统奖励积分的关系如下所示:

非调查类比赛:
浏览数小于10,000不奖励积分;
浏览数大于10,000小于25,000,奖励50分;
浏览数大于25,000小于50,000,奖励100分;
浏览数大于50,000 奖励200分;

调查类比赛:
浏览数不到1000, 不奖励积分,
浏览数大于1000小于5000,奖励25分;
浏览数大于5000小于10,000,奖励50分;
浏览数大于10,000小于50,000,奖励100分;
浏览数大于50,000 奖励300分;

非调查类比赛,系统奖励的积分有多种分配方式:
1、比赛创建者得到10%积分,排名第一的图片的上传者得45%,第二得30%
2、比赛创建者得到10%积分,排名第一的图片的上传者得35%,第二得25%
3、比赛创建者得到10%积分,排名第一的图片的上传者得25%,第二得18%
4、最后各人得分比例之和为100%

首先确定两个域NoSuerveyContestCreditAllocate和SuerveyContestCreditAllocate。

先实现SuerveyContestCreditAllocate。一阵鼓捣之后,写出的代码大致如下:

class NonSurveyContestCreditAllocation < Dun::Land

    data_reader :contest

    def call
	  allocate_mapper.each {|mapper| allocate_credit mapper }
	end

    def allocate_mapper
	  ... # 省略具体实现
	end

    def allocate_credit(mapper)
	  ... # 省略具体实现
	end

    def credit
      get_or_set :credit do
        if view_count < 25_000
          0
        elsif view_count  < 50_000 and view_count >= 25_000
          100
        elsif view_count >= 50_000
          200
        end
      end
    end

    def rates
      get_or_set :rates do
        if view_count < 25_000
          [0.10, 0.45, 0.30, 0.15]
        elsif view_count < 50_000 and view_count >= 25_000
          [0.10, 0.35, 0.25, 0.30]
        elsif view_count >= 50_000
          [0.10, 0.25, 0.18, 0.47]
        end
      end
    end

  end
end

在实现此功能的过程中,写了比较完善的单元测试。

然后又是一阵鼓捣,实现了SuerveyContestCreditAllocate,同样写了比较完善的单元测试,


class SurveyContestCreditAllocation < Dun::Land

   data_reader :contest

    def call
	  allocate_mapper.each {|mapper| allocate_credit mapper }
	end

    def allocate_mapper
	  ... # 省略具体实现
	end

    def allocate_credit(mapper)
	  ... # 省略具体实现
	end

    def credit
      get_or_set :credit do
        if view_count < 50_00
          0
        elsif view_count < 50_00 and view_count >= 10_000
          50
        elsif view_count >= 10_000 and view_count <= 50_000
          200
        elsif view_count >= 50_000 and view_count < 100_000
          300
        end
      end
    end

    def rates
      get_or_set :rates do
        [1.0, 0.0]
      end
    end

  end
end

通过前面的开发实践,我对积分分配这个问题理解清楚了,测试也达到了预期的效果,并且了解到NonSurveyContestCreditAllocation和SurveyContestCreditAllocation这两个域的区别就在于credit和rates这两个补丁方法上面,于是开始造语法和词汇,


class NonSurveyContestCreditAllocation

  credit view_count_less_than: 10_000, then: 0
  credit view_count_between: [10_000, 25_000], then: 50
  credit view_count_between: [25_001, 50_000], then: 100
  credit view_count_more_than: 50_000, then: 200

  rate view_count_less_than: 25_000, then: [0.10, 0.45, 0.30, 0.15]
  rate view_count_between: [25_000, 50_000], then: [0.10, 0.35, 0.25, 0.30]
  rate view_count_more_than: 50_000, then: [0.10, 0.25, 0.18, 0.47]

end

class SurveyContestCreditAllocation

  credit view_count_less_than: 1000, then: 0
  credit view_count_between: [1000, 5000], then: 25
  credit view_count_between: [5001, 10_000], then: 50
  credit view_count_between: [10_001, 50_000], then: 100
  credit view_count_more_than: 50_000, then: 300

  rate default: [1.0, 0.0]

end

读起来还蛮通顺的,剩下的工作就是写解释器来实现自己刚才读到的语言。这个小语言有两条语法: credit 和 rate;七个词汇: credit, rate, view_count_less_than, view_count_between, view_count_more_than,then 和 default。 首先定义一个叫CreditAllocation的域,这个域就是我们要实现的解释器,同时CreditAllocation还可以作为上面两个域的namespace。接下来一阵鼓捣,感谢前面写的单元测试,我们的CreditAllocation解释器通过测试,顺利完成了,最终的代码如下,

class CreditAllocation
  class NonSurveyContest < self

    credit view_count_less_than: 10_000, then: 0
    credit view_count_between: [10_000, 25_000], then: 50
    credit view_count_between: [25_001, 50_000], then: 100
    credit view_count_more_than: 50_000, then: 200

    rate view_count_less_than: 25_000, then: [0.10, 0.45, 0.30, 0.15]
    rate view_count_between: [25_000, 50_000], then: [0.10, 0.35, 0.25, 0.30]
    rate view_count_more_than: 50_000, then: [0.10, 0.25, 0.18, 0.47]

  end
end

CreditAllocation::NonSurveyContest contest: non_suervey_contest

class CreditAllocation
  class SurveyContest < self

    credit view_count_less_than: 1000, then: 0
    credit view_count_between: [1000, 5000], then: 25
    credit view_count_between: [5001, 10_000], then: 50
    credit view_count_between: [10_001, 50_000], then: 100
    credit view_count_more_than: 50_000, then: 300

    rate default: [1.0, 0.0]

  end
end

CreditAllocation::SurveyContest contest: suervey_contest

现在我们有积分奖励语言并且有健壮的CreditAllocation解释器(通过了完善的单元测试),那么我们会发现给系统增加一种新的,有不同积分奖励规则的比赛是一件令人愉快的事情。

Mark24

Everything can Mix.