Книга: Программирование на языке Ruby

11.2.4. Создание параметрических классов

11.2.4. Создание параметрических классов

Изучи правила, потом нарушай их.

Басё

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

class Terran
 @@home_planet = "Earth"
 def Terran.home_planet
  @@home_planet
 end
 def Terran.home_planet= (x)
  @@home_planet = x
 end
 #...
end

Все замечательно, но что если нам нужно определить несколько подобных классов? Новичок подумает: «Ну так я просто определю суперкласс!» (листинг 11.12).

Листинг 11.12. Параметрические классы: неправильное решение

class IntelligentLife # Неправильный способ решения задачи!
 @@home_planet = nil
 def IntelligentLife.home_planet
  @@home _planet
 end
 def IntelligentLife.home_planet=(x)
  @@home_planet = x
 end
 #...
end
class Terran < IntelligentLife
 @@home_planet = "Earth"
 #...
end
class Martian < IntelligentLife
 @@home_planet = "Mars"
 #...
end

Но это работать не будет. Вызов Terran.home_planet напечатает не "Earth", а "Mars"! Почему так? Дело в том, что переменные класса — на практике не вполне переменные класса; они принадлежат не одному классу, а всей иерархии наследования. Переменная класса не копируется из родительского класса, а разделяется родителем (и, стало быть, со всеми братьями).

Можно было бы вынести определение переменной класса в базовый класс, но тогда перестали бы работать определенные нами методы класса! Можно было исправить и это, перенеся определения в дочерние классы, однако тем самым губится первоначальная идея, ведь таким образом объявляются отдельные классы без какой бы то ни было «параметризации».

Мы предлагаем другое решение. Отложим вычисление переменной класса до момента выполнения, воспользовавшись методом class_eval. Полное решение приведено в листинге 11.13.

Листинг 11.13. Параметрические классы: улучшенное решение

class IntelligentLife
 def IntelligentLife.home_planet
  class_eval("@@home_planet")
 end
 def IntelligentLife.home_planet=(x)
  class_eval("@@home_planet = #{x}")
 end
 # ...
end
class Terran < IntelligentLife
 @@home_planet = "Earth"
 # ...
end
class Martian < IntelligentLife
 @@home_planet = "Mars"
 # ...
end
puts Terran.home_planet  # Earth
puts Martian.home_planet # Mars

Не стоит и говорить, что механизм наследования здесь по-прежнему работает. Все методы и переменные экземпляра, определенные в классе IntelligentLife, наследуются классами Terran и Martian.

В листинге 11.14 предложено, наверное, наилучшее решение. В нем используются только переменные экземпляра, а от переменных класса мы вообще отказались.

Листинг 11.14. Параметрические классы: самое лучшее решение

class IntelligentLife
 class << self
  attr_accessor :home_planet
 end
 # ...
end
class Terran < IntelligentLife
 self.home_planet = "Earth"
 #...
end
class Martian < IntelligentLife
 self.home_planet = "Mars"
 #...
end
puts Terran.home_planet  # Earth
puts Martian.home_planet # Mars

Здесь мы открываем синглетный класс и определяем метод доступа home_planet. В двух подклассах определяются собственные методы доступа и устанавливается переменная. Теперь методы доступа работают строго в своих классах.

В качестве небольшого усовершенствования добавим еще вызов метода private в синглетный класс:

private :home_planet=

Сделав метод установки закрытым, мы запретили изменять значение вне иерархии данного класса. Как всегда, private реализует «рекомендательную» защиту, которая легко обходится. Но объявление метода закрытым по крайней мере говорит, что мы не хотели, чтобы метод вызывался вне определенного контекста.

Есть и другие способы решения этой задачи. Проявите воображение.

Оглавление книги


Генерация: 1.131. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз