Введение в объектно-ориентированный Ruby
В этой статье раскрывается понятие объекта сквозь призму языка Ruby.
Что такое объект?
Объект в программировании — это черный ящик. Коробочка, внутри которой что-то происходит. Что именно, известно только тому, кто спроектировал этот объект, нам же видны только результаты его работы.
Плохо ли это, не знать, что там творится всередине? И да, и нет.
Если у нас есть кофемашина, всё, что от нас требуется — загрузить зёрна, налить воды и воткнуть сетевую вилку в розетку. Какие процессы начинают происходить внутри, нам по фонарю. Может быть, сейчас кофемашина начинает прожарку зерен, а затем будет их молоть и пропускать через них кипяток. А быть может, она соединяется со Всемирной базой готовых вещей и телепортирует оттуда одну порцию горячего кофе. На самом деле, нас волнует только то, насколько будет вкусным напиток, который польется в подставленную кружку.
С другой стороны, если мы не знаем, что машина спроектирована не совсем гуд, и в процессе помола вибрирует настолько, что может свалиться со стола, то не сможем в этот момент придержать ее рукой. Быть в курсе таких тонкостей — ненормально. Но иногда это суровая необходимость, потому что не всегда можно выкинуть такой агрегат на свалку и купить в магазине адекватную модель.
Значит, хорошо спроектированный объект — это штука, которая выполняет некую полезную работу и в которой нас интересует лишь две вещи: что мы подаем на вход и что получаем на выходе.
Методы как задачи
Каждая задача, которую выполняет объект, оформляется в виде метода. Например,
логично ожидать, что у нашего объекта-кофемашины есть как минимум один —
make_coffee
(сварить кофе).
Чем сложнее объект, тем больше у него задач, а значит — и методов. Но в правильно спроектированном объекте все задачи, независимо от количества, не выходят за рамки основного предназначения объекта. Если он начинает напоминать швейцарский нож, в котором есть вилка, шило, отвертка и открывашка, это тревожный звоночек, который говорит о необходимости перепоручения части функционала другому объекту.
Как методы появляются в объекте? Для этого в Ruby существует целых три возможности. Во-первых, объект получает методы от класса, который его создал.
Классы
Класс в Ruby — это особый объект-родитель, который несет в себе знания о методах
объекта-сына (instance methods) и может создавать неограниченное число сыновей.
При этом отцу запрещено пользоваться методами сына, но он может иметь собственные,
которые сыну не достаются (в их числе, например, метод new
, который создает нового сына).
Порожденный объект (его еще называют экземпляром класса) теряет способность класса хранить в себе методы «для сыновей» (другими словами, утрачивает репродуктивную способность).
Классы — объекты?!
Хмм, но если класс является объектом, значит, должен быть какой-то мегакласс, который его породил? В конце концов, методы объекта в классе как-то должны были появиться? Ответ на этот вопрос неоднозначный.
С одной стороны, в Ruby у каждого объекта (в том числе и у класса) есть т. н. синглтон класс (singleton class). Этот «класс» на самом деле не совсем класс, потому что не является первопричиной появления объекта, наоборот, он возникает как фантомный сателлит уже после его рождения (Ruby создает этот класс автоматически).
Синглтон классы существуют для того, чтобы обеспечить каждый объект собственными методами (и это второй путь получения методов для объекта). Для класса они являются единственным источником методов, для его сыновей — дополнительным (вместе с методами экземпляров отца).
Однако, теория синглтон класса никак не объясняет, откуда берутся сами классы в Ruby, поэтому для сбережения собственного психического здоровья проще считать, что они саморождающиеся и возникают…ну, допустим, в результате Большого Взрыва.
Класс как фабрика
Практика показывает: даже если в приложении предполагается создание единственного объекта, под него всё равно стоит завести собственный класс (хотя в Ruby это совершенно не обязательно): для переноса кода между приложениями он подходит как нельзя лучше. А уж если объектов с одинаковым функционалом нужно много, без класса не обойтись.
Представьте себе компанию «Гурман», которая производит кофемашины. Сегодня великий день, они выходят на рынок с бюджетной моделью «Кофемашина». В ней нет всех этих дизайнерских наворотов, зато она умеет хорошо варить кофе.
Прежде, чем на складе появились коробки с «Кофемашинами», инженеры начертили чертеж, в котором подробно указали, как именно должны работать все эти объекты. Затем наладчики по этому чертежу настроили конвейер, запустили его и получили на выходе серию однотипных объектов. Так вот, класс в Ruby — это и чертеж, и конвейер.
Чтобы создать класс, достаточно его объявить с помощью ключевого слова class
, после
которого указывается константа. Она становится именем класса и ссылкой
на созданный объект-класс. Затем (словно в чертеже) записывают методы экземпляров
класса:
class CoffeeMachine
def make_coffee
puts "Готовим воду и зёрна"
puts "Варим и наливаем кофе"
end
end
Вызов метода
Чтобы вызвать метод, Ruby необходима пара: объект и имя метода. Не бывает метода «просто так», у каждого есть хозяин.
Указанному объекту Ruby отправляет сообщение (запрос)
с названием метода. Объект сверяется со своим списком методов, и если таковой найден,
выполняет его и возвращает результат. Если запросить у объекта несуществующий
метод, это приведет к ошибке NoMethodError
.
Самый простой вызов метода выглядит так:
имя_объекта.название_метода
Объект перед вызовом должен существовать. Классы
создаются в процессе своего объявления, экземпляры классов (преимущественно) —
с помощью метода класса new
.
:001 > class CoffeeMachine # создаем объект-класс
:002?> def make_coffee
:003?> puts "Готовим воду и зёрна"
:004?> puts "Варим и наливаем кофе"
:005?> end
:006?> end
=> nil
:007 > saeco = CoffeeMachine.new # создаем экземпляр класса (включаем конвейер)
=> #<CoffeeMachine:0x85dd718>
:008 > saeco.make_coffee
Готовим воду и зёрна
Варим и наливаем кофе
=> nil
:009 > CoffeeMachine.make_coffee
NoMethodError: undefined method 'make_coffee' for CoffeeMachine:Class
Текущий объект
Ruby разрешает вызывать методы без указания имени объекта, потому что постоянно
отслеживает т. н. текущий объект (self). На место пропущенного объекта неявно
подставляется self
, чтобы сохранить пару:
make_coffee # здесь Ruby предполагает self.make_coffee
Текущий объект в разных точках программы ссылается на разные объекты. Внутри метода объекта он является этим объектом. Мы можем легко это проверить:
:001 > class Father
:002?> def check_self(object)
:003?> object == self
:004?> end
:005?> end
=> nil
:006 > son = Father.new
=> #<Father:0x82507d0>
:007 > son.check_self(son)
=> true
:008 > son.check_self(Father)
=> false
Прочувствовать self
внутри метода поначалу нелегко. Если путаетесь, вспоминайте
о том, что методы экземпляра — собственность не класса, а будущих объектов, им
порожденных. Хозяин метода и есть текущий объект внутри него.
Зато внутри объявления класса (вне методов экземпляра) self
указывает на этот
объект-класс, что выглядит вполне логично.
:001 > class Father
:002?> puts "Внутри объявления класса self — это #{self}"
:003?> def print_self
:004?> puts "Внутри метода self — это #{self}"
:005?> end
:006?> end
Внутри объявления класса self — это Father
=> nil
:007 > son = Father.new
=> #<Father:0x8d29800>
:008 > son.print_self
Внутри метода self — это #<Father:0x8d29800>
=> nil
Хорошо, вернемся к предыдущему примеру с кофемашиной. Там в строке 008 вызывается
make_coffee
объекта saeco
, значит, внутри этого метода self
является
объектом saeco
. В вызове метода puts
пропущен хозяин-объект, поэтому на его
месте надо представлять self
:
self.puts "Готовим воду и зёрна"
self.puts "Варим и наливаем кофе"
Но постойте, у объекта saeco
есть только метод make_coffee
! Или нет?
Наследование и класс Object
Все классы в Ruby наследуют от системного класса Object
(если явно не указан
другой класс). Запись
class CoffeeMachine
end
является синтаксическим сахаром для
class CoffeeMachine < Object
end
Знак «меньше» не выполняет никакого
сравнения, здесь его нужно воспринимать как стрелку влево, и читать «класс Object
является предком класса CoffeeMachine
».
Когда класс наследует от другого, происходит две вещи. Во-первых, сын этого класса будет получать в наследство не только методы экземпляра отца, но и всех предков отца. Во-вторых, сам класс получает в наследство методы всех синглтон классов своих предков. В общей стопке методы предков оказываются выше, собственные — в самом низу. Это важно, потому что поиск методов происходит снизу вверх.
Класс Object
не корневой в цепочке наследования, таковым является BasicObject
.
Создание этих классов происходит автоматически во время запуска любого приложения.
В редких случаях разработчику может понадобится наследовать прямо от BasicObject
,
в этой статье они не рассматриваются.
Таким образом, любой созданный объект получает методы классов Object
и
BasicObject
, среди них: методы для самоидентификации class
, superclass
,
object_id
, nil?
, is_a?
, ==
, ===
; клонирования — clone
, dup
;
семейство eval
и exec
методов; методы-«операторы» lambda
,
raise
, require
, rand
, puts
. А объекты-классы ко всему прочему приобретают
популярные методы attr_accessor
, attr_reader
, private
и т. д.
Конечно, для самого объекта этот внушительный арсенал, с точки зрения выполнения его задачи, бесполезен, зато это позволяет вызывать вышеперечисленные методы (важные для самого приложения) у каждого объекта, а значит, в любом контексте.
Организация методов в классе
Вновь обратим наши взоры на компанию «Гурман». Дела там улучшаются: продажи «Кофемашины» превысили ожидаемые в два раза и держатся на этой отметке, пресса публикует восторженные рецензии. Руководство и инвесторы выдохнули с облегчением, появилось даже свободное время. А что делают в больших фирмах, когда нечего делать? Верно — сражаются за качество!
Атомарные методы
Правила хорошего тона в программировании гласят, что каждую задачу нужно разбивать на подзадачи, а подзадачи — на подподзадачи и т. д., до тех пор, пока не останутся только атомарные (неделимые) задачи и задачи-менеджеры, в которых выполняются первые.
Такой подход улучшает читаемость кода, последний становится легче тестировать, а объекту проще добавлять функционал без создания «кодосвалки».
Инженеры «Гурмана» проанализировали код «Кофемашины» и заметили, что метод make_coffee
—
хороший кандидат для расчленения, что и было сделано:
class CoffeeMachine
def make_coffee
get_water(200) # набираем воду
get_beans(50) # набираем зёрна
prepare_beans # готовим зёрна
boil_water # кипятим воду
pour_coffee # наливаем кофе в чашку
end
def get_water(mls)
puts "Набираем в ёмкость #{mls} мл воды."
end
def get_beans(grams)
puts "Отбираем из контейнера #{grams} г зёрен кофе."
end
#...
end
Видимость методов
По умолчанию все методы объектов публичные (public). Такие можно вызывать в любой точке программы. Это означает, что случайно (или даже предумышленно) кто угодно способен запустить одну из подзадач «Кофемашины», что может нарушить объект (повредить в нем данные или вызвать ошибку). Например, достаточно изменить порядок вызова, запустив кипячение до набора воды, и нагревательный элемент кофемашины сгорит.
Чтобы скрыть часть методов объекта от внешнего мира, их делают частными (private) или защищенными (protected). Отличие между ними тонкое, как лезвие японского меча.
И частный, и защищенный методы могут быть вызваны только там, где он есть у
self
. Грубо говоря, такие методы получится применить только внутри
других методов этого же объекта (как boil_water
вызывается внутри
make_coffee
) или его родственника .
Отличие между ними в том, что у частного метода объектом вызова может быть
только self
, причем запрещено даже его явно указывать. Метод puts
является
частным, поэтому мы не имеем права писать
def boil_water
self.puts "Кипятим воду в ёмкости."
end
Только так:
def boil_water
puts "Кипятим воду в ёмкости."
end
Изменить видимость можно с помощью методов класса public
, private
и protected
.
Если они вызываются без аргументов, то выступают в качестве переключателей
режима: методы, записанные под ними, получают соответствующую видимость.
class CoffeeMachine
def make_coffee
get_water(200)
get_beans(50)
#...
end
protected
def get_water(mls)
# ...
end
#...
end
:030 > saeco = CoffeeMachine.new
=> #<CoffeeMachine:0x8df6940>
:031 > puts self
main
=> nil
:032 > saeco.boil_water
NoMethodError: protected method 'boil_water' called for #<CoffeeMachine:0x8df6940>
:033 > saeco.make_coffee
Набираем в ёмкость 200 мл воды.
Отбираем из контейнера 50 г зёрен кофе.
Жарим, мелем зёрна и засыпаем в ёмкость.
Кипятим воду в ёмкости.
Наливаем кофе в кружку.
=> nil
Будь в примере метод boil_water
частным, а не защищенным, результат
был бы точно таким же. Чтобы показать между ними разницу, я создам объект-чистильщик.
Чистильщик устроен очень просто: он берет объект-кофемашину и запускает у нее подзадачи набора, кипячения и слива воды (фактически, он будет пытаться налить кофе, но сливаться-то будет только кипяток), таким образом, ёмкость очищается от кофейных смол.
Чистильщиков будет двое: один будет создан с нуля, а другой — наследован от «Кофемашины», поэтому у него будут те же методы, что и у самой кофемашины.
:034 > class Cleaner
:035?> def clean(machine)
:036?> machine.get_water(200)
:037?> machine.boil_water
:038?> machine.pour_coffee
:039?> end
:040?> end
=> nil
:041 > class MachineCleaner < CoffeeMachine
:042?> def clean(machine)
:043?> machine.get_water(200)
:044?> machine.boil_water
:045?> machine.pour_coffee
:046?> end
:047?> end
=> nil
:048 > cleaner = Cleaner.new
=> #<Cleaner:0x8dbc2cc>
:049 > machine_cleaner = MachineCleaner.new
=> #<MachineCleaner:0x8e2450c>
:050 > cleaner.clean(saeco)
NoMethodError: protected method 'get_water' called for #<CoffeeMachine:0x8df6940>
:051 > machine_cleaner.clean(saeco)
Набираем в ёмкость 200 мл воды.
Кипятим воду в ёмкости.
Наливаем кофе в кружку.
=> nil
Наследование на практике
Наследование — это первая вещь, о которой вспоминают в программировании, когда нужно модифицировать поведение объекта, но как и любая другая практика, оно не является панацеей для всех архитектурных бед.
Новые функции
Тем временем руководство «Гурмана» поручило инженерам разработать модель «Кофемашины» с капучинатором. Раз готовится запуск серии, без класса никуда.
Конечно, очень хотелось добавить вспомогательный функционал прямо в CoffeeMachine
,
но его трогать нельзя: выпуск старых моделей идёт полным ходом! Что ж, организовали
новый класс, с кодовым названием «Капучинщик».
Капучинатор чего должен делать? Взбивать молочную пенку. Создали для этого методы:
class CappuccinoMachine
def create_foam
prepare_milk
push_foam
end
private
def prepare_milk
puts "Отбираем и кипятим молоко"
end
def push_foam
puts "Выпускаем молочную пенку в чашку"
end
end
Но ведь основное предназначение машины — всё-таки варить кофе. «Кофемашина» с этим
отлично справлялась. Может,
скопировать метод make_coffee
со всеми его подзадачами оттуда? Нет, это
порочный путь дублирования кода, надежнее использовать наследование:
class CappuccinoMachine < CoffeeMachine
def create_foam
#...
end
#...
end
Таким образом, экземпляры класса CappuccinoMachine
получат в распоряжение
все методы «Кофемашины», что и требовалось. Так расширяют функционал базового
класса.
Несомненный плюс этого подхода в том, что если технологи решат, например, подправить в «Кофемашине» количество заправляемого кофе, чтобы улучшить вкус напитка, эти изменения автоматически отразятся и в «Капучинщике».
Замена запчастей
Невероятно, но капучинатор произвел фурор у покупателей, модели «Капучинщика» сметают с прилавков в день поступления товара! Однако, руководству этого мало. Чтобы окончательно завоевать рынок, принято решение выпускать модель для бизнес-персонала. Деловым людям всегда некогда: некогда жить, некогда ждать, пока машина пожарит, помелет, им нужен мгновенный результат, и многие из-за этого стали переходить на растворимый кофе (о ужас-ужас!).
Но технологи в «Гурмане» не зря хлеб едят, они придумали капсульный процесс. Покупаешь готовую капсулу с молотым кофе, закладываешь в машину — и варка пошла.
Однако, проблема у инженеров: есть уже хорошо зарекомендовавшая себя «Кофемашина»,
от которой так и хочется позаимствовать части, но вот сам метод make_coffee
—
у него же лишние для капсульной машины get_beans
и prepare_beans
!
Как быть? Начать класс с чистого листа? Но тогда придется дублировать вполне
себе рабочие get_water
, boil_water
, pour_coffee
.
А что, если опять применить наследование?
Оказывается, это может помочь, ведь Ruby всегда ищет методы в
объекте снизу вверх и вызывает первый попавшийся, у которого совпало имя.
Тогда задав make_coffee
в классе-потомке, мы фактически перезаписываем его, потому что
методы потомков всегда в самом низу, и Ruby не успевает добраться до
реализации этого же метода в предке.
Готовый класс будет выглядеть так:
class CapsuleMachine < CoffeeMachine
def make_coffee
get_water(200)
prepare_capsule
boil_water
pour_coffee
end
private
def prepare_capsule
puts "Вскрываем капсулу и высыпаем кофе в ёмкость."
end
end
Несколько слов о super
Иногда бывает необходимо выполнить закрытый потомком метод предка. Конечно, нет смысла перезаписывать метод, чтобы тут же в нем вызывать «оригинальную» версию. Зато бывает полезно выполнить какие-то действия, а затем передать управление старой реализации метода (или наоборот, выполнить что-то после).
Для этого внутри метода вызывается ключевое слово super
. Это
означает «вызови одноименный метод, который определен выше в цепочке наследования».
Если в найденном методе тоже есть super
, Ruby будет подыматься еще выше в
своем поиске. Может оказаться,
что методов с таким именем уже нет, это равносильно вызову несуществующего метода.
:001 > class OldMan
:002?> def say_wisdom
:003?> puts "Народу много, а людей немного."
:004?> end
:005?> end
=> nil
:006 > class Man < OldMan
:007?> def say_wisdom
:008?> super
:009?> puts "Так мой предок говаривал"
:010?> end
:011?> end
=> nil
:012 > man = Man.new
=> #<Man:0x88a0d9c>
:013 > man.say_wisdom
Народу много, а людей немного.
Так мой предок говаривал
=> nil
method_missing
Если объект после сверки со своим списком заявляет, что запрашиваемый метод
отсутствует, Ruby вызывает специальный метод, отвечающий за обработку этого
события — method_missing
.
method_missing
есть в каждом объекте, потому что он достается в наследство
от класса BasicObject
, в его стандартной реализации возбуждается ошибка
NoMethodError
.
Однако, за счет того, что method_missing
находится на самой вершине списка
методов, его можно перезаписать в любом потомке. Что это дает? Имея на руках
имя отсутствующего метода, его можно создать и тут же вызвать, на лету.
Такой подход оправдывает себя, например, когда объект выступает посредником и перенаправляет
вызовы методов другому объекту (получателю). Вместо того, чтобы дублировать
все методы получателя, они создаются в посреднике по мере выполнения запросов
с помощью method_missing
.
Следует помнить, что объекты, использующие такую технику, получают серьезный
пенальти по времени в сравнении с вызовом заранее определенных методов (т. к.
Ruby приходится вначале пройтись в поиске по всем методам объекта, прежде чем
вызвать method_missing
).
Модули
Что ж, рано или поздно это должно было случиться. Маркетологи «Гурмана», вдохновленные успешными продажами капсульной модели, требуют срочно начать выпуск делюкс-версии: капсульной кофемашины с капучинатором!
Кажется, инженеры зашли в тупик. У них есть классическая «Кофемашина» на зёрнах и
два ее наследника: «Капучинщик» с капучинатором и капсульная
модель с модифицированным методом make_coffee
. Если продолжать наследование и
дальше, чтобы получить капсульный make_coffee
+ капучинатор, уйти от дублирования
кода не получится, какую из последних двух моделей не взять!
И тут на помощь приходят модули Ruby. Модуль представляет собой именованную группу. Если в ней разместить методы, их можно будет «подмешать» к методам любого класса.
Сила модулей в том, что он может одновременно входить в состав различных классов, а в один класс к тому же можно «подмешивать» различные модули, за счет чего достигается сумасшедшее количество комбинаций функциональных возможностей объекта без дублирования кода.
Модули в Ruby тоже являются объектами, однако, в отличие от классов, как объекты используются редко. Основное предназначение модуля — быть поставщиком методов для классов и отдельных объектов.
Методы капучинатора — очень хорошие кандидаты для вынесения в модуль, потому что они должны присутствовать сразу в двух моделях (классах) и отсутствовать в двух других.
module Cappuccinator
def create_foam
prepare_milk
push_foam
end
private
def prepare_milk
puts "Отбираем и кипятим молоко"
end
def push_foam
puts "Выпускаем молочную пенку в чашку"
end
end
Теперь надо включить этот модуль в «Капучинщик», удалив его старые методы:
class CappuccinoMachine < CoffeeMachine
include Cappuccinator
end
Модель капсульной кофемашины с капучинатором получается так же просто:
class CapsuleCappuccino < CapsuleMachine
include Cappuccinator
end
Обратите внимание, что методы модуля располагаются над методами экземпляра класса, к которому подключается этот модуль. Это означает, что задав одноименный метод модуля в классе, мы его перекроем.
Раз капсульную машину с капучинатором отнесли к топовой модели, можно запрограммировать в ней основы латте-арта:
class CapsuleCappuccino < CapsuleMachine
include Cappuccinator
def push_foam
puts "Красивыми узорами выкладываем пену в чашку."
end
private :push_foam
end
Но инженеры уже задницей чувствуют, что завтра может поступить команда включить латте-арт еще в какую-то модель! Наверное, лучше вынести его в отдельный модуль:
module LatteArt
private
def push_foam
puts "Красивыми узорами выкладываем пену в чашку."
end
end
Тогда CapsuleCappuccino
, очевидно, можно переписать:
class CapsuleCappuccino < CapsuleMachine
include Cappuccinator
include LatteArt
end
Порядок подключения модулей имеет значение, потому что модуль, включенный в класс первым, оказывается над модулем, «подмешанным» после него (точно, как они записаны в объявлении класса). Следовательно, методы последнего будут перекрывать методы все остальных модулей (но все равно не смогут перекрыть методы экземпляра класса).
Переменные объекта
У каждого объекта есть внутреннее состояние. Кофемашина должна «знать», включена она сейчас или нет, сколько времени осталось до конца приготовления, какой объем чашки потребуется, чтобы налить туда готовый кофе и т. д. При этом внутреннее состояние объекта должно быть невидимым и недосягаемым для внешнего мира (черный ящик всё-таки).
Локальные переменные методов удовлетворяют последнему условию, но для хранения долгосрочной информации не годятся, потому что уничтожаются после выполнения метода. Кроме того, они даже не видны в соседних методах, что затрудняет обмен информацией между подзадачами.
Для этих целей в Ruby существуют переменные объекта, их имена всегда начинаются с @. Присвоив значение такой переменной, можно быть уверенным, что оно будет прочитано из любого метода этого объекта и будет сохранятся до тех пор, пока объект существует.
В отличие от локальных переменных, обращение к неинициализированной (без присвоенного
раньше значения) переменной объекта не приводит к ошибке, просто возвращается nil
.
Управление состоянием
Чтобы извлечь пользу из объекта, с ним нужно взаимодействовать и в какой-то мере контролировать процессы, происходящие внутри него. Та же кофемашина будет бесполезным хламом, если у нее не будет кнопки «Пуск/Стоп», носика, из которого льется приготовленный кофе, и индикатора, показывающего, что устройство работает.
В Ruby всё взаимодействие с объектом происходит через его методы и только через них. Как только это понимаешь, объектно-ориентированный мир становится простым до невозможности: есть только объекты, ощетинившиеся иголками методов, и всё!
Поскольку методы могут принимать аргументы, с их помощью мы можем впустить внутрь объекта какую-то информацию. А раз каждый метод в Ruby возвращает значение, таким образом объект может сообщить нам о своем состоянии или передать результаты своей работы.
Геттеры и сеттеры
Информация, хранящаяся в переменных объекта, секретна и доступна только этому объекту. Но часто возникает ситуация, когда нужно позволить сторонним объектам считывать и/или менять значения этих переменных.
Например, хорошо бы позволить устанавливать количество кофе (в чашках), которое должна сварить кофемашина, и заодно проверять, какое оно сейчас. Для это создается пара обычных методов: геттер и сеттер.
class CoffeeMachine
# геттер
def cups_count
@cups_count # отдаем наружу (показываем) значение переменной
end
# сеттер
def cups_count=(count)
@cups_count = count # присваиваем значение, пришедшее извне
end
end
Кстати, в Ruby есть очень удобный метод класса — attr_accessor
, который создаст эту
пару методов на лету.
class CoffeeMachine
attr_accessor :cups_count
end
Имя сеттера вовсе не обязательно заканчивать символом =
, можно использовать, например:
class CoffeeMachine2
# неудобный сеттер
def set_cups_count(count)
@cups_count = count
end
end
но тогда присваивать значение придется так:
machine = CoffeeMachine2.new
machine.set_cups_count(1)
А вот если сеттер был задан со знаком равенства в конце имени, Ruby добавляет к присваиванию ложку синтаксического сахара:
machine = CoffeeMachine.new
machine.cups_count = 1 # то же, что и machine.cups_count=(1)
Этот синтаксический сахар, правда, может привести к недоразумениям, потому что по умолчанию Ruby воспринимает присваивание как команду инициализировать локальную переменную.
class CoffeeMachine
attr_accessor :cups_count
def set_default_cups_count
cups_count = 1
end
end
:008 > nespresso = CoffeeMachine.new
=> #<CoffeeMachine:0x904d61c>
:009 > nespresso.set_default_cups_count
=> 1
:010 > nespresso.cups_count
=> nil
По этой причине у всех сеттеров обязательно нужно указывать объект вызова, даже
если это self
(если сеттер является частным, где self
указывать запрещено,
Ruby делает поблажку).
class CoffeeMachine
attr_accessor :cups_count
def set_default_cups_count
self.cups_count = 1
end
end
:008 > bosch = CoffeeMachine.new
=> #<CoffeeMachine:0x914801c>
:009 > bosch.set_default_cups_count
=> 1
:010 > bosch.cups_count
=> 1
Создание объекта
Практически всегда при создании объекта приходится задавать его начальное состояние. Например, хотелось бы сразу при создании кофемашины устанавливать объем варимого кофе в чашках.
Проблема состоит в том, что сам объект создается методом класса new
, а у объекта-класса
нет доступа к переменным объекта-сына. Чтобы ее решить, существует соглашение,
по которому все действия, связанные с созданием объекта, нужно проводить в его
собственном методе
initialize
. Ruby автоматически делает этот метод частным и вызывает одновременно с
методом new
класса.
class CoffeeMachine
attr_accessor :cups_count
def initialize(count = 1)
@cups_count = count
end
end
:008 > bosch = CoffeeMachine.new(2)
=> #<CoffeeMachine:0x917f5f8>
:009 > bosch.cups_count
=> 2
Синглтон методы
Пока мы разбирались с геттеро-сеттерами, руководство «Гурмана» поставило перед инженерами очередную задачу: каждый раз, когда с конвейера сходит тысячная кофемашина, должна раздаваться радостная мелодия, вдохновляющая работников на трудовые подвиги.
До сих пор инженеры занимались проектированием собственно кофемашин, но ведь это не обязанность кофемашины — считать, сколько ее было выпущено, и уж тем более подавать какие-то увеселительные сигналы. И вряд ли это обязанность конвейера (класса) — играть мелодии. Скорее всего, этим должен заниматься специальный объект, Радостный Счетчик.
Однако, тут у инженеров проблема: как Счетчик узнает о том, что с конвейера сошел
очередной готовый объект? Если задуматься, то это событие происходит одновременно с
вызовом метода new
класса. И уж если с чего начинать, так с него: нужно перезаписать
этот метод, вклинив в него оповещение Счетчика о созданном объекте.
Мы уже знаем, что метод new
класса попадает из синглтон класса BasicObject
.
Но как его перезаписать (перекрыть)? Для этого нужно задать метод экземпляра в синглтон классе,
который ближе к объекту — в его собственном.
Методы синглтон класса еще называют синглтон методами. Чтобы их задать, существует простая форма записи:
def имя_объекта.название_синглтон_метода
#...
end
Значит, можно перезаписать метод new
следующим образом:
def CoffeeMachine.new
#...
end
Еще мы знаем, что внутри объявления класса self
и есть этот класс. Поэтому
синглтон методы можно определять прямо внутри класса:
class CoffeeMachine
def self.new # self == CoffeeMachine
#...
end
end
Наконец, в Ruby есть возможность открыть объявление синглтон класса и записывать там его методы:
class << CoffeeMachine
# ...
end
а внутри объявления класса это будет выглядеть как:
class CoffeeMachine
class << self
def new
#...
end
end
end
Что же должен делать новый метод new
? Очевидно, он должен обратиться к «оригинальному»
методу и создать объект, после чего отправить сообщение Радостному Счетчику.
Однако, поскольку сам метод должен возвращать созданный объект, его придется
временно сохранять в локальной переменной:
class CoffeeMachine
def self.new
machine = super # создаем объект с помощью метода new предка
# как-то уведомляем Счетчик
machine # возвращаем созданный объект (неявный return)
end
end
Попробуем теперь представить сам Радостный Счетчик. Бесспорно, он должен хранить у себя количество созданных объектов — нужна переменная. Когда должен происходить проигрыш мелодии? Когда это количество кратно тысяче. Значит, нужен хитрый сеттер, который будет не только присваивать переменной значение, но и делать проверку на кратность.
class HappyCounter
attr_reader :count # удобный способ создать геттер
# сеттер
def count=(value)
@count = value
play_melody if premium_count?
end
def initialize
@count = 0
end
def premium_count?
@count % 1000 == 0 # проверяем остаток от деления
end
private
def play_melody
puts "Та-дааам!!!"
end
end
Хммм, сеттер публичный, выходит, любители танцев могут безнаказанно устанавливать
любое количество выпущенных объектов, кратное тысяче, и плясать до упаду. Но и
сделать его частным нельзя, иначе как тогда увеличивать переменную @count
?
Но ведь можно изолировать весь Счетчик, сделав его собственностью объекта
CoffeeMachine
! Тогда взаимодействовать со Счетчиком сможет только сам конвейер.
Для этого в CoffeeMachine
понадобится переменная для хранения объекта и сеттер
(а вот геттера не будет, тогда к созданному Счетчику извне никто не доберется).
Сеттер можно задать так:
class CoffeeMachine
def self.counter=(object)
@counter = object
end
#...
end
А можно воспользоваться методом класса attr_writer
, но для этого понадобится
«войти» в синглтон класс:
class CoffeeMachine
class << self
attr_writer :counter
end
#...
end
Теперь становится понятно, как уведомить Счетчик о создании нового объекта:
class CoffeeMachine
def self.new
machine = super
@counter.count += 1 # то же, что и @counter.count = @counter.count + 1
machine
end
#...
end
:030 > CoffeeMachine.counter = HappyCounter.new
=> #<HappyCounter:0x8738edc @count=0>
:031 > 999.times do
:032 > CoffeeMachine.new
:033?> end
=> 999
:034 > premium_machine = CoffeeMachine.new
Та-дааам!!!
=> #<CoffeeMachine:0x8930c80>
Объекты повсюду
Очень легко дается понимание объектов, которые являются примерными копиями материальных вещей из реальной жизни. Вот объект Автомобиль: у него есть масса, цвет и максимальная скорость, он может проехать из точки А в точку Б, причем не один, а вместе с объектом Груз и объектом Водитель. Мы даже легко можем представить, что Автомобиль — составной объект, внутри которого есть Двигатель, Коробка передач и Топливный бак, а масса и максимальная скорость являются сложными и непостоянными величинами, зависящими от всех этих составляющих.
Гораздо сложнее увидеть в качестве объекта совершенно абстрактные понятия.
Число в Ruby — объект. В жизни они — самая настоящая абстракция: если семь самураев или семь футов под килем можно хотя бы увидеть, то просто семь существует только в голове мыслящего существа. И тут — бац! — Ruby, словно пифагореец, который считал, что весь мир построен из чисел, шокирует нас заявлением, что число — объект, и его можно пощупать.
Но что полезного может делать семерка как объект? Это же просто число! Оказывается, работы немало: в Ruby каждое число отвечает за сложение себя с другим объектом. И за вычитание, и остальные арифметические операции — тоже. Там, где другой интерпретатор сказал бы «ага, 2 плюс 3 — это же пять!», Ruby говорит двойке «вот тебе тройка, складывайся с ней как хочешь».
Может, арифметика за счет объекта и кажется немного притянутой за уши, тем более, что Ruby всё равно дает использовать соответствующие методы под видом привычных операторов (синтаксический сахар). Вместо того, чтобы писать
2.+(3)
пишем обычное
2 + 3
Правомерность обеих записей, скорее, дает ощущение целосности и последовательности в мире Ruby. Ведь не одним сложением ограничивается число. Взять, к примеру, определение его четности. Можно использовать известную технику с остатком от деления на два:
if 7 % 2 == 0
# если семь — четное, что-то делаем
end
Можно даже вынести эту логику в отдельный метод:
def is_even?(number)
number % 2 == 0
end
is_even?(7) # => false
Но в любом случае мы пытаемся определить внутреннее свойство числа с помощью внешней функции, в то время, как более натуральным было бы отдать это на откуп черному ящику. И то, что числа являются в Ruby объектами, позволяет легко это сделать:
7.even? # => false
Подобная «самостоятельность» простых типов данных и контейнеров (массивы, хеши) выглядит настолько логичной и правильной, что привыкаешь к ней мгновенно. Например, строки «знают» о том, какой они длины и умеют переводить себя в верхний регистр, массивы могут сортироваться и отбрасывать элементы-дубликаты.
"Ruby".length # => 4
"Ruby".upcase # => "RUBY"
[1, 3, 4, 2].sort # => [1, 2, 3, 4]
[1, 3, 3, 2].uniq # => [1, 3, 2]
true
, false
и nil
, которые во многих языках являются ключевыми словами, в
Ruby тоже объекты.
false.nil? # => false
nil.nil? # => true
Объекты, представляющие простые типы данных, создаются в процессе упоминания их
в тексте программы, нет особой необходимости использовать Array.new
или Hash.new
,
достаточно записать нужный литерал: [ ]
или { }
.
Единственная магия, которую невозможно объяснить в рамках языка — как Ruby получает само значение из объекта, который представляет простой тип данных. Даже если бы и существовал метод, который возвращал это значение, оно всё равно было бы еще одним объектом (поскольку в Ruby всё — объект). Но к этому дуализму тоже быстро привыкаешь: тройка для Ruby одновременно и число 3, и объект.
Замысловатые термины
- способность объекта быть черным ящиком — инкапсуляция;
- совокупность публичных методов объекта составляют его интерфейс;
- возможность перекрывать методы в потомках — полиморфизм.