Ruby и .

Введение в объектно-ориентированный 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, и объект.

Замысловатые термины

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