Search code examples
rubyincludeextend

How to include a module that extends classmethods and correctly reference class var


My expectations are in the comments.

require 'logger'

module Logging
  attr_accessor :logger

  def logger
    return @logger if @logger # allow items to have own loggers
    @@logger ||= Logger.new(STDERR)
    puts "Instance Class REF ID#{@@logger.__id__}"
    puts "Class ID #{self.class.logger.__id__}"
    @@logger
  end

  module ClassMethods
    def logger= logger
      @logger = logger
    end

    def logger
      @logger ||= Logger.new(STDERR)
      puts "Class Instance REF ID #{@logger.__id__}"
      @logger
    end
  end

  def self.included(base)
    base.extend(ClassMethods)
  end
end

class Test

  include Logging

  def wow
    logger.info 'wow'
  end
end

t = Test.new

# should be the same
puts "Loggers are #{t.logger.__id__ == Test.logger.__id__ ? '' : 'not '}the same"

Test.logger = Logger.new('/dev/null')

# should still be the same
puts "Loggers are #{t.logger.__id__ == Test.logger.__id__ ? '' : 'not '}the same"

lg = Test.logger.__id__
t.logger =  Logger.new(STDERR)

# class logger should be same
puts "Class Logger is #{Test.logger.__id__ == lg ? 'still the' : 'not'} same"

# class and instance logger should be different
puts "Loggers are #{t.logger.__id__ == Test.logger.__id__ ? '' : 'not '}the same"

When executed:

➜  sandbox  irb
1.9.3-p392 :001 > load 'test_l.rb'
Instance Class REF ID70203753590760
Class Instance REF ID 70203753590500
Class ID 70203753590500
Class Instance REF ID 70203753590500

Loggers are not the same  # I expected to be same... :(

Instance Class REF ID70203753590760
Class Instance REF ID 70203753590000
Class ID 70203753590000
Class Instance REF ID 70203753590000

Loggers are not the same  # I expected to be same... :(

Class Instance REF ID 70203753590000
Class Instance REF ID 70203753590000

Class Logger is still the same

Class Instance REF ID 70203753590000

Loggers are not the same 

Solution

  • I've deliberately forgotten how to use @@ variables because they are so confusing and rarely needed.

    Instead, consider only using instance variables, but delegate up to the class level if needed:

    module Logging
      attr_writer :logger
    
      def logger
        defined?(@logger) ? @logger : self.class.logger
      end
    
      module ClassMethods
        def logger=(logger)
          @logger = logger
        end
    
        def logger
          @logger ||= Logger.new(STDERR)
        end
      end
    
      def self.included(base)
        base.extend(ClassMethods)
      end
    end
    
    class Test
      include Logging
    
      # ...
    end