Omvang van constanten in Ruby-modules

Ik heb een klein probleem met constant bereik in mixin-modules. Laten we zeggen dat ik zoiets heb

module Auth
  USER_KEY = "user" unless defined? USER_KEY
  def authorize
    user_id = session[USER_KEY]
  def
end

De USER_KEY-constante moet standaard “gebruiker” zijn, tenzij deze al is gedefinieerd. Nu zou ik dit op een aantal plaatsen kunnen mixen, maar op een van die plaatsen moet de USER_KEY anders zijn, dus misschien hebben we zoiets als dit

class ApplicationController < ActionController::Base
  USER_KEY = "my_user"
  include Auth
  def test_auth
    authorize
  end
end

Ik zou verwachten dat USER_KEY “my_user” zou zijn bij gebruik in authorize, aangezien het al gedefinieerd is, maar het is nog steeds “user”, overgenomen uit de moduledefinitie van USER_KEY. Heeft iemand enig idee hoe je autorisatie kunt krijgen om de klassenversie van USER_KEY te gebruiken?


Antwoord 1, autoriteit 100%

De USER_KEYdie je hebt opgegeven (zelfs voorwaardelijk) in Authstaat wereldwijd bekend als Auth::USER_KEY. Het wordt niet “vermengd” met het opnemen van modules, hoewel het opnemen van modules kanverwijzen naar de sleutel op een niet-volledig gekwalificeerde manier.

Als je wilt dat elke module (bijv. ApplicationController) zijn eigen USER_KEYkan definiëren, probeer dan dit:

module Auth
  DEFAULT_USER_KEY = 'user'
  def self.included(base)
    unless base.const_defined?(:USER_KEY)
      base.const_set :USER_KEY, Auth::DEFAULT_USER_KEY
    end
  end
  def authorize
    user_id = session[self.class.const_get(:USER_KEY)]
  end
end
class ApplicationController < ActionController::Base
  USER_KEY = 'my_user'
  include Auth
end

Als je echter al deze moeite wilt doen, kun je er net zo goed een klassenmethode van maken:

module Auth
  DEFAULT_USER_KEY = 'user'
  def self.included(base)
    base.extend Auth::ClassMethods
    base.send :include, Auth::InstanceMethods
  end
  module ClassMethods
    def user_key
      Auth::DEFAULT_USER_KEY
    end
  end
  module InstanceMethods
    def authorize
      user_id = session[self.class.user_key]
    end
  end
end
class ApplicationController < ActionController::Base
  def self.user_key
    'my_user'
  end
end

of een accessor op klasniveau:

module Auth
  DEFAULT_USER_KEY = 'user'
  def self.included(base)
    base.send :attr_accessor :user_key unless base.respond_to?(:user_key=)
    base.user_key ||= Auth::DEFAULT_USER_KEY
  end
  def authorize
    user_id = session[self.class.user_key]
  end
end
class ApplicationController < ActionController::Base
  include Auth
  self.user_key = 'my_user'
end

Antwoord 2, autoriteit 78%

Constanten hebben geen globaal bereik in Ruby. Constanten kunnen vanuit elk bereik zichtbaar zijn, maar u moet specificeren waar de constante te vinden is. Wanneer u een nieuwe klasse, module of def begint, begint u een nieuwe scope, en als u een constante van een andere scope wilt, moet u specificeren waar u deze kunt vinden.

X = 0
class C
  X = 1
  module M
    X = 2
    class D
      X = 3
      puts X          # => 3
      puts C::X       # => 1
      puts C::M::X    # => 2
      puts M::X       # => 2
      puts ::X        # => 0
    end
  end
end

Antwoord 3, autoriteit 22%

Hier is een eenvoudige oplossing.

Wijzigingen:

  • Het is niet nodig om te controleren op het bestaan ​​van USER_KEY.
  • Probeer de constante op te zoeken in de module/klasse van de ontvanger (in jouw geval zou dat de controller zijn). Als het bestaat, gebruik het dan, gebruik anders de standaard module/klasse (zie hieronder wat de standaard is).

.

