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

Глава 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.

Сlаss 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, который обеспечивает нелокальный возврат в конец блока саllсс. Выйти из метода 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.