In Progress
Unit 1, Lesson 1
In Progress

Class Accessors

Video transcript & code

In Ruby, when we want to write a simple "getter" or "setter" method for a class, we don't usually take the time to write it out. Instead we use the macros attr_reader, attr_writer, or attr_accessor.

class Foo
  attr_reader :read_only_attribute
  attr_writer :write_only_attribute
  attr_accessor :read_write_attribute
end

Sometimes we want to define class-level accessors rather than, or in addition to, these instance-level accessors. For instance, we might want to be able to get and set a global logger object for a library. And here Ruby lets us down a little; there is no class-level equivalent for these macros.

class MyLib
  # ...
end

MyLib.logger = Logger.new($stderr)

One way to get around this omission is with the class << self syntax. We can open up the class object's singleton class and then use the attr* macros within this block.

class MyLib
  class << self
    attr_accessor :logger
  end
end

MyLib.logger = Logger.new($stderr)

As I mentioned in a previous episode, I'm not a fan of defining class-level methods inside a class << self block. This is one case, however, where I'm halfway willing to make an exception.

I say "halfway", though, because in practice I don't find myself using this technique very often. Instead, because the need to write class-level attribute accessors is relatively rare, I usually just write them out in full.

class MyLib
  def self.logger
    @logger
  end

  def self.logger=(new_logger)
    @logger = new_logger
  end
end

MyLib.logger = Logger.new($stderr)

This also meshes well with the fact that in class-level reader methods, more often than not I want to conditionally set the variable to some sensible default if it hasn't been explicitly set.

class MyLib
  def self.logger
    @logger ||= Logger.new($stderr)
  end

  def self.logger=(new_logger)
    @logger = new_logger
  end
end  

In codebases where ActiveSupport is in use, we can alternatively use the cattr_* macros to establish class-level getter and setter methods. These macros have some nice extra features as well: we can pass a block which will be used to generate a default value; and they also generate convenient instance-level accessors for the class-level attributes.

require 'active_support/core_ext'
require 'logger'

class MyLib
  cattr_accessor(:logger) { Logger.new($stderr) }
end

MyLib.logger # => #<Logger:0x0000000147d1e0 @progname=nil, @level=0, @default_formatter=#<Logger::Formatter:0x0000000147d190 @datetime_format=nil>, @formatter=#<Logger::SimpleFormatter:0x0000000147d0a0 @datetime_format=nil>, @logdev=#<Logger::LogDevice:0x0000000147d140 @shift_size=nil, @shift_age=nil, @filename=nil, @dev=#<IO:<STDERR>>, @mutex=#<Logger::LogDevice::LogDeviceMutex:0x0000000147d118 @mon_owner=nil, @mon_count=0, @mon_mutex=#<Mutex:0x0000000147d0c8>>>>

obj = MyLib.new
obj.logger                      # => #<Logger:0x0000000147d1e0 @progname=nil, @level=0, @default_formatter=#<Logger::Formatter:0x0000000147d190 @datetime_format=nil>, @formatter=#<Logger::SimpleFormatter:0x0000000147d0a0 @datetime_format=nil>, @logdev=#<Logger::LogDevice:0x0000000147d140 @shift_size=nil, @shift_age=nil, @filename=nil, @dev=#<IO:<STDERR>>, @mutex=#<Logger::LogDevice::LogDeviceMutex:0x0000000147d118 @mon_owner=nil, @mon_count=0, @mon_mutex=#<Mutex:0x0000000147d0c8>>>>

But in the absence of ActiveSupport, I generally don't find hand-writing the class attribute accessors to be that much of a chore.

That's it for today. Happy hacking!

Responses