RoR开发日志五

Ruby on Rails Development Log Five

# Development must go on…

前几日观摩Web Dev,与会嘉宾口若悬河,称道自己两天内写了两千行代码,还故作谦虚,惊诧之余,暗暗感叹自己边学边写,速度实在有如龟行。一段代码,改了又改,改了又改,遇到难写处,我便喜欢起身倒水,一天能喝上十数杯。倒也不是企图从杯水中得些什么灵感,只不过是给自己僵住的大脑来上那么点distraction罢了。话虽然这么说,不断的refactoring确确实实相当痛苦,但最终看到自己的新设想得到实现,心中总有那么一点说不出的喜。

有时会对自己说,万一真的遇到我和我的老伙计——StackOverflow都解决不了的问题该怎么办。说真的,我还确实没怎么想过这个问题,不过也许那时候,我还可以请教我的朋友,我还可以一遍一遍地去思索。有人说要用有限的生命去做无限的为人民服务,我倒是想说,我或许会用无限的时间去解决那么几个有限的问题,如果我告诉你这是勇于追寻真理,那么朋友,或许你应该点醒我:那是因为当下你有的,除了时间,还是时间。

# 全局Logger

我正在制作的战斗系统包括战场、人员两大主要的对象,我的目标是在任何地方使用日志时,都能获取到同一个日志变量,并对战斗进行记录。似乎在类与类之间没法做到这一点,不过凑巧的是StackOverflow上已经有了靠谱的解决方法——Module。

有趣之处在于,在将Module包含到Class中时,Instance methods会被加载到Class中,而Module中的Class methods(事实上并不存在Module methods,他们应该被叫做Class methods)仍然可以直接在Module上直接调用。

module Foo
  # Instance method of module Foo
  def method_a
    puts "a"
  end

  # Class method of module Foo
  def self.method_b
    puts "b"
  end
end

class Bar
  include Foo
end

上面的例子中,运行以下代码会有如下的输出。

irb(main):001:1> bar = Bar.new
irb(main):002:1> bar.method_a
# => a
irb(main):003:1> bar.method_b
# => undefined method `method_b' for #<Bar:0x000055ac39b5d020>
irb(main):004:1> Bar.method_b
# => undefined method `method_b' for #<Bar: Class>
irb(main):004:1> Foo.method_b
# => b

可见,Module中的Class Methods不会被添加到include该Module的Class中,仍然可以正常对Module调用,而这些Class Methods中的Class Variable也相应的可以留存在Module中,从而可以通过Module.accessor_method顺利地读写。如此以来,以下的写法就可以让我们在多个Class中用同样的方法来读写同一个变量了:

module Logging
  def logger
    Logging.logger
  end

  def self.logger
    @logger ||= Logger.new
  end
end

class Battlefield
  include Logging
end

class Combatant
  include Logging
end

如此一来,两个类中的#logger方法便都指向了Logging.logger,而后者正是Module Logging中的@logger。简单的说,我们可以在两个类中操作同一个@logger了。

上面的代码还可以用Skeleton Class的写法class << self来改写:

module Logging
  # 相当于定义了self.logger与self.logger=两个accessor
  class << self
    attr_accessor :logger
  end

  def logger
    Logging.logger
  end

  def logger=(logger)
    Logging.logger = logger
  end
end

这个办法不仅仅可以用来读写日志,在我的实践中,我还用他记录战斗的回合数,相信他还有更多的妙用,等你去发现。

# 控制器名称

我知道给View加上太多logic是不合理的,不过有的时候还是想这么干,比如只有在技能的View里才显示增益效果的表单。这时候可以读取resource的class.name,不过我还是做了做功课,看了看有没有更好的办法。

结果是,并没有,不过热心的网友告诉我,也可以用Controller的名称来辨别。

controller.controller_name

controller.controller_name包含了当前控制器的名称,例如你的控制器叫SkillsController,那么这一方法的返回值就是字符串skills。配合判断语句,可以在不用到resource的情况下判断当前View的位置。

# Custom Seeding

上次问我的攻城狮朋友,该怎么处理游戏的基础数据,例如升级所需经验、英雄属性的种类,他告诉我存数据库方便修改。一开始我并不确信,不过在几个月的实践之后,我发现似乎确实如此,当你想添加一个属性,一种技能类型的时候,如果你没有把他们存在一个地方,那么这一次refactoring将让你难忘至少半个月。

问题就在于,Development和Production并不共用数据库,这意味着我把基础数据存进开发数据库后,我还得手动到Production创建另一份,况且我把这些基础数据用来作了Enum,如果这些数据不存在,服务器甚至没法启动,这时候只能用Seed来代劳了。但Seed文件只能有db/seeds.rb一个,想要分模块导入数据应该怎么做呢?

万能的Stacky告诉了我一个完美的方案,只消建立一个Rake task!

# lib/tasks/custom_seed.rake
namespace :db do
  namespace :seed do

    Dir[File.join(Rails.root, 'db', 'seeds', '*.rb')].each do |filename|
      task_name = File.basename(filename, '.rb').intern

      task task_name => :environment do
        load(filename)
      end
    end

    task :all => :environment do
      Dir[File.join(Rails.root, 'db', 'seeds', '*.rb')].sort.each do |filename|
        load(filename)
      end
    end

  end
end

创建Rake task之后,我把我的多个Seed文件放到db/seeds文件夹下,如此一来,只要用rails db:seed:file_name就可以Seed一个单独的文件,而不必将整个db/seeds.rb都更新到数据库中了。

rails db:seed:all则会导入整个db/seeds文件夹中的Seed。

另一个值得一提的地方是,Rails的find_or_create_by方法让更新基础数据非常方便。

Game.find_or_create_by(name: "initial_experience").update(value: 100000)

这样的Seed可以在没有找到条目的时候自动创建,而如果条目已经存在时则直接更新,相当人性化。

# 时间

我有的是时间吗?似乎也不尽然,开学在即,不知道此后还有多少时间供我挥霍。身边的同学不是在实习,就是在找寻工作,求索落脚之处,或许不知何时起,我也当开始珍惜,珍惜逝去的秋水。

只不过现下还可略作最后的狂欢罢了。