module Auth
  USER_KEY = "user"
  def authorize
    user_key = self.class.const_defined?(:USER_KEY) ? self.class::USER_KEY : USER_KEY
    user_id = session[user_key]
  def
end

Uitleg

Het gedrag dat u ziet, is niet specifiek voor rails, maar is te wijten aan waar ruby ​​naar constanten zoekt als het niet expliciet is vastgelegd via ::(wat ik de “standaard” hierboven noem). Constanten worden opgezocht met behulp van de “lexicale omvang van de code die momenteel wordt uitgevoerd”. Dit betekent dat ruby ​​eerst zoekt naar de constante in de module (of klasse) van de uitvoerende code, en dan naar buiten gaat naar elke volgende omsluitende module (of klasse) totdat hij de constante vindt die op dat bereik is gedefinieerd.

In je controller roep je authorizeaan. Maar wanneer authorizewordt uitgevoerd, bevindt de code die momenteel wordt uitgevoerd zich in Auth. Dus daar worden constanten opgezocht. Als Auth USER_KEYniet had, maar een omsluitende module heeft het, dan zou de omsluitende module worden gebruikt. Voorbeeld:

module Outer
  USER_KEY = 'outer_key'
  module Auth
     # code here can access USER_KEY without specifying "Outer::"
     # ...
  end
end

Een speciaal geval hiervan is de uitvoeringsomgeving op het hoogste niveau, die wordt behandeld als behorend tot de klasse Object.

USER_KEY = 'top-level-key'
module Auth
  # code here can access the top-level USER_KEY (which is actually Object::USER_KEY)
  # ...
end

Een valkuil is het definiëren van een module of klasse met de scoping-operator (::):

module Outer
  USER_KEY = 'outer_key'
end
module Outer::Auth
  # methods here won't be able to use USER_KEY,
  # because Outer isn't lexically enclosing Auth.
  # ...
end

Merk op dat de constante veel later kan worden gedefinieerd dan de methode is gedefinieerd. Het opzoeken gebeurt alleen wanneer USER_KEY wordt geopend, dus dit werkt ook:

module Auth
  # don't define USER_KEY yet
  # ...
end
# you can't call authorize here or you'll get an uninitialized constant error
Auth::USER_KEY = 'user'
# now you can call authorize.

Antwoord 4, autoriteit 6%

Als uw project zich in Rails bevindt, of op zijn minst de module ActiveSupportgebruikt, kunt u de benodigde logische suiker aanzienlijk verminderen:

module Auth
  extend ActiveSupport::Concern
  included do
    # set a global default value
    unless self.const_defined?(:USER_KEY)
      self.const_set :USER_KEY, 'module_user'
    end
  end
end
class ApplicationController < ActionController::Base
  # set an application default value
  USER_KEY = "default_user"
  include Auth  
end
class SomeController < ApplicationController
  # set a value unique to a specific controller
  USER_KEY = "specific_user"
end

Het verbaast me dat niemand deze aanpak heeft voorgesteld, aangezien het scenario van de OP zich in een Rails-app bevond…


Antwoord 5

Er is een veel eenvoudigere oplossing voor de vraag van de OP dan de andere antwoorden hier onthullen:

module Foo
  THIS_CONST = 'foo'
  def show_const
    self.class::THIS_CONST
  end
end
class Bar
  include Foo
  THIS_CONST ='bar'
  def test_it
    show_const
  end
end
class Baz
  include Foo
  def test_it
    show_const
  end
end
2.3.1 :004 > r = Bar.new
 => #<Bar:0x000000008be2c8> 
2.3.1 :005 > r.test_it
 => "bar" 
2.3.1 :006 > z = Baz.new
 => #<Baz:0x000000008658a8> 
2.3.1 :007 > z.test_it
 => "foo" 

Het was het antwoord van @james-a-rosen dat me de inspiratie gaf om dit te proberen. Ik wilde zijn route niet volgen omdat ik verschillende constanten had die door verschillende klassen worden gedeeld, elk met een andere waarde, en zijn methode leek op veel typen.

Other episodes