ตัวแปรคลาสใน Ruby

วันนี้เขียน ๆ Ruby อยู่ ก็เจอเรื่องพิศดารอีกเรื่องหนึ่ง เกี่ยวกับเรื่องของตัวแปรคลาส (class variable) กับการสืบทอดคุณสมบัติ (inheritance)

ถ้าอ่าน ๆ ตามหนังสือทั่วไป จะบอกว่าถ้าต้องการตัวแปรสำหรับคลาส ให้ขึ้นหน้าชื่อตัวแปรด้วย @@ ตัวแปรลักษณะนี้ดูเผิน ๆ เหมือน static ใน C++ หรือใน Java แต่ทำไปทำมามันไม่ใช่ และทำให้เกิดความงงงวยอย่างยิ่ง

พิจารณาโปรแกรมด้านล่างนะครับ

class Parent
  @@a = 0
 
  def set_a(b)
    @@a = b
  end
 
  def p_a
    puts @@a
  end
end
 
class Child < Parent
  def do_something
    @@a = 300
  end
end

ในโปรแกรมเรามีคลาสอยู่สองคลาส แล้วก็มีตัวแปรคลาส @@a อยู่

ลองดูตัวอย่างการทำงานนี้นะครับ

irb(main):019:0* p = Parent.new      # 
irb(main):020:0> p.p_a               # output: 0
irb(main):021:0> p.set_a(20)
irb(main):022:0> p.p_a               # output: 20
irb(main):023:0> c = Child.new
irb(main):024:0> c.p_a               # output: 20   (!)
irb(main):025:0> c.do_something
irb(main):026:0> c.p_a               # output: 300
irb(main):027:0> p.p_a               # output: 300  (!!!!)

จะเห็นได้ว่าตัวแปรคลาส @@a ที่ประกาศที่คลาส Parent มันใช้ร่วมกันกับ @@a ที่คลาส Child

ทีนี้พอเราจะใช้ตัวแปรดังกล่าวเก็บค่าที่ใช้เฉพาะในคลาส มันเลยจะไปปนกันเละเทะไปหมด

แล้วทางออกล่ะ?

ไปค้น ๆ มีเขียนถึงเรื่องนี้ไว้หลายที่ เช่น

จะเรียกว่าภาษานี้แข็งแกร่งหรือว่าภาษานี้ประหลาดหรือว่าภาษานี้พิศดารก็เลือกเอาเองแล้วกันนะครับ ขอไปแก้โปรแกรมต่อก่อน ;)

ใน rails ก็มีใช้เยอะเหมือนกัน โดย rails open class Class แล้วเพิ่ม class method ที่ชื่อ cattr_reader, cattr_writer, cattr_accessor หน้าตาประมาณนี้

  def cattr_reader(*syms)
    syms.flatten.each do |sym|
      next if sym.is_a?(Hash)
      class_eval(<<-EOS, __FILE__, __LINE__)
        unless defined? @@#{sym}
          @@#{sym} = nil
        end
 
        def self.#{sym}
          @@#{sym}
        end
 
        def #{sym}
          @@#{sym}
        end
      EOS
    end
  end

ตัวอย่างที่นำไปใช้ ก็เช่น ActiveRecord

$ grep -r cattr_ *
active_record/attribute_methods.rb:      base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
active_record/base.rb:    cattr_accessor :logger, :instance_writer => false
active_record/base.rb:    cattr_accessor :configurations, :instance_writer => false
active_record/base.rb:    cattr_accessor :primary_key_prefix_type, :instance_writer => false
active_record/base.rb:    cattr_accessor :table_name_prefix, :instance_writer => false
active_record/base.rb:    cattr_accessor :table_name_suffix, :instance_writer => false
active_record/base.rb:    cattr_accessor :pluralize_table_names, :instance_writer => false
active_record/base.rb:    cattr_accessor :colorize_logging, :instance_writer => false
active_record/base.rb:    cattr_accessor :default_timezone, :instance_writer => false
active_record/base.rb:    cattr_accessor :allow_concurrency, :instance_writer => false
active_record/base.rb:    cattr_accessor :schema_format , :instance_writer => false
active_record/connection_adapters/abstract/connection_specification.rb:    cattr_accessor :verification_timeout, :instance_writer => false
active_record/connection_adapters/mysql_adapter.rb:      cattr_accessor :emulate_booleans
active_record/fixtures.rb:  cattr_accessor :all_loaded_fixtures
active_record/locking/optimistic.rb:        base.cattr_accessor :lock_optimistically, :instance_writer => false
active_record/migration.rb:    cattr_accessor :verbose
active_record/schema_dumper.rb:    cattr_accessor :ignore_tables 
active_record/validations.rb:    cattr_accessor :default_error_messages

โอ้ว ไม่นึกว่าจะมีใช้เยอะขนาดนี้…

ย้าย Codenone

ประกาศย้าย Codenone ไปใช้ Forum ของ Blognone แทนครับ ตามไปตั้งกระทู้ต่อได้ที่ Codenone Forum (รายละเอียดอ่านจากกระทู้ ย้าย Codenone ไปรวมกับ Blognone)

กระทู้เก่าๆ จะย้ายตามไปในภายหลัง ตอนนี้ปิดการโพสต์กระทู้ไว้ เหลือไว้เฉพาะอ้างอิงเท่านั้น