Статьи

Переменные класса — A Ruby Gotcha

Переменные класса в Ruby имеют дурное имя. Это правда, они считаются вредными, пограничным злом. Так почему плохой рэп? Ну, большинство стонов о том, как они ведут себя, когда наследство вступает в драку. Исправление: это все о том, как они ведут себя по сравнению с тем, что мы ожидаем.

Чтобы нарисовать картину нашего маленького проблемного ребенка, давайте посмотрим, что мы ожидаем от PHP.

<?php class DemoClass { public $myvar; public function __construct() { $this->myvar = "hello world : "; } public function getMyVar() { echo $this->myvar; } } class Demo2Class extends DemoClass { public function __construct() { $this->myvar = "goodbye world : "; } } $demo1 = new DemoClass(); $demo2 = new Demo2Class(); $demo1->getMyVar(); $demo2->getMyVar(); $demo1->getMyVar(); // Produces // hello world : goodbye world : hello world : " 

Это имеет смысл для меня. Мы определяем класс с конкретным конструктором и абстрактным геттером. При наследовании мы можем переопределить конструктор и установить «переменную класса» в другое значение. Базовый класс не затронут, мир продолжает вращаться.

В Ruby, если мы пытаемся сделать что-то подобное, используя переменные класса, мы сталкиваемся с одной или двумя проблемами.

 class DemoClass @@my_var = nil def initialize @@my_var = "hello world" end def my_var puts @@my_var end end class Demo2Class < DemoClass def initialize @@my_var = "goodby world" end end demo1 = DemoClass.new demo1.my_var demo2 = Demo2Class.new demo2.my_var demo1.my_var # Produces #hello world #goodbye world #goodbye world 

Когда вы подходите к переменным класса в Ruby таким образом, они просто кажутся неправильными. Изменение в my_var в Demo2Class до нашего базового класса. Чтобы смягчить проблему, скажем, мы снова подклассем DemoClass поведение переменной класса my_var — не что иное, как непредсказуемое. Это скрытые глобальные переменные.

Так зачем использовать переменные класса?

На первый взгляд слухи о переменных класса верны. Но теперь мы знаем, что не нужно делать, есть ли сценарий, где переменные уровня класса действительно полезны? Ну, я признаюсь. Я использую их почти в каждом веб-приложении.

Использование методов, описанных в наших примерах, в зависимости от данных инициализации, реализованных с использованием переменных уровня класса как класса «состояние», является довольно плохой идеей. Тем не менее, я использую переменные класса для данных инициализации.

В Rails-приложениях обычно есть как минимум 4 среды: производство, подготовка, разработка и тестирование. Весьма вероятно, что конфигурация меняется в зависимости от среды, будь то адреса электронной почты для исключений почты или URL-адрес изолированной программной среды веб-службы. Сам код выглядит так:

 class AppConfig @@app_config = YAML::load(ERB.new((IO.read("#{RAILS_ROOT}/config/app_config.yml"))).result)[RAILS_ENV] def self.method_missing(sym, *args, &block) @@app_config[sym.to_s] end end 

