|
||||
|
Глава 11. ООП и динамические механизмы в Ruby
Это необычная глава. В большинстве других глав рассматривается какой-то конкретный аспект, например строки или файлы, но в этой все иначе. Если расположить «пространство задачи» по одной оси системы координат, то данная глава окажется на другой оси, поскольку содержит по кусочку из всех других областей. Связано это с тем, что объектно-ориентированное программирование и динамичность сами по себе являются не задачами, а парадигмами, которые могут быть применены к решению любой задачи, будь то системное администрирование, низкоуровневое сетевое программирование или разработка приложений для Web. Вот почему значительная часть материала данной главы должна быть уже знакома любому программисту, знающему Ruby. На самом деле все остальное в этой книге не имеет смысла без понимания изложенных здесь основ. Например, любой программист на Ruby знает, как создать подкласс. Возникает вопрос: что включить, а без чего можно обойтись? Знает ли любой программист о методе extend? А о методе instance_eval? То, что одному представляется очевидным, может оказаться откровением для другого. Мы решили отдать предпочтение полноте. В главу включены некоторые экзотические вещи, которые можно делать с помощью динамического ООП в Ruby, но не забыты и рутинные задачи — на случай, если кто-то не знаком с ними. Мы спустились до самого простого уровня, поскольку многие по-разному представляют себе, где кончается «средний» уровень. И попытались дать кое-какую дополнительную информацию даже при изложении самых базовых вопросов, чтобы оправдать включение их в эту главу. С другой стороны, тем, которые раскрываются в других частях книги, мы здесь не касались. Еще два замечания. Во-первых, ничего магического в динамическом ООП нет. Объектная ориентированность языка Ruby и его динамическая природа прекрасно уживаются между собой, но неотъемлемой связи между ними нет. Мы рассказываем о том и другом в одной главе только для удобства. Во-вторых, мы затрагиваем кое-какие особенности языка, которые, строго говоря, не относятся ни к одной из двух заявленных тем. Если хотите, считайте это мелким обманом. Но надо же было поместить их куда-то. 11.1. Рутинные объектно-ориентированные задачи(Вильям Шекспир. Сонет 113[12])Of his quick objects hath the mind no part, Если вы вообще не знакомы с ООП, то эта глава вас ничему не научит. А если вы понимаете, что такое ООП в языке Ruby, то, наверное, ее и читать не стоит. Если понятия ООП не слишком свежи в памяти, просмотрите главу 1, где мы приводим их краткий обзор (или обратитесь к другой книге). С другой стороны, большая часть материала в этой главе изложена в виде руководства и довольно элементарна. Поэтому она будет полезна начинающему и менее интересна для программиста на Ruby среднего уровня. Эта книга организована как устройство ввода/вывода с произвольной выборкой, так что можете свободно пропускать те части, которые вас не интересуют. 11.1.1. Применение нескольких конструкторовВ Ruby нет «настоящих» конструкторов, как в C++ или в Java. Сама идея, конечно, никуда не делась, поскольку объекты необходимо создавать и инициализировать, но реализация выглядит иначе. В Ruby каждый класс имеет метод класса new, который вызывается для создания новых объектов. Метод new вызывает специальный определяемый пользователем метод initialize, который инициализирует атрибуты объекта, после чего new возвращает ссылку на новый объект. А если мы хотим иметь несколько конструкторов? Как быть в этом случае? Ничто не мешает завести дополнительные методы класса, которые возвращают новые объекты. В листинге 11.1 приведен искусственный пример класса для представления прямоугольника, у которого есть две длины сторон и три значения цвета. Мы создали дополнительные методы класса, предполагающие определенные умолчания для каждого параметра. (Например, квадрат — это прямоугольник, у которого все стороны равны.) Листинг 11.1. Несколько конструкторовclass ColoredRectangle def initialize(r, g, b, s1, s2) @r, @g, @b, @s1, @s2 = r, g, b, s1, s2 end def ColoredRectangle.white_rect(s1, s2) new(0xff, 0xff, 0xff, s1, s2) end def ColoredRectangle.gray_rect(s1, s2) new(0x88, 0x88, 0x88, s1, s2) end def ColoredRectangle.colored_square(r, g, b, s) new(r, g, b, s, s) end def ColoredRectangle.red_square(s) new(0xff, 0, 0, s, s) end def inspect "#@r #@g #@b #@s1 #@s2" end end a = ColoredRectangle.new(0x88, 0xaa, 0xff, 20, 30) b = ColoredRectangle.white_rect(15,25) с = ColoredRectangle.red_square(40) Таким образом, можно определить любое число методов, создающих объекты по различным спецификациям. Вопрос о том, уместен ли здесь термин «конструктор», мы оставим «языковым адвокатам». 11.1.2. Создание атрибутов экземпляраИмени атрибута экземпляра в Ruby всегда предшествует знак @. Это обычная переменная в том смысле, что она начинает существовать после первого присваивания. В ОО-языках часто создаются методы для доступа к атрибутам, чтобы обеспечить сокрытие данных. Мы хотим контролировать доступ к «внутренностям» объекта извне. Обычно для данной цели применяются методы чтения и установки (getter и setter), хотя в Ruby эта терминология не используется. Они просто читают (get) или устанавливают (set) значение атрибута. Можно, конечно, запрограммировать такие функции «вручную», как показано ниже: class Person def name @name end def name=(x) @name = x end def age @age end # ... end Ho Ruby предоставляет более короткий способ. Метод attrпринимает в качестве параметра символ и создает соответствующий атрибут. Кроме того, он создает одноименный метод чтения, а если необязательный второй параметр равен true, то и метод установки. class Person attr :name, true # Создаются @name, name, name= attr :age # Создаются @age, age end Методы attr_reader, attr_writerи attr_accessorпринимают в качестве параметров произвольное число символов. Первый создает только «методы чтения» (для получения значения атрибута); второй — только «методы установки», а третий — то и другое. Пример: class SomeClass attr_reader :a1, :a2 # Создаются @a1, a1, @a2, a2 attr_writer :b1, :b2 # Создаются @b1, b1=, @b2, b2 = attr_accessor :c1, :c2 # Создаются @c1, c1, c1=, @c2, c2, c2= # ... end Напомним, что для выполнения присваивания атрибуту необходимо указывать вызывающий объект, а внутри метода нужно в качестве такого объекта указывать self. 11.1.3. Более сложные конструкторыПо мере усложнения объектов у них появляется все больше атрибутов, которые необходимо инициализировать в момент создания. Соответствующий конструктор может оказаться длинным и запутанным, его параметры даже не будут помещаться на одной строке. Чтобы справиться со сложностью, можно передать методу initializeблок (листинг 11.2). Тогда инициализация объекта выполняется в процессе вычисления этого блока. Хитрость в том, что вместо обычного evalдля вычисления блока в контексте объекта, а не вызывающей программы, следует использовать метод instance_eval. Листинг 11.2. «Хитрый» конструктор class PersonalComputer attr_accessor :manufacturer, :model, :processor, :clock, :ram, :disk, :monitor, :colors, :vres, :hres, :net def initialize(&block) instance_eval &block end # Прочие методы... end desktop = PersonalComputer.new do self.manufacturer = "Acme" self.model = "THX-1138" self.processor = "986" self.clock = 9.6 # ГГц self.ram =16 # Гб self.disk =20 # T6 self.monitor = 25 # дюймы self.colors = 16777216 self.vres = 1280 self.hres = 1600 self.net = "T3" end p desktop Отметим несколько нюансов. Во-первых, мы пользуемся методами доступа к атрибутам, поэтому присваивание им значений интуитивно понятно. Во-вторых, ссылка на selfнеобходима, поскольку метод установки требует явного указания вызывающего объекта, чтобы можно было отличить вызов метода от обычного присваивания локальной переменной. Конечно, можно было не определять методы доступа, а воспользоваться функциями установки. Ясно, что в теле блока можно делать все, что угодно. Например, можно было бы вычислить некоторые поля на основе других. А если вам не нужны методы доступа для всех атрибутов? Если хотите, можете избавиться от лишних, вызвав для них метод undefв конце конструирующего блока. Как минимум, это предотвратит «случайное» присваивание значения атрибуту извне объекта. 11.1.4. Создание атрибутов и методов уровня классаМетод или атрибут не всегда ассоциируются с конкретным экземпляром класса, они могут принадлежать самому классу. Типичным примером метода класса может служить new, он вызывается для создания новых экземпляров, а потому не может принадлежать никакому конкретному экземпляру. Мы можем определять собственные методы класса, как показано в разделе 11.1.1. Конечно, их функциональность не ограничивается конструированием — они могут выполнять любые операции, имеющие смысл именно на уровне класса. В следующем далеко не полном фрагменте предполагается, что мы создаем класс для проигрывания звуковых файлов. Метод playестественно реализовать как метод экземпляра, ведь можно создать много объектов, каждый из которых будет проигрывать свой файл. Но у метода detect_hardwareконтекст более широкий; в зависимости от реализации может оказаться, что создавать какие-либо объекты вообще не имеет смысла, если этот метод возвращает ошибку. Следовательно, его контекст — вся среда воспроизведения звука, а не конкретный звуковой файл. class SoundPlayer MAX_SAMPLE = 192 def SoundPlayer.detect_hardware # ... end def play # ... end end Есть еще один способ объявить этот метод класса. В следующем фрагменте делается практически то же самое: class SoundPlayer MAX_SAMPLE =192 def play # ... end end def SoundPlayer.detect_hardware # ... end Единственная разница касается использования объявленных в классе констант. Если метод класса объявлен вне объявления самого класса, то эти константы оказываются вне области видимости. Например, в первом фрагменте метод detect_hardwareможет напрямую обращаться к константе MAX_SAMPLE, а во втором придется пользоваться нотацией SoundPlayer::MAX_SAMPLE. Не удивительно, что помимо методов класса есть еще и переменные класса. Их имена начинаются с двух знаков @, а областью видимости является весь класс, а не конкретный его экземпляр. Традиционный пример использования переменных класса - подсчет числа его экземпляров. Но они могут применяться всегда, когда информации имеет смысл в контексте класса в целом, а не отдельного объекта. Другой пример приведен в листинге 11.3. Листинг 11.3. Переменные и методы классаclass Metal @@current_temp = 70 attr_accessor :atomic_number def Metal.current_temp=(x) @@current_temp = x end def Metal.current_temp @@current_temp end def liquid? @@current_temp >= @melting end def initialize(atnum, melt) @atomic_number = atnum @melting = melt end end aluminum = Metal.new(13, 1236) copper = Metal.new(29, 1982) gold = Metal.new(79, 1948) Metal.current_temp = 1600 puts aluminum.liquid? # true puts copper.liquid? # false puts gold.liquid? # false Metal.current_temp = 2100 puts aluminum.liquid? # true puts copper.liquid? # true puts gold.liquid? # true Здесь переменная класса инициализируется до того, как впервые используется в методе класса. Отметим также, что мы можем обратиться к переменной класса из метода экземпляра, но обратиться к переменной экземпляра из метода класса нельзя. Немного подумав, вы поймете, что так и должно быть. А если попытаться, что произойдет? Что если мы попробуем напечатать атрибут @atomic_numberиз метода Metal.current_temp? Обнаружится, что переменная вроде бы существует — никакой ошибки не возникает, — но имеет значение nil. В чем дело? В том, что на самом деле мы обращаемся вовсе не к переменной экземпляра класса Metal, а к переменной экземпляра класса Class. (Напомним, что в Ruby Class— это класс!) Мы столкнулись с переменной экземпляра класса (термин заимствован из языка Smalltalk). Дополнительные замечания на эту тему приводятся в разделе 11.2.4. В листинге 11.4 иллюстрируются все аспекты этой ситуации. Листинг 11.4. Данные класса и экземпляраclass MyClass SOME_CONST = "alpha" # Константа уровня класса. @@var = "beta" # Переменная класса. @var = "gamma" # Переменная экземпляра класса. def initialize @var = "delta" # Переменная экземпляра. end def mymethod puts SOME_CONST # (Константа класса.) puts @@var # (Переменная класса.) puts @var # (Переменная экземпляра.) end def MyClass.classmeth1 puts SOME_CONST # (Константа класса.) puts @@var # (Переменная класса.) puts @var # (Переменная экземпляра класса.) end end def MyClass.classmeth2 puts MyClass::SOME_CONST # (Константа класса.) # puts @@var # Ошибка: вне области видимости. puts @var # (Переменная экземпляра класса.) end myobj = MyClass.new MyClass.classmeth1 # alpha, beta, gamma MyClass.classmeth2 # alpha, gamma myobj.mymethod # alpha, beta, delta Следует еще сказать, что метод класса можно сделать закрытым, воспользовавшись методом private_class_method. Это аналог метода privateна уровне экземпляра. См. также раздел 11.2.10. 11.1.5. Наследование суперклассуМожно унаследовать класс, воспользовавшись символом <: class Boojum < Snark # ... end Это объявление говорит, что класс Boojumявляется подклассом класса Snarkили — что то же самое — класс Snarkявляется суперклассом класса Boojum. Всем известно, что каждый буюм является снарком, но не каждый снарк — буюм. Ясно, что цель наследования — расширить или специализировать функциональность. Мы хотим получить из общего нечто более специфическое. Попутно отметим, что во многих языках, например в C++, допускается множественное наследование (МН). В Ruby, как и в Java, и в некоторых других языках, множественного наследования нет, но наличие классов-примесей компенсирует его отсутствие (см. раздел 11.1.12). Рассмотрим несколько более реалистичный пример. У нас есть класс Person(человек), а мы хотим создать производный от него класс Student(студент). Определим класс Personследующим образом: class Person attr_accessor :name, :age, :sex def initialize(name, age, sex) @name, @age, @sex = name, age, sex end # ... end А класс Student— так: class Student < Person attr_accessor :idnum, :hours def initialize(name, age, sex, idnum, hours) super(name, age, sex) @idnum = idnum @hours = hours end # ... end # Создать два объекта. a = Person.new("Dave Bowman", 37, "m") b = Student.new("Franklin Poole", 36, "m", "000-13-5031", 24) Посмотрим внимательно, что здесь сделано. Что за super, вызываемый из метода initializeкласса Student? Это просто вызов соответствующего метода родительского класса. А раз так, то ему передается три параметра (хотя наш собственный метод initializeпринимает пять). Не всегда необходимо использовать слово superподобным образом, но часто это удобно. В конце концов, атрибуты любого класса образуют надмножество множества атрибутов его родительского класса, так почему не воспользоваться для их инициализации конструктором родительского класса? Если говорить об истинном смысле наследования, то оно, безусловно, описывает отношение «является». Студент является человеком, как и следовало ожидать. Сделаем еще три замечания: • Каждый атрибут (и метод) родительского класса отражается в его потомках. Если в классе Personесть атрибут height, то класс Studentунаследует его, а если родитель имеет метод say_hello, такой метод будет и у потомка. • Потомок может иметь дополнительные атрибуты и методы, мы это только что видели. Поэтому создание подкласса часто еще называют расширением суперкласса. • Потомок может переопределять любые атрибуты и методы своего родителя. Последнее замечание подводит нас к вопросу о том, как разрешается вызов метода. Откуда я знаю, вызывается ли метод конкретного класса или его суперкласса? Краткий ответ таков: не знаю и не интересуюсь. Если вызывается некий метод от имени объекта класса Student, то будет вызван метод, определенный в этом классе, если он существует. А если нет, вызывается метод суперкласса и так далее вверх по иерархии наследования. Мы говорим «и так далее», потому что у каждого класса (кроме Object) есть суперкласс. А что если мы хотим вызвать метод суперкласса, но не из соответствующего метода подкласса? Можно сначала создать в подклассе синоним: class Student # Повторное открытие класса. # Предполагается, что в классе Person есть метод say_hello... alias :say_hi :say_hello def say_hello puts "Привет." end def formal_greeting # Поприветствовать так, как принято в суперклассе. say_hi end end У наследования есть разные тонкости, которых мы здесь касаться не будем. Общий принцип мы изложили, но не пропустите следующий раздел. 11.1.6. Опрос класса объектаЧасто возникает вопрос: «Что это за объект? Как он соотносится с данным классом?» Есть много способов получить тот или иной ответ. Во-первых, метод экземпляра classвсегда возвращает класс объекта. Применявшийся ранее синоним typeобъявлен устаревшим. s = "Hello" n = 237 sc = s.class # String nc = n.class # Fixnum He думайте, будто методы classили typeвозвращают строку, представляющую имя класса. На самом деле возвращается экземпляр класса Class! При желании мы могли бы вызвать метод класса, определенный в этом типе, как если бы это был метод экземпляра класса Class(каковым он в действительности и является). s2 = "some string" var = s2.class # String my_str = var.new("Hi...") # Новая строка. Можно сравнить такую переменную с константным именем класса и выяснить, равны ли они; можно даже использовать переменную в роли суперкласса и определить на ее основе подкласс! Запутались? Просто помните, что в Ruby Class— это объект, a Object— это класс. Иногда нужно сравнить объект с классом, чтобы понять, принадлежит ли данный объект указанному классу. Для этого служит метод instance_of?, например: puts (5.instance_of? Fixnum) # true puts ("XYZZY".instance_of? Fixnum) # false puts ("PLUGH".instance_of? String) # true А если нужно принять во внимание еще и отношение наследования? К вашим услугам метод kind_of?(похожий на instance_of?). У него есть синоним is_a?, что вполне естественно, ибо мы описываем классическое отношение «является». n = 9876543210 flag1 = n.instance_of? Bignum # true flag2 = n.kind_of? Bignum # true flag3 = n.is_a? Bignum # true flag3 = n.is_a? Integer # true flag4 = n.is_a? Numeric # true flag5 = n.is_a? Object # true flag6 = n.is_a? String # false flag7 = n.is_a? Array # false Ясно, что метод kind_ofили is_a?более общий, чем instance_of?. Например, всякая собака — млекопитающее, но не всякое млекопитающее — собака. Для новичков в Ruby приготовлен один сюрприз. Любой модуль, подмешиваемый в класс, становится субъектом отношения «является» для экземпляров этого класса. Например, в класс Arrayподмешан модуль Enumerable; это означает, что всякий массив является перечисляемым объектом. x = [1, 2, 3] flag8 = x.kind_of? Enumerable # true flag9 = x.is_a? Enumerable # true Для сравнения двух классов можно пользоваться также операторами сравнения. Интуитивно очевидно, что оператор «меньше» обозначает наследование суперклассу. flag1 = Integer < Numeric # true flag2 = Integer < Object # true flag3 = Object == Array # false flag4 = IO >= File # true flag5 = Float < Integer # nil В любом классе обычно определен оператор «тройного равенства» ===. Выражение class === instanceистинно, если экземпляр instanceпринадлежит классу class. Этот оператор еще называют оператором ветвящегося равенства, потому что он неявно используется в предложении case. Дополнительную информацию о нем вы найдете в разделе 11.1.7. Упомянем еще метод respond_to. Он используется, когда нам безразлично, какому классу принадлежит объект, но мы хотим знать, реализует ли он конкретный метод. Это рудиментарный вид получения информации о типе. (Вообще-то можно сказать, что это самая важная информация о типе.) Методу respond_toпередается символ и необязательный флаг, который говорит, нужно ли включать в рассмотрение также и закрытые методы. # Искать открытые методы. if wumpus.respond_to?(:bite) puts "У него есть зубы!" else puts "Давай-ка подразним его." end # Необязательный второй параметр позволяет # просматривать и закрытые методы. if woozle.respond_to?(:bite,true) puts "Вузлы кусаются!" else puts "Ага, это не кусающийся вузл." end Иногда нужно знать, является ли данный класс непосредственным родителем объекта или класса. Ответ на этот вопрос дает метод superclassкласса Class. array_parent = Array.superclass # Object fn_parent = 237.class.superclass # Integer obj_parent = Object.superclass # nil У любого класса, кроме Object, есть суперкласс. 11.1.7. Проверка объектов на равенство
При написании своих классов желательно, чтобы семантика типичных операций была такой же, как у встроенных в Ruby классов. Например, если объекты класса можно упорядочивать, то имеет смысл реализовать метод <=>и подмешать модуль Comparable. Тогда к объектам вашего класса будут применимы все обычные операции сравнения. Однако картина перестает быть такой однозначной, когда дело доходит до проверки объектов на равенство. В Ruby объекты реализуют пять разных методов для этой операции. И в ваших классах придется реализовать хотя бы некоторые из них, поэтому рассмотрим этот вопрос подробнее. Самым главным является метод equal?(унаследованный от класса Object); он возвращает true, если вызывающий объект и параметр имеют один и тот же идентификатор объекта. Это фундаментальный аспект семантики объектов, поэтому переопределять его не следует. Самым распространенным способом проверки на равенство является старый добрый оператор ==, который сравнивает значения вызывающего объекта и аргумента. Наверно, интуитивно это наиболее очевидный способ. Следующим в шкале абстракции стоит метод eql?— тоже часть класса Object. (На самом деле метод eql?реализован в модуле Kernel, который подмешивается в Object.) Как и оператор ==, этот метод сравнивает значения вызывающего объекта и аргумента, но несколько более строго. Например, разные числовые объекты при сравнении с помощью ==приводятся к общему типу, но метод eql?никогда не считает объекты разных типов равными. flag1 = (1 == 1.0) # true flag2 = (1.eql?(1.0)) # false Метод eql?существует по одной-единственной причине: для сравнения значений ключей хэширования. Если вы хотите переопределить стандартное поведение Ruby при использовании объектов в качестве ключей хэша, то переопределите методы eql?и hash. Любой объект реализует еще два метода сравнения. Метод ===применяется для сравнения проверяемого значения в предложении caseс каждым селектором: selector===target. Хотя правило на первый взгляд кажется сложным, на практике оно делает предложения caseв Ruby интуитивно очевидными. Например, можно выполнить ветвление по классу объекта: case an_object when String puts "Это строка." when Numeric puts "Это число." else puts "Это что-то совсем другое." end Эта конструкция работает, потому что в классе Moduleреализован метод ===, проверяющий, принадлежит ли параметр тому же классу, что вызывающий объект (или одному из его предков). Поэтому, если an_object— это строка «cat», выражение string === an_objectокажется истинным и будет выбрана первая ветвь в предложении case. Наконец, в Ruby реализован оператор сопоставления с образцом =~. Традиционно он применяется для сопоставления строки с регулярным выражением. Но если вы найдете ему применение в других классах, то можете переопределить. У операторов ==и =~есть противоположные формы: !=и !~соответственно. Внутри они реализованы путем обращения значения основной формы. Это означает, что если, например, вы реализовали метод ==, то метод !=получаете задаром. 11.1.8. Управление доступом к методамВ Ruby объект определяется, прежде всего, своим интерфейсом: теми методами, которые он раскрывает внешнему миру. Но при написании класса часто возникает необходимость во вспомогательных методах, вызывать которые извне класса опасно. Тут-то и приходит на помощь метод privateкласса Module. Использовать его можно двумя способами. Если в теле класса или модуля вы вызовете privateбез параметров, то все последующие методы будут закрытыми в данном классе или модуле. Если же вы передадите ему список имен методов (в виде символов), то эти и только эти методы станут закрытыми. В листинге 11.5 показаны оба варианта. Листинг 11.5. Закрытые методы class Bank def open_safe # ... end def close_safe # ... end private :open_safe, :close_safe def make_withdrawal(amount) if access_allowed open_safe get_cash(amount) close_safe end end # Остальные методы закрытые. private def get_cash # ... end def access_allowed # ... end end Поскольку методы из семейства attrпросто определяют методы, метод privateопределяет и видимость атрибутов. Реализация метода privateможет показаться странной, но на самом деле она весьма хитроумна. К закрытым методам нельзя обратиться, указав вызывающий объект; они вызываются только от имени неявно подразумеваемого объекта self. То есть вызвать закрытый метод из другого объекта не удастся: просто не существует способа указать объект, от имени которого данный метод вызывается. Заодно это означает, что закрытые методы доступны подклассам того класса, в котором определены, но опять же в рамках одного объекта. Модификатор доступа protectedналагает меньше ограничений. Защищенные методы доступны только экземплярам того класса, в котором определены, и его подклассов. Для защищенного метода разрешается указывать вызывающий объект, так что к ним можно обращаться из других объектов (при условии, что вызывающий и вызываемый объекты принадлежат одному классу). Обычно защищенные методы применяются для определения методов доступа, чтобы два объекта одного типа могли взаимодействовать. В следующем примере объекты класс Personможно сравнивать по возрасту, но сам возраст недоступен вне класса Person: class Person def initialize(name, age) @name, @age = name, age end def <=>(other) age <=> other.age end attr_reader :name, :age protected :age end p1 = Person.new("fred", 31) p2 = Person.new("agnes", 43) compare = (p1 <=> p2) # -1 x = p1.age # Ошибка! Чтобы завершить картину, модификатор publicделает метод открытым. Неудивительно!.. И последнее: методы, определенные вне любого класса и модуля (то есть на верхнем уровне программы), по умолчанию закрыты. Поскольку они определены в классе Object, то видимы глобально, но обращаться к ним с указанием вызывающего объекта нельзя. 11.1.9. Копирование объектовВстроенные методы Object#cloneи #dupпорождают копию вызывающего объекта. Различаются они объемом копируемого контекста. Метод #dupкопирует только само содержимое объекта, тогда как cloneсохраняет и такие вещи, как синглетные классы, ассоциированные с объектом. s1 = "cat" def s1.upcase "CaT" end s1_dup = s1.dup s1_clone = s1.clone s1 #=> "cat" s1_dup.upcase #=> "CAT" (синглетный метод не копируется) s1_clone.upcase #=> "СаТ" (используется синглетный метод) И dup, и cloneвыполняют поверхностное копирование, то есть копируют лишь содержимое самого вызывающего объекта. Если вызывающий объект содержит ссылки на другие объекты, то последние не копируются — копия будет ссылаться на те же самые объекты. Проиллюстрируем это на примере. Объект arr2— копия arr1, поэтому изменение элемента целиком, например arr2[2], не оказывает влияния на arr1. Но исходный массив и его копия содержат ссылку на один и тот же объект String, поэтому изменение строки через arr2приведет к такому же изменению значения, на которое ссылается arr1. arr1 = [ 1, "flipper", 3 ] arr2 = arr1.dup arr2[2] = 99 arr2[1][2] = 'a' arr1 # [1, "flapper", 3] arr2 # [1, "flapper", 99] Иногда необходимо глубокое копирование, при котором копируется все дерево объектов с корнем в исходном объекте. В этом случае между оригиналом и копией гарантированно не будет никакой интерференции. Ruby не предоставляет встроенного метода для глубокого копирования, но есть приемы, позволяющие достичь желаемого результата. Самый «чистый» способ — потребовать, чтобы классы реализовывали метод deep_copy. Он мог бы рекурсивно обходить все объекты, на которые ссылается исходный объект, и вызывать для них метод deep_copy. Необходимо было бы еще добавить метод deep_copyво все встроенные классы Ruby, которыми вы пользуетесь. Но есть и более быстрый способ с использованием модуля Marshal. Если вы сериализуете исходный объект, представив его в виде строки, а затем загрузите в новый объект, то этот новый объект будет копией исходного. arr1 = [ 1, "flipper", 3 ] arr2 = Marshal.load(Marshal.dump(arr1)) arr2[2] = 99 arr2[1][2] = 'a' arr1 # [1, "flipper", 3] arr2 # [1, "flapper", 99] Обратите внимание, что изменение строки через arr2не отразилось на строке, на которую ссылается arr1. 11.1.10. Метод initialize_copyПри копировании объекта методом dupили cloneконструктор не вызывается. Копируется вся информация о состоянии. Но что делать, если вам такое поведение не нужно? Рассмотрим пример: class Document attr_accessor :title, :text attr_reader :timestamp def initialize(title, text) @title, @text = title, text @timestamp = Time.now end end doc1 = Document.new("Random Stuff",File.read("somefile")) sleep 300 # Немного подождем... doc2 = doc1.clone doc1.timestamp == doc2.timestamp # true # Оп... временные штампы одинаковы! При создании объекта Documentс ним ассоциируется временной штамп. При копировании объекта копируется и его временной штамп. А как быть, если мы хотим запомнить время, когда было выполнено копирование? Для этого нужно определить метод initialize_copy. Он вызывается как раз при копировании объекта. Этот метод аналогичен initializeи позволяет полностью контролировать состояние объекта. class Document # Определяем новый метод в классе. def initialize_copy(other) @timestamp = Time.now end end doc3 = Document.new("More Stuff", File.read("otherfile")) sleep 300 # Немного подождем... doc4 = doc3.clone doc3.timestamp == doc4.timestamp # false # Теперь временные штампы правильны. Отметим, что метод initialize_copyвызывается после того, как вся информация скопирована. Поэтому мы и опустили строку: @title, @text = other.title, other.text Кстати, если метод initialize_copyпуст, то поведение будет такое же, как если бы он не был определен вовсе. 11.1.11. Метод allocateРедко, но бывает, что нужно создать объект, не вызывая его конструктор (в обход метода initialize). Например, может статься, что состояние объекта полностью определяется методами доступа к нему; тогда не нужно вызывать метод new(который вызовет initialize), разве что вы сами захотите это сделать. Представьте, что для инициализации состояния объекта вы собираете данные по частям: начать следует с «пустого» объекта, а не получить все данные заранее, а потом вызвать конструктор. Метод allocateпоявился в версии Ruby 1.8, чтобы упростить решение этой задачи. Он возвращает «чистый», еще не инициализированный объект класса. class Person attr_accessor :name, :age, :phone def initialize(n,a,p) @name, @age, @phone = n, a, p end end p1 = Person.new("John Smith",29,"555-1234") p2 = Person.allocate p p1.age # 29 p p2.age # nil 11.1.12. МодулиДля использования модулей в Ruby есть две основных причины. Первая — облегчить управление пространством имен; если поместить константы и методы в модули, то будет меньше конфликтов имен. Хранящийся таким образом метод (метод модуля) вызывается с указанием имени модуля, то есть без вызывающего объекта. Точно так же вызывается и метод класса. Увидев вызовы вида File.ctimeили FileTest.exist?, мы не можем определить по контексту, что File— это класс, а FileTest— модуль. Вторая причина более интересна: мы можем использовать модуль как примесь. Примеси — это способ реализации множественного наследования, при котором наследуется только интерфейс. Мы уже говорили о методах модуля, а как насчет методов экземпляра? Модуль — это не класс, у него не может быть экземпляров, а к методу экземпляра нельзя обратиться, не указав вызывающий объект. Но оказывается, модуль может иметь методы экземпляра. Они становятся частью класса, который включил модуль директивой include. module MyMod def meth1 puts "Это метод 1." end end class MyClass include MyMod # ... end x = MyClass.new x.meth1 # Это метод 1. Здесь модуль MyModподмешан к классу MyClass, а метод экземпляра meth1унаследован. Вы видели также, как директива includeупотребляется на верхнем уровне программы; в таком случае модуль подмешивается к классу Object. А что происходит с методами модуля, если таковые определены? Если вы думаете, что они становятся методами класса, то ошибаетесь. Методы модуля не подмешиваются. Но если такое поведение желательно, то его можно реализовать с помощью нехитрого трюка. Существует метод append_features, который можно переопределить. Он вызывается с параметром — «целевым» классом или модулем (в который включается данный модуль). Пример приведен в листинге 11.6. Листинг 11.6. Включение модуля с переопределенным методом append_features module MyMod def MyMod.append_features(someClass) def someClass.modmeth puts "Метод модуля (класса) " end super # Этот вызов обязателен! end def meth1 puts "Метод 1" end end class MyClass include MyMod def MyClass.classmeth puts "Метод класса" end def meth2 puts "Метод 2" end end x = MyClass.new # Выводится: MyClass.classmeth # Метод класса x.meth1 # Метод 1 MyClass.modmeth # Метод модуля (класса) x.meth2 # Метод 2 Этот пример заслуживает детального рассмотрения. Во-первых, надо понимать, что метод append_featuresне просто вызывается в ходе выполнения include; на самом деле именно он и несет ответственность за включение. Поэтому-то вызов super необходим, без него оставшаяся часть модуля (в данном случае метод meth1) вообще не была бы включена. Отметим также, что внутри тела append_featuresимеется определение метода. Выглядит это необычно, но работает, поскольку вложенное определение порождает синглетный метод (уровня класса или модуля). Попытка определить таким образом метод экземпляра привела бы к ошибке Nested method error(Ошибка при определении вложенного метода). Модуль мог бы захотеть узнать, кто был инициатором примеси. Для этого тоже можно воспользоваться методом append_features, потому что класс-инициатор передается ему в качестве параметра. Можно также подмешивать методы экземпляра модуля как методы класса. В листинге 11.7 приведен соответствующий пример. Листинг 11.7. Методы экземпляра модуля становятся методами классаmodule MyMod def meth3 puts "Метод экземпляра модуля meth3" puts "может стать методом класса." end end class MyClass class << self # Здесь self - это MyClass. include MyMod end end MyClass.meth3 # Выводится: # Метод экземпляра модуля meth3 # может стать методом класса. Здесь полезен метод extend. Тогда пример можно записать так: class MyClass extend MyMod end Мы все время говорим о методах. А как насчет переменных экземпляра? Конечно, модуль может иметь собственные данные экземпляра, но обычно так не делают. Впрочем, если вы решили, что без этого никак не обойтись, ничто вас не остановит. Можно подмешивать модуль к объекту, а не классу (например, методом extend), см. по этому поводу раздел 11.2.2. Важно понимать еще одну вещь. В вашем классе можно определить методы, которые будут вызываться из примеси. Это удивительно мощный прием, знакомый тем, кто пользовался интерфейсами в Java. Классический пример, с которым мы уже сталкивались ранее, — подмешивание модуля Comparableи определение метода <=>. Поскольку подмешанные методы могут вызывать метод сравнения, то мы получаем операторы <, >, <=и т.д. Другой пример — подмешивание модуля Enumerableи определение метода <=>и итератора each. Тем самым мы получаем целый ряд полезных методов: collect, sort, min, maxи select. Можно также определять и собственные модули, ведущие себя подобным образом. Возможности ограничены только вашим воображением. 11.1.13. Трансформация или преобразование объектовИногда объект имеет нужный вид в нужное время, а иногда хочется преобразовать его во что-то другое или сделать вид, что он является чем-то, чем на самом деле не является. Всем известный пример — метод to_s. Каждый объект можно тем или иным способом представить в виде строки. Но не каждый объект может успешно «прикинуться» строкой. Именно в этом и состоит различие между методами to_sи to_str. Рассмотрим этот вопрос подробнее. При использовании метода putsили интерполяции в строку (в контексте #{...}) ожидается, что в качестве параметра будет передан объект string. Если это не так, объект просят преобразовать себя в string, посылая ему сообщение to_s. Именно здесь вы можете определить, как объект следует отобразить; просто реализуйте метод to_sв своем классе так, чтобы он возвращал подходящую строку. class Pet def initialize(name) @name = name end # ... def to_s "Pet: #@name" end end Другие методы (например, оператор конкатенации строк +) не так требовательны, они ожидают получить нечто достаточно близкое к объекту string. В этом случае Мац решил, что интерпретатор не будет вызывать метод to_sдля преобразования нестроковых аргументов, поскольку это могло бы привести к большому числу ошибок. Вместо этого вызывается более строгий метод to_str. Из всех встроенных классов только Stringи Exceptionреализуют to_str, и лишь String, Regexpи Marshalвызывают его. Увидев сообщение TypeError: Failed to convert xyz into string, можно смело сказать, что интерпретатор пытался вызвать to_strи потерпел неудачу. Вы можете реализовать метод to_strи самостоятельно, например, если хотите, чтобы строку можно было конкатенировать с числом: class Numeric def to_str to_s end end label = "Число " + 9 # "Число 9" Аналогично обстоит дело и в отношении массивов. Для преобразования объекта в массив служит метод to_a, а метод to_aryвызывается, когда ожидается массив и ничего другого, например в случае множественного присваивания. Допустим, есть предложение такого вида: а, b, с = x Если x— массив из трех элементов, оно будет работать ожидаемым образом. Но если это не массив, интерпретатор попытается вызвать метод to_aryдля преобразования в массив. В принципе это может быть даже синглетный метод (принадлежащий конкретному объекту). На само преобразование не налагается никаких ограничений, ниже приведен пример (нереалистичный), когда строка преобразуется в массив строк: class String def to_ary return self.split("") end end str = "UFO" a, b, с = str # ["U", "F", "O"] Метод inspectреализует другое соглашение. Отладчики, утилиты типа irbи метод отладочной печати pвызывают inspect, чтобы преобразовать объект к виду, пригодному для вывода на печать. Если вы хотите, чтобы во время отладки объект раскрывал свое внутреннее устройство, переопределите inspect. Есть и еще одна ситуация, когда желательно выполнять такие преобразования «за кулисами». Пользователь языка ожидает, что Fixnumможно прибавить к Float, а комплексное число Complexразделить на рациональное. Но для проектировщика языка это проблема. Если метод +класса Fixnumполучает аргумент типа Float, то что он должен с ним делать? Он знает лишь, как складывать значения типа Fixnum. Для решения проблемы в Ruby реализован механизм приведения типов coerce. Когда оператор +(к примеру) получает аргумент, которого не понимает, он пытается привести вызывающий объект и аргумент к совместимым типам, а затем значения этих типов сложить. Общий принцип использования метода coerceпрямолинеен: class MyNumberSystem def +(other) if other.kind_of?(MyNumberSystem) result = some_calculation_between_self_and_other MyNumberSystem.new(result) else n1, n2 = other.coerce(self) n1 + n2 end end end Метод coerceвозвращает массив из двух элементов: аргумент и вызывающий объект, приведенные к совместимым типам. В примере выше мы полагались на то, что класс аргумента умеет как-то выполнять приведение. Будь мы законопослушными гражданами, реализовали бы приведение и в собственном классе, чтобы он мог работать с числами других видов. Для этого нужно знать, с какими типами мы можем работать напрямую, и приводить объект к одному из этих типов, когда возникает необходимость. Если мы сами не знаем, как это сделать, следует спросить у родителя: def coerce(other) if other.kind_of?(Float) return other, self.to_f elsif other.kind_of?(Integer) return other, self.to_i else super end end Конечно, чтобы этот пример работал, наш объект должен реализовывать методы to_iи to_f. Метод coerceможно использовать для реализации автоматического преобразования строк в числа, как это делается в языке Perl: class String def coerce(n) if self['.'] [n, Float(self)] else [n, Integer(self)] end end end x = 1 + "23" # 24 y = 23 * "1.23" # 29.29 Впрочем, поступать так необязательно. Однако мы настоятельно рекомендуем реализовывать метод coerceпри разработке разного рода числовых классов. 11.1.14. Классы, содержащие только данные (Struct)Иногда нужно просто сгруппировать взаимосвязанные данные, не определяя никакие специфические методы обработки. Можно для этого создать класс: class Address attr_accessor :street, :city, :state def initialize(street1, city, state) @street, @city, @state = street, city, state end end books = Address.new("411 Elm St", "Dallas", "TX") Такое решение годится, но каждый раз прибегать к нему утомительно; к тому же здесь слишком много повторов. Тут-то и приходит на помощь встроенный класс Struct. Если вспомогательные методы типа attr_accessorопределяют методы доступа к атрибутам, то Structопределяет целый класс, который может содержать только атрибуты. Такие классы называются структурными шаблонами. Address = Struct.new("Address", :street, :city, :state) books = Address.new("411 Elm St", "Dallas", "TX") Зачем передавать первым параметром конструктора имя создаваемой структуры и присваивать результат константе (в данном случае Address)? При вызове Struct.newдля создания нового структурного шаблона на самом деле создается новый класс внутри самого класса Struct. Этому классу присваивается имя, переданное первым параметром, а остальные параметры становятся именами его атрибутов. При желании к вновь созданному классу можно было бы получить доступ, указав пространство имен Struct: Struct.new("Address", :street, :city, :state) books = Struct::Address.new("411 Elm St", "Dallas", "TX") Создав структурный шаблон, вы вызываете его метод new для создания новых экземпляров данной конкретной структуры. Необязательно присваивать значения всем атрибутам в конструкторе. Опущенные атрибуты получат значение nil. После того как структура создана, к ее атрибутам можно обращаться с помощью обычного синтаксиса или указывая их имена в скобках в качестве индекса, как будто структура - это объект класса Hash. Более подробную информацию о классе Structможно найти в любом справочном руководстве (например, на сайте ruby.doc.org). Кстати, не рекомендуем создавать структуру с именем Tms, так как уже есть предопределенный класс Struct::Tms. 11.1.15. Замораживание объектовИногда необходимо воспрепятствовать изменению объекта. Это позволяет сделать метод freeze(определенный в классе Object). По существу, он превращает объект в константу. Попытка модифицировать замороженный объект приводит к исключению TypeError. В листинге 11.8 приведено два примера. Листинг 11.8. Замораживание объекта str = "Это тест. " str.freeze begin str << " He волнуйтесь." # Попытка модифицировать. rescue => err puts "#{err.class} #{err}" end arr = [1, 2, 3] arr.freeze begin arr << 4 # Попытка модифицировать. rescue => err puts "#{err.class} #{err}" end # Выводится: # TypeError: can't modify frozen string # TypeError: can't modify frozen array Однако имейте в виду, что метод freezeприменяется к ссылке на объект, а не к переменной! Это означает, что любая операция, приводящая к созданию нового объекта, завершится успешно. Иногда это противоречит интуиции. В примере ниже мы ожидаем, что операция +=не выполнится, но все работает нормально. Дело в том, что присваивание — не вызов метода. Эта операция воздействует на переменные, а не на объекты, поэтому новый объект создается беспрепятственно. Старый объект по-прежнему заморожен, но переменная ссылается уже не на него. str = "counter-" str.freeze str += "intuitive" # "counter-intuitive" arr = [8, 6, 7] arr.freeze arr += [5, 3, 0, 9] # [8, 6, 7, 5, 3, 0, 9] Почему так происходит? Предложение a += xсемантически эквивалентно a = a + x. При вычислении выражения a + xсоздается новый объект, который затем присваивается переменной a! Все составные операторы присваивания работают подобным образом, равно как и другие методы. Всегда задавайте себе вопрос: «Что я делаю — создаю новый объект или модифицирую существующий?» И тогда поведение freezeне станет для вас сюрпризом. Существует метод frozen?, который сообщает, заморожен ли данный объект. hash = { 1 => 1, 2 => 4, 3 => 9 } hash.freeze arr = hash.to_a puts hash.frozen? # true puts arr.frozen? # false hash2 = hash puts hash2.frozen? # true Как видите (на примере hash2), замораживается именно объект, а не переменная. 11.2. Более сложные механизмыНе все в модели ООП, реализованной в Ruby, одинаково очевидно. Что-то сложнее, что-то применяется реже. Линия раздела для каждого программиста проходит в разных местах. В этой части главы мы попытались собрать те средства, которые не так просты или не так часто встречаются в программах. Иногда вы задаетесь вопросом, можно ли решить на Ruby ту или иную задачу. Краткий ответ таков: Ruby — богатый, динамический, объектно-ориентированный язык с широким набором разумно ортогональных средств; если нечто можно сделать на каком-то другом языке, то, скорее всего, можно и на Ruby. Теоретически все полные по Тьюрингу языки более или менее одинаковы. Весь смысл проектирования языков в поиске осмысленной, удобной нотации. Читателю, сомневающемуся в важности нотации, стоит попробовать написать интерпретатор LISP на языке COBOL или выполнить деление чисел, записанных римскими цифрами. Конечно, мы не хотим сказать, что любая задача на Ruby решается элегантно или естественно. Попытайся мы высказать такое утверждение, кто-нибудь очень быстро докажет, что мы не правы. В этом разделе мы поговорим также о реализации на Ruby различных стилей программирования, например функционального и аспектно-ориентированного. Мы не претендуем на роль экспертов в этих областях, просто приводим мнение других. Относитесь к этому с долей скепсиса. 11.2.1. Отправка объекту явного сообщенияВ статическом языке вы считаете очевидным, что имя вызываемой функции «зашито» в программу, это часть исходного текста. Динамический язык обладает в данном отношении большей гибкостью. При любом вызове метода вы посылаете объекту сообщение. Обычно эти сообщения так же жестко «зашиты» в код, как и в статическом языке, но это необязательно. Можно написать программу, которая во время выполнения решает, какой метод вызывать. Метод sendпозволяет использовать Symbolдля представления имени метода. Пусть, например, имеется массив объектов, который нужно отсортировать, причем в качестве ключей сортировки хотелось бы использовать разные поля. Не проблема - можно просто написать специализированные блоки для сортировки. Но хотелось бы найти более элегантное решение, позволяющее обойтись одной процедурой, способной выполнить сортировку по любому указанному ключу. В листинге 11.9 такое решение приведено. Этот пример был написан для первого издания книги. Теперь метод sort_byстал стандартным и даже более эффективным, поскольку реализует преобразование Шварца (по имени известного гуру в языке Perl Рэндала Шварца) и сохраняет преобразованные значения вместо многократного их вычисления. Впрочем, листинг 11.9 по-прежнему дает пример использования метода send. Листинг 11.9. Сортировка по любому ключу class Person attr_reader :name, :age, :height def initialize(name, age, height) @name, @age, @height = name, age, height end def inspect "#@name #@age #@height" end end class Array def sort_by(sym) # Наш вариант метода sort_by. self.sort {|x,y| x.send(sym) <=> y.send(sym) } end end people = [] people << Person.new("Hansel", 35, 69) people << Person.new("Gretel", 32, 64) people << Person.new("Ted", 36, 68) people << Person.new("Alice", 33, 63) p1 = people.sort_by(:name) p2 = people.sort_by(:age) p3 = people.sort_by(:height) p p1 # [Alice 33 63, Gretel 32 64, Hansel 35 69, Ted 36 68] p p2 # [Gretel 32 64, Alice 33 63, Hansel 35 69, Ted 36 68] p p3 # [Alice 33 63, Gretel 32 64, Ted 36 68, Hansel 35 69] Отметим еще, что синоним __send__делает в точности то же самое. Такое странное имя объясняется, вероятно, опасением, что имя sendуже может быть задействовано (случайно или намеренно) для определенного пользователем метода. 11.2.2. Специализация отдельного объекта
В большинстве объектно-ориентированных языков все объекты одного класса ведут себя одинаково. Класс — это шаблон, порождающий объекты с одним и тем же интерфейсом при каждом вызове конструктора. Ruby ведет себя так же, но это не конец истории. Получив объект, вы можете изменить его поведение на лету. По сути дела, вы ассоциируете с объектом частный, анонимный подкласс, все методы исходного подкласса остаются доступными, но добавляется еще и поведение, уникальное для данного объекта. Поскольку это поведение присуще лишь данному объекту, оно встречается только один раз. Нечто, встречающееся только один раз, называется синглетом (singleton). Так, мы имеем синглетные методы и синглетные классы. Слово «синглет» может стать источником путаницы, потому что оно употребляется и в другом смысле - как название хорошо известного паттерна проектирования, описывающего класс, для которого может существовать лишь один объект. Если вас интересует такое использование, обратитесь к библиотеке singleton.rb. В следующем примере мы видим два объекта, оба строки. Для второго мы добавляем метод upcase, который переопределяет существующий метод с таким же именем. а = "hello" b = "goodbye" def b.upcase # Создать синглетный метод. gsub(/(.)(.)/) { $1.upcase + $2 } end puts a.upcase # HELLO puts b.upcase # GoOdBye Добавление синглетного метода к объекту порождает синглетный класс для данного объекта, если он еще не был создан ранее. Родителем синглетного класса является исходный класс объекта. (Можно считать, что это анонимный подкласс исходного класса.) Если вы хотите добавить к объекту несколько методов, то можете создать синглетный класс явно: b = "goodbye" class << b def upcase # Создать синглетный метод. gsub(/(.){.)/) { $1.upcase + $2 } end def upcase! gsub!(/(.)(.)/) { $1.upcase + $2 } end end puts b.upcase # GoOdBye puts b # goodbye b.upcase! puts b # GoOdBye Отметим попутно, что у более «примитивных» объектов (например, Fixnum) не может быть добавленных синглетных методов. Связано это с тем, что такие объекты хранятся как непосредственные значения, а не как ссылки. Впрочем, реализация подобной функциональности планируется в будущих версиях Ruby (хотя непосредственные значения сохранятся). Если вам приходилось разбираться в коде библиотек, то наверняка вы сталкивались с идиоматическим использованием синглетных классов. В определении класса иногда встречается такой код: class SomeClass # Stuff... class << self # Какой-то код end # ...продолжение. end В теле определения класса слово selfобозначает сам определяемый класс, поэтому создание наследующего ему синглета модифицирует класс этого класса. Можно сказать, что методы экземпляра синглетного класса извне выглядят как методы самого класса. class TheClass class << self def hello puts "hi" end end end # вызвать метод класса TheClass.hello # hi Еще одно распространенное применение такой техники — определение на уровне класса вспомогательных функций, к которым можно обращаться из других мест внутри определения класса. Например, мы хотим определить несколько функций доступа, которые преобразуют результат своей работы в строку. Можно, конечно, написать отдельно код каждой такой функции. Но есть и более элегантное решение — определить функцию уровня класса accessor_string, которая сгенерирует необходимые нам функции (как показано в листинге 11.10). Листинг 11.10. Метод уровня класса accessor_string сlass MyClass class << self def accessor_string(*names) names.each do |name| class_eval <<-EOF def #{name} @#{name}.to_s end EOF end end end def initialize @a = [1,2,3] @b = Time.now end accessor_string :a, :b end о = MyClass.new puts o.a # 123 puts o.b # Mon Apr 30 23:12:15 CDT 2001 Вы наверняка сможете придумать и другие, более изобретательные применения. Метод extendподмешивает к объекту модуль. Методы экземпляра, определенные в модуле, становятся методами экземпляра объекта. Взгляните на листинг 11.11. Листинг 11.11. Использование метода extend module Quantifier def any? self.each { |x| return true if yield x } false end def all? self.each { |x| return false if not yield x } true end end list = [1, 2, 3, 4, 5] list.extend(Quantifier) flag1 = list.any? {|x| x > 5 } # false flag2 = list.any? {|x| x >= 5 } # true flag3 = list.all? {|x| x <= 10 } # true flag4 = list.all? {|x| x % 2 == 0 } # false В этом примере к массиву listподмешаны методы any?и all?. 11.2.3. Вложенные классы и модулиКлассы и модули можно вкладывать друг в друга произвольным образом. Программисты, приступающие к изучению Ruby, могут этого и не знать. Основная цель данного механизма — упростить управление пространствами имен. Скажем, в класс Fileвложен класс Stat. Это помогает «инкапсулировать» класс Statвнутри тесно связанного с ним класса, а заодно оставляет возможность в будущем определить класс Stat, не конфликтуя с существующим (скажем, для сбора статистики). Другой пример дает класс Struct::Tms. Любая новая структура Structпомещается в это пространство имен, не «загрязняя» расположенные выше, a Tms— в действительности тоже Struct. Кроме того, вложенный класс можно создавать просто потому, что внешний мир не должен знать о нем или обращаться к нему. Иными словами, можно создавать целые классы, подчиняющиеся тому же принципу «сокрытия данных», которому переменные и методы экземпляра следуют на более низком уровне. class BugTrackingSystem class Bug #... end #... end # Никто снаружи не знает о классе Bug. Можно вкладывать класс в модуль, модуль в класс и т.д. Если вы придумаете интересные и изобретательные способы применения этой техники, дайте нам знать. 11.2.4. Создание параметрических классов
Предположим, что нужно создать несколько классов, отличающихся только начальными значениями переменных уровня класса. Напомним, что переменная класса обычно инициализируется в самом определении класса. class Terran @@home_planet = "Earth" def Terran.home_planet @@home_planet end def Terran.home_planet= (x) @@home_planet = x end #... end Все замечательно, но что если нам нужно определить несколько подобных классов? Новичок подумает: «Ну так я просто определю суперкласс!» (листинг 11.12). Листинг 11.12. Параметрические классы: неправильное решениеclass IntelligentLife # Неправильный способ решения задачи! @@home_planet = nil def IntelligentLife.home_planet @@home _planet end def IntelligentLife.home_planet=(x) @@home_planet = x end #... end class Terran < IntelligentLife @@home_planet = "Earth" #... end class Martian < IntelligentLife @@home_planet = "Mars" #... end Но это работать не будет. Вызов Terran.home_planetнапечатает не "Earth", а "Mars"! Почему так? Дело в том, что переменные класса — на практике не вполне переменные класса; они принадлежат не одному классу, а всей иерархии наследования. Переменная класса не копируется из родительского класса, а разделяется родителем (и, стало быть, со всеми братьями). Можно было бы вынести определение переменной класса в базовый класс, но тогда перестали бы работать определенные нами методы класса! Можно было исправить и это, перенеся определения в дочерние классы, однако тем самым губится первоначальная идея, ведь таким образом объявляются отдельные классы без какой бы то ни было «параметризации». Мы предлагаем другое решение. Отложим вычисление переменной класса до момента выполнения, воспользовавшись методом class_eval. Полное решение приведено в листинге 11.13. Листинг 11.13. Параметрические классы: улучшенное решение class IntelligentLife def IntelligentLife.home_planet class_eval("@@home_planet") end def IntelligentLife.home_planet=(x) class_eval("@@home_planet = #{x}") end # ... end class Terran < IntelligentLife @@home_planet = "Earth" # ... end class Martian < IntelligentLife @@home_planet = "Mars" # ... end puts Terran.home_planet # Earth puts Martian.home_planet # Mars Не стоит и говорить, что механизм наследования здесь по-прежнему работает. Все методы и переменные экземпляра, определенные в классе IntelligentLife, наследуются классами Terranи Martian. В листинге 11.14 предложено, наверное, наилучшее решение. В нем используются только переменные экземпляра, а от переменных класса мы вообще отказались. Листинг 11.14. Параметрические классы: самое лучшее решениеclass IntelligentLife class << self attr_accessor :home_planet end # ... end class Terran < IntelligentLife self.home_planet = "Earth" #... end class Martian < IntelligentLife self.home_planet = "Mars" #... end puts Terran.home_planet # Earth puts Martian.home_planet # Mars Здесь мы открываем синглетный класс и определяем метод доступа home_planet. В двух подклассах определяются собственные методы доступа и устанавливается переменная. Теперь методы доступа работают строго в своих классах. В качестве небольшого усовершенствования добавим еще вызов метода privateв синглетный класс: private :home_planet= Сделав метод установки закрытым, мы запретили изменять значение вне иерархии данного класса. Как всегда, privateреализует «рекомендательную» защиту, которая легко обходится. Но объявление метода закрытым по крайней мере говорит, что мы не хотели, чтобы метод вызывался вне определенного контекста. Есть и другие способы решения этой задачи. Проявите воображение. 11.2.5. Использование продолжений для реализации генератораОдно из самых трудных для понимания средств Ruby — продолжение (continuation). Это структурированный способ выполнить нелокальный переход и возврат. В объекте продолжения хранятся адрес возврата и контекст выполнения. В каком-то смысле это аналог функций setjmp/ longjmpв языке С, но объем сохраняемого контекста больше. Метод callccиз модуля Kernelпринимает блок и возвращает объект класса Continuation. Возвращаемый объект передается в блок как параметр, что еще больше все запутывает. В классе Continuationесть всего один метод call, который обеспечивает нелокальный возврат в конец блока callсс. Выйти из метода callccможно либо достигнув конца блока, либо вызвав метод call. Считайте, что продолжение — что-то вроде операции «сохранить игру» в классических «бродилках». Вы сохраняете игру в точке, где все спокойно, а потом пробуете выполнить нечто потенциально опасное. Если эксперимент заканчивается гибелью, то вы восстанавливаете сохраненное состояние игры и пробуете пойти другим путем. Самый лучший способ разобраться в продолжениях — посмотреть фильм «Беги, Лола, беги». Есть несколько хороших примеров того, как пользоваться продолжениями. Самые лучшие предложил Джим Вайрих (Jim Weirich). Ниже показано, как Джим реализовал «генератор» после дискуссии еще с одним программистом на Ruby, Хью Сассе (Hugh Sasse). Идея генератора навеяна методом suspendиз языка Icon (он есть также в Prolog), который позволяет возобновить выполнение функции с места, следующего за тем, где она в последний раз вернула значение. Хью называет это «yield наоборот». Библиотека generatorтеперь входит в дистрибутив Ruby. Дополнительную информацию по этому вопросу вы найдете в разделе 8.3.7. В листинге 11.15 представлена предложенная Джимом реализация генератора чисел Фибоначчи. Продолжения применяются для того, чтобы сохранить состояние между вызовами. Листинг 11.15. Генератор чисел Фибоначчиclass Generator def initialize do_generation end def next callcc do |here| @main_context = here; @generator_context.call end end private def do_generation callcc do |context| @generator_context = context; return end generating_loop end def generate(value) callcc do |context| @generator_context = context; @main_context.call(value) end end end # Порождаем подкласс и определяем метод generating_loop. class FibGenerator < Generator def generating_loop generate(1) a, b = 1, 1 loop do generate(b) a, b = b, a+b end end end # Создаем объект этого класса... fib = FibGenerator.new puts fib.next # 1 puts fib.next # 1 puts fib.next # 2 puts fib.next # 3 puts fib.next # 5 puts fib.next # 8 puts fib.next # 13 # И так далее... Есть, конечно, и более практичные применения продолжений. Один из примеров — каркас Borgesдля разработки Web-приложений (названный в честь Хорхе Луиса Борхеса), который построен по образу Seaside. В этой парадигме традиционный поток управления в Web-приложении «вывернут с изнанки на лицо», так что логика представляется «нормальной». Например, вы отображаете страницу, получаете результат из формы, отображаете следующую страницу и так далее, ни в чем не противореча интуитивным ожиданиям. Проблема в том, что продолжение — «дорогая» операция. Необходимо сохранить состояние и потратить заметное время на переключение контекста. Если производительность для вас критична, прибегайте к продолжениям с осторожностью. 11.2.6. Хранение кода в виде объектаНеудивительно, что Ruby предлагает несколько вариантов хранения фрагмента кода в виде объекта. В этом разделе мы рассмотрим объекты Proc, Methodи UnboundMethod. Встроенный класс Procпозволяет обернуть блок в объект. Объекты Proc, как и блоки, являются замыканиями, то есть запоминают контекст, в котором были определены. myproc = Proc.new { |a| puts "Параметр равен #{а}" } myproc.call(99) # Параметр равен 99 Кроме того, Ruby автоматически создает объект Proc, когда метод, последний параметр которого помечен амперсандом, вызывается с блоком в качестве параметра: def take_block(x, &block) puts block.class x.times {|i| block[i, i*i] } end take_block(3) { |n,s| puts "#{n} в квадрате равно #{s}" } В этом примере демонстрируется также применение квадратных скобок как синонима метода call. Вот что выводится в результате исполнения: Proc 0 в квадрате 0 1 в квадрате 1 2 в квадрате 4 Объект Procможно передавать методу, который ожидает блок, предварив имя знаком &: myproc = proc { |n| print n, "... " } (1..3).each(&myproc) # 1... 2... 3... Ruby позволяет также превратить метод в объект. Исторически для этого применяется метод Object#method, который создает объект класса Methodкак замыкание в конкретном объекте. str = "cat" meth = str.method(:length) a = meth.call # 3 (длина "cat") str << "erpillar" b = meth.call # 11 (длина "caterpillar") str = "dog" # Обратите внимание на следующий вызов! Переменная str теперь ссылается # на новый объект ("dog"), но meth по-прежнему связан со старым объектом. с = meth.call # 11 (длина "caterpillar") Начиная с версии Ruby 1.6.2, можно также применять метод Module#instance_methodдля создания объектов UnboundMethod. С их помощью представляется метод, ассоциированный с классом, а не с конкретным объектом. Прежде чем вызывать объект UnboundMethod, нужно связать его с каким-то объектом. Результатом операции связывания является объект Method, который можно вызывать как обычно: umeth = String.instance_method(:length) m1 = umeth.bind("cat") m1.call # 3 m2 = umeth.bind("caterpillar") m2.call # 11 Явное связывание делает объект UnboundMethodинтуитивно более понятным, чем Method. 11.2.7. Как работает включение модулей?Когда модуль включается в класс, Ruby на самом деле создает прокси-класс, являющийся непосредственным родителем данного класса. Возможно, вам это покажется интуитивно очевидным, возможно, нет. Все методы включаемого модуля «маскируются» методами, определенными в классе. module MyMod def meth "из модуля" end end class ParentClass def meth "из родителя" end end class ChildClass < ParentClass include MyMod def meth "из потомка" end end x = ChildClass.new p p x.meth # Из потомка. Выглядит это как настоящее наследование: все, что потомок переопределил, становится действующим определением вне зависимости от того, вызывается ли includeдо или после переопределения. Вот похожий пример, в котором метод потомка вызывает super, а не просто возвращает строку. Как вы думаете, что будет возвращено? # Модуль MyMod и класс ParentClass не изменились. class ChildClass < ParentClass include MyMod def meth "Из потомка: super = " + super end end x = ChildClass.new p x.meth # Из потомка: super = из модуля Отсюда видно, что модуль действительно является новым родителем класса. А что если мы точно также вызовем superиз модуля? module MyMod def meth "Из модуля: super = " + super end end # ParentClass не изменился. class ChildClass < ParentClass include MyMod def meth "Из потомка: super = " + super end end x = ChildClass.new p x.meth # Из потомка: super = из модуля: super = из родителя. Метод meth, определенный в модуле MyMod, может вызвать superтолько потому, что в суперклассе (точнее, хотя бы в одном из его предков) действительно есть метод meth. А что произошло бы, вызови мы этот метод при других обстоятельствах? module MyMod def meth "Из модуля: super = " + super end end class Foo include MyMod end x = Foo.new x.meth При выполнении этого кода мы получили бы ошибку NoMethodError(или обращение к методу method_missing, если бы таковой существовал). 11.2.8. Опознание параметров, заданных по умолчаниюВ 2004 году Ян Макдональд (Ian Macdonald) задал в списке рассылки вопрос: «Можно ли узнать, был ли параметр задан вызывающей программой или взято значение по умолчанию?» Вопрос интересный. Не каждый день он возникает, но от того не менее интересен. Было предложено по меньшей мере три решения. Самое удачное и простое нашел Нобу Накада (Nobu Nakada). Оно приведено ниже: def meth(a, b=(flag=true; 345)) puts "b равно #{b}, a flag равно #{flag.inspect}" end meth(123) # b равно 345, a flag равно true meth(123,345) # b равно 345, a flag равно nil meth(123,456) # b равно 456, a flag равно nil Как видим, этот подход работает даже, если вызывающая программа явно указала значение параметра, совпадающее с подразумеваемым по умолчанию. Трюк становится очевидным, едва вы его увидите: выражение в скобках устанавливает локальную переменную flagв true, а затем возвращает значение по умолчанию 345. Это дань могуществу Ruby. 11.2.9. Делегирование или перенаправлениеВ Ruby есть две библиотеки, которые предлагают решение задачи о делегировании или перенаправлении вызовов методов другому объекту. Они называются delegateи forwardable; мы рассмотрим обе. Библиотека delegateпредлагает три способа решения задачи. Класс SimpleDelegatorполезен, когда объект, которому делегируется управление (делегат), может изменяться на протяжении времени жизни делегирующего объекта. Чтобы выбрать объект-делегат, используется метод __setobj__. Однако мне этот способ представляется слишком примитивным. Поскольку я не думаю, что это существенно лучше, чем то же самое, сделанное вручную, задерживаться на классе SimpleDelegatorне стану. Метод верхнего уровня DelegateClassпринимает в качестве параметра класс, которому делегируется управление. Затем он создает новый класс, которому мы можем унаследовать. Вот пример создания класса Queue, который делегирует объекту Array: require 'delegate' class MyQueue < DelegateClass(Array) def initialize(arg=[]) super(arg) end alias_method :enqueue, :push alias_method :dequeue, :shift end mq = MyQueue.new mq.enqueue(123) mq.enqueue(234) p mq.dequeue # 123 p mq.dequeue # 234 Можно также унаследовать класс Delegatorи реализовать метод __getobj__; именно таким образом реализован класс SimpleDelegator. При этом мы получаем больший контроль над делегированием. Но если вам необходим больший контроль, то, вероятно, вы все равно осуществляете делегирование на уровне отдельных методов, а не класса в целом. Тогда лучше воспользоваться библиотекой forwardable. Вернемся к примеру очереди: require 'forwardable' class MyQueue extend Forwardable def initialize(obj=[]) @queue = obj # Делегировать этому объекту. end def_delegator :@queue, :push, :enqueue def_delegator :@queue, :shift, :dequeue def_delegators :@queue, :clear, :empty?, :length, :size, :<< # Прочий код... end Как видно из этого примера, метод def_delegatorассоциирует вызов метода (скажем, enqueue) с объектом-делегатом @queueи одним из методов этого объекта ( push). Иными словами, когда мы вызываем метод enqueueдля объекта MyQueue, производится делегирование методу push объекта @queue(который обычно является массивом). Обратите внимание, мы пишем :@queue, а не :queueили @queue. Объясняется это тем, как написан класс Forwardable; можно было бы сделать и по-другому. Иногда нужно делегировать методы одного объекта одноименным методам другого объекта. Метод def_delegatorsпозволяет задать произвольное число таких методов. Например, в примере выше показано, что вызов метода lengthобъекта MyQueueприводит к вызову метода lengthобъекта @queue. В отличие от первого примера, остальные методы делегирующим объектом просто не поддерживаются. Иногда это хорошо, ведь не хотите же вы вызывать метод []или []=для очереди; если вы так поступаете, то очередь перестает быть очередью. Отметим еще, что показанный выше код позволяет вызывающей программе передавать объект конструктору (для использования в качестве объекта-делегата). В полном соответствии с духом «утилизации» это означает, что я могу выбирать вид объекта, которому делегируется управление, коль скоро он поддерживает те методы, которые вызываются в программе. Например, все приведенные ниже вызовы допустимы. (В последних двух предполагается, что предварительно было выполнено предложение require 'thread'.) q1 = MyQueue.new # Используется любой массив. q2 = MyQueue.new(my_array) # Используется конкретный массив. q3 = MyQueue.new(Queue.new) # Используется Queue (thread.rb). q4 = MyQueue.new(SizedQueue.new) # Используется SizedQueue (thread.rb). Так, объекты q3и q4волшебным образом становятся безопасными относительно потоков, поскольку делегируют управление безопасному в этом отношении объекту (если, конечно, какой-нибудь не показанный здесь код не нарушит эту гарантию). Существует также класс SingleForwardable, который воздействует на один экземпляр, а не на класс в целом. Это полезно, если вы хотите, чтобы какой-то конкретный объект делегировал управление другому объекту, а все остальные объекты того же класса так не поступали. Быть может, вы задумались о том, что лучше — делегирование или наследование. Но это неправильный вопрос. В некоторых ситуациях делегирование оказывается более подходящим решением. Предположим, к примеру, что имеется класс, у которого уже есть родитель. Унаследовать еще от одного родителя мы не можем (в Ruby множественное наследование запрещено), но делегирование в той или иной форме вполне допустимо. 11.2.10. Автоматическое определение методов чтения и установки на уровне классаМы уже рассматривали методы attr_reader, attr_writerи attr_accessor, которые немного упрощают определение методов чтения и установки атрибутов экземпляра. А как быть с атрибутами уровня класса? В Ruby нет аналогичных средств для их автоматического создания. Но можно написать нечто подобное самостоятельно. В первом издании этой книги была показана хитроумная схема на основе метода class_eval. С ее помощью мы создали такие методы, как cattr_readerи cattr_writer. Но есть более простой путь. Откроем синглетный класс и воспользуемся в нем семейством методов attr. Получающиеся переменные экземпляра для синглетного класса станут переменными экземпляра класса. Часто это оказывается лучше, чем переменные класса, поскольку они принадлежат данному и только данному классу, не распространяясь вверх и вниз по иерархии наследования. class MyClass @alpha = 123 # Инициализировать @alpha. class << self attr_reader :alpha # MyClass.alpha() attr_writer :beta # MyClass.beta=() attr_accessor :gamma # MyClass.gamma() и end # MyClass.gamma=() def MyClass.look puts " #@alpha, #@beta, #@gamma" end #... end puts MyClass.alpha # 123 MyClass.beta = 456 MyClass.gamma = 789 puts MyClass.gamma # 789 MyClass.look # 123, 456, 789 Как правило, класс без переменных экземпляра бесполезен. Но здесь мы их для краткости опустили. 11.2.11. Поддержка различных стилей программирования
В различных кругах популярны разные философии программирования. Часто их трудно охарактеризовать с точки зрения объектной ориентированности или динамичности, а некоторые вообще не зависят от того, является ли язык динамическим или объектно-ориентированным. Поскольку мы отнюдь не эксперты в этих вопросах, будем полагаться в основном на чужие слова. Так что воспринимайте то, что написано ниже, с некоторой долей скепсиса. Некоторые программисты предпочитают стиль ООП на основе прототипов (или ООП без классов). В этом мире объект не описывается как экземпляр класса, а строится с нуля. На базе такого прототипа могут создаваться другие объекты. В 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. Динамические механизмы
Многие читатели имеют опыт работы со статическими языками, например С. Им я адресую риторический вопрос: «Можете ли вы представите себе написанную на С функцию, которая принимает строку, рассматривает ее как имя переменной и возвращает значение этой переменной?» Нет? А как насчет того, чтобы удалить или заменить определение функции? А перехватить обращения к несуществующим функциям? Или узнать имя вызывающей функции? Или автоматически получить список определенных пользователем элементов программы (например, перечень всех написанных вами функций)? В 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. Просмотр стека вызовов(Talking Heads, «Once in a Lifetime»)And you may ask yourself: Иногда необходимо знать, кто вызвал метод. Эта информация полезна, если, например, произошло неисправимое исключение. Метод caller, определенный в модуле Kernel, дает ответ на этот вопрос. Он возвращает массив строк, в котором первый элемент соответствует вызвавшему методу, следующий — методу, вызвавшему этот метод, и т.д. def func1 puts caller[0] end def func2 func1 end func2 # Печатается: somefile.rb:6:in 'func2' Строка имеет формат «файл;строка» или «файл;строка в методе». 11.3.10. Мониторинг выполнения программыПрограмма на Ruby может следить за собственным выполнением. У этой возможности есть много применений; интересующийся читатель может заглянуть в исходные тексты программ debug.rb, profile.rbи tracer.rb. С ее помощью можно даже создать библиотеку для «проектирования по контракту» (design-by-contract, DBC), хотя наиболее популярная в данный момент библиотека такого рода этим средством не пользуется. Интересно, что этот фокус реализован целиком на Ruby. Мы пользуемся методом set_trace_func, который позволяет вызывать указанный блок при возникновении значимых событий в ходе исполнения программы. В справочном руководстве описывается последовательность вызова set_trace_func, поэтому здесь мы ограничимся простым примером: def meth(n) sum = 0 for i in 1..n sum += i end sum end set_trace_func(proc do |event, file, line, id, binding, klass, *rest| printf "%8s %s:%d %s/%s\n", event, file, line, klass, id end) meth(2) Отметим, что здесь соблюдается стандартное соглашение о заключении многострочного блока в операторные скобки do-end. Круглые скобки обязательны из-за особенностей синтаксического анализатора Ruby. Можно было бы, конечно, вместо этого поставить фигурные скобки. Вот что будет напечатано в результате выполнения этого кода: line prog.rb:13 false/ call prog.rb:1 Object/meth line prog.rb:2 Object/meth line prog.rb:3 Object/meth c-call prog.rb:3 Range/each line prog.rb:4 Object/meth c-call prog.rb:4 Fixnum/+ c-return prog.rb:4 Fixnum/+ line prog.rb:4 Object/meth c-call prog.rb:4 Fixnum/+ c-return prog.rb:4 Fixnum/+ c-return prog.rb:4 Range/each line prog.rb:6 Object/meth return prog.rb:6 Object/meth С этим методом тесно связан метод Kernel#trace_var, который вызывает указанный блок при каждом присваивании значения глобальной переменной. Предположим, что вам нужно извне протрассировать выполнение программы в целях отладки. Проще всего воспользоваться для этого библиотекой tracer. Пусть имеется следующая программа prog.rb: def meth(n) (1..n).each {|i| puts i} end meth(3) Можно запустить tracerиз командной строки: % ruby -r tracer prog.rb #0:prog.rb:1::-: def meth(n) #0:prog.rb:1:Module:>: def meth(n) #0:prog.rb:1:Module:<: def meth(n) #0:prog.rb:8::-: meth(2) #0:prog.rb:1:Object:>: def meth(n) #0:prog.rb:2:Object:-: sum = 0 #0:prog.rb:3:Object:-: for i in 1..n #0:prog.rb:3:Range:>: for i in 1..n #0:prog.rb:4:Object:-: sum += i #0:prog.rb:4:Fixnum:>: sum += i #0:prog.rb:4:Fixnum:<: sum += i #0:prog.rb:4:Object:-: sum += i #0:prog.rb:4:Fixnum:>: sum += i #0:prog.rb:4:Fixnum:<: sum += i #0:prog.rb:4:Range:<: sum += i #0:prog.rb:6:Object:-: sum #0:prog.rb:6:Object:<: sum Программа tracerвыводит номер потока, имя файла и номер строки, имя класса, тип события и исполняемую строку исходного текста трассируемой программы. Бывают следующие типы событий: '-'— исполняется строка исходного текста, '>'— вызов, '<'— возврат, 'С'— класс, 'Е'— конец. (Если вы автоматически включите эту библиотеку с помощью переменной окружения RUBYOPTили каким-то иным способом, то может быть напечатано много тысяч строк.) 11.3.11. Обход пространства объектовСистема исполнения Ruby должна отслеживать все известные объекты (хотя бы для того, чтобы убрать мусор, когда на объект больше нет ссылок). Информацию о них можно получить с помощью метода ObjectSpace.each_object. ObjectSpace.each_object do |obj| printf "%20s: %s\n", obj.class, obj.inspect end Если задать класс или модуль в качестве параметра each_object, то будут возвращены лишь объекты указанного типа. Модуль Object Space полезен также для определения чистильщиков объектов (см. раздел 11.3.14). 11.3.12. Обработка вызовов несуществующих методовИногда бывают полезны классы, отвечающие на вызовы произвольных методов. Например, для того чтобы обернуть обращения к внешним программам в класс, который представляет каждое такое обращение как вызов метода. Заранее имена всех программ вы не знаете, поэтому написать определения всех методов при создании класса не получится. На помощь приходит метод Object#method_missing. Если объект Ruby получает сообщение для метода, который в нем не реализован, то вызывается метод method_missing. Этим можно воспользоваться для превращения ошибки в обычный вызов метода. Реализуем класс, обертывающий команды операционной системы: class CommandWrapper def method_missing(method, *args) system (method.to_s, *args) end end cw = CommandWrapper.new cw.date # Sat Apr 28 22:50:11 CDT 2001 cw.du '-s', '/tmp' # 166749 /tmp Первый параметр метода method_missing— имя вызванного метода (которое не удалось найти). Остальные параметры — все то, что было передано при вызове этого метода. Если написанная вами реализация method_missingне хочет обрабатывать конкретный вызов, она должна вызвать super, а не возбуждать исключение. Тогда методы method_missingв суперклассах получат возможность разобраться с ситуацией. В конечном счете будет вызван method_missing, определенный в классе Object, который и возбудит исключение. 11.3.13. Отслеживание изменений в определении класса или объектаА зачем, собственно? Кому интересны изменения, которым подвергался класс? Одна возможная причина — желание следить за состоянием выполняемой программы на Ruby. Быть может, мы реализуем графический отладчик, который должен обновлять список методов, добавляемых «на лету». Другая причина: мы хотим вносить соответствующие изменения в другие классы. Например, мы разрабатываем модуль, который можно включить в определение любого класса. С момента включения будут трассироваться любые обращения к методам этого класса. Что-то в этом роде: class MyClass include Tracing def one end def two(x, y) end end m = MyClass.new m.one # Вызван метод one. Параметры = m.two(1, 'cat') # Вызван метод two. Параметры = 1, cat Он должен работать также для всех подклассов трассируемого класса: class Fred < MyClass def meth(*a) end end Fred.new.meth{2,3,4,5) # вызван метод meth. Параметры =2, 3, 4, 5 Возможная реализация такого модуля показана в листинге 11.18. Листинг 11.18. Трассирующий модульmodule Tracing def Tracing.included(into) into.instance_methods(false).each { |m| Tracing.hook_method(into, m) } def into.method_added(meth) unless @adding @adding = true Tracing.hook_method(self, meth) @adding = false end end end def Tracing.hook_method(klass, meth) klass.class_eval do alias_method "old_#{meth}", "#{meth}" define_method(meth) do |*args| puts "Вызван метод #{meth}. Параметры = #{args.join(', ')}" self.send("old_#{meth}",*args) end end end end class MyClass include Tracing def first_meth end def second_meth(x, y) end end m = MyClass.new m.first_meth # Вызван метод first_meth. Параметры = m.second_meth(1, 'cat') # Вызван метод second_meth. Параметры = 1, cat В этом коде два основных метода. Первый, included, вызывается при каждой вставке модуля в класс. Наша версия делает две вещи: вызывает метод hook_methodкаждого метода, уже определенного в целевом классе, и вставляет определение метода method_addedв этот класс. В результате любой добавленный позже метод тоже будет обнаружен и для него вызван hook_method. Сам метод hook_methodработает прямолинейно. При добавлении метода ему назначается синоним old_name. Исходный метод заменяется кодом трассировки, который выводит имя и параметры метода, а затем вызывает метод, к которому было обращение. Обратите внимание на использование конструкции alias_method. Работает она почти так же, как alias, но только для методов (да и сама является методом, а не ключевым словом). Можно было бы записать эту строку иначе: # Еще два способа записать эту строку... # Символы с интерполяцией: alias_method :"old_#{meth}", :"#{meth}" # Преобразование строк с помощью to_sym: alias_method "old_#{meth}".to_sym, meth.to_sym Чтобы обнаружить добавление нового метода класса в класс или модуль, можно определить метод класса singleton_method_addedвнутри данного класса. (Напомним, что синглетный метод в этом смысле — то, что мы обычно называем методом класса, поскольку Class — это объект.) Этот метод определен в модуле Kernelи по умолчанию ничего не делает, но мы можем переопределить его, как сочтем нужным. class MyClass def MyClass.singleton_method_added(sym) puts "Добавлен метод #{sym.to_s} в класс MyClass." end def MyClass.meth1 puts "Я meth1." end end def MyClass.meth2 puts "А я meth2." end В результате выводится следующая информация: Добавлен метод singleton_method_added в класс MyClass. Добавлен метод meth1 в класс MyClass. Добавлен метод meth2 в класс MyClass. Отметим, что фактически добавлено три метода. Возможно, это противоречит вашим ожиданиям, но метод singleton_method_addedможет отследить и добавление самого себя. Метод inherited(из Class) используется примерно так же. Он вызывается в момент создания подкласса. class MyClass def MyClass.inherited(subclass) puts "#{subclass} наследует MyClass." end # ... end class OtherClass < MyClass # ... end # Выводится: OtherClass наследует MyClass. Можно также следить за добавлением методов экземпляра модуля к объекту (с помощью метода extend). При каждом выполнении extend вызывается метод extend_object. module MyMod def MyMod.extend_object(obj) puts "Расширяется объект id #{obj.object_id}, класс #{obj.class}" super end # ... end x = [1, 2, 3] x.extend(MyMod) # Выводится: # Расширяется объект id 36491192, класс Array Обращение к superнеобходимо для того, чтобы мог отработать исходный метод extend_object. Это напоминает поведение метода append_features(см. раздел 11.1.12); данный метод годится также для отслеживания использования модулей. 11.3.14. Определение чистильщиков для объектовУ классов в Ruby есть конструкторы (методы newи initialize), но нет деструкторов (методов, которые уничтожают объекты). Объясняется это тем, что в Ruby применяется алгоритм пометки и удаления объектов, на которые не осталось ссылок (сборка мусора); вот почему деструктор просто не имеет смысла. Однако тем, кто переходит на Ruby с таких языков, как C++, этот механизм представляется необходимым — часто задается вопрос, как написать код очистки уничтожаемых объектов. Простой ответ звучит так: невозможно сделать это надежно. Но можно написать код, который будет вызываться, когда сборщик мусора уничтожает объект. а = "hello" puts "Для строки 'hello' ИД объекта равен #{a.id}." ObjectSpace.define_finalizer(а) { |id| puts "Уничтожается #{id}." } puts "Нечего убирать." GC.start a = nil puts "Исходная строка - кандидат на роль мусора." GC.start Этот код выводит следующее: Для строки 'hello' ИД объекта равен 537684890. Нечего убирать. Исходная строка - кандидат на роль мусора. Уничтожается 537684890. Подчеркнем, что к моменту вызова чистильщика объект уже фактически уничтожен. Попытка преобразовать идентификатор в ссылку на объект с помощью метода ObjectSpace._id2refприведет к исключению RangeErrorс сообщением о том, что вы пытаетесь воспользоваться уничтоженным объектом. Имейте в виду, что в Ruby применяется консервативный вариант сборки мусора по алгоритму пометки и удаления. Нет гарантии, что любой объект будет убран до завершения программы. Однако все это может оказаться и ненужным. В Ruby существует стиль программирования, в котором для инкапсуляции работы с ресурсами служат блоки. В конце блока ресурс освобождается, и жизнь продолжается без помощи чистильщиков. Рассмотрим, например, блочную форму метода File.open: File.open("myfile.txt") do |file| line1 = file.read # ... end Здесь в блок передается объект File, а по выходе из блока файл закрывается, причем все это делается под контролем метода open. Функциональное подмножество метода File.openна чистом Ruby (сейчас этот метод ради эффективности написан на С) могло бы выглядеть так: def File.open(name, mode = "r") f = os_file_open(name, mode) if block_given? begin yield f ensure f.close end return nil else return f end end Мы проверяем наличие блока. Если блок был передан, то мы вызываем его, передавая открытый файл. Делается это в контексте блока begin-end, который гарантирует, что файл будет закрыт по выходе из блока, даже если произойдет исключение. 11.4. ЗаключениеВ этой главе были приведены примеры использования более сложных и даже экзотических механизмов ООП, а также решения некоторых рутинных задач. Мы видели, как реализуются некоторые паттерны проектирования. Познакомились мы и с API отражения в Ruby, продемонстрировали ряд интересных следствий динамической природы Ruby и некоторые трюки, возможные в динамическом языке. Пришло время вернуться в реальный мир. Ведь ООП — не самоцель, а всего лишь средство достижения цели. Последняя же заключается в написании эффективных, безошибочных и удобных для сопровождения приложений. В современном окружении таким приложениям часто необходим графический интерфейс. В главе 12 мы рассмотрим создание графических интерфейсов на языке Ruby. Примечания:1 Огромное спасибо (яп.) 12 Ни сердцу, ни сознанью беглый взгляд((Пер. С. Маршака)) 13 И задаешь себе вопрос: |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|