Программирование на языке Ruby.
11.2. Более сложные механизмы.
Не все в модели ООП, реализованной в Ruby, одинаково очевидно. Что-то сложнее, что-то применяется реже. Линия раздела для каждого программиста проходит в разных местах. В этой части главы мы попытались собрать те средства, которые не так просты или не так часто встречаются в программах.
Иногда вы задаетесь вопросом, можно ли решить на Ruby ту или иную задачу. Краткий ответ таков: Ruby — богатый, динамический, объектно-ориентированный язык с широким набором разумно ортогональных средств; если нечто можно сделать на каком-то другом языке, то, скорее всего, можно и на Ruby.
Теоретически все полные по Тьюрингу языки более или менее одинаковы. Весь смысл проектирования языков в поиске осмысленной, удобной нотации. Читателю, сомневающемуся в важности нотации, стоит попробовать написать интерпретатор LISP на языке COBOL или выполнить деление чисел, записанных римскими цифрами.
Конечно, мы не хотим сказать, что любая задача на Ruby решается элегантно или естественно. Попытайся мы высказать такое утверждение, кто-нибудь очень быстро докажет, что мы не правы.
В этом разделе мы поговорим также о реализации на Ruby различных стилей программирования, например функционального и аспектно-ориентированного. Мы не претендуем на роль экспертов в этих областях, просто приводим мнение других. Относитесь к этому с долей скепсиса.
11.2.1. Отправка объекту явного сообщения.
В статическом языке вы считаете очевидным, что имя вызываемой функции «зашито» в программу, это часть исходного текста. Динамический язык обладает в данном отношении большей гибкостью.
При любом вызове метода вы посылаете объекту сообщение. Обычно эти сообщения так же жестко «зашиты» в код, как и в статическом языке, но это необязательно. Можно написать программу, которая во время выполнения решает, какой метод вызывать. Метод send позволяет использовать Symbol для представления имени метода.
Пусть, например, имеется массив объектов, который нужно отсортировать, причем в качестве ключей сортировки хотелось бы использовать разные поля. Не проблема - можно просто написать специализированные блоки для сортировки. Но хотелось бы найти более элегантное решение, позволяющее обойтись одной процедурой, способной выполнить сортировку по любому указанному ключу. В листинге 11.9 такое решение приведено.
Этот пример был написан для первого издания книги. Теперь метод sort_by стал стандартным и даже более эффективным, поскольку реализует преобразование Шварца (по имени известного гуру в языке Perl Рэндала Шварца) и сохраняет преобразованные значения вместо многократного их вычисления. Впрочем, листинг 11.9 по-прежнему дает пример использования метода send.
Листинг 11.9. Сортировка по любому ключу.
Class Person.
attr_reader :name, :age, :height.
def initialize(name, age, height).
@name, @age, @height = name, age, height.
end.
def inspect.
"#@name #@age #@height".
end.
End.
Class Array.
def sort_by(sym) # Наш вариант метода sort_by.
self.sort {|x,y| x.send(sym) <=> y.send(sym) }.
end.
End.
People = [].
People << Person.new("Hansel", 35, 69).
People << Person.new("Gretel", 32, 64).
People << Person.new("Ted", 36, 68).
People << Person.new("Alice", 33, 63).
P1 = people.sort_by(:name).
P2 = people.sort_by(:age).
P3 = people.sort_by(:height).
P p1 # [Alice 33 63, Gretel 32 64, Hansel 35 69, Ted 36 68].
P p2 # [Gretel 32 64, Alice 33 63, Hansel 35 69, Ted 36 68].
P p3 # [Alice 33 63, Gretel 32 64, Ted 36 68, Hansel 35 69].
Отметим еще, что синоним __send__ делает в точности то же самое. Такое странное имя объясняется, вероятно, опасением, что имя send уже может быть задействовано (случайно или намеренно) для определенного пользователем метода.
11.2.2. Специализация отдельного объекта.
Я солипсист и, признаться, удивлен, что большинство из нас таковыми не являются.В большинстве объектно-ориентированных языков все объекты одного класса ведут себя одинаково. Класс — это шаблон, порождающий объекты с одним и тем же интерфейсом при каждом вызове конструктора.
Ruby ведет себя так же, но это не конец истории. Получив объект, вы можете изменить его поведение на лету. По сути дела, вы ассоциируете с объектом частный, анонимный подкласс, все методы исходного подкласса остаются доступными, но добавляется еще и поведение, уникальное для данного объекта. Поскольку это поведение присуще лишь данному объекту, оно встречается только один раз. Нечто, встречающееся только один раз, называется синглетом (singleton). Так, мы имеем синглетные методы и синглетные классы.
Слово «синглет» может стать источником путаницы, потому что оно употребляется и в другом смысле - как название хорошо известного паттерна проектирования, описывающего класс, для которого может существовать лишь один объект. Если вас интересует такое использование, обратитесь к библиотеке singleton.rb.
В следующем примере мы видим два объекта, оба строки. Для второго мы добавляем метод upcase, который переопределяет существующий метод с таким же именем.
А = "hello".
B = "goodbye".
Def b.upcase # Создать синглетный метод.
gsub(/(.)(.)/) { $1.upcase + $2 }.
End.
Puts a.upcase # HELLO.
Puts b.upcase # GoOdBye.
Добавление синглетного метода к объекту порождает синглетный класс для данного объекта, если он еще не был создан ранее. Родителем синглетного класса является исходный класс объекта. (Можно считать, что это анонимный подкласс исходного класса.) Если вы хотите добавить к объекту несколько методов, то можете создать синглетный класс явно:
B = "goodbye".
Class << b.
def upcase # Создать синглетный метод.
gsub(/(.){.)/) { $1.upcase + $2 }.
end.
def upcase!
gsub!(/(.)(.)/) { $1.upcase + $2 }.
end.
End.
Puts b.upcase # GoOdBye.
Puts b # goodbye.
B.upcase!
Puts b # GoOdBye.
Отметим попутно, что у более «примитивных» объектов (например, Fixnum) не может быть добавленных синглетных методов. Связано это с тем, что такие объекты хранятся как непосредственные значения, а не как ссылки. Впрочем, реализация подобной функциональности планируется в будущих версиях Ruby (хотя непосредственные значения сохранятся).
Если вам приходилось разбираться в коде библиотек, то наверняка вы сталкивались с идиоматическим использованием синглетных классов. В определении класса иногда встречается такой код:
Class SomeClass.
# Stuff...
class << self.
# Какой-то код.
end.
# ...продолжение.
End.
В теле определения класса слово self обозначает сам определяемый класс, поэтому создание наследующего ему синглета модифицирует класс этого класса. Можно сказать, что методы экземпляра синглетного класса извне выглядят как методы самого класса.
Class TheClass.
class << self.
def hello.
puts "hi".
end.
end.
End.
# вызвать метод класса.
TheClass.hello # hi.
Еще одно распространенное применение такой техники — определение на уровне класса вспомогательных функций, к которым можно обращаться из других мест внутри определения класса. Например, мы хотим определить несколько функций доступа, которые преобразуют результат своей работы в строку. Можно, конечно, написать отдельно код каждой такой функции. Но есть и более элегантное решение — определить функцию уровня класса accessor_string, которая сгенерирует необходимые нам функции (как показано в листинге 11.10).
Листинг 11.10. Метод уровня класса accessor_string.
Сlаss MyClass.
class << self.
def accessor_string(*names).
names.each do |name|
class_eval <<-EOF.
def #{name}.
@#{name}.to_s.
end.
EOF.
end.
end.
end.
def initialize.
@a = [1,2,3].
@b = Time.now.
end.
accessor_string :a, :b.
End.
О = MyClass.new.
Puts o.a # 123.
Puts o.b # Mon Apr 30 23:12:15 CDT 2001.
Вы наверняка сможете придумать и другие, более изобретательные применения. Метод extend подмешивает к объекту модуль. Методы экземпляра, определенные в модуле, становятся методами экземпляра объекта. Взгляните на листинг 11.11.
Листинг 11.11. Использование метода extend.
Module Quantifier.
def any?
self.each { |x| return true if yield x }.
false.
end.
def all?
self.each { |x| return false if not yield x }.
true.
end.
End.
List = [1, 2, 3, 4, 5].
List.extend(Quantifier).
Flag1 = list.any? {|x| x > 5 } # false.
Flag2 = list.any? {|x| x >= 5 } # true.
Flag3 = list.all? {|x| x <= 10 } # true.
Flag4 = list.all? {|x| x % 2 == 0 } # false.
В этом примере к массиву list подмешаны методы any? и all?.
11.2.3. Вложенные классы и модули.
Классы и модули можно вкладывать друг в друга произвольным образом. Программисты, приступающие к изучению Ruby, могут этого и не знать.
Основная цель данного механизма — упростить управление пространствами имен. Скажем, в класс File вложен класс Stat. Это помогает «инкапсулировать» класс Stat внутри тесно связанного с ним класса, а заодно оставляет возможность в будущем определить класс Stat, не конфликтуя с существующим (скажем, для сбора статистики).
Другой пример дает класс Struct::Tms. Любая новая структура Struct помещается в это пространство имен, не «загрязняя» расположенные выше, a Tms — в действительности тоже Struct.
Кроме того, вложенный класс можно создавать просто потому, что внешний мир не должен знать о нем или обращаться к нему. Иными словами, можно создавать целые классы, подчиняющиеся тому же принципу «сокрытия данных», которому переменные и методы экземпляра следуют на более низком уровне.
Class BugTrackingSystem.
class Bug.
#...
end.
#...
End.
# Никто снаружи не знает о классе Bug.
Можно вкладывать класс в модуль, модуль в класс и т.д. Если вы придумаете интересные и изобретательные способы применения этой техники, дайте нам знать.
11.2.4. Создание параметрических классов.
Изучи правила, потом нарушай их.Предположим, что нужно создать несколько классов, отличающихся только начальными значениями переменных уровня класса. Напомним, что переменная класса обычно инициализируется в самом определении класса.
Class Terran.
@@home_planet = "Earth".
def Terran.home_planet.
@@home_planet.
end.
def Terran.home_planet= (x).
@@home_planet = x.
end.
#...
End.
Все замечательно, но что если нам нужно определить несколько подобных классов? Новичок подумает: «Ну так я просто определю суперкласс!» (листинг 11.12).
Листинг 11.12. Параметрические классы: неправильное решение.
Class IntelligentLife # Неправильный способ решения задачи!
@@home_planet = nil.
def IntelligentLife.home_planet.
@@home _planet.
end.
def IntelligentLife.home_planet=(x).
@@home_planet = x.
end.
#...
End.
Class Terran < IntelligentLife.
@@home_planet = "Earth".
#...
End.
Class Martian < IntelligentLife.
@@home_planet = "Mars".
#...
End.
Но это работать не будет. Вызов Terran.home_planet напечатает не "Earth", а "Mars"! Почему так? Дело в том, что переменные класса — на практике не вполне переменные класса; они принадлежат не одному классу, а всей иерархии наследования. Переменная класса не копируется из родительского класса, а разделяется родителем (и, стало быть, со всеми братьями).
Можно было бы вынести определение переменной класса в базовый класс, но тогда перестали бы работать определенные нами методы класса! Можно было исправить и это, перенеся определения в дочерние классы, однако тем самым губится первоначальная идея, ведь таким образом объявляются отдельные классы без какой бы то ни было «параметризации».
Мы предлагаем другое решение. Отложим вычисление переменной класса до момента выполнения, воспользовавшись методом class_eval. Полное решение приведено в листинге 11.13.
Листинг 11.13. Параметрические классы: улучшенное решение.
Class IntelligentLife.
def IntelligentLife.home_planet.
class_eval("@@home_planet").
end.
def IntelligentLife.home_planet=(x).
class_eval("@@home_planet = #{x}").
end.
# ...
End.
Class Terran < IntelligentLife.
@@home_planet = "Earth".
# ...
End.
Class Martian < IntelligentLife.
@@home_planet = "Mars".
# ...
End.
Puts Terran.home_planet # Earth.
Puts Martian.home_planet # Mars.
Не стоит и говорить, что механизм наследования здесь по-прежнему работает. Все методы и переменные экземпляра, определенные в классе IntelligentLife, наследуются классами Terran и Martian.
В листинге 11.14 предложено, наверное, наилучшее решение. В нем используются только переменные экземпляра, а от переменных класса мы вообще отказались.
Листинг 11.14. Параметрические классы: самое лучшее решение.
Class IntelligentLife.
class << self.
attr_accessor :home_planet.
end.
# ...
End.
Class Terran < IntelligentLife.
self.home_planet = "Earth".
#...
End.
Class Martian < IntelligentLife.
self.home_planet = "Mars".
#...
End.
Puts Terran.home_planet # Earth.
Puts Martian.home_planet # Mars.
Здесь мы открываем синглетный класс и определяем метод доступа home_planet. В двух подклассах определяются собственные методы доступа и устанавливается переменная. Теперь методы доступа работают строго в своих классах.
В качестве небольшого усовершенствования добавим еще вызов метода private в синглетный класс:
Private :home_planet=
Сделав метод установки закрытым, мы запретили изменять значение вне иерархии данного класса. Как всегда, private реализует «рекомендательную» защиту, которая легко обходится. Но объявление метода закрытым по крайней мере говорит, что мы не хотели, чтобы метод вызывался вне определенного контекста.
Есть и другие способы решения этой задачи. Проявите воображение.
11.2.5. Использование продолжений для реализации генератора.
Одно из самых трудных для понимания средств Ruby — продолжение (continuation). Это структурированный способ выполнить нелокальный переход и возврат. В объекте продолжения хранятся адрес возврата и контекст выполнения. В каком-то смысле это аналог функций setjmp/ longjmp в языке С, но объем сохраняемого контекста больше.
Метод callcc из модуля Kernel принимает блок и возвращает объект класса Continuation. Возвращаемый объект передается в блок как параметр, что еще больше все запутывает.
В классе Continuation есть всего один метод call, который обеспечивает нелокальный возврат в конец блока саllсс. Выйти из метода callcc можно либо достигнув конца блока, либо вызвав метод call.
Считайте, что продолжение — что-то вроде операции «сохранить игру» в классических «бродилках». Вы сохраняете игру в точке, где все спокойно, а потом пробуете выполнить нечто потенциально опасное. Если эксперимент заканчивается гибелью, то вы восстанавливаете сохраненное состояние игры и пробуете пойти другим путем.
Самый лучший способ разобраться в продолжениях — посмотреть фильм «Беги, Лола, беги».
Есть несколько хороших примеров того, как пользоваться продолжениями. Самые лучшие предложил Джим Вайрих (Jim Weirich). Ниже показано, как Джим реализовал «генератор» после дискуссии еще с одним программистом на Ruby, Хью Сассе (Hugh Sasse).
Идея генератора навеяна методом suspend из языка Icon (он есть также в Prolog), который позволяет возобновить выполнение функции с места, следующего за тем, где она в последний раз вернула значение. Хью называет это «yield наоборот».
Библиотека generator теперь входит в дистрибутив Ruby. Дополнительную информацию по этому вопросу вы найдете в разделе 8.3.7.
В листинге 11.15 представлена предложенная Джимом реализация генератора чисел Фибоначчи. Продолжения применяются для того, чтобы сохранить состояние между вызовами.
Листинг 11.15. Генератор чисел Фибоначчи.
Class Generator.
def initialize.
do_generation.
end.
def next.
callcc do |here|
@main_context = here;
@generator_context.call.
end.
end.
private.
def do_generation.
callcc do |context|
@generator_context = context;
return.
end.
generating_loop.
end.
def generate(value).
callcc do |context|
@generator_context = context;
@main_context.call(value).
end.
end.
End.
# Порождаем подкласс и определяем метод generating_loop.
Class FibGenerator < Generator.
def generating_loop.
generate(1).
a, b = 1, 1.
loop do.
generate(b).
a, b = b, a+b.
end.
end.
End.
# Создаем объект этого класса...
Fib = FibGenerator.new.
Puts fib.next # 1.
Puts fib.next # 1.
Puts fib.next # 2.
Puts fib.next # 3.
Puts fib.next # 5.
Puts fib.next # 8.
Puts fib.next # 13.
# И так далее...
Есть, конечно, и более практичные применения продолжений. Один из примеров — каркас Borges для разработки Web-приложений (названный в честь Хорхе Луиса Борхеса), который построен по образу Seaside. В этой парадигме традиционный поток управления в Web-приложении «вывернут с изнанки на лицо», так что логика представляется «нормальной». Например, вы отображаете страницу, получаете результат из формы, отображаете следующую страницу и так далее, ни в чем не противореча интуитивным ожиданиям.
Проблема в том, что продолжение — «дорогая» операция. Необходимо сохранить состояние и потратить заметное время на переключение контекста. Если производительность для вас критична, прибегайте к продолжениям с осторожностью.
11.2.6. Хранение кода в виде объекта.
Неудивительно, что Ruby предлагает несколько вариантов хранения фрагмента кода в виде объекта. В этом разделе мы рассмотрим объекты Proc, Method и UnboundMethod.
Встроенный класс Proc позволяет обернуть блок в объект. Объекты Proc, как и блоки, являются замыканиями, то есть запоминают контекст, в котором были определены.
Myproc = Proc.new { |a| puts "Параметр равен #{а}" }.
Myproc.call(99) # Параметр равен 99.
Кроме того, Ruby автоматически создает объект Proc, когда метод, последний параметр которого помечен амперсандом, вызывается с блоком в качестве параметра:
Def take_block(x, &block).
puts block.class.
x.times {|i| block[i, i*i] }.
End.
Take_block(3) { |n,s| puts "#{n} в квадрате равно #{s}" }.
В этом примере демонстрируется также применение квадратных скобок как синонима метода call. Вот что выводится в результате исполнения:
Proc.
0 в квадрате 0.
1 в квадрате 1.
2 в квадрате 4.
Объект Proc можно передавать методу, который ожидает блок, предварив имя знаком &:
Myproc = proc { |n| print n, "... " }.
(1..3).each(&myproc) # 1... 2... 3...
Ruby позволяет также превратить метод в объект. Исторически для этого применяется метод Object#method, который создает объект класса Method как замыкание в конкретном объекте.
Str = "cat".
Meth = str.method(:length).
A = meth.call # 3 (длина "cat").
Str << "erpillar".
B = meth.call # 11 (длина "caterpillar").
Str = "dog".
# Обратите внимание на следующий вызов! Переменная str теперь ссылается.
# на новый объект ("dog"), но meth по-прежнему связан со старым объектом.
С = meth.call # 11 (длина "caterpillar").
Начиная с версии Ruby 1.6.2, можно также применять метод Module#instance_method для создания объектов UnboundMethod. С их помощью представляется метод, ассоциированный с классом, а не с конкретным объектом. Прежде чем вызывать объект UnboundMethod, нужно связать его с каким-то объектом. Результатом операции связывания является объект Method, который можно вызывать как обычно:
Umeth = String.instance_method(:length).
M1 = umeth.bind("cat").
M1.call # 3.
M2 = umeth.bind("caterpillar").
M2.call # 11.
Явное связывание делает объект UnboundMethod интуитивно более понятным, чем Method.
11.2.7. Как работает включение модулей?
Когда модуль включается в класс, Ruby на самом деле создает прокси-класс, являющийся непосредственным родителем данного класса. Возможно, вам это покажется интуитивно очевидным, возможно, нет. Все методы включаемого модуля «маскируются» методами, определенными в классе.
Module MyMod.
def meth.
"из модуля".
end.
End.
Class ParentClass.
def meth.
"из родителя".
end.
End.
Class ChildClass < ParentClass.
include MyMod.
def meth.
"из потомка".
end.
End.
X = ChildClass.new p.
P x.meth # Из потомка.
Выглядит это как настоящее наследование: все, что потомок переопределил, становится действующим определением вне зависимости от того, вызывается ли include до или после переопределения.
Вот похожий пример, в котором метод потомка вызывает super, а не просто возвращает строку. Как вы думаете, что будет возвращено?
# Модуль MyMod и класс ParentClass не изменились.
Class ChildClass < ParentClass.
include MyMod.
def meth.
"Из потомка: super = " + super.
end.
End.
X = ChildClass.new.
P x.meth # Из потомка: super = из модуля.
Отсюда видно, что модуль действительно является новым родителем класса. А что если мы точно также вызовем super из модуля?
Module MyMod.
def meth.
"Из модуля: super = " + super.
end.
End.
# ParentClass не изменился.
Class ChildClass < ParentClass.
include MyMod.
def meth.
"Из потомка: super = " + super.
end.
End.
X = ChildClass.new.
P x.meth # Из потомка: super = из модуля: super = из родителя.
Метод meth, определенный в модуле MyMod, может вызвать super только потому, что в суперклассе (точнее, хотя бы в одном из его предков) действительно есть метод meth. А что произошло бы, вызови мы этот метод при других обстоятельствах?
Module MyMod.
def meth.
"Из модуля: super = " + super.
end.
End.
Class Foo include MyMod.
End.
X = Foo.new.
X.meth.
При выполнении этого кода мы получили бы ошибку NoMethodError (или обращение к методу method_missing, если бы таковой существовал).
11.2.8. Опознание параметров, заданных по умолчанию.
В 2004 году Ян Макдональд (Ian Macdonald) задал в списке рассылки вопрос: «Можно ли узнать, был ли параметр задан вызывающей программой или взято значение по умолчанию?» Вопрос интересный. Не каждый день он возникает, но от того не менее интересен.
Было предложено по меньшей мере три решения. Самое удачное и простое нашел Нобу Накада (Nobu Nakada). Оно приведено ниже:
Def meth(a, b=(flag=true; 345)).
puts "b равно #{b}, a flag равно #{flag.inspect}".
End.
Meth(123) # b равно 345, a flag равно true.
Meth(123,345) # b равно 345, a flag равно nil.
Meth(123,456) # b равно 456, a flag равно nil.
Как видим, этот подход работает даже, если вызывающая программа явно указала значение параметра, совпадающее с подразумеваемым по умолчанию. Трюк становится очевидным, едва вы его увидите: выражение в скобках устанавливает локальную переменную flag в true, а затем возвращает значение по умолчанию 345. Это дань могуществу Ruby.
11.2.9. Делегирование или перенаправление.
В Ruby есть две библиотеки, которые предлагают решение задачи о делегировании или перенаправлении вызовов методов другому объекту. Они называются delegate и forwardable; мы рассмотрим обе.
Библиотека delegate предлагает три способа решения задачи. Класс SimpleDelegator полезен, когда объект, которому делегируется управление (делегат), может изменяться на протяжении времени жизни делегирующего объекта. Чтобы выбрать объект-делегат, используется метод __setobj__.
Однако мне этот способ представляется слишком примитивным. Поскольку я не думаю, что это существенно лучше, чем то же самое, сделанное вручную, задерживаться на классе SimpleDelegator не стану.
Метод верхнего уровня DelegateClass принимает в качестве параметра класс, которому делегируется управление. Затем он создает новый класс, которому мы можем унаследовать. Вот пример создания класса Queue, который делегирует объекту Array:
Require 'delegate'.
Class MyQueue < DelegateClass(Array).
def initialize(arg=[]).
super(arg).
end.
alias_method :enqueue, :push.
alias_method :dequeue, :shift.
End.
Mq = MyQueue.new.
Mq.enqueue(123).
Mq.enqueue(234).
P mq.dequeue # 123.
P mq.dequeue # 234.
Можно также унаследовать класс Delegator и реализовать метод __getobj__; именно таким образом реализован класс SimpleDelegator. При этом мы получаем больший контроль над делегированием.
Но если вам необходим больший контроль, то, вероятно, вы все равно осуществляете делегирование на уровне отдельных методов, а не класса в целом. Тогда лучше воспользоваться библиотекой forwardable. Вернемся к примеру очереди:
Require 'forwardable'.
Class MyQueue.
extend Forwardable.
def initialize(obj=[]).
@queue = obj # Делегировать этому объекту.
end.
def_delegator :@queue, :push, :enqueue.
def_delegator :@queue, :shift, :dequeue.
def_delegators :@queue, :clear, :empty?, :length, :size, :<<
# Прочий код...
End.
Как видно из этого примера, метод def_delegator ассоциирует вызов метода (скажем, enqueue) с объектом-делегатом @queue и одним из методов этого объекта ( push). Иными словами, когда мы вызываем метод enqueue для объекта MyQueue, производится делегирование методу push объекта @queue (который обычно является массивом).
Обратите внимание, мы пишем :@queue, а не :queue или @queue. Объясняется это тем, как написан класс Forwardable; можно было бы сделать и по-другому.
Иногда нужно делегировать методы одного объекта одноименным методам другого объекта. Метод def_delegators позволяет задать произвольное число таких методов. Например, в примере выше показано, что вызов метода length объекта MyQueue приводит к вызову метода length объекта @queue.
В отличие от первого примера, остальные методы делегирующим объектом просто не поддерживаются. Иногда это хорошо, ведь не хотите же вы вызывать метод [] или []= для очереди; если вы так поступаете, то очередь перестает быть очередью.
Отметим еще, что показанный выше код позволяет вызывающей программе передавать объект конструктору (для использования в качестве объекта-делегата). В полном соответствии с духом «утилизации» это означает, что я могу выбирать вид объекта, которому делегируется управление, коль скоро он поддерживает те методы, которые вызываются в программе.
Например, все приведенные ниже вызовы допустимы. (В последних двух предполагается, что предварительно было выполнено предложение require 'thread'.)
Q1 = MyQueue.new # Используется любой массив.
Q2 = MyQueue.new(my_array) # Используется конкретный массив.
Q3 = MyQueue.new(Queue.new) # Используется Queue (thread.rb).
Q4 = MyQueue.new(SizedQueue.new) # Используется SizedQueue (thread.rb).
Так, объекты q3 и q4 волшебным образом становятся безопасными относительно потоков, поскольку делегируют управление безопасному в этом отношении объекту (если, конечно, какой-нибудь не показанный здесь код не нарушит эту гарантию).
Существует также класс SingleForwardable, который воздействует на один экземпляр, а не на класс в целом. Это полезно, если вы хотите, чтобы какой-то конкретный объект делегировал управление другому объекту, а все остальные объекты того же класса так не поступали.
Быть может, вы задумались о том, что лучше — делегирование или наследование. Но это неправильный вопрос. В некоторых ситуациях делегирование оказывается более подходящим решением. Предположим, к примеру, что имеется класс, у которого уже есть родитель. Унаследовать еще от одного родителя мы не можем (в Ruby множественное наследование запрещено), но делегирование в той или иной форме вполне допустимо.
11.2.10. Автоматическое определение методов чтения и установки на уровне класса.
Мы уже рассматривали методы attr_reader, attr_writer и attr_accessor, которые немного упрощают определение методов чтения и установки атрибутов экземпляра. А как быть с атрибутами уровня класса?
В Ruby нет аналогичных средств для их автоматического создания. Но можно написать нечто подобное самостоятельно.
В первом издании этой книги была показана хитроумная схема на основе метода class_eval. С ее помощью мы создали такие методы, как cattr_reader и cattr_writer.
Но есть более простой путь. Откроем синглетный класс и воспользуемся в нем семейством методов attr. Получающиеся переменные экземпляра для синглетного класса станут переменными экземпляра класса. Часто это оказывается лучше, чем переменные класса, поскольку они принадлежат данному и только данному классу, не распространяясь вверх и вниз по иерархии наследования.
Class MyClass.
@alpha = 123 # Инициализировать @alpha.
class << self.
attr_reader :alpha # MyClass.alpha().
attr_writer :beta # MyClass.beta=().
attr_accessor :gamma # MyClass.gamma() и.
end # MyClass.gamma=().
def MyClass.look.
puts " #@alpha, #@beta, #@gamma".
end.
#...
End.
Puts MyClass.alpha # 123.
MyClass.beta = 456.
MyClass.gamma = 789.
Puts MyClass.gamma # 789.
MyClass.look # 123, 456, 789.
Как правило, класс без переменных экземпляра бесполезен. Но здесь мы их для краткости опустили.
11.2.11. Поддержка различных стилей программирования.
Brother, can you paradigm?В различных кругах популярны разные философии программирования. Часто их трудно охарактеризовать с точки зрения объектной ориентированности или динамичности, а некоторые вообще не зависят от того, является ли язык динамическим или объектно-ориентированным.
Поскольку мы отнюдь не эксперты в этих вопросах, будем полагаться в основном на чужие слова. Так что воспринимайте то, что написано ниже, с некоторой долей скепсиса.
Некоторые программисты предпочитают стиль ООП на основе прототипов (или ООП без классов). В этом мире объект не описывается как экземпляр класса, а строится с нуля. На базе такого прототипа могут создаваться другие объекты. В 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.
Есть и другие полезные ресурсы, особенно для тех, кто говорит по-японски. Трудно перечислять онлайновые ресурсы в печатном издании, поскольку они постоянно изменяются. Поисковая машина — ваш лучший друг.
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
- 359.
- 360.
- 361.
- 362.
- 363.
- 364.
- 365.
- 366.
- 367.
- 368.
- 369.
- 370.
- 371.
- 372.
- 373.
- 374.
- 375.
- 376.
- 377.
- 378.
- 379.
- 380.
- 381.
- 382.
- 383.
- 384.
- 385.
- 386.
- 387.
- 388.
- 389.
- 390.
- 391.
- 392.
- 393.
- 394.
- 395.
- 396.
- 397.
- 398.
- 399.
- 400.
- 401.
- 402.
- 403.
- 404.
- 405.
- 406.
- 407.
- 408.
- 409.
- 410.
- 411.
- 412.
- 413.
- 414.
- 415.
- 416.
- 417.
- 418.
- 419.
- 420.
- 421.
- 422.
- 423.
- 424.
- 425.
- 426.
- 427.
- 428.
- 429.
- 430.
- 431.
- 432.
- 433.
- 434.
- 435.
- 436.
- 437.
- 438.
- 439.
- 440.
- 441.
- 442.
- 443.
- 444.
- 445.
- 446.
- 447.
- 448.
- 449.
- 450.
- 451.
- 452.
- 453.
- 454.
- 455.
- 456.
- 457.
- 458.
- 459.
- 460.
- 461.
- 462.
- 463.
- 464.
- 465.
- 466.
- 467.
- 468.
- 469.
- 470.
- 471.
- 472.
- 473.
- 474.
- 475.
- 476.
- 477.
- 478.
- 479.
- 480.
- 481.
- 482.
- 483.
- 484.
- 485.
- 486.
- 487.
- 488.
- 489.
- 490.
- 491.
- 492.
- 493.
- 494.
- 495.
- 496.
- 497.
- 498.
- 499.
- 500.
- 501.
- 502.
- 503.
- 504.
- 505.
- 506.
- 507.
- 508.
- 509.
- 510.
- 511.
- 512.
- 513.
- 514.
- 515.
- 516.
- 517.
- 518.
- 519.
- 520.
- 521.
- 522.
- 523.
- 524.
- 525.
- 526.
- 527.
- 528.
- 529.
- 530.
- 531.
- 532.
- 533.
- 534.
- 535.
- 536.
- 537.
- 538.
- 539.
- 540.
- 541.
- 542.
- 543.
- 544.
- 545.
- 546.
- 547.
- 548.
- 549.
- 550.
- 551.
- 552.
- 553.
- 554.
- 555.
- 556.
- 557.
- 558.
- 559.
- 560.
- 561.
- 562.
- 563.
- 564.
- 565.
- 566.
- 567.
- 568.
- 569.
- 570.
- 571.
- 572.
- 573.