Это должно быть довольно понятно. Файл YAML анализируется в переменной класса app_config . Then using . Then using method_missing at class level I can retrieve values easily with AppConfig.exception_email_addresses`.

Конфигурация уровня класса

Так что теперь, мы надеемся, мы можем согласиться, что переменные класса не являются полностью злыми. Иногда мы хотим поделиться чертами на уровне классов в иерархии классов. Обычная техника для достижения этой цели без возникновения проблем, описанных ранее, — это использование переменных экземпляра класса.

Несмотря на то, что переменные экземпляра класса выглядят довольно просто, (исходя из PHP они поначалу не имели смысла), мы просто должны немного изменить свое мышление. Я всегда нахожу лучший способ открыть для себя это irb .

 ruby-1.9.2-p290 :001 > class Demo ruby-1.9.2-p290 :002?> @my_instance_variable = "whaaa?" ruby-1.9.2-p290 :003?> end => "whaaa?" ruby-1.9.2-p290 :004 > Demo.class => Class ruby-1.9.2-p290 :005 > Demo.superclass => Object ruby-1.9.2-p290 :006 > Demo.instance_variables => [:my_instance_variable] 

Там, в отличие от более классических языков, классы Ruby (как и все остальное) являются просто объектами с переменными экземпляра. Так как же это знание помогает нам? Возвращаясь к предыдущему примеру и используя небольшой трюк с аксессорами.

 class DemoClass class << self attr_accessor :my_var end @my_var = nil def initialize self.class.my_var = "hello world" end def my_var puts self.class.my_var end end class Demo2Class < DemoClass def initialize self.class.my_var = "goodby world" end end demo1 = DemoClass.new demo1.my_var demo2 = Demo2Class.new demo2.my_var demo1.my_var # Produces # hello world # goodbye world # hello world 

Пример сейчас выглядит довольно ужасно, прежде чем какое-либо объяснение позволяет нам его очистить, чтобы он больше не оскорблял наши глаза.

 class DemoClass class << self attr_accessor :my_var end @my_var = "hello world" end class Demo2Class < DemoClass @my_var = "goodby world" end puts DemoClass.my_var puts Demo2Class.my_var puts DemoClass.my_var 

Если вы не уверены в собственных классах (так называемые синглтон-классы, но изначально я парень PHP, а синглтон-класс поначалу подразумевал что-то совершенно другое), то в этой демонстрации мы просто используем его для создания attr_accessor как обычно, только делая это. этот способ создает его для нашей переменной экземпляра класса.

Завершение

Помните, когда я говорил, что переменные класса не всегда плохи, и показывал пример, когда они совершенно приемлемы (в любом случае, в моей книге). Я столкнулся с проблемой с этим фрагментом AppConfig .

Мне пришлось дублировать заявку для двух разных клиентов. Очевидно, что данные конфигурации были другими, но кодовая база должна была быть такой же. Я хотел сохранить кодовую базу в одном репозитории SCM (исправления ошибок или обновления будут применяться к обеим версиям), и я не хотел использовать git merge mashes, фактически я не хотел, чтобы SCM играл новую роль в Мой рабочий процесс, он предназначен только для управления SCM, а не для управления настройкой между клиентами.

Очевидно, мы бы хотели сохранить конфигурацию в базе данных, чего легко добиться в Rails, используя модель ActiveRecord например:

 # app/models/application_config.rb class ApplicationConfig < ActiveRecord::Base serialize :config_value after_save :reset_config def reset_config AppConfig.reset_configs end end # lib/app_config.rb class AppConfig class << self attr_accessor :configs end @configs = {} def self.method_missing(sym, *args, &block) @configs[sym.to_s] ||= "" end def self.reset_configs self.configs = {} configs = ApplicationConfig.all configs.each do |setting| self.configs = {setting.config_key => setting.config_value}.merge self.configs end end end 

Используя приведенный выше код, я мог оставить постоянную конфигурацию для приложения без рефакторинга какого-либо кода, к которому осуществлялся доступ к данным конфигурации (это был большой выигрыш, поскольку это было довольно большое приложение). Я также мог бы использовать простой эшафот для обновления и редактирования конфигурации на лету. Использование переменных экземпляра класса позволило мне кэшировать данные конфигурации, не method_missing к базе данных для каждого сообщения method_missing полученного классом.

Надеюсь, что рассеянные переменные класса являются бесполезным / злым мифом. Как и большинство вещей, отсутствие понимания и наивных реализаций приводят к тому, что переменные класса имеют плохое имя. Конечно, мне потребовалось некоторое время (и множество наивных реализаций), чтобы понять, что с ними происходит. Что касается вещей собственного класса, ознакомьтесь со статьей Натана Клейнса о метапрограммировании для более подробной информации.