Загрузка...


  • 11.1. Рутинные объектно-ориентированные задачи
  • 11.1.1. Применение нескольких конструкторов
  • 11.1.2. Создание атрибутов экземпляра
  • 11.1.3. Более сложные конструкторы
  • 11.1.4. Создание атрибутов и методов уровня класса
  • 11.1.5. Наследование суперклассу
  • 11.1.6. Опрос класса объекта
  • 11.1.7. Проверка объектов на равенство
  • 11.1.8. Управление доступом к методам
  • 11.1.9. Копирование объектов
  • 11.1.10. Метод initialize_copy
  • 11.1.11. Метод allocate
  • 11.1.12. Модули
  • 11.1.13. Трансформация или преобразование объектов
  • 11.1.14. Классы, содержащие только данные (Struct)
  • 11.1.15. Замораживание объектов
  • 11.2. Более сложные механизмы
  • 11.2.1. Отправка объекту явного сообщения
  • 11.2.2. Специализация отдельного объекта
  • 11.2.3. Вложенные классы и модули
  • 11.2.4. Создание параметрических классов
  • 11.2.5. Использование продолжений для реализации генератора
  • 11.2.6. Хранение кода в виде объекта
  • 11.2.7. Как работает включение модулей?
  • 11.2.8. Опознание параметров, заданных по умолчанию
  • 11.2.9. Делегирование или перенаправление
  • 11.2.10. Автоматическое определение методов чтения и установки на уровне класса
  • 11.2.11. Поддержка различных стилей программирования
  • 11.3. Динамические механизмы
  • 11.3.1. Динамическая интерпретация кода
  • 11.3.2. Метод const_get
  • 11.3.3. Динамическое создание экземпляра класса, заданного своим именем
  • 11.3.4. Получение и установка переменных экземпляра
  • 11.3.5. Метод define_method
  • 11.3.6. Метод const_missing
  • 11.3.7. Удаление определений
  • 11.3.8. Получение списка определенных сущностей
  • 11.3.9. Просмотр стека вызовов
  • 11.3.10. Мониторинг выполнения программы
  • 11.3.11. Обход пространства объектов
  • 11.3.12. Обработка вызовов несуществующих методов
  • 11.3.13. Отслеживание изменений в определении класса или объекта
  • 11.3.14. Определение чистильщиков для объектов
  • 11.4. Заключение
  • Глава 11. ООП и динамические механизмы в Ruby

    Как введение иррациональных чисел… стало удобным мифом упростившим законы арифметики… так физические объекты и постулированные сущности делают наше восприятие реальности более простым и завершенным… Концептуальная схема физических объектов напоминает удобный миф, который проще, чем истина, но при этом содержит тут и там частички истины.

    (Уиллард Ван Орман Квин)

    Это необычная глава. В большинстве других глав рассматривается какой-то конкретный аспект, например строки или файлы, но в этой все иначе. Если расположить «пространство задачи» по одной оси системы координат, то данная глава окажется на другой оси, поскольку содержит по кусочку из всех других областей. Связано это с тем, что объектно-ориентированное программирование и динамичность сами по себе являются не задачами, а парадигмами, которые могут быть применены к решению любой задачи, будь то системное администрирование, низкоуровневое сетевое программирование или разработка приложений для Web.

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

    extend
    ? А о методе
    instance_eval
    ? То, что одному представляется очевидным, может оказаться откровением для другого.

    Мы решили отдать предпочтение полноте. В главу включены некоторые экзотические вещи, которые можно делать с помощью динамического ООП в Ruby, но не забыты и рутинные задачи — на случай, если кто-то не знаком с ними. Мы спустились до самого простого уровня, поскольку многие по-разному представляют себе, где кончается «средний» уровень. И попытались дать кое-какую дополнительную информацию даже при изложении самых базовых вопросов, чтобы оправдать включение их в эту главу. С другой стороны, тем, которые раскрываются в других частях книги, мы здесь не касались.

    Еще два замечания. Во-первых, ничего магического в динамическом ООП нет. Объектная ориентированность языка Ruby и его динамическая природа прекрасно уживаются между собой, но неотъемлемой связи между ними нет. Мы рассказываем о том и другом в одной главе только для удобства. Во-вторых, мы затрагиваем кое-какие особенности языка, которые, строго говоря, не относятся ни к одной из двух заявленных тем. Если хотите, считайте это мелким обманом. Но надо же было поместить их куда-то.

    11.1. Рутинные объектно-ориентированные задачи

    Of his quick objects hath the mind no part,
    Nor his own vision holds what it doth catch…
    (Вильям Шекспир. Сонет 113[12])

    Если вы вообще не знакомы с ООП, то эта глава вас ничему не научит. А если вы понимаете, что такое ООП в языке Ruby, то, наверное, ее и читать не стоит. Если понятия ООП не слишком свежи в памяти, просмотрите главу 1, где мы приводим их краткий обзор (или обратитесь к другой книге).

    С другой стороны, большая часть материала в этой главе изложена в виде руководства и довольно элементарна. Поэтому она будет полезна начинающему и менее интересна для программиста на Ruby среднего уровня. Эта книга организована как устройство ввода/вывода с произвольной выборкой, так что можете свободно пропускать те части, которые вас не интересуют.

    11.1.1. Применение нескольких конструкторов

    В Ruby нет «настоящих» конструкторов, как в C++ или в Java. Сама идея, конечно, никуда не делась, поскольку объекты необходимо создавать и инициализировать, но реализация выглядит иначе.

    В Ruby каждый класс имеет метод класса

    new
    , который вызывается для создания новых объектов. Метод new вызывает специальный определяемый пользователем метод
    initialize
    , который инициализирует атрибуты объекта, после чего new возвращает ссылку на новый объект.

    А если мы хотим иметь несколько конструкторов? Как быть в этом случае?

    Ничто не мешает завести дополнительные методы класса, которые возвращают новые объекты. В листинге 11.1 приведен искусственный пример класса для представления прямоугольника, у которого есть две длины сторон и три значения цвета. Мы создали дополнительные методы класса, предполагающие определенные умолчания для каждого параметра. (Например, квадрат — это прямоугольник, у которого все стороны равны.)

    Листинг 11.1. Несколько конструкторов

    class ColoredRectangle


     def initialize(r, g, b, s1, s2)

      @r, @g, @b, @s1, @s2 = r, g, b, s1, s2

     end


     def ColoredRectangle.white_rect(s1, s2)

      new(0xff, 0xff, 0xff, s1, s2)

     end


     def ColoredRectangle.gray_rect(s1, s2)

      new(0x88, 0x88, 0x88, s1, s2)

     end


     def ColoredRectangle.colored_square(r, g, b, s)

      new(r, g, b, s, s)

     end


     def ColoredRectangle.red_square(s)

      new(0xff, 0, 0, s, s)

     end


     def inspect

      "#@r #@g #@b #@s1 #@s2"

     end

    end


    a = ColoredRectangle.new(0x88, 0xaa, 0xff, 20, 30)

    b = ColoredRectangle.white_rect(15,25)

    с = ColoredRectangle.red_square(40)

    Таким образом, можно определить любое число методов, создающих объекты по различным спецификациям. Вопрос о том, уместен ли здесь термин «конструктор», мы оставим «языковым адвокатам».

    11.1.2. Создание атрибутов экземпляра

    Имени атрибута экземпляра в Ruby всегда предшествует знак

    @
    . Это обычная переменная в том смысле, что она начинает существовать после первого присваивания.

    В ОО-языках часто создаются методы для доступа к атрибутам, чтобы обеспечить сокрытие данных. Мы хотим контролировать доступ к «внутренностям» объекта извне. Обычно для данной цели применяются методы чтения и установки (getter и setter), хотя в Ruby эта терминология не используется. Они просто читают (get) или устанавливают (set) значение атрибута.

    Можно, конечно, запрограммировать такие функции «вручную», как показано ниже:

    class Person


     def name

      @name

     end


     def name=(x)

      @name = x

     end


     def age

      @age

     end


     # ...


    end

    Ho Ruby предоставляет более короткий способ. Метод

    attr
    принимает в качестве параметра символ и создает соответствующий атрибут. Кроме того, он создает одноименный метод чтения, а если необязательный второй параметр равен true, то и метод установки.

    class Person

     attr :name, true # Создаются @name, name, name=

     attr :age        # Создаются @age, age

    end

    Методы

    attr_reader
    ,
    attr_writer
    и
    attr_accessor
    принимают в качестве параметров произвольное число символов. Первый создает только «методы чтения» (для получения значения атрибута); второй — только «методы установки», а третий — то и другое. Пример:

    class SomeClass

     attr_reader :a1, :a2   # Создаются @a1, a1, @a2, a2

     attr_writer :b1, :b2   # Создаются @b1, b1=, @b2, b2 =

     attr_accessor :c1, :c2 # Создаются @c1, c1, c1=, @c2, c2, c2=

     # ...

    end

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

    self
    .

    11.1.3. Более сложные конструкторы

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

    Чтобы справиться со сложностью, можно передать методу

    initialize
    блок (листинг 11.2). Тогда инициализация объекта выполняется в процессе вычисления этого блока. Хитрость в том, что вместо обычного
    eval
    для вычисления блока в контексте объекта, а не вызывающей программы, следует использовать метод
    instance_eval
    .

    Листинг 11.2. «Хитрый» конструктор

    class PersonalComputer

     attr_accessor :manufacturer,

      :model, :processor, :clock,

      :ram, :disk, :monitor,

      :colors, :vres, :hres, :net


     def initialize(&block)

      instance_eval &block

     end


     # Прочие методы...

    end


    desktop = PersonalComputer.new do

     self.manufacturer = "Acme"

     self.model = "THX-1138"

     self.processor = "986"

     self.clock = 9.6  # ГГц

     self.ram =16      # Гб

     self.disk =20     # T6

     self.monitor = 25 # дюймы

     self.colors = 16777216

     self.vres = 1280

     self.hres = 1600

     self.net = "T3"

    end


    p desktop

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

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

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

    А если вам не нужны методы доступа для всех атрибутов? Если хотите, можете избавиться от лишних, вызвав для них метод

    undef
    в конце конструирующего блока. Как минимум, это предотвратит «случайное» присваивание значения атрибуту извне объекта.

    11.1.4. Создание атрибутов и методов уровня класса

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

    new
    , он вызывается для создания новых экземпляров, а потому не может принадлежать никакому конкретному экземпляру.

    Мы можем определять собственные методы класса, как показано в разделе 11.1.1. Конечно, их функциональность не ограничивается конструированием — они могут выполнять любые операции, имеющие смысл именно на уровне класса.

    В следующем далеко не полном фрагменте предполагается, что мы создаем класс для проигрывания звуковых файлов. Метод

    play
    естественно реализовать как метод экземпляра, ведь можно создать много объектов, каждый из которых будет проигрывать свой файл. Но у метода
    detect_hardware
    контекст более широкий; в зависимости от реализации может оказаться, что создавать какие-либо объекты вообще не имеет смысла, если этот метод возвращает ошибку. Следовательно, его контекст — вся среда воспроизведения звука, а не конкретный звуковой файл.

    class SoundPlayer


     MAX_SAMPLE = 192


     def SoundPlayer.detect_hardware

      # ...

     end


     def play

      # ...

     end

    end

    Есть еще один способ объявить этот метод класса. В следующем фрагменте делается практически то же самое:

    class SoundPlayer


    MAX_SAMPLE =192


     def play

      # ...

     end


    end


    def SoundPlayer.detect_hardware

     # ...

    end

    Единственная разница касается использования объявленных в классе констант. Если метод класса объявлен вне объявления самого класса, то эти константы оказываются вне области видимости. Например, в первом фрагменте метод

    detect_hardware
    может напрямую обращаться к константе
    MAX_SAMPLE
    , а во втором придется пользоваться нотацией
    SoundPlayer::MAX_SAMPLE
    .

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

    @
    , а областью видимости является весь класс, а не конкретный его экземпляр.

    Традиционный пример использования переменных класса - подсчет числа его экземпляров. Но они могут применяться всегда, когда информации имеет смысл в контексте класса в целом, а не отдельного объекта. Другой пример приведен в листинге 11.3.

    Листинг 11.3. Переменные и методы класса

    class Metal


     @@current_temp = 70


     attr_accessor :atomic_number


     def Metal.current_temp=(x)

      @@current_temp = x

     end


     def Metal.current_temp

      @@current_temp

     end


     def liquid?

      @@current_temp >= @melting

     end


     def initialize(atnum, melt)

      @atomic_number = atnum

      @melting = melt

     end


    end


    aluminum = Metal.new(13, 1236)

    copper = Metal.new(29, 1982)

    gold = Metal.new(79, 1948)


    Metal.current_temp = 1600


    puts aluminum.liquid? # true

    puts copper.liquid?   # false

    puts gold.liquid?     # false


    Metal.current_temp = 2100


    puts aluminum.liquid? # true

    puts copper.liquid?   # true

    puts gold.liquid?     # true

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

    А если попытаться, что произойдет? Что если мы попробуем напечатать атрибут

    @atomic_number
    из метода
    Metal.current_temp
    ? Обнаружится, что переменная вроде бы существует — никакой ошибки не возникает, — но имеет значение
    nil
    . В чем дело?

    В том, что на самом деле мы обращаемся вовсе не к переменной экземпляра класса

    Metal
    , а к переменной экземпляра класса
    Class
    . (Напомним, что в Ruby
    Class
    — это класс!)

    Мы столкнулись с переменной экземпляра класса (термин заимствован из языка Smalltalk). Дополнительные замечания на эту тему приводятся в разделе 11.2.4.

    В листинге 11.4 иллюстрируются все аспекты этой ситуации.

    Листинг 11.4. Данные класса и экземпляра

    class MyClass


     SOME_CONST = "alpha"     # Константа уровня класса.


     @@var = "beta"           # Переменная класса.

     @var = "gamma"           # Переменная экземпляра класса.


     def initialize

      @var = "delta"          # Переменная экземпляра.

     end


     def mymethod

      puts SOME_CONST         # (Константа класса.)

      puts @@var              # (Переменная класса.)

      puts @var               # (Переменная экземпляра.)

     end


     def MyClass.classmeth1

      puts SOME_CONST         # (Константа класса.)

      puts @@var              # (Переменная класса.)

      puts @var               # (Переменная экземпляра класса.)

     end


    end


    def MyClass.classmeth2

     puts MyClass::SOME_CONST # (Константа класса.)

     # puts @@var             # Ошибка: вне области видимости.

     puts @var                # (Переменная экземпляра класса.)

    end


    myobj = MyClass.new

    MyClass.classmeth1        # alpha, beta, gamma

    MyClass.classmeth2        # alpha, gamma

    myobj.mymethod            # alpha, beta, delta

    Следует еще сказать, что метод класса можно сделать закрытым, воспользовавшись методом

    private_class_method
    . Это аналог метода
    private
    на уровне экземпляра. См. также раздел 11.2.10.

    11.1.5. Наследование суперклассу

    Можно унаследовать класс, воспользовавшись символом

    <
    :

    class Boojum < Snark

     # ...

    end

    Это объявление говорит, что класс

    Boojum
    является подклассом класса
    Snark
    или — что то же самое — класс
    Snark
    является суперклассом класса
    Boojum
    . Всем известно, что каждый буюм является снарком, но не каждый снарк — буюм.

    Ясно, что цель наследования — расширить или специализировать функциональность. Мы хотим получить из общего нечто более специфическое.

    Попутно отметим, что во многих языках, например в C++, допускается множественное наследование (МН). В Ruby, как и в Java, и в некоторых других языках, множественного наследования нет, но наличие классов-примесей компенсирует его отсутствие (см. раздел 11.1.12).

    Рассмотрим несколько более реалистичный пример. У нас есть класс

    Person
    (человек), а мы хотим создать производный от него класс
    Student
    (студент).

    Определим класс

    Person
    следующим образом:

    class Person


     attr_accessor :name, :age, :sex


     def initialize(name, age, sex)

      @name, @age, @sex = name, age, sex

     end


     # ...


    end

    А класс

    Student
    — так:

    class Student < Person


     attr_accessor :idnum, :hours


     def initialize(name, age, sex, idnum, hours)

      super(name, age, sex)

      @idnum = idnum

      @hours = hours

     end


     # ...


    end


    # Создать два объекта.

    a = Person.new("Dave Bowman", 37, "m")

    b = Student.new("Franklin Poole", 36, "m", "000-13-5031", 24)

    Посмотрим внимательно, что здесь сделано. Что за

    super
    , вызываемый из метода
    initialize
    класса
    Student
    ? Это просто вызов соответствующего метода родительского класса. А раз так, то ему передается три параметра (хотя наш собственный метод
    initialize
    принимает пять).

    Не всегда необходимо использовать слово

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

    Если говорить об истинном смысле наследования, то оно, безусловно, описывает отношение «является». Студент является человеком, как и следовало ожидать. Сделаем еще три замечания:

    • Каждый атрибут (и метод) родительского класса отражается в его потомках. Если в классе

    Person
    есть атрибут
    height
    , то класс
    Student
    унаследует его, а если родитель имеет метод
    say_hello
    , такой метод будет и у потомка.

    • Потомок может иметь дополнительные атрибуты и методы, мы это только что видели. Поэтому создание подкласса часто еще называют расширением суперкласса.

    • Потомок может переопределять любые атрибуты и методы своего родителя.

    Последнее замечание подводит нас к вопросу о том, как разрешается вызов метода. Откуда я знаю, вызывается ли метод конкретного класса или его суперкласса?

    Краткий ответ таков: не знаю и не интересуюсь. Если вызывается некий метод от имени объекта класса

    Student
    , то будет вызван метод, определенный в этом классе, если он существует. А если нет, вызывается метод суперкласса и так далее вверх по иерархии наследования. Мы говорим «и так далее», потому что у каждого класса (кроме
    Object
    ) есть суперкласс.

    А что если мы хотим вызвать метод суперкласса, но не из соответствующего метода подкласса? Можно сначала создать в подклассе синоним:

    class Student # Повторное открытие класса.


     # Предполагается, что в классе Person есть метод say_hello...


     alias :say_hi :say_hello


     def say_hello

      puts "Привет."

     end


     def formal_greeting

      # Поприветствовать так, как принято в суперклассе.

      say_hi

     end


    end

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

    11.1.6. Опрос класса объекта

    Часто возникает вопрос: «Что это за объект? Как он соотносится с данным классом?» Есть много способов получить тот или иной ответ.

    Во-первых, метод экземпляра

    class
    всегда возвращает класс объекта. Применявшийся ранее синоним
    type
    объявлен устаревшим.

    s = "Hello"

    n = 237

    sc = s.class # String

    nc = n.class # Fixnum

    He думайте, будто методы

    class
    или
    type
    возвращают строку, представляющую имя класса. На самом деле возвращается экземпляр класса
    Class
    ! При желании мы могли бы вызвать метод класса, определенный в этом типе, как если бы это был метод экземпляра класса
    Class
    (каковым он в действительности и является).

    s2 = "some string"

    var = s2.class            # String

    my_str = var.new("Hi...") # Новая строка.

    Можно сравнить такую переменную с константным именем класса и выяснить, равны ли они; можно даже использовать переменную в роли суперкласса и определить на ее основе подкласс! Запутались? Просто помните, что в Ruby

    Class
    — это объект, a
    Object
    — это класс.

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

    instance_of?
    , например:

    puts (5.instance_of? Fixnum)       # true

    puts ("XYZZY".instance_of? Fixnum) # false

    puts ("PLUGH".instance_of? String) # true

    А если нужно принять во внимание еще и отношение наследования? К вашим услугам метод

    kind_of?
    (похожий на
    instance_of?
    ). У него есть синоним
    is_a?
    , что вполне естественно, ибо мы описываем классическое отношение «является».

    n = 9876543210

    flag1 = n.instance_of? Bignum # true

    flag2 = n.kind_of? Bignum     # true

    flag3 = n.is_a? Bignum        # true

    flag3 = n.is_a? Integer       # true

    flag4 = n.is_a? Numeric       # true

    flag5 = n.is_a? Object        # true

    flag6 = n.is_a? String        # false

    flag7 = n.is_a? Array         # false

    Ясно, что метод

    kind_of
    или
    is_a?
    более общий, чем
    instance_of?
    . Например, всякая собака — млекопитающее, но не всякое млекопитающее — собака.

    Для новичков в Ruby приготовлен один сюрприз. Любой модуль, подмешиваемый в класс, становится субъектом отношения «является» для экземпляров этого класса. Например, в класс

    Array
    подмешан модуль
    Enumerable
    ; это означает, что всякий массив является перечисляемым объектом.

    x = [1, 2, 3]

    flag8 = x.kind_of? Enumerable # true

    flag9 = x.is_a? Enumerable    # true

    Для сравнения двух классов можно пользоваться также операторами сравнения. Интуитивно очевидно, что оператор «меньше» обозначает наследование суперклассу.

    flag1 = Integer < Numeric # true

    flag2 = Integer < Object  # true

    flag3 = Object == Array   # false

    flag4 = IO >= File        # true

    flag5 = Float < Integer   # nil

    В любом классе обычно определен оператор «тройного равенства»

    ===
    . Выражение
    class === instance
    истинно, если экземпляр
    instance
    принадлежит классу
    class
    . Этот оператор еще называют оператором ветвящегося равенства, потому что он неявно используется в предложении
    case
    . Дополнительную информацию о нем вы найдете в разделе 11.1.7.

    Упомянем еще метод

    respond_to
    . Он используется, когда нам безразлично, какому классу принадлежит объект, но мы хотим знать, реализует ли он конкретный метод. Это рудиментарный вид получения информации о типе. (Вообще-то можно сказать, что это самая важная информация о типе.) Методу
    respond_to
    передается символ и необязательный флаг, который говорит, нужно ли включать в рассмотрение также и закрытые методы.

    # Искать открытые методы.

    if wumpus.respond_to?(:bite)

     puts "У него есть зубы!"

    else

     puts "Давай-ка подразним его."

    end


    # Необязательный второй параметр позволяет

    # просматривать и закрытые методы.

    if woozle.respond_to?(:bite,true)

     puts "Вузлы кусаются!"

    else

     puts "Ага, это не кусающийся вузл."

    end

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

    superclass
    класса
    Class
    .

    array_parent = Array.superclass  # Object

    fn_parent = 237.class.superclass # Integer

    obj_parent = Object.superclass   # nil

    У любого класса, кроме

    Object
    , есть суперкласс.

    11.1.7. Проверка объектов на равенство

    Все животные равны, но некоторые равнее других.

    (Джордж Оруэлл, «Скотный двор»)

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

    <=>
    и подмешать модуль
    Comparable
    . Тогда к объектам вашего класса будут применимы все обычные операции сравнения.

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

    Самым главным является метод

    equal?
    (унаследованный от класса
    Object
    ); он возвращает
    true
    , если вызывающий объект и параметр имеют один и тот же идентификатор объекта. Это фундаментальный аспект семантики объектов, поэтому переопределять его не следует.

    Самым распространенным способом проверки на равенство является старый добрый оператор

    ==
    , который сравнивает значения вызывающего объекта и аргумента. Наверно, интуитивно это наиболее очевидный способ.

    Следующим в шкале абстракции стоит метод

    eql?
    — тоже часть класса
    Object
    . (На самом деле метод
    eql?
    реализован в модуле
    Kernel
    , который подмешивается в
    Object
    .) Как и оператор
    ==
    , этот метод сравнивает значения вызывающего объекта и аргумента, но несколько более строго. Например, разные числовые объекты при сравнении с помощью
    ==
    приводятся к общему типу, но метод
    eql?
    никогда не считает объекты разных типов равными.

    flag1 = (1 == 1.0)    # true

    flag2 = (1.eql?(1.0)) # false

    Метод

    eql?
    существует по одной-единственной причине: для сравнения значений ключей хэширования. Если вы хотите переопределить стандартное поведение Ruby при использовании объектов в качестве ключей хэша, то переопределите методы
    eql?
    и
    hash
    .

    Любой объект реализует еще два метода сравнения. Метод

    ===
    применяется для сравнения проверяемого значения в предложении
    case
    с каждым селектором:
    selector===target
    . Хотя правило на первый взгляд кажется сложным, на практике оно делает предложения
    case
    в Ruby интуитивно очевидными. Например, можно выполнить ветвление по классу объекта:

    case an_object

     when String

      puts "Это строка."

     when Numeric

      puts "Это число."

     else

      puts "Это что-то совсем другое."

    end

    Эта конструкция работает, потому что в классе

    Module
    реализован метод
    ===
    , проверяющий, принадлежит ли параметр тому же классу, что вызывающий объект (или одному из его предков). Поэтому, если
    an_object
    — это строка «cat», выражение
    string === an_object
    окажется истинным и будет выбрана первая ветвь в предложении
    case
    .

    Наконец, в Ruby реализован оператор сопоставления с образцом

    =~
    . Традиционно он применяется для сопоставления строки с регулярным выражением. Но если вы найдете ему применение в других классах, то можете переопределить.

    У операторов

    ==
    и
    =~
    есть противоположные формы:
    !=
    и
    !~
    соответственно. Внутри они реализованы путем обращения значения основной формы. Это означает, что если, например, вы реализовали метод
    ==
    , то метод
    !=
    получаете задаром.

    11.1.8. Управление доступом к методам

    В Ruby объект определяется, прежде всего, своим интерфейсом: теми методами, которые он раскрывает внешнему миру. Но при написании класса часто возникает необходимость во вспомогательных методах, вызывать которые извне класса опасно. Тут-то и приходит на помощь метод

    private
    класса
    Module
    .

    Использовать его можно двумя способами. Если в теле класса или модуля вы вызовете

    private
    без параметров, то все последующие методы будут закрытыми в данном классе или модуле. Если же вы передадите ему список имен методов (в виде символов), то эти и только эти методы станут закрытыми. В листинге 11.5 показаны оба варианта.

    Листинг 11.5. Закрытые методы

    class Bank

     def open_safe

      # ...

     end


     def close_safe

      # ...

     end


     private :open_safe, :close_safe


     def make_withdrawal(amount)

      if access_allowed

       open_safe

       get_cash(amount)

       close_safe

      end

     end


     # Остальные методы закрытые.


    private


     def get_cash

      # ...

     end


     def access_allowed

      # ...

     end

    end

    Поскольку методы из семейства

    attr
    просто определяют методы, метод
    private
    определяет и видимость атрибутов.

    Реализация метода

    private
    может показаться странной, но на самом деле она весьма хитроумна. К закрытым методам нельзя обратиться, указав вызывающий объект; они вызываются только от имени неявно подразумеваемого объекта
    self
    . То есть вызвать закрытый метод из другого объекта не удастся: просто не существует способа указать объект, от имени которого данный метод вызывается. Заодно это означает, что закрытые методы доступны подклассам того класса, в котором определены, но опять же в рамках одного объекта.

    Модификатор доступа

    protected
    налагает меньше ограничений. Защищенные методы доступны только экземплярам того класса, в котором определены, и его подклассов. Для защищенного метода разрешается указывать вызывающий объект, так что к ним можно обращаться из других объектов (при условии, что вызывающий и вызываемый объекты принадлежат одному классу). Обычно защищенные методы применяются для определения методов доступа, чтобы два объекта одного типа могли взаимодействовать. В следующем примере объекты класс
    Person
    можно сравнивать по возрасту, но сам возраст недоступен вне класса
    Person
    :

    class Person

     def initialize(name, age)

      @name, @age = name, age

     end


     def <=>(other)

      age <=> other.age

     end


     attr_reader :name, :age

     protected :age

    end


    p1 = Person.new("fred", 31)

    p2 = Person.new("agnes", 43)

    compare = (p1 <=> p2) # -1

    x = p1.age            # Ошибка!

    Чтобы завершить картину, модификатор

    public
    делает метод открытым. Неудивительно!..

    И последнее: методы, определенные вне любого класса и модуля (то есть на верхнем уровне программы), по умолчанию закрыты. Поскольку они определены в классе

    Object
    , то видимы глобально, но обращаться к ним с указанием вызывающего объекта нельзя.

    11.1.9. Копирование объектов

    Встроенные методы

    Object#clone
    и
    #dup
    порождают копию вызывающего объекта. Различаются они объемом копируемого контекста. Метод
    #dup
    копирует только само содержимое объекта, тогда как
    clone
    сохраняет и такие вещи, как синглетные классы, ассоциированные с объектом.

    s1 = "cat"


    def s1.upcase

     "CaT"

    end


    s1_dup = s1.dup

    s1_clone = s1.clone

    s1              #=> "cat"

    s1_dup.upcase   #=> "CAT" (синглетный метод не копируется)

    s1_clone.upcase #=> "СаТ" (используется синглетный метод)

    И

    dup
    , и
    clone
    выполняют поверхностное копирование, то есть копируют лишь содержимое самого вызывающего объекта. Если вызывающий объект содержит ссылки на другие объекты, то последние не копируются — копия будет ссылаться на те же самые объекты. Проиллюстрируем это на примере. Объект
    arr2
    — копия
    arr1
    , поэтому изменение элемента целиком, например
    arr2[2]
    , не оказывает влияния на
    arr1
    . Но исходный массив и его копия содержат ссылку на один и тот же объект
    String
    , поэтому изменение строки через
    arr2
    приведет к такому же изменению значения, на которое ссылается
    arr1
    .

    arr1 = [ 1, "flipper", 3 ]

    arr2 = arr1.dup


    arr2[2] = 99

    arr2[1][2] = 'a'


    arr1 # [1, "flapper", 3]

    arr2 # [1, "flapper", 99]

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

    Самый «чистый» способ — потребовать, чтобы классы реализовывали метод

    deep_copy
    . Он мог бы рекурсивно обходить все объекты, на которые ссылается исходный объект, и вызывать для них метод
    deep_copy
    . Необходимо было бы еще добавить метод
    deep_copy
    во все встроенные классы Ruby, которыми вы пользуетесь.

    Но есть и более быстрый способ с использованием модуля

    Marshal
    . Если вы сериализуете исходный объект, представив его в виде строки, а затем загрузите в новый объект, то этот новый объект будет копией исходного.

    arr1 = [ 1, "flipper", 3 ]

    arr2 = Marshal.load(Marshal.dump(arr1))


    arr2[2] = 99

    arr2[1][2] = 'a'


    arr1 # [1, "flipper", 3]

    arr2 # [1, "flapper", 99]

    Обратите внимание, что изменение строки через

    arr2
    не отразилось на строке, на которую ссылается
    arr1
    .

    11.1.10. Метод initialize_copy

    При копировании объекта методом

    dup
    или
    clone
    конструктор не вызывается. Копируется вся информация о состоянии.

    Но что делать, если вам такое поведение не нужно? Рассмотрим пример:

    class Document

     attr_accessor :title, :text

     attr_reader :timestamp


     def initialize(title, text)

      @title, @text = title, text

      @timestamp = Time.now

     end

    end


    doc1 = Document.new("Random Stuff",File.read("somefile"))

    sleep 300 # Немного подождем...

    doc2 = doc1.clone


    doc1.timestamp == doc2.timestamp # true

    # Оп... временные штампы одинаковы!

    При создании объекта

    Document
    с ним ассоциируется временной штамп. При копировании объекта копируется и его временной штамп. А как быть, если мы хотим запомнить время, когда было выполнено копирование?

    Для этого нужно определить метод

    initialize_copy
    . Он вызывается как раз при копировании объекта. Этот метод аналогичен
    initialize
    и позволяет полностью контролировать состояние объекта.

    class Document # Определяем новый метод в классе.

     def initialize_copy(other)

      @timestamp = Time.now

     end

    end


    doc3 = Document.new("More Stuff", File.read("otherfile"))

    sleep 300                        # Немного подождем...

    doc4 = doc3.clone


    doc3.timestamp == doc4.timestamp # false

    # Теперь временные штампы правильны.

    Отметим, что метод

    initialize_copy
    вызывается после того, как вся информация скопирована. Поэтому мы и опустили строку:

    @title, @text = other.title, other.text

    Кстати, если метод

    initialize_copy
    пуст, то поведение будет такое же, как если бы он не был определен вовсе.

    11.1.11. Метод allocate

    Редко, но бывает, что нужно создать объект, не вызывая его конструктор (в обход метода

    initialize
    ). Например, может статься, что состояние объекта полностью определяется методами доступа к нему; тогда не нужно вызывать метод
    new
    (который вызовет
    initialize
    ), разве что вы сами захотите это сделать. Представьте, что для инициализации состояния объекта вы собираете данные по частям: начать следует с «пустого» объекта, а не получить все данные заранее, а потом вызвать конструктор.

    Метод

    allocate
    появился в версии Ruby 1.8, чтобы упростить решение этой задачи. Он возвращает «чистый», еще не инициализированный объект класса.

    class Person

     attr_accessor :name, :age, :phone


     def initialize(n,a,p)

      @name, @age, @phone = n, a, p

     end

    end


    p1 = Person.new("John Smith",29,"555-1234")


    p2 = Person.allocate


    p p1.age # 29

    p p2.age # nil

    11.1.12. Модули

    Для использования модулей в Ruby есть две основных причины. Первая — облегчить управление пространством имен; если поместить константы и методы в модули, то будет меньше конфликтов имен. Хранящийся таким образом метод (метод модуля) вызывается с указанием имени модуля, то есть без вызывающего объекта. Точно так же вызывается и метод класса. Увидев вызовы вида

    File.ctime
    или
    FileTest.exist?
    , мы не можем определить по контексту, что
    File
    — это класс, а
    FileTest
    — модуль.

    Вторая причина более интересна: мы можем использовать модуль как примесь. Примеси — это способ реализации множественного наследования, при котором наследуется только интерфейс.

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

    Но оказывается, модуль может иметь методы экземпляра. Они становятся частью класса, который включил модуль директивой

    include
    .

    module MyMod


     def meth1

      puts "Это метод 1."

     end


    end


    class MyClass


     include MyMod


     # ...

    end


    x = MyClass.new

    x.meth1 # Это метод 1.

    Здесь модуль

    MyMod
    подмешан к классу
    MyClass
    , а метод экземпляра
    meth1
    унаследован. Вы видели также, как директива
    include
    употребляется на верхнем уровне программы; в таком случае модуль подмешивается к классу
    Object
    .

    А что происходит с методами модуля, если таковые определены? Если вы думаете, что они становятся методами класса, то ошибаетесь. Методы модуля не подмешиваются.

    Но если такое поведение желательно, то его можно реализовать с помощью нехитрого трюка. Существует метод

    append_features
    , который можно переопределить. Он вызывается с параметром — «целевым» классом или модулем (в который включается данный модуль). Пример приведен в листинге 11.6.

    Листинг 11.6. Включение модуля с переопределенным методом append_features

    module MyMod


     def MyMod.append_features(someClass)

      def someClass.modmeth

       puts "Метод модуля (класса) "

      end

      super # Этот вызов обязателен!

     end


     def meth1

      puts "Метод 1"

     end

    end


    class MyClass


     include MyMod


     def MyClass.classmeth

      puts "Метод класса"

     end


     def meth2

      puts "Метод 2"

     end

    end


    x = MyClass.new


    # Выводится:

    MyClass.classmeth # Метод класса

    x.meth1           # Метод 1

    MyClass.modmeth   # Метод модуля (класса)

    x.meth2           # Метод 2

    Этот пример заслуживает детального рассмотрения. Во-первых, надо понимать, что метод

    append_features
    не просто вызывается в ходе выполнения
    include
    ; на самом деле именно он и несет ответственность за включение. Поэтому-то вызов super необходим, без него оставшаяся часть модуля (в данном случае метод
    meth1
    ) вообще не была бы включена.

    Отметим также, что внутри тела

    append_features
    имеется определение метода. Выглядит это необычно, но работает, поскольку вложенное определение порождает синглетный метод (уровня класса или модуля). Попытка определить таким образом метод экземпляра привела бы к ошибке
    Nested method error
    (Ошибка при определении вложенного метода).

    Модуль мог бы захотеть узнать, кто был инициатором примеси. Для этого тоже можно воспользоваться методом

    append_features
    , потому что класс-инициатор передается ему в качестве параметра.

    Можно также подмешивать методы экземпляра модуля как методы класса. В листинге 11.7 приведен соответствующий пример.

    Листинг 11.7. Методы экземпляра модуля становятся методами класса

    module MyMod


     def meth3

      puts "Метод экземпляра модуля meth3"

      puts "может стать методом класса."

     end


    end


    class MyClass


     class << self # Здесь self - это MyClass.

      include MyMod

     end


    end


    MyClass.meth3


    # Выводится:

    # Метод экземпляра модуля meth3

    # может стать методом класса.

    Здесь полезен метод

    extend
    . Тогда пример можно записать так:

    class MyClass

     extend MyMod

    end

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

    Можно подмешивать модуль к объекту, а не классу (например, методом

    extend
    ), см. по этому поводу раздел 11.2.2.

    Важно понимать еще одну вещь. В вашем классе можно определить методы, которые будут вызываться из примеси. Это удивительно мощный прием, знакомый тем, кто пользовался интерфейсами в Java.

    Классический пример, с которым мы уже сталкивались ранее, — подмешивание модуля

    Comparable
    и определение метода
    <=>
    . Поскольку подмешанные методы могут вызывать метод сравнения, то мы получаем операторы
    <
    ,
    >
    ,
    <=
    и т.д.

    Другой пример — подмешивание модуля

    Enumerable
    и определение метода
    <=>
    и итератора
    each
    . Тем самым мы получаем целый ряд полезных методов:
    collect
    ,
    sort
    ,
    min
    ,
    max
    и
    select
    .

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

    11.1.13. Трансформация или преобразование объектов

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

    to_s
    .

    Каждый объект можно тем или иным способом представить в виде строки. Но не каждый объект может успешно «прикинуться» строкой. Именно в этом и состоит различие между методами

    to_s
    и
    to_str
    . Рассмотрим этот вопрос подробнее.

    При использовании метода

    puts
    или интерполяции в строку (в контексте
    #{...}
    ) ожидается, что в качестве параметра будет передан объект
    string
    . Если это не так, объект просят преобразовать себя в
    string
    , посылая ему сообщение
    to_s
    . Именно здесь вы можете определить, как объект следует отобразить; просто реализуйте метод
    to_s
    в своем классе так, чтобы он возвращал подходящую строку.

    class Pet


     def initialize(name)

      @name = name

     end


     # ...


     def to_s

      "Pet: #@name"

     end

    end

    Другие методы (например, оператор конкатенации строк

    +
    ) не так требовательны, они ожидают получить нечто достаточно близкое к объекту
    string
    . В этом случае Мац решил, что интерпретатор не будет вызывать метод
    to_s
    для преобразования нестроковых аргументов, поскольку это могло бы привести к большому числу ошибок. Вместо этого вызывается более строгий метод
    to_str
    . Из всех встроенных классов только
    String
    и
    Exception
    реализуют
    to_str
    , и лишь
    String
    ,
    Regexp
    и
    Marshal
    вызывают его. Увидев сообщение
    TypeError: Failed to convert xyz into string
    , можно смело сказать, что интерпретатор пытался вызвать
    to_str
    и потерпел неудачу.

    Вы можете реализовать метод

    to_str
    и самостоятельно, например, если хотите, чтобы строку можно было конкатенировать с числом:

    class Numeric


     def to_str

      to_s

     end


    end


    label = "Число " + 9 # "Число 9"

    Аналогично обстоит дело и в отношении массивов. Для преобразования объекта в массив служит метод

    to_a
    , а метод
    to_ary
    вызывается, когда ожидается массив и ничего другого, например в случае множественного присваивания. Допустим, есть предложение такого вида:

    а, b, с = x

    Если

    x
    — массив из трех элементов, оно будет работать ожидаемым образом. Но если это не массив, интерпретатор попытается вызвать метод
    to_ary
    для преобразования в массив. В принципе это может быть даже синглетный метод (принадлежащий конкретному объекту). На само преобразование не налагается никаких ограничений, ниже приведен пример (нереалистичный), когда строка преобразуется в массив строк:

    class String


     def to_ary

      return self.split("")

     end


    end


    str = "UFO"

    a, b, с = str # ["U", "F", "O"]

    Метод

    inspect
    реализует другое соглашение. Отладчики, утилиты типа
    irb
    и метод отладочной печати
    p
    вызывают
    inspect
    , чтобы преобразовать объект к виду, пригодному для вывода на печать. Если вы хотите, чтобы во время отладки объект раскрывал свое внутреннее устройство, переопределите
    inspect
    .

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

    Fixnum
    можно прибавить к
    Float
    , а комплексное число
    Complex
    разделить на рациональное. Но для проектировщика языка это проблема. Если метод
    +
    класса
    Fixnum
    получает аргумент типа
    Float
    , то что он должен с ним делать? Он знает лишь, как складывать значения типа
    Fixnum
    . Для решения проблемы в Ruby реализован механизм приведения типов
    coerce
    .

    Когда оператор

    +
    (к примеру) получает аргумент, которого не понимает, он пытается привести вызывающий объект и аргумент к совместимым типам, а затем значения этих типов сложить. Общий принцип использования метода
    coerce
    прямолинеен:

    class MyNumberSystem


     def +(other)

      if other.kind_of?(MyNumberSystem)

       result = some_calculation_between_self_and_other

       MyNumberSystem.new(result)

      else

       n1, n2 = other.coerce(self)

       n1 + n2

      end

     end


    end

    Метод

    coerce
    возвращает массив из двух элементов: аргумент и вызывающий объект, приведенные к совместимым типам.

    В примере выше мы полагались на то, что класс аргумента умеет как-то выполнять приведение. Будь мы законопослушными гражданами, реализовали бы приведение и в собственном классе, чтобы он мог работать с числами других видов. Для этого нужно знать, с какими типами мы можем работать напрямую, и приводить объект к одному из этих типов, когда возникает необходимость. Если мы сами не знаем, как это сделать, следует спросить у родителя:

    def coerce(other)

     if other.kind_of?(Float)

      return other, self.to_f

     elsif other.kind_of?(Integer)

      return other, self.to_i

     else

      super

     end

    end

    Конечно, чтобы этот пример работал, наш объект должен реализовывать методы

    to_i
    и
    to_f
    .

    Метод

    coerce
    можно использовать для реализации автоматического преобразования строк в числа, как это делается в языке Perl:

    class String


     def coerce(n)

      if self['.']

       [n, Float(self)]

      else

       [n, Integer(self)]

      end

     end

    end


    x = 1 + "23" # 24

    y = 23 * "1.23" # 29.29

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

    coerce
    при разработке разного рода числовых классов.

    11.1.14. Классы, содержащие только данные (Struct)

    Иногда нужно просто сгруппировать взаимосвязанные данные, не определяя никакие специфические методы обработки. Можно для этого создать класс:

    class Address


     attr_accessor :street, :city, :state


     def initialize(street1, city, state)

      @street, @city, @state = street, city, state

     end

    end


    books = Address.new("411 Elm St", "Dallas", "TX")

    Такое решение годится, но каждый раз прибегать к нему утомительно; к тому же здесь слишком много повторов. Тут-то и приходит на помощь встроенный класс

    Struct
    . Если вспомогательные методы типа
    attr_accessor
    определяют методы доступа к атрибутам, то
    Struct
    определяет целый класс, который может содержать только атрибуты. Такие классы называются структурными шаблонами.

    Address = Struct.new("Address", :street, :city, :state)

    books = Address.new("411 Elm St", "Dallas", "TX")

    Зачем передавать первым параметром конструктора имя создаваемой структуры и присваивать результат константе (в данном случае

    Address
    )?

    При вызове

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

    Struct.new("Address", :street, :city, :state)

    books = Struct::Address.new("411 Elm St", "Dallas", "TX")

    Создав структурный шаблон, вы вызываете его метод new для создания новых экземпляров данной конкретной структуры. Необязательно присваивать значения всем атрибутам в конструкторе. Опущенные атрибуты получат значение

    nil
    . После того как структура создана, к ее атрибутам можно обращаться с помощью обычного синтаксиса или указывая их имена в скобках в качестве индекса, как будто структура - это объект класса
    Hash
    . Более подробную информацию о классе
    Struct
    можно найти в любом справочном руководстве (например, на сайте ruby.doc.org).

    Кстати, не рекомендуем создавать структуру с именем

    Tms
    , так как уже есть предопределенный класс
    Struct::Tms
    .

    11.1.15. Замораживание объектов

    Иногда необходимо воспрепятствовать изменению объекта. Это позволяет сделать метод

    freeze
    (определенный в классе
    Object
    ). По существу, он превращает объект в константу.

    Попытка модифицировать замороженный объект приводит к исключению

    TypeError
    . В листинге 11.8 приведено два примера.

    Листинг 11.8. Замораживание объекта

    str = "Это тест. "

    str.freeze


    begin

     str << " He волнуйтесь." # Попытка модифицировать.

    rescue => err

     puts "#{err.class} #{err}"

    end


    arr = [1, 2, 3]

    arr.freeze


    begin

     arr << 4 # Попытка модифицировать.

    rescue => err

     puts "#{err.class} #{err}"

    end


    # Выводится:

    # TypeError: can't modify frozen string

    # TypeError: can't modify frozen array

    Однако имейте в виду, что метод

    freeze
    применяется к ссылке на объект, а не к переменной! Это означает, что любая операция, приводящая к созданию нового объекта, завершится успешно. Иногда это противоречит интуиции. В примере ниже мы ожидаем, что операция
    +=
    не выполнится, но все работает нормально. Дело в том, что присваивание — не вызов метода. Эта операция воздействует на переменные, а не на объекты, поэтому новый объект создается беспрепятственно. Старый объект по-прежнему заморожен, но переменная ссылается уже не на него.

    str = "counter-"

    str.freeze

    str += "intuitive" # "counter-intuitive"


    arr = [8, 6, 7]

    arr.freeze

    arr += [5, 3, 0, 9] # [8, 6, 7, 5, 3, 0, 9]

    Почему так происходит? Предложение

    a += x
    семантически эквивалентно
    a = a + x
    . При вычислении выражения
    a + x
    создается новый объект, который затем присваивается переменной
    a
    ! Все составные операторы присваивания работают подобным образом, равно как и другие методы. Всегда задавайте себе вопрос: «Что я делаю — создаю новый объект или модифицирую существующий?» И тогда поведение
    freeze
    не станет для вас сюрпризом.

    Существует метод

    frozen?
    , который сообщает, заморожен ли данный объект.

    hash = { 1 => 1, 2 => 4, 3 => 9 }

    hash.freeze

    arr = hash.to_a

    puts hash.frozen?  # true

    puts arr.frozen?   # false

    hash2 = hash

    puts hash2.frozen? # true

    Как видите (на примере

    hash2
    ), замораживается именно объект, а не переменная.

    11.2. Более сложные механизмы

    Не все в модели ООП, реализованной в Ruby, одинаково очевидно. Что-то сложнее, что-то применяется реже. Линия раздела для каждого программиста проходит в разных местах. В этой части главы мы попытались собрать те средства, которые не так просты или не так часто встречаются в программах.

    Иногда вы задаетесь вопросом, можно ли решить на Ruby ту или иную задачу. Краткий ответ таков: Ruby — богатый, динамический, объектно-ориентированный язык с широким набором разумно ортогональных средств; если нечто можно сделать на каком-то другом языке, то, скорее всего, можно и на Ruby.

    Теоретически все полные по Тьюрингу языки более или менее одинаковы. Весь смысл проектирования языков в поиске осмысленной, удобной нотации. Читателю, сомневающемуся в важности нотации, стоит попробовать написать интерпретатор LISP на языке COBOL или выполнить деление чисел, записанных римскими цифрами.

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

    В этом разделе мы поговорим также о реализации на Ruby различных стилей программирования, например функционального и аспектно-ориентированного. Мы не претендуем на роль экспертов в этих областях, просто приводим мнение других. Относитесь к этому с долей скепсиса.

    11.2.1. Отправка объекту явного сообщения

    В статическом языке вы считаете очевидным, что имя вызываемой функции «зашито» в программу, это часть исходного текста. Динамический язык обладает в данном отношении большей гибкостью.

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

    send
    позволяет использовать
    Symbol
    для представления имени метода.

    Пусть, например, имеется массив объектов, который нужно отсортировать, причем в качестве ключей сортировки хотелось бы использовать разные поля. Не проблема - можно просто написать специализированные блоки для сортировки. Но хотелось бы найти более элегантное решение, позволяющее обойтись одной процедурой, способной выполнить сортировку по любому указанному ключу. В листинге 11.9 такое решение приведено.

    Этот пример был написан для первого издания книги. Теперь метод

    sort_by
    стал стандартным и даже более эффективным, поскольку реализует преобразование Шварца (по имени известного гуру в языке Perl Рэндала Шварца) и сохраняет преобразованные значения вместо многократного их вычисления. Впрочем, листинг 11.9 по-прежнему дает пример использования метода
    send
    .

    Листинг 11.9. Сортировка по любому ключу

    class Person

     attr_reader :name, :age, :height


     def initialize(name, age, height)

      @name, @age, @height = name, age, height

     end


     def inspect

      "#@name #@age #@height"

     end

    end


    class Array

     def sort_by(sym) # Наш вариант метода sort_by.

      self.sort {|x,y| x.send(sym) <=> y.send(sym) }

     end

    end


    people = []

    people << Person.new("Hansel", 35, 69)

    people << Person.new("Gretel", 32, 64)

    people << Person.new("Ted", 36, 68)

    people << Person.new("Alice", 33, 63)


    p1 = people.sort_by(:name)

    p2 = people.sort_by(:age)

    p3 = people.sort_by(:height)


    p p1 # [Alice 33 63, Gretel 32 64, Hansel 35 69, Ted 36 68]

    p p2 # [Gretel 32 64, Alice 33 63, Hansel 35 69, Ted 36 68]

    p p3 # [Alice 33 63, Gretel 32 64, Ted 36 68, Hansel 35 69]

    Отметим еще, что синоним

    __send__
    делает в точности то же самое. Такое странное имя объясняется, вероятно, опасением, что имя
    send
    уже может быть задействовано (случайно или намеренно) для определенного пользователем метода.

    11.2.2. Специализация отдельного объекта

    Я солипсист и, признаться, удивлен, что большинство из нас таковыми не являются.

    (Из письма, полученного Бертраном Расселом)

    В большинстве объектно-ориентированных языков все объекты одного класса ведут себя одинаково. Класс — это шаблон, порождающий объекты с одним и тем же интерфейсом при каждом вызове конструктора.

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

    Слово «синглет» может стать источником путаницы, потому что оно употребляется и в другом смысле - как название хорошо известного паттерна проектирования, описывающего класс, для которого может существовать лишь один объект. Если вас интересует такое использование, обратитесь к библиотеке

    singleton.rb
    .

    В следующем примере мы видим два объекта, оба строки. Для второго мы добавляем метод

    upcase
    , который переопределяет существующий метод с таким же именем.

    а = "hello"

    b = "goodbye"


    def b.upcase # Создать синглетный метод.

     gsub(/(.)(.)/) { $1.upcase + $2 }

    end


    puts a.upcase # HELLO

    puts b.upcase # GoOdBye

    Добавление синглетного метода к объекту порождает синглетный класс для данного объекта, если он еще не был создан ранее. Родителем синглетного класса является исходный класс объекта. (Можно считать, что это анонимный подкласс исходного класса.) Если вы хотите добавить к объекту несколько методов, то можете создать синглетный класс явно:

    b = "goodbye"


    class << b


     def upcase # Создать синглетный метод.

      gsub(/(.){.)/) { $1.upcase + $2 }

     end


     def upcase!

      gsub!(/(.)(.)/) { $1.upcase + $2 }

     end


    end


    puts b.upcase # GoOdBye

    puts b        # goodbye

    b.upcase!

    puts b        # GoOdBye

    Отметим попутно, что у более «примитивных» объектов (например,

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

    Если вам приходилось разбираться в коде библиотек, то наверняка вы сталкивались с идиоматическим использованием синглетных классов. В определении класса иногда встречается такой код:

    class SomeClass


     # Stuff...


     class << self

      # Какой-то код

     end


     # ...продолжение.


    end

    В теле определения класса слово

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

    class TheClass

     class << self

      def hello

       puts "hi"

      end

     end

    end


    # вызвать метод класса

    TheClass.hello # hi

    Еще одно распространенное применение такой техники — определение на уровне класса вспомогательных функций, к которым можно обращаться из других мест внутри определения класса. Например, мы хотим определить несколько функций доступа, которые преобразуют результат своей работы в строку. Можно, конечно, написать отдельно код каждой такой функции. Но есть и более элегантное решение — определить функцию уровня класса

    accessor_string
    , которая сгенерирует необходимые нам функции (как показано в листинге 11.10).

    Листинг 11.10. Метод уровня класса accessor_string

    сlass MyClass


     class << self


      def accessor_string(*names)

       names.each do |name|

        class_eval <<-EOF

        def #{name}

         @#{name}.to_s

        end

        EOF

       end

      end


     end


     def initialize

      @a = [1,2,3]

      @b = Time.now

     end


     accessor_string :a, :b


    end


    о = MyClass.new

    puts o.a # 123

    puts o.b # Mon Apr 30 23:12:15 CDT 2001

    Вы наверняка сможете придумать и другие, более изобретательные применения. Метод

    extend
    подмешивает к объекту модуль. Методы экземпляра, определенные в модуле, становятся методами экземпляра объекта. Взгляните на листинг 11.11.

    Листинг 11.11. Использование метода extend

    module Quantifier


     def any?

      self.each { |x| return true if yield x }

      false

     end


     def all?

      self.each { |x| return false if not yield x }

      true

     end


    end


    list = [1, 2, 3, 4, 5]


    list.extend(Quantifier)


    flag1 = list.any? {|x| x > 5 }      # false

    flag2 = list.any? {|x| x >= 5 }     # true

    flag3 = list.all? {|x| x <= 10 }    # true

    flag4 = list.all? {|x| x % 2 == 0 } # false

    В этом примере к массиву

    list
    подмешаны методы
    any?
    и
    all?
    .

    11.2.3. Вложенные классы и модули

    Классы и модули можно вкладывать друг в друга произвольным образом. Программисты, приступающие к изучению Ruby, могут этого и не знать.

    Основная цель данного механизма — упростить управление пространствами имен. Скажем, в класс

    File
    вложен класс
    Stat
    . Это помогает «инкапсулировать» класс
    Stat
    внутри тесно связанного с ним класса, а заодно оставляет возможность в будущем определить класс
    Stat
    , не конфликтуя с существующим (скажем, для сбора статистики).

    Другой пример дает класс

    Struct::Tms
    . Любая новая структура
    Struct
    помещается в это пространство имен, не «загрязняя» расположенные выше, a
    Tms
    — в действительности тоже
    Struct
    .

    Кроме того, вложенный класс можно создавать просто потому, что внешний мир не должен знать о нем или обращаться к нему. Иными словами, можно создавать целые классы, подчиняющиеся тому же принципу «сокрытия данных», которому переменные и методы экземпляра следуют на более низком уровне.

    class BugTrackingSystem


     class Bug

      #...

     end


     #...


    end

    # Никто снаружи не знает о классе Bug.

    Можно вкладывать класс в модуль, модуль в класс и т.д. Если вы придумаете интересные и изобретательные способы применения этой техники, дайте нам знать.

    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
    реализует «рекомендательную» защиту, которая легко обходится. Но объявление метода закрытым по крайней мере говорит, что мы не хотели, чтобы метод вызывался вне определенного контекста.

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

    11.2.5. Использование продолжений для реализации генератора

    Одно из самых трудных для понимания средств Ruby — продолжение (continuation). Это структурированный способ выполнить нелокальный переход и возврат. В объекте продолжения хранятся адрес возврата и контекст выполнения. В каком-то смысле это аналог функций

    setjmp
    /
    longjmp
    в языке С, но объем сохраняемого контекста больше.

    Метод

    callcc
    из модуля
    Kernel
    принимает блок и возвращает объект класса
    Continuation
    . Возвращаемый объект передается в блок как параметр, что еще больше все запутывает.

    В классе

    Continuation
    есть всего один метод
    call
    , который обеспечивает нелокальный возврат в конец блока
    callсс
    . Выйти из метода
    callcc
    можно либо достигнув конца блока, либо вызвав метод
    call
    .

    Считайте, что продолжение — что-то вроде операции «сохранить игру» в классических «бродилках». Вы сохраняете игру в точке, где все спокойно, а потом пробуете выполнить нечто потенциально опасное. Если эксперимент заканчивается гибелью, то вы восстанавливаете сохраненное состояние игры и пробуете пойти другим путем.

    Самый лучший способ разобраться в продолжениях — посмотреть фильм «Беги, Лола, беги».

    Есть несколько хороших примеров того, как пользоваться продолжениями. Самые лучшие предложил Джим Вайрих (Jim Weirich). Ниже показано, как Джим реализовал «генератор» после дискуссии еще с одним программистом на Ruby, Хью Сассе (Hugh Sasse).

    Идея генератора навеяна методом

    suspend
    из языка Icon (он есть также в Prolog), который позволяет возобновить выполнение функции с места, следующего за тем, где она в последний раз вернула значение. Хью называет это «yield наоборот».

    Библиотека

    generator
    теперь входит в дистрибутив Ruby. Дополнительную информацию по этому вопросу вы найдете в разделе 8.3.7.

    В листинге 11.15 представлена предложенная Джимом реализация генератора чисел Фибоначчи. Продолжения применяются для того, чтобы сохранить состояние между вызовами.

    Листинг 11.15. Генератор чисел Фибоначчи

    class Generator


     def initialize

      do_generation

     end


     def next

      callcc do |here|

       @main_context = here;

       @generator_context.call

      end

     end


     private


     def do_generation

      callcc do |context|

       @generator_context = context;

       return

      end

      generating_loop

     end

     def generate(value)

      callcc do |context|

       @generator_context = context;

       @main_context.call(value)

      end

     end

    end


    # Порождаем подкласс и определяем метод generating_loop.


    class FibGenerator < Generator

     def generating_loop

      generate(1)

      a, b = 1, 1

      loop do

       generate(b)

       a, b = b, a+b

      end

     end

    end


    # Создаем объект этого класса...


    fib = FibGenerator.new


    puts fib.next # 1

    puts fib.next # 1

    puts fib.next # 2

    puts fib.next # 3

    puts fib.next # 5

    puts fib.next # 8

    puts fib.next # 13


    # И так далее...

    Есть, конечно, и более практичные применения продолжений. Один из примеров — каркас

    Borges
    для разработки Web-приложений (названный в честь Хорхе Луиса Борхеса), который построен по образу
    Seaside
    . В этой парадигме традиционный поток управления в Web-приложении «вывернут с изнанки на лицо», так что логика представляется «нормальной». Например, вы отображаете страницу, получаете результат из формы, отображаете следующую страницу и так далее, ни в чем не противореча интуитивным ожиданиям.

    Проблема в том, что продолжение — «дорогая» операция. Необходимо сохранить состояние и потратить заметное время на переключение контекста. Если производительность для вас критична, прибегайте к продолжениям с осторожностью.

    11.2.6. Хранение кода в виде объекта

    Неудивительно, что Ruby предлагает несколько вариантов хранения фрагмента кода в виде объекта. В этом разделе мы рассмотрим объекты

    Proc
    ,
    Method
    и
    UnboundMethod
    .

    Встроенный класс

    Proc
    позволяет обернуть блок в объект. Объекты
    Proc
    , как и блоки, являются замыканиями, то есть запоминают контекст, в котором были определены.

    myproc = Proc.new { |a| puts "Параметр равен #{а}" }


    myproc.call(99) # Параметр равен 99

    Кроме того, Ruby автоматически создает объект Proc, когда метод, последний параметр которого помечен амперсандом, вызывается с блоком в качестве параметра:

    def take_block(x, &block)

     puts block.class

     x.times {|i| block[i, i*i] }

    end


    take_block(3) { |n,s| puts "#{n} в квадрате равно #{s}" }

    В этом примере демонстрируется также применение квадратных скобок как синонима метода

    call
    . Вот что выводится в результате исполнения:

    Proc

    0 в квадрате 0

    1 в квадрате 1

    2 в квадрате 4

    Объект

    Proc
    можно передавать методу, который ожидает блок, предварив имя знаком
    &
    :

    myproc = proc { |n| print n, "... " }

    (1..3).each(&myproc) # 1... 2... 3...

    Ruby позволяет также превратить метод в объект. Исторически для этого применяется метод

    Object#method
    , который создает объект класса
    Method
    как замыкание в конкретном объекте.

    str = "cat"

    meth = str.method(:length)


    a = meth.call # 3 (длина "cat")


    str << "erpillar"


    b = meth.call # 11 (длина "caterpillar")


    str = "dog"


    # Обратите внимание на следующий вызов! Переменная str теперь ссылается

    # на новый объект ("dog"), но meth по-прежнему связан со старым объектом.


    с = meth.call # 11 (длина "caterpillar")

    Начиная с версии Ruby 1.6.2, можно также применять метод

    Module#instance_method
    для создания объектов
    UnboundMethod
    . С их помощью представляется метод, ассоциированный с классом, а не с конкретным объектом. Прежде чем вызывать объект
    UnboundMethod
    , нужно связать его с каким-то объектом. Результатом операции связывания является объект
    Method
    , который можно вызывать как обычно:

    umeth = String.instance_method(:length)


    m1 = umeth.bind("cat")

    m1.call # 3


    m2 = umeth.bind("caterpillar")

    m2.call # 11

    Явное связывание делает объект

    UnboundMethod
    интуитивно более понятным, чем
    Method
    .

    11.2.7. Как работает включение модулей?

    Когда модуль включается в класс, Ruby на самом деле создает прокси-класс, являющийся непосредственным родителем данного класса. Возможно, вам это покажется интуитивно очевидным, возможно, нет. Все методы включаемого модуля «маскируются» методами, определенными в классе.

    module MyMod

     def meth

      "из модуля"

     end

    end


    class ParentClass

     def meth

      "из родителя"

     end

    end


    class ChildClass < ParentClass

     include MyMod

     def meth

      "из потомка"

     end

    end


    x = ChildClass.new p

    p x.meth # Из потомка.

    Выглядит это как настоящее наследование: все, что потомок переопределил, становится действующим определением вне зависимости от того, вызывается ли

    include
    до или после переопределения.

    Вот похожий пример, в котором метод потомка вызывает

    super
    , а не просто возвращает строку. Как вы думаете, что будет возвращено?

    # Модуль MyMod и класс ParentClass не изменились.


    class ChildClass < ParentClass

     include MyMod

     def meth

      "Из потомка: super = " + super

     end

    end


    x = ChildClass.new

    p x.meth # Из потомка: super = из модуля

    Отсюда видно, что модуль действительно является новым родителем класса. А что если мы точно также вызовем

    super
    из модуля?

    module MyMod

     def meth

      "Из модуля: super = " + super

     end

    end


    # ParentClass не изменился.


    class ChildClass < ParentClass

     include MyMod

     def meth

      "Из потомка: super = " + super

     end

    end


    x = ChildClass.new

    p x.meth # Из потомка: super = из модуля: super = из родителя.

    Метод

    meth
    , определенный в модуле
    MyMod
    , может вызвать
    super
    только потому, что в суперклассе (точнее, хотя бы в одном из его предков) действительно есть метод
    meth
    . А что произошло бы, вызови мы этот метод при других обстоятельствах?

    module MyMod

     def meth

      "Из модуля: super = " + super

     end

    end


    class Foo include MyMod

    end


    x = Foo.new

    x.meth

    При выполнении этого кода мы получили бы ошибку

    NoMethodError
    (или обращение к методу
    method_missing
    , если бы таковой существовал).

    11.2.8. Опознание параметров, заданных по умолчанию

    В 2004 году Ян Макдональд (Ian Macdonald) задал в списке рассылки вопрос: «Можно ли узнать, был ли параметр задан вызывающей программой или взято значение по умолчанию?» Вопрос интересный. Не каждый день он возникает, но от того не менее интересен.

    Было предложено по меньшей мере три решения. Самое удачное и простое нашел Нобу Накада (Nobu Nakada). Оно приведено ниже:

    def meth(a, b=(flag=true; 345))

     puts "b равно #{b}, a flag равно #{flag.inspect}"

    end


    meth(123)     # b равно 345, a flag равно true

    meth(123,345) # b равно 345, a flag равно nil

    meth(123,456) # b равно 456, a flag равно nil

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

    flag
    в
    true
    , а затем возвращает значение по умолчанию 345. Это дань могуществу Ruby.

    11.2.9. Делегирование или перенаправление

    В Ruby есть две библиотеки, которые предлагают решение задачи о делегировании или перенаправлении вызовов методов другому объекту. Они называются

    delegate
    и
    forwardable
    ; мы рассмотрим обе.

    Библиотека

    delegate
    предлагает три способа решения задачи. Класс
    SimpleDelegator
    полезен, когда объект, которому делегируется управление (делегат), может изменяться на протяжении времени жизни делегирующего объекта. Чтобы выбрать объект-делегат, используется метод
    __setobj__
    .

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

    SimpleDelegator
    не стану.

    Метод верхнего уровня

    DelegateClass
    принимает в качестве параметра класс, которому делегируется управление. Затем он создает новый класс, которому мы можем унаследовать. Вот пример создания класса
    Queue
    , который делегирует объекту
    Array
    :

    require 'delegate'


    class MyQueue < DelegateClass(Array)


     def initialize(arg=[])

      super(arg)

     end


     alias_method :enqueue, :push

     alias_method :dequeue, :shift

    end


    mq = MyQueue.new


    mq.enqueue(123)

    mq.enqueue(234)


    p mq.dequeue # 123

    p mq.dequeue # 234

    Можно также унаследовать класс

    Delegator
    и реализовать метод
    __getobj__
    ; именно таким образом реализован класс
    SimpleDelegator
    . При этом мы получаем больший контроль над делегированием.

    Но если вам необходим больший контроль, то, вероятно, вы все равно осуществляете делегирование на уровне отдельных методов, а не класса в целом. Тогда лучше воспользоваться библиотекой

    forwardable
    . Вернемся к примеру очереди:

    require 'forwardable'


    class MyQueue

     extend Forwardable


     def initialize(obj=[])

      @queue = obj # Делегировать этому объекту.

     end


     def_delegator :@queue, :push, :enqueue


     def_delegator :@queue, :shift, :dequeue


     def_delegators :@queue, :clear, :empty?, :length, :size, :<<


     # Прочий код...

    end

    Как видно из этого примера, метод

    def_delegator
    ассоциирует вызов метода (скажем,
    enqueue
    ) с объектом-делегатом
    @queue
    и одним из методов этого объекта (
    push
    ). Иными словами, когда мы вызываем метод
    enqueue
    для объекта
    MyQueue
    , производится делегирование методу push объекта
    @queue
    (который обычно является массивом).

    Обратите внимание, мы пишем

    :@queue
    , а не
    :queue
    или
    @queue
    . Объясняется это тем, как написан класс
    Forwardable
    ; можно было бы сделать и по-другому.

    Иногда нужно делегировать методы одного объекта одноименным методам другого объекта. Метод

    def_delegators
    позволяет задать произвольное число таких методов. Например, в примере выше показано, что вызов метода
    length
    объекта
    MyQueue
    приводит к вызову метода
    length
    объекта
    @queue
    .

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

    []
    или
    []=
    для очереди; если вы так поступаете, то очередь перестает быть очередью.

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

    Например, все приведенные ниже вызовы допустимы. (В последних двух предполагается, что предварительно было выполнено предложение

    require 'thread'
    .)

    q1 = MyQueue.new                 # Используется любой массив.

    q2 = MyQueue.new(my_array)       # Используется конкретный массив.

    q3 = MyQueue.new(Queue.new)      # Используется Queue (thread.rb).

    q4 = MyQueue.new(SizedQueue.new) # Используется SizedQueue (thread.rb).

    Так, объекты

    q3
    и
    q4
    волшебным образом становятся безопасными относительно потоков, поскольку делегируют управление безопасному в этом отношении объекту (если, конечно, какой-нибудь не показанный здесь код не нарушит эту гарантию).

    Существует также класс

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

    Быть может, вы задумались о том, что лучше — делегирование или наследование. Но это неправильный вопрос. В некоторых ситуациях делегирование оказывается более подходящим решением. Предположим, к примеру, что имеется класс, у которого уже есть родитель. Унаследовать еще от одного родителя мы не можем (в Ruby множественное наследование запрещено), но делегирование в той или иной форме вполне допустимо.

    11.2.10. Автоматическое определение методов чтения и установки на уровне класса

    Мы уже рассматривали методы

    attr_reader
    ,
    attr_writer
    и
    attr_accessor
    , которые немного упрощают определение методов чтения и установки атрибутов экземпляра. А как быть с атрибутами уровня класса?

    В Ruby нет аналогичных средств для их автоматического создания. Но можно написать нечто подобное самостоятельно.

    В первом издании этой книги была показана хитроумная схема на основе метода

    class_eval
    . С ее помощью мы создали такие методы, как
    cattr_reader
    и
    cattr_writer
    .

    Но есть более простой путь. Откроем синглетный класс и воспользуемся в нем семейством методов

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

    class MyClass


     @alpha = 123          # Инициализировать @alpha.


     class << self

      attr_reader :alpha   # MyClass.alpha()

      attr_writer :beta    # MyClass.beta=()

      attr_accessor :gamma # MyClass.gamma() и

     end                   # MyClass.gamma=()


     def MyClass.look

      puts " #@alpha, #@beta, #@gamma"

     end


     #...

    end


    puts MyClass.alpha # 123

    MyClass.beta = 456

    MyClass.gamma = 789

    puts MyClass.gamma # 789


    MyClass.look       # 123, 456, 789

    Как правило, класс без переменных экземпляра бесполезен. Но здесь мы их для краткости опустили.

    11.2.11. Поддержка различных стилей программирования

    Brother, can you paradigm?

    (Граффити на здании IBM в Остине, 1989)

    В различных кругах популярны разные философии программирования. Часто их трудно охарактеризовать с точки зрения объектной ориентированности или динамичности, а некоторые вообще не зависят от того, является ли язык динамическим или объектно-ориентированным.

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

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

    OpenStruct
    для построения объектов в духе языка Python; не забывайте также о том, как работает метод
    method_missing
    .

    Парочка ограничений в Ruby препятствует реализации ООП без классов. Некоторые объекты, например

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

    В функциональном программировании (ФП) упор делается на вычисление выражений, а не на исполнение команд. Функциональным называется язык, поддерживающий ФП, но на этом всякая определенность заканчивается. Почти все согласятся, что Haskell — настоящий функциональный язык, a Ruby таковым, безусловно, не является.

    Но в Ruby есть минимальная поддержка ФП, он располагает богатым набором методов для манипулирования массивами (списками) и поддерживает объекты

    Proc
    , позволяющие инкапсулировать и многократно вызывать код. Ruby также допускает сцепление методов, весьма распространенное в ФП. Правда, дело портят «восклицательные» методы (например,
    sort!
    или
    gsub!
    ), которые возвращают
    nil
    , если вызывающий объект не изменился в результате выполнения.

    Предпринимались попытки создать библиотеку, которая стала бы «уровнем совместимости» с ФП, заимствуя некоторые идеи из языка Haskell. Пока эти попытки ни к чему завершенному не привели.

    Интересна идея аспектно-ориентированного программирования (АОП). Это попытка рассечь модульную структуру программы. Иными словами, некоторые задачи и механизмы системы разбросаны по разным участкам кода, а не собраны в одном месте. То есть мы пытаемся придать модульность вещам, которым в традиционном объектно-ориентированном или процедурном программировании с трудом поддаются «модуляризации». Взгляд на программу оказывается перпендикулярен обычному.

    Разумеется, Ruby создавался без учета АОП. Но это гибкий и динамический язык, поэтому не исключено, что такой подход может быть реализован в виде библиотеки. Уже сейчас существует библиотека AspectR, представляющая собой первую попытку внести аспектно-ориентированные черты в Ruby; последнюю ее версию можно найти в архиве приложений Ruby.

    Идея «проектирования по контракту» (Design by Contract — DBC) хороша знакома поклонникам языка Eiffel, хотя и вне этого круга она тоже известна. Смысл состоит в том, что некоторый кусок кода (метод или класс) реализует контракт; чтобы код правильно работал, должны выполняться определенные предусловия, и тогда гарантируется, что по завершении работы будут выполнены некоторые постусловия. Надежность системы можно существенно повысить, введя возможность формулировать контракт явно и автоматически проверять его во время выполнения. Полезность такого подхода подкрепляется наследованием информации о контракте при расширении классов.

    В язык Eiffel методология DBC встроена явно, в Ruby — нет. Однако имеется по крайней мере две работающие библиотеки, реализующие DBC, и мы рекомендуем вам выбрать одну из них и изучить внимательнее.

    Паттерны проектирования стали темой оживленных дискуссий на протяжении последних нескольких лет. Конечно, они мало зависят от конкретного языка и могут быть реализованы на самых разных языках. Но необычайная гибкость Ruby, возможно, делает их практически более полезными, чем в других средах. Хорошо известные примеры приведены в других местах; паттерн Visitor (Посетитель) реализуется стандартным итератором

    each
    , а многие другие паттерны входят в стандартный дистрибутив Ruby (библиотеки
    delegator.rb
    и
    singleton.rb
    ).

    С каждым днем все больше приверженцев завоевывает методология экстремального программирования (Extreme Programming — XP), поощряющая, среди прочего, раннее тестирование и постоянную переработку (рефакторинг).

    XP — технология, не зависящая от языка, хотя к некоторым языкам она, возможно, более приспособлена. Разумеется, на наш взгляд, в Ruby рефакторинг реализуется проще, чем во многих языках, но это субъективное мнение. Однако, наличие библиотеки

    Test::Unit
    (и других) позволяет «поженить» Ruby и XP. Эта библиотека облегчает автономное тестирование компонентов, она функциональна богата, проста в использовании и доказала свою полезность в ходе разработки эксплуатируемых в настоящее время программ на Ruby. Мы горячо поддерживаем рекомендуемое XP раннее и частое тестирование, а тем, кто желает воплотить этот совет в Ruby, предлагаем ознакомиться с
    Test::Unit
    . (
    ZenTest
    — еще один отличный пакет, включающий некоторые возможности, которые в
    Test::Unit
    отсутствуют.)

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

    Конференция comp.lang.ruby

    Архив приложений Ruby

    rubyforge.org

    ruby-doc.org

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

    11.3. Динамические механизмы

    Скайнет осознал себя в 2:14 утра по восточному времени 29 августа 1997 года.

    (Терминатор 2, Судный День)

    Многие читатели имеют опыт работы со статическими языками, например С. Им я адресую риторический вопрос: «Можете ли вы представите себе написанную на С функцию, которая принимает строку, рассматривает ее как имя переменной и возвращает значение этой переменной?»

    Нет? А как насчет того, чтобы удалить или заменить определение функции? А перехватить обращения к несуществующим функциям? Или узнать имя вызывающей функции? Или автоматически получить список определенных пользователем элементов программы (например, перечень всех написанных вами функций)?

    В Ruby все это возможно. Такая гибкость во время выполнения, способность опрашивать и изменять программные элементы во время выполнения намного упрощают решение задач. Утилиту трассировки выполнения, отладчик, профилировщик — все это легко написать на Ruby и для Ruby. Хорошо известные программы

    irb
    и
    xmp
    , используя динамические возможности Ruby, творят это волшебство.

    К подобным возможностям нужно привыкнуть, их легко употребить во вред. Все эти идеи появились отнюдь не вчера (они стары по крайней мере так же, как язык LISP) и считаются «проверенными и доказанными» в сообществах пользователей Scheme и Smalltalk. Даже в языке Java, который так многим обязан С и C++, есть некоторые динамические средства, поэтому мы ожидаем, что со временем их популярность будет только расти.

    11.3.1. Динамическая интерпретация кода

    Глобальная функция

    eval
    компилирует и исполняет строку, содержащую код на Ruby. Это очень мощный (и вместе с тем опасный) механизм, поскольку позволяет строить подлежащий исполнению код во время работы программы. Например, в следующем фрагменте считываются строки вида «имя = выражение», затем каждое выражение вычисляется, а результат сохраняется в хэше, индексированном именем переменной.

    parameters = {}


    ARGF.each do |line|

     name, expr = line.split(/\s*=\s*/, 2)

     parameters[name] = eval expr

    end

    Пусть на вход подаются следующие строки:

    а = 1

    b = 2 + 3

    с = 'date'

    Тогда в результате мы получим такой хэш:

    {"а"=>1, "b"=>5,"с"=>"Mon Apr 30 21:17:47 CDT 2001\n"}
    . На этом примере демонстрируется также опасность вычисления с помощью
    eval
    строк, содержимое которых вы не контролируете; злонамеренный пользователь может подсунуть строку
    d= 'rm *'
    и стереть всю вашу дневную работу.

    В Ruby есть еще три метода, которые интерпретируют код «на лету»:

    class_eval
    ,
    module_eval
    и
    instance_eval
    . Первые два — синонимы, и все они выполняют одно и то же: интерпретируют строку или блок, но при этом изменяют значение псевдопеременной
    self
    так, что она указывает на объект, от имени которого эти методы вызваны. Наверное, чаще всего метод
    class_eval
    применяется для добавления методов в класс, на который у вас имеется только ссылка. Мы продемонстрируем это в коде метода
    hook_method
    в примере утилиты
    Trace
    в разделе 11.3.13. Другие примеры вы найдете в динамических библиотечных модулях, например
    delegate.rb
    .

    Метод

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

    Ruby ассоциирует локальные переменные с блоками, с определениями высокоуровневых конструкций (класса, модуля и метода) и с верхним уровнем программы (кодом, расположенным вне любых определений). С каждой из этих областей видимости ассоциируются привязки переменных и другие внутренние детали. Наверное, самым главным потребителем информации о привязках является программа

    irb
    — интерактивная оболочка для Ruby, которая пользуется привязками, чтобы отделить собственные переменные от тех, которые принадлежат вводимой программе.

    Можно инкапсулировать текущую привязку в объект с помощью метода

    Kernel#binding
    . Тогда вы сможете передать привязку в виде второго параметра методу
    eval
    , установив контекст исполнения для интерпретируемого кода.

    def some_method

     а = "local variable"

     return binding

    end


    the_binding = some_method

    eval "a", the_binding # "local variable"

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

    def some_method

     return binding

    end


    the_binding = some_method { puts "hello" }

    eval "yield", the_binding # hello

    11.3.2. Метод const_get

    Метод

    const_get
    получает значение константы с заданным именем из модуля или класса, которому она принадлежит.

    str = "PI"

    Math.const_get(str) # Значение равно Math::PI.

    Это способ избежать обращения к методу

    eval
    , которое иногда считается неэлегантным. Такой подход дешевле с точки зрения потребления ресурсов и безопаснее. Есть и другие аналогичные методы:
    instance_variable_set
    ,
    instance_variable_get
    и
    define_method
    .

    Метод

    const_get
    действительно работает быстрее, чем
    eval
    . В неформальных тестах — на 350% быстрее, хотя у вас может получиться другой результат. Но так ли это важно? Ведь в тестовой программе на 10 миллионов итераций цикла все равно ушло менее 30 секунд.

    Истинная полезность метода

    const_get
    в том, что его проще читать, он более специфичен и лучше самодокументирован. Даже если бы он был всего лишь синонимом
    eval
    , все равно это стало бы большим шагом вперед.

    11.3.3. Динамическое создание экземпляра класса, заданного своим именем

    Такой вопрос мы видели многократно. Пусть дана строка, содержащая имя класса; как можно создать экземпляр этого класса?

    Правильный способ — воспользоваться методом

    const_get
    , который мы только что рассмотрели. Имена всех классов в Ruby — константы в «глобальном» пространстве имен, то есть члены класса
    Object
    .

    classname = "Array"


    klass = Object.const_get(classname)

    x = klass.new(4, 1) # [1, 1, 1, 1]

    А если имена вложены? Как выясняется, следующий код не работает:

    class Alpha

     class Beta

      class Gamma

       FOOBAR =237

      end

     end

    end


    str = "Alpha::Beta::Gamma::FOOBAR"

    val = Object.const_get(str) # Ошибка!

    Дело в том, что метод

    const_get
    недостаточно «умен», чтобы распознать такие вложенные имена. Впрочем, в следующем примере приведена работающая идиома:

    # Структура класса та же


    str = "Alpha::Beta::Gamma::FOOBAR"

    val = str.split("::").inject(Object) {|x,y| x.const_get(y) } # 237

    Такой код встречается часто (и демонстрирует интересное применение

    inject
    ).

    11.3.4. Получение и установка переменных экземпляра

    Отвечая на пожелание употреблять

    eval
    как можно реже, в Ruby теперь включены методы, которые могут получить или присвоить новое значение переменной экземпляра, имя которой задано в виде строки:

    class MyClass

     attr_reader :alpha, :beta


     def initialize(a,b,g)

      @alpha, @beta, @gamma = a, b, g

     end

    end


    x = MyClass.new(10,11,12)


    x.instance_variable_set("@alpha",234)

    p x.alpha # 234


    x.instance_variable_set("@gamma",345) # 345

    v = x.instance_variable_get("@gamma") # 345

    Прежде всего, отметим, что имя переменной должно начинаться со знака

    @
    , иначе произойдет ошибка. Если это кажется вам неочевидным, вспомните, что метод
    attr_accessor
    (и ему подобные) принимает для формирования имени метода символ, поэтому-то знак
    @
    и опускается.

    Не нарушает ли существование таких методов принцип инкапсуляции? Нет. Конечно, эти методы потенциально опасны. Пользоваться ими следует с осторожностью, а не при всяком удобном случае. Но нельзя говорить, что инкапсуляция нарушена, не видя, как эти инструменты применяются в конкретном случае. Если это делается обдуманно, ради ясно осознанной цели, то все хорошо. Если же цель состоит в том, чтобы нарушить проект или обойти неудачное проектное решение, это печально. Ruby намеренно предоставляет доступ к внутренним деталям объектов тем, кому это действительно нужно; ответственный программист не станет пользоваться свободой во вред.

    11.3.5. Метод define_method

    Помимо ключевого слова

    def
    , единственный нормальный способ добавить метод в класс или объект — воспользоваться методом
    define_method
    , причем он позволяет сделать это во время выполнения.

    Конечно, в Ruby практически все происходит во время выполнения. Если окружить определение метода обращениями к

    puts
    , как в примере ниже, вы это сами увидите.

    class MyClass

     puts "до"


     def meth

      #...

     end


     puts "после"

    end

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

    eval
    , теперь же у нас есть метод
    define_method
    . Он принимает символ (имя метода) и блок (тело метода).

    Первая (ошибочная) попытка воспользоваться этим методом могла бы выглядеть так:

    # Не работает, так как метод define_method закрытый.


    if today =~ /Saturday | Sunday/

     define_method(:activity) { puts "Отдыхаем!" }

    else

     define_method(:activity) { puts "Работаем!" }

    end


    activity

    Поскольку

    define_method
    — закрытый метод, приходится поступать так:

    # Работает (Object - это контекст верхнего уровня).


    if today =~ /Saturday | Sunday/

     Object.class_eval { define_method(:activity) { puts "Отдыхаем!" } }

    else

     Object.class_eval { define_method(:activity) { puts "Работаем!" } }

    end


    activity

    Можно было бы поступить так же внутри определения класса (в применении к классу

    Object
    или любому другому). Такое редко бывает оправданно, но если вы можете сделать это внутри определения класса, вопрос о закрытости не встает.

    class MyClass

     define_method(:mymeth) { puts "Это мой метод." }

    end

    Есть еще один трюк: включить в класс метод, который сам вызывает

    define_method
    , избавляя от этого программиста:

    class MyClass

     def self.new_method(name, &block)

      define_method(name, &block)

     end

    end


    MyClass.new_method(:mymeth) { puts "Это мой метод." }

    x = MyClass.new

    x.mymeth # Печатается "Это мой метод."

    То же самое можно сделать и на уровне экземпляра, а не класса:

    class MyClass

     def new_method(name, &block)

      self.class.send(:define_method,name, &block)

     end

    end


    x = MyClass.new

    x.new_method(:mymeth) { puts "Это мой метод." }

    x.mymeth # Печатается "Это мой метод."

    Здесь метод экземпляра тоже определен динамически. Изменился только способ реализации метода

    new_method
    . Обратите внимание на трюк с
    send
    , позволивший нам обойти закрытость метода
    define_method
    . Он работает, потому что в текущей версии Ruby метод
    send
    позволяет вызывать закрытые методы. (Некоторые сочтут это «дыркой»; как бы то ни было, пользоваться этим механизмом следует с осторожностью.)

    По поводу метода

    define_method
    нужно сделать еще одно замечание. Он принимает блок, а в Ruby блок — замыкание. Это означает, что в отличие от обычного определения метода, мы запоминаем контекст, в котором метод был определен. Следующий пример практически бесполезен, но этот момент иллюстрирует:

    class MyClass

     def self.new_method(name, &block)

      define_method(name, &block)

     end

    end


    a,b = 3,79


    MyClass.new_method(:compute) { a*b }

    x = MyClass.new

    puts x.compute # 237


    a,b = 23,24

    puts x.compute # 552

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

    Отметим, что замыкание оказывается таковым только тогда, когда имя переменной то же самое. Изредка из-за этого могут возникать сложности. Ниже мы воспользовались методом

    define_method
    , чтобы предоставить доступ к переменной класса (вообще-то это следует делать не так, но для иллюстрации подойдет):

    class SomeClass

     @@var = 999


     define_method(:peek) { @@var }

    end


    x = SomeClass.new p

    x.peek # 999

    А теперь попробуем проделать с переменной экземпляра класса такой трюк:

    class SomeClass

     @var = 999


     define_method(:peek) { @var }

    end


    x = SomeClass.new

    p x.peek # Печатается nil

    Мы ожидали, что будет напечатано 999, а получили

    nil
    . Почему? Объясню чуть позже.

    С другой стороны, такой код работает правильно:

    class SomeClass

     @var = 999

     x = @var


     define_method(:peek) { x }

    end


    x = SomeClass.new p

    x.peek # 999

    Так что же происходит? Да, замыкание действительно запоминает переменные в текущем контексте. Но ведь контекст нового метода - это контекст экземпляра объекта, а не самого класса.

    Поскольку имя

    @var
    в этом контексте относится к переменной экземпляра объекта, а не класса, то переменная экземпляра класса оказывается скрыта переменной экземпляра объекта, хотя последняя никогда не использовалась и технически не существует.

    В предыдущих версиях Ruby мы часто определяли методы во время выполнения с помощью

    eval
    . В принципе во всех таких случаях может и должен использоваться метод
    define_method
    . Некоторые тонкости вроде рассмотренной выше не должны вас останавливать.

    11.3.6. Метод const_missing

    Метод

    const_missing
    аналогичен методу
    method_missing
    . При попытке обратиться к неизвестной константе вызывается этот метод — если он, конечно, определен. В качестве параметра ему передается символ, ссылающийся на константу.

    Чтобы перехватывать обращения к отсутствующим константам глобально, определите следующий метод в самом классе

    Module
    (это родитель класса
    Class
    ).

    class Module

     def const_missing(x)

      "Из Module"

     end

    end


    class X

    end


    p X::BAR     # "Из Module"

    p BAR        # "Из Module"

    p Array::BAR # "Из Module"

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

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

    class Module

     def const_missing(name)

      Roman.decode(name)

     end

    end


    year1 = MCMLCCIV # 1974

    year2 = MMVIII   # 2008

    Если такая глобальность вам не нужна, определите этот метод на уровне конкретного класса. Тогда он будет вызываться из этого класса и его потомков.

    class Alpha

     def self.const_missing(sym)

      "В Alpha нет #{sym}"

     end

    end


    class Beta

     def self.const_missing(sym)

      "В Beta нет #{sym}."

     end

    end


    class A < Alpha

    end


    class В < Beta

    end


    p Alpha::FOO # "В Alpha нет FOO"

    p Beta::FOO  # "В Beta нет FOO"

    p A::FOO     # "В Alpha нет FOO"

    p В::FOO     # "В Beta нет FOO"

    11.3.7. Удаление определений

    Вследствие динамичности Ruby практически все, что можно определить, можно и уничтожить. Это может пригодиться, например, для того, чтобы «развязать» два куска кода в одной и той же области действия, избавляясь от переменных после того, как они были использованы. Другой повод — запретить вызовы некоторых потенциально опасных методов. Но по какой бы причине вы ни удаляли определение, делать это нужно крайне осторожно, чтобы не создать себе проблемы во время отладки.

    Радикальный способ уничтожить определение — воспользоваться ключевым словом

    undef
    (неудивительно, что его действие противоположно действию
    def
    ). Уничтожать можно определения методов, локальных переменных и констант на верхнем уровне. Хотя имя класса — тоже константа, удалить определение класса таким способом невозможно.

    def asbestos

     puts "Теперь не огнеопасно"

    end


    tax =0.08


    PI = 3


    asbestos

    puts "PI=#{PI}, tax=#{tax}"


    undef asbestos

    undef tax

    undef PI


    # Любое обращение к этим трем именам теперь приведет к ошибке.

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

    undef
    внутри определения метода, а также к переменной экземпляра.

    Существуют (определены в классе

    Module
    ) также методы
    remove_method
    и
    undef_method
    . Разница между ними тонкая: remove_method удаляет текущее (или ближайшее) определение метода, a
    undef_method
    ко всему прочему удаляет его и из суперклассов, не оставляя от метода даже следа. Это различие иллюстрирует листинг 11.6.

    Листинг 11.16. Методы remove_method и undef_method

    class Parent


     def alpha

      puts "alpha: родитель"

     end


     def beta

      puts "beta: родитель"

     end

    end


    class Child < Parent


     def alpha

      puts "alpha: потомок"

     end


     def beta

      puts "beta: потомок"

     end


     remove_method :alpha # Удалить "этот" alpha.

     undef_method :beta   # Удалить все beta.


    end


    x = Child.new


    x.alpha               # alpha: родитель

    x.beta                # Ошибка!

    Метод

    remove_const
    удаляет константу.

    module Math


    remove_const :PI


     end


    # PI больше нет!

    Отметим, что таким способом можно удалить и определение класса (потому что идентификатор класса — это просто константа):

    class BriefCandle

     #...

    end


    out_out = BriefCandle.new

    class Object

     remove_const :BriefCandle

    end


    # Создать еще один экземпляр класса BriefCandle не получится!

    # (Хотя out_out все еще существует...)

    Такие методы, как

    remove_const
    и
    remove_method
    , являются закрытыми (что и понятно). Поэтому во всех примерах они вызываются изнутри определения класса или модуля, а не снаружи.

    11.3.8. Получение списка определенных сущностей

    API отражения в Ruby позволяет опрашивать классы и объекты во время выполнения. Рассмотрим методы, имеющиеся для этой цели в

    Module
    ,
    Class
    и
    Object
    .

    В модуле

    Module
    есть метод constants, который возвращает массив всех констант, определенных в системе (включая имена классов и модулей). Метод
    nesting
    возвращает массив всех вложенных модулей, видимых в данной точке программы.

    Метод экземпляра

    Module#ancestors
    возвращает массив всех предков указанного класса или модуля.

    list = Array.ancestors

    # [Array, Enumerable, Object, Kernel]

    Метод

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

    list = Math.constants # ["E", "PI"]

    Метод

    class_variables
    возвращает список всех переменных класса в данном классе и его суперклассах. Метод
    included_modules
    возвращает список модулей, включенных в класс.

    class Parent

     @@var1 = nil

    end


    class Child < Parent

     @@var2 = nil

    end


    list1 = Parent.class_variables # ["@@var1"]

    list2 = Array.included_modules # [Enumerable, Kernel]

    Методы

    instance_methods
    и
    public_instance_methods
    класса
    Class
    — синонимы; они возвращают список открытых методов экземпляра, определенных в классе. Методы
    private_instance_methods
    и
    protected_instance_methods
    ведут себя аналогично. Любой из них принимает необязательный булевский параметр, по умолчанию равный
    true
    ; если его значение равно
    false
    , то суперклассы не учитываются, так что список получается меньше.

    n1 = Array.instance_methods.size               # 121

    n2 = Array.public_instance_methods.size        # 121

    n3 = Array.private_instance_methods.size       # 71

    n4 = Array.protected_instance_methods.size     # 0

    n5 = Array.public_instance_methods(false).size # 71

    В классе

    Object
    есть аналогичные методы, применяющиеся к экземплярам (листинг 11.17). Метод
    methods
    возвращает список всех методов, которые можно вызывать для данного объекта. Метод
    public_methods
    возвращает список открытых методов и принимает параметр, равный по умолчанию
    true
    , который говорит, нужно ли включать также методы суперклассов. Методы
    private_methods
    ,
    protected_methods
    и
    singleton_methods
    тоже принимают такой параметр.

    Листинг 11.17. Отражение и переменные экземпляра

    class SomeClass


     def initialize

      @a = 1

      @b = 2

     end


     def mymeth

      # ...

     end


     protected :mymeth


    end


    x = SomeClass.new


    def

     x.newmeth

     # ...

    end


    iv = x.instance_variables       # ["@b", "@a"]


    p x.methods.size                # 42


    p x.public_methods.size         # 41

    p x.public_methods(false).size  # 1


    p x.private_methods.size        # 71

    p x.private_methods(false).size # 1


    p x.protected_methods.size      # 1

    p x.singleton_methods.size      # 1

    Если вы работаете с Ruby уже несколько лет, то заметите, что эти методы немного изменились. Теперь параметры по умолчанию равны

    true
    , а не
    false
    .

    11.3.9. Просмотр стека вызовов

    And you may ask yourself:
    Well, how did I get here?[13]
    (Talking Heads, «Once in a Lifetime»)

    Иногда необходимо знать, кто вызвал метод. Эта информация полезна, если, например, произошло неисправимое исключение. Метод

    caller
    , определенный в модуле
    Kernel
    , дает ответ на этот вопрос. Он возвращает массив строк, в котором первый элемент соответствует вызвавшему методу, следующий — методу, вызвавшему этот метод, и т.д.

    def func1

     puts caller[0]

    end


    def func2

     func1

    end


    func2 # Печатается: somefile.rb:6:in 'func2'

    Строка имеет формат «файл;строка» или «файл;строка в методе».

    11.3.10. Мониторинг выполнения программы

    Программа на Ruby может следить за собственным выполнением. У этой возможности есть много применений; интересующийся читатель может заглянуть в исходные тексты программ

    debug.rb
    ,
    profile.rb
    и
    tracer.rb
    . С ее помощью можно даже создать библиотеку для «проектирования по контракту» (design-by-contract, DBC), хотя наиболее популярная в данный момент библиотека такого рода этим средством не пользуется.

    Интересно, что этот фокус реализован целиком на Ruby. Мы пользуемся методом

    set_trace_func
    , который позволяет вызывать указанный блок при возникновении значимых событий в ходе исполнения программы. В справочном руководстве описывается последовательность вызова
    set_trace_func
    , поэтому здесь мы ограничимся простым примером:

    def meth(n)

     sum = 0

     for i in 1..n

      sum += i

     end

     sum

    end


    set_trace_func(proc do |event, file, line,

     id, binding, klass, *rest|

     printf "%8s %s:%d %s/%s\n", event, file, line,

      klass, id

     end)


    meth(2)

    Отметим, что здесь соблюдается стандартное соглашение о заключении многострочного блока в операторные скобки

    do-end
    . Круглые скобки обязательны из-за особенностей синтаксического анализатора Ruby. Можно было бы, конечно, вместо этого поставить фигурные скобки.

    Вот что будет напечатано в результате выполнения этого кода:

    line prog.rb:13 false/

        call prog.rb:1 Object/meth

        line prog.rb:2 Object/meth

        line prog.rb:3 Object/meth

      c-call prog.rb:3 Range/each

        line prog.rb:4 Object/meth

      c-call prog.rb:4 Fixnum/+

    c-return prog.rb:4 Fixnum/+

        line prog.rb:4 Object/meth

      c-call prog.rb:4 Fixnum/+

    c-return prog.rb:4 Fixnum/+

    c-return prog.rb:4 Range/each

        line prog.rb:6 Object/meth

      return prog.rb:6 Object/meth

    С этим методом тесно связан метод

    Kernel#trace_var
    , который вызывает указанный блок при каждом присваивании значения глобальной переменной.

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

    tracer
    . Пусть имеется следующая программа
    prog.rb
    :

    def meth(n)

     (1..n).each {|i| puts i}

    end


    meth(3)

    Можно запустить

    tracer
    из командной строки:

    % ruby -r tracer prog.rb

    #0:prog.rb:1::-:       def meth(n)

    #0:prog.rb:1:Module:>: def meth(n)

    #0:prog.rb:1:Module:<: def meth(n)

    #0:prog.rb:8::-:       meth(2)

    #0:prog.rb:1:Object:>: def meth(n)

    #0:prog.rb:2:Object:-: sum = 0

    #0:prog.rb:3:Object:-: for i in 1..n

    #0:prog.rb:3:Range:>:  for i in 1..n

    #0:prog.rb:4:Object:-: sum += i

    #0:prog.rb:4:Fixnum:>: sum += i

    #0:prog.rb:4:Fixnum:<: sum += i

    #0:prog.rb:4:Object:-: sum += i

    #0:prog.rb:4:Fixnum:>: sum += i

    #0:prog.rb:4:Fixnum:<: sum += i

    #0:prog.rb:4:Range:<:  sum += i

    #0:prog.rb:6:Object:-: sum

    #0:prog.rb:6:Object:<: sum

    Программа

    tracer
    выводит номер потока, имя файла и номер строки, имя класса, тип события и исполняемую строку исходного текста трассируемой программы. Бывают следующие типы событий:
    '-'
    — исполняется строка исходного текста,
    '>'
    — вызов,
    '<'
    — возврат,
    'С'
    — класс,
    'Е'
    — конец. (Если вы автоматически включите эту библиотеку с помощью переменной окружения
    RUBYOPT
    или каким-то иным способом, то может быть напечатано много тысяч строк.)

    11.3.11. Обход пространства объектов

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

    ObjectSpace.each_object
    .

    ObjectSpace.each_object do |obj|

     printf "%20s: %s\n", obj.class, obj.inspect

    end

    Если задать класс или модуль в качестве параметра

    each_object
    , то будут возвращены лишь объекты указанного типа.

    Модуль Object Space полезен также для определения чистильщиков объектов (см. раздел 11.3.14).

    11.3.12. Обработка вызовов несуществующих методов

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

    Object#method_missing
    . Если объект Ruby получает сообщение для метода, который в нем не реализован, то вызывается метод
    method_missing
    . Этим можно воспользоваться для превращения ошибки в обычный вызов метода. Реализуем класс, обертывающий команды операционной системы:

    class CommandWrapper


     def method_missing(method, *args)

      system (method.to_s, *args)

     end


    end


    cw = CommandWrapper.new

    cw.date # Sat Apr 28 22:50:11 CDT 2001

    cw.du '-s', '/tmp' # 166749 /tmp

    Первый параметр метода

    method_missing
    — имя вызванного метода (которое не удалось найти). Остальные параметры — все то, что было передано при вызове этого метода.

    Если написанная вами реализация

    method_missing
    не хочет обрабатывать конкретный вызов, она должна вызвать
    super
    , а не возбуждать исключение. Тогда методы
    method_missing
    в суперклассах получат возможность разобраться с ситуацией. В конечном счете будет вызван
    method_missing
    , определенный в классе
    Object
    , который и возбудит исключение.

    11.3.13. Отслеживание изменений в определении класса или объекта

    А зачем, собственно? Кому интересны изменения, которым подвергался класс?

    Одна возможная причина — желание следить за состоянием выполняемой программы на Ruby. Быть может, мы реализуем графический отладчик, который должен обновлять список методов, добавляемых «на лету».

    Другая причина: мы хотим вносить соответствующие изменения в другие классы. Например, мы разрабатываем модуль, который можно включить в определение любого класса. С момента включения будут трассироваться любые обращения к методам этого класса. Что-то в этом роде:

    class MyClass

     include Tracing


     def one

     end


     def two(x, y)

     end


    end


    m = MyClass.new

    m.one           # Вызван метод one. Параметры =

    m.two(1, 'cat') # Вызван метод two. Параметры = 1, cat

    Он должен работать также для всех подклассов трассируемого класса:

    class Fred < MyClass


     def meth(*a)

     end


    end


    Fred.new.meth{2,3,4,5) # вызван метод meth. Параметры =2, 3, 4, 5

    Возможная реализация такого модуля показана в листинге 11.18.

    Листинг 11.18. Трассирующий модуль

    module Tracing

     def Tracing.included(into)

      into.instance_methods(false).each { |m|

       Tracing.hook_method(into, m) }

      def into.method_added(meth)

       unless @adding

        @adding = true

        Tracing.hook_method(self, meth)

        @adding = false

       end

      end

     end


     def Tracing.hook_method(klass, meth)

      klass.class_eval do

       alias_method "old_#{meth}", "#{meth}"

       define_method(meth) do |*args|

        puts "Вызван метод #{meth}. Параметры = #{args.join(', ')}"

        self.send("old_#{meth}",*args)

       end

      end

     end

    end


    class MyClass

     include Tracing


     def first_meth

     end


     def second_meth(x, y)

     end

    end


    m = MyClass.new

    m.first_meth            # Вызван метод first_meth. Параметры =

    m.second_meth(1, 'cat') # Вызван метод second_meth. Параметры = 1, cat

    В этом коде два основных метода. Первый,

    included
    , вызывается при каждой вставке модуля в класс. Наша версия делает две вещи: вызывает метод
    hook_method
    каждого метода, уже определенного в целевом классе, и вставляет определение метода
    method_added
    в этот класс. В результате любой добавленный позже метод тоже будет обнаружен и для него вызван
    hook_method
    . Сам метод
    hook_method
    работает прямолинейно. При добавлении метода ему назначается синоним
    old_name
    . Исходный метод заменяется кодом трассировки, который выводит имя и параметры метода, а затем вызывает метод, к которому было обращение.

    Обратите внимание на использование конструкции

    alias_method
    . Работает она почти так же, как
    alias
    , но только для методов (да и сама является методом, а не ключевым словом). Можно было бы записать эту строку иначе:

    # Еще два способа записать эту строку...

    # Символы с интерполяцией:

    alias_method :"old_#{meth}", :"#{meth}"


    # Преобразование строк с помощью to_sym:

    alias_method "old_#{meth}".to_sym, meth.to_sym

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

    singleton_method_added
    внутри данного класса. (Напомним, что синглетный метод в этом смысле — то, что мы обычно называем методом класса, поскольку Class — это объект.) Этот метод определен в модуле
    Kernel
    и по умолчанию ничего не делает, но мы можем переопределить его, как сочтем нужным.

    class MyClass


     def MyClass.singleton_method_added(sym)

      puts "Добавлен метод #{sym.to_s} в класс MyClass."

     end


     def MyClass.meth1 puts "Я meth1."

     end


    end


    def MyClass.meth2

     puts "А я meth2."

    end

    В результате выводится следующая информация:

    Добавлен метод singleton_method_added в класс MyClass.

    Добавлен метод meth1 в класс MyClass.

    Добавлен метод meth2 в класс MyClass.

    Отметим, что фактически добавлено три метода. Возможно, это противоречит вашим ожиданиям, но метод

    singleton_method_added
    может отследить и добавление самого себя.

    Метод

    inherited
    (из
    Class
    ) используется примерно так же. Он вызывается в момент создания подкласса.

    class MyClass


     def MyClass.inherited(subclass)

      puts "#{subclass} наследует MyClass."

     end


     # ...

    end


    class OtherClass < MyClass

     # ...

    end


    # Выводится: OtherClass наследует MyClass.

    Можно также следить за добавлением методов экземпляра модуля к объекту (с помощью метода

    extend
    ). При каждом выполнении extend вызывается метод
    extend_object
    .

    module MyMod


     def MyMod.extend_object(obj)

      puts "Расширяется объект id #{obj.object_id}, класс #{obj.class}"

      super

     end


     # ...


    end


    x = [1, 2, 3]

    x.extend(MyMod)


    # Выводится:

    # Расширяется объект id 36491192, класс Array

    Обращение к

    super
    необходимо для того, чтобы мог отработать исходный метод
    extend_object
    . Это напоминает поведение метода
    append_features
    (см. раздел 11.1.12); данный метод годится также для отслеживания использования модулей.

    11.3.14. Определение чистильщиков для объектов

    У классов в Ruby есть конструкторы (методы

    new
    и
    initialize
    ), но нет деструкторов (методов, которые уничтожают объекты). Объясняется это тем, что в Ruby применяется алгоритм пометки и удаления объектов, на которые не осталось ссылок (сборка мусора); вот почему деструктор просто не имеет смысла.

    Однако тем, кто переходит на Ruby с таких языков, как C++, этот механизм представляется необходимым — часто задается вопрос, как написать код очистки уничтожаемых объектов. Простой ответ звучит так: невозможно сделать это надежно. Но можно написать код, который будет вызываться, когда сборщик мусора уничтожает объект.

    а = "hello"

    puts "Для строки 'hello' ИД объекта равен #{a.id}."

    ObjectSpace.define_finalizer(а) { |id| puts "Уничтожается #{id}." }

    puts "Нечего убирать."

    GC.start

    a = nil

    puts "Исходная строка - кандидат на роль мусора."

    GC.start

    Этот код выводит следующее:

    Для строки 'hello' ИД объекта равен 537684890.

    Нечего убирать.

    Исходная строка - кандидат на роль мусора.

    Уничтожается 537684890.

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

    ObjectSpace._id2ref
    приведет к исключению
    RangeError
    с сообщением о том, что вы пытаетесь воспользоваться уничтоженным объектом.

    Имейте в виду, что в Ruby применяется консервативный вариант сборки мусора по алгоритму пометки и удаления. Нет гарантии, что любой объект будет убран до завершения программы.

    Однако все это может оказаться и ненужным. В Ruby существует стиль программирования, в котором для инкапсуляции работы с ресурсами служат блоки. В конце блока ресурс освобождается, и жизнь продолжается без помощи чистильщиков. Рассмотрим, например, блочную форму метода

    File.open
    :

    File.open("myfile.txt") do |file|

     line1 = file.read

     # ...

    end

    Здесь в блок передается объект

    File
    , а по выходе из блока файл закрывается, причем все это делается под контролем метода
    open
    . Функциональное подмножество метода
    File.open
    на чистом Ruby (сейчас этот метод ради эффективности написан на С) могло бы выглядеть так:

    def File.open(name, mode = "r")

     f = os_file_open(name, mode)

     if block_given?

      begin

       yield f

      ensure

       f.close

      end

      return nil

     else

      return f

     end

    end

    Мы проверяем наличие блока. Если блок был передан, то мы вызываем его, передавая открытый файл. Делается это в контексте блока

    begin-end
    , который гарантирует, что файл будет закрыт по выходе из блока, даже если произойдет исключение.

    11.4. Заключение

    В этой главе были приведены примеры использования более сложных и даже экзотических механизмов ООП, а также решения некоторых рутинных задач. Мы видели, как реализуются некоторые паттерны проектирования. Познакомились мы и с API отражения в Ruby, продемонстрировали ряд интересных следствий динамической природы Ruby и некоторые трюки, возможные в динамическом языке.

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

    В современном окружении таким приложениям часто необходим графический интерфейс. В главе 12 мы рассмотрим создание графических интерфейсов на языке Ruby.


    Примечания:



    1

    Огромное спасибо (яп.)



    12

    Ни сердцу, ни сознанью беглый взгляд
    Не хочет дать о виденном отчет.
    ((Пер. С. Маршака))


    13

    И задаешь себе вопрос:
    Как же я оказался здесь?








    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх