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