RubyDev Ruby Tutorial #3 > Объектно-ориентированное Программирование (ООП) в Ruby

апреля 2, 2011  |  Published in Ruby, Основы  |  10 Comments

Ruby — объектно-ориентированный язык программирования, поэтому знание парадигмы объектно-ориентированного программирования (ООП) является обязательным.

Тема объектно-ориентированного программирования достаточно сложна, поскольку в ней переплетаются множество подтем, которые, разделить нельзя, поэтому данная глава будет иметь итерационной подход к изложению материала, то есть мы будем двигаться по спирали захватывая все больший и большый объем информации.

Что есть Объектно-ориентированное программирование?

Объектно — ориентированное программирование — это программирование основанное на использовании объектов — экземпляров абстрактных типов (классах). Объект в ООП — это программная модель какого-то реально существующего объекта, например вас или меня, при этом будучи объектами реального мира мы будем являться экземплярами некоторого типа (вида) — Человека Разумного. Объекты являющиеся экземплярами одного класса называются родственными или братьями, а класс от которого они произошли, если провести аналогию, является шаблоном ДНК человека в котором описываются все общие моменты производных объектов, однако каждый объект может иметь свои отличия от другого объекта того же типа. Все люди одинаковые в том, что  имеют две ноги, два глаза и штуку на животе именуемую пупком, однако у всех людей различается цвет глаз, отпечатки пальцев, имя, характер, лицо. Не смотря на эти различия между людьми, мы все-таки отличаем других людей, скажем от макак или павианов. Все общее в людях и общий набор свойств определяется в классе, а все различное определяется в самих объектах.

ООП является значительным шагом в перед, который позволяет писать программисту не просто код, а описывать реальную модель взаимодействия объектов. Объектно — ориентированное программирование повышает читабельность кода за счет его систематизации и сходства с тем, как человек мыслит и представляет мир.

Три кита объектно-ориентированного программирования: Инкапсуляция, Наследование, Полиморфизм

Для начала давайте создадим код, на котором мы будем разбирать примеры,он будет состоять из двух классов: Monkey и Human, где Human, согласно революционной эволюционной теории Чарльза Дарвина наследуется от класса Monkey:

class Monkey
def initialize(height, weight)
@height = height
@weight = weight
end

def height
@height
end

def weight
@weight
end

def height=(height)
@height = height
end

def weight=(weight)
@weight = weight
end
end

class Human < Monkey
def initialize(height, weight, name)
super(height, weight)
@name = name
end

def name
@name
end

def name=(name)
@name = name
end
end

m = Monkey.new(120,55)
h = Human.new(180, 90, 'Ivan')

Что есть Инкупсуляция?

Инкапсуляция — это самый большой и жирный кит объектно-ориентированного программирования. Инкапсуляция заключается в том, что присущие функциональному программирования данные и функции над ними превратились в свойства и методы объекта, которые заключены в объект (запаяны в капсулу). Доступ к этим свойствам и методам доступен только через специально определяемые интерфейсы, таким образом скрывается внутреннее устройство объекта. если вы заходите покушать, вам не нужно знать все методы вашего организма по перевариванию пищи,вам не нужно обращаться к языку, чтобы тот чувствовал вкус,к слюнным железам, чтобы те выделяли слюну смачивающую пищу, к нижней челюсти, чтобы та пережевывала пищу до необходимой кондиции, снова к языку и пищеводу, чтобы пища попала в желудок, затем к желудку, чтобы тот обработал пищу желудочным соком и так далее. Все, что вам нужно — это воспользоваться специальным интерфейсом — ртом, положив туда пищу, все, внутренние методы вашего организва запустятся сами этим одним единственным интерфейсом (методом — аксессором, от access — доступ). Ваши свойства, как экземпляра Хомо Сапиенса, это размеры элементов вашего тела, цвет кожи, цвет глаз, родинки и веснушки, ваше имя и фамилия, ваше физическое,  духовное и психологическое состояние. Вы хороший, здоровый, темноглазый, бледнолицый, девяносто килограммовый и сто-восьмидесята сантиметровый Иван — и все, что я перечислил — это ваши свойства. Ваши методы — это ходьба, бег, употребление пищи, сон, чтение, писание, программирование и так далее. Свойства к которым можно получить доступ называются открытыми, открыты они не из-за какой-то своей особенности, а из-за того, что для них определены методы — аксессоры. Посмотрте код Monkey и вы увидите там четыре метода аксессора: #weight, #height, #weight= и #height=. Первых два называются ридерами (от read — читать), они занимаются тем, что предоставляют вам текущее значение одноименных свойств объекта @weight и @height. Другие два метода, #height= и #weight= являются аксессорами — врайтерами (от write — писать) и из задача состоит в определении (записи) нового значения свойствам объекта. Без методов аксессоров вы не можете получить доступ к свойствам объекта, вы не можете знать имя девушки, и есть ли у нее парень, пока не воспользуетесь соответствующими методами аксессорами: #name и #has_boyfriend?. Аксессоры read и write также часто называют get и set — аксессорами (от get — получать, set — устанавливать).

Расследование что есть Наследование

Наследование — чуть менее упитанный кит ООП, тем не менее он очень важен. Наследование заключается в эволюции классов. Как человек эволюционировал из обезъяны, так и один класс может наследоваться от другого. Наследование заключается в передаче всей структуры класса Monkey в класс Human, при этом наследование реализуется очень просто, при определении класса Human достаточно дописать < Monkey, что и указывает на наследование класса Human от класса Monkey. Говорят, что Monkey наследует Human, а Human наследуется от Monkey. Говорят, что Monkey — супер класс или родительский класс, а Human — подкласс, субкласс или дочерний класс. На практике это означает, что для Human полностью копируется реализация класса Monkey. Такое копирование и есть Наследованием.

Наследование — очень полезная вещь, так как: предоставляет возможность повторного использования кода, спасает от повторений в коде. Скажем имея класс Human, мы можем наследоваться от него классами WhiteHuman и BlackHuman, которые будут иметь абсолютно одиинаковые свойства, за исключением цвета кожи и формы носа. Вместо написания двух новых классов, вы просто наследуетесь ими от базового класса — Human дополняя его новыми свойствами и методами, либо переписывая старые.

class BlackHuman < Human
def initialize(height, weight, name)
super(height, weight, name)
@skin_color = :black
end

def skin_color
@skin_color
end

def skin_color=(skin_color)
@skin_color = skin_color
end
end

На случай, если темнокожего человека зовут Майкл Джексон, мы определяем метод-аксессор #skin_color=, который позволяет изменить цвет кожи.

Полиморфизм — все течет, все меняется

Полиморфизм — самый щуплый кит объектно-ориентированного программирования, который тесно связан с китом наследования и делает наследование еще более мощным. Полиморфизм (поли — много, морфе — форма) заключается в том, что наследуясь, в классе — потомке (дочернем классе или подклассе или субклассе или …) вы можете переопределять унаследованные свойства и методы. Во всех трех примерах выше имеется метод #initialize. Метод #initialize, согласно договоренности, является тем методом, что автоматически выполняется при создании нового экземпляра класса, такие методы еще называют методами — конструкторами. В классах Monkey и Human данный метод занимается тем, что принимает данные переданные в метод .new при создании нового объекта и устанавливает эти данные в свойства создаваемого объекта. Поскольку у экзмепляров классов Monkey и Human набор свойств разный, то и разное количечество аргументов должно быть принято, от чего следует в классе Human переопределить данный метод таким образом, чтобы он мог задавать дополнительные свойства. В классе BlackHuman мы также переопределяем метод #initialize, однако он сам устанавливает свойству @skin_color по умолчанию значение :black. В обоих классах Human и BlackHuman метод #initialize снабжен выражением super. Super — это очень полезное выражение, которое позволяет в контексте метода вызывать одноименный метод из родительского класса, другими словами, super позволяет реализовать что-то вроде «наследования методов» при котором мы в контексте переопределенного метода, вызываем старый метод и дополняем его. Все эти операции по переопределению и носят имя полиморфизм.

Области видимости и типы переменных

Невозможно объяснять более-менее сложные вещи не объяснив о том, что такое область видимости и какие переменный бывают. Область видимости — это способность переменной быть использованой в контексте определенного фрагмента кода. Класс, объект, процедура, метод, код, вложеный в блоки условных операторов и циклов — все это может быть областью видимости. В Ruby существует 5 типов переменных:

variable — локальная переменная, она доступна только в той области видимости, где была определена, а также во всех вложенных областях видимости.

@variable - переменная объекта (экземпляра класса), имена таких переменных должны начинаться с одного знака @ — это еще одно соглашение в Ruby, которое вы обязаны знать. переменные экземпляра класса доступны только в том объекте, где они определены и вроженных в него областях видимости.

@@variable — переменная класса, имена таких переменных должны начинаться с двух символов @. Их область видимости — класс в котором они определены и все экземпляры данного класса.

$variable — глобальные переменные, их имена должны начинаться с символа $, а область видимости — вся программа.

Constant - константы это не переменные, но предназначение похожее — хранить ссылку на объект. Имена констант должны начинаться с большой буквы. Область видимости константы такая же как и у глобальной переменной, единственное отличие заключается в том, что значение константы следует использовать для «статики». При попытке изменить значение константы, значение будет изменено, однако будет возвращено предупреждение. В большинстве других языков программирования констаты не могут переопределяться.

Создание объектов

В Ruby существут предопределенные типы данных, которые были рассмотрены в прошлой главе и абстрактные — то, что мы рассматриваем в данной главе RubyDev Ruby Tutorial. Отличие в создании базовых (предопределенных) типов и абстрактных заключается в том, что для базовых типов существует специальный упрощенный синтаксис:

5.object_id #=> 11
"string".object_id #=> 82742600
[1,2,3].object_id #=> 82800010 

Просто используя тот или иной литерал, мы автоматически создаем соответствующий литералу объект. Работая с абстрактными типами данных, нам необходимо явно определять объект при помощи метода .new класса, примеры:

m = Monkey.new(120,55)
h = Human.new(180, 90, 'Ivan')

m и h являются объектами, соответственно, классов Monkey и Human, что означает, что они имеют тип Monkey и Human. На самом деле m и h являются всеголишь переменными ссылающимися на реальные объекты в памяти компьютера, которые не имеют имен, но имеют уникальные идентификаторы объектов, по которым к ним происходит обращение. Несмотря на это m и h, для простоты можно также называть объектами.

Создавая объект (экземпляр класса) нам необходимо воспользоваться методом .new. Метод .new принимает аргументы для создания класса, которые затем могут быть использованы в методе #initialize и создает новый объект.

Методы классов и методы объектов

Вы наверняка заметили, что в данном учебнике перед именами одних методов я ставлю знак #, а перед именами других я ставлю точку. Это такая договоренность, согласно которой имена методов вне текста программы начинаются с # если метод принадлежит объекту и с . , если метод принадлежит классу. Метод класса отличается от метода объекта тем, что приемником метода класса является класс, а приемником метода экземпляра класса является экземпляр класса (объект). Хочу заметить, что в Ruby даже сами классы являютя объектами. Отличие классов от объектов заключается в том, что каждый класс является экземпляром базового класса Class. Любой метод объявленный следущим образом:

def method_name()
end

является методом экземпляра класса. Для того, чтобы объявить метод класса, вам необходимо использовать следущую конструкцию:

def self.method_name()
end

, либо следущую:

class BlackHuman < Human

class << self
def obj_count
count = 0
ObjectSpace.each_object(self){|obj| count += 1}
end
end

def initialize(height, weight, name)
super(height, weight, name)
@skin_color = :black
end

def skin_color
@skin_color
end

def skin_color=(skin_color)
@skin_color = skin_color
end
end

bh = BlackHuman.new(180,90,"John")
bh2 = BlackHuman.new(185,90,"Robert")
bh3 = BlackHuman.new(170,70,"Thomas")

puts BlackHuman.obj_count # 3

Как видно из примера, вызов методов класса не отличается от вызова методов объекта, единственное отличие — это приемник метода.

Указатель self

В предыдущем примере вы могли видеть указатель self. self — это указатель на текущий объект, объект, в контексте которого проиходит действие. Когда вы пишите:

class ClassName
def self.method_name()
end
end

Это в конечном счете интерпретируется как:

class ClassName
def ClassName.method_name()
end
end

Когда вы используете self в контексте метода экземпляра класса, то self будет ссылаться на текущий экземпляр класса, пример:

class ClassName
def obj_info
puts "ObjectID:   #{self.object_id}"
puts "ObjectType: #{self.class}"
end
end

o = ClassName.new
puts o.object_id #=> 83402010

o.obj_info
#ObjectID:   83402010
#ObjectType: ClassName

Метакласс он же Singleton — класс (класс одиночка), он же Eigenclass, он же Virtual Class, он же …

Объект, по сути является копией класса. При этом объект не может иметь методов, он использует методы объявленные в классе. Такой способ работы, очевидно, основан на желании экономии оперативной памяти, ведь вместо полного копирование объектом методов класса, объект просто обращается к классу за методами. Ruby обладает грандиозными возможностями метапрограммирования, например, вы можете объявлять методы для уже существующего объекта. Поскольку объект не может иметь методов, а методы объявляются для определенного одного объекта, а не для всех, то была введена такая концепция как метаклассы. Метакласс — это такой невидимый класс, который наследуется от того класса, от которого произошел объект. Другими словами, каждый объект имеет за собой метакласс и по настоящему является его экземпляром, что скрывается. Сам метакласс наследуется от класса, которому, якобы реально принадлежит объект. Уникальные методы объекта называются в честьэтого самого singleton — класса singleton-методами. Ниже приведен пример создания singleton метода:

def bh3.upcase_name
@name.upcase
end

puts bh3.upcase_name #=> THOMAS
puts bh2.upcase_name #=> NoMethodError

Как видите из примера, объект bh3 являющийся экземпляром класса BlackHuman имеет уникальный метод #upcase_name, которого не имеют другие объекты того же типа. Это и есть singleton-метод, который определен в singleton классе. Обычно Singleton класс не заметен, вызвам метод #class для bh3, мы получим имя класса BlackHuman, но не singleton класс. Для того, чтобы получить метакласс объекта, следует воспользоваться методом #singleton_class:

singleton = bh3.singleton_class #=> #<Class:#<BlackHuman:0x827c59c>>
singleton.superclass #BlackHuman

Первая строка кода возвращает полное имя класса, которое состоит из имени самого класса и всех его супперклассов. Метаклассы не имеют имени, а имеют лишь идентификаторы, что и показано в выводе: 0x827c59c.

Еще один полезный метод для работы с метаклассами — это метод #singleton_methods, который возвращает все уникальные методы объекта, то есть те, которые не определены в его классе, то есть те, которые определены в его метаклассе, пример:

bh3.singleton_methods #=> [:upcase_name]

Когда вы объявляете методы класса в один из описанных выше способов, то эти методы класса определяются в его метаклассе, поскольку, как уже говорилось, каждый класс также является объектом.

Техника MonkeyPatching

Данная техника не рекомендуется к использованию, хотя иногда она бывает самым удачным решением. Данная техника основана на том, что вы можте изменять реализацию класса непосредственно во время исполнения кода и в любом месте программы, где к классу имеется доступ. Ниже приведен простой пример MonkeyPathching’а:

class String
def upcase_reverse
self.upcase.reverse
end
end

"hello".upcase_reverse #OLLEH
"rubydev.ru".upcase_reverse #UR.VEDYBUR

Опасным моментом использования MonkeyPatching’а является переопределение существующих методов, особенно когда переопределение происходит произвольно, то есть объявляя метод вы переопределяете уже существующий, о существовании которого вам не было известно.

Немного Метапрограммирования

Метапрограммирование — это техника написания программ, которые могут изменять сами себя или другие программы во время выполнения. Ruby предоставляет очень мощные возможности метапрограмирования, благодаря чему можно творить действительно волшебные вещи.

В примерах выше мы определяли различные методы аксессоры для доступа к свойстам объектов. Для каждого свойства нам необходимо было определить 2 метода аксессора для получения и установки значения свойству объекта. Представьте, что у объекта имеется несколько десятков свойств, тогда вам придется объявлять множество методов, которые могут составлять большую часть кода класса. Для получения методов аксессоров без необходимости определения каждого из них имеются специальные методы — генераторы аксессоров: .attr_reader и .attr_writer, которые создают соответственно методы чтения и установки свойств объекта, примеры:

class Monkey
def initialize(height, weight)
@height = height
@weight = weight
end

attr_reader :height, :weight
attr_writer :height, :weight
end

class Human < Monkey
def initialize(height, weight, name)
super(height, weight)
@name = name
end

attr_reader :name
attr_writer :name
end

Методы .attr_reader и .attr_writer принимают в качестве аргументов символы соответствующие именам свойств объекта. В Ruby также существует метод .attr_accessor, который объединяет в себе .attr_reader и .attr_writer, пример:

class Monkey
def initialize(height, weight)
@height = height
@weight = weight
end

attr_accessor :height, :weight
end

Как работают генераторы аксессоров, я покажу на примере attr_accessor, который объединяет функционал обоих read и write генараторов:

class Monkey
def initialize(height, weight)
@height = height
@weight = weight
end

def self.attr_accessor(*args)
args.each do |accessor|
define_method accessor do
instance_variable_get "@#{accessor}"
end
define_method "#{accessor}=" do |value|
instance_variable_set "@#{accessor}", value
end
end
end

attr_accessor :height, :weight
end

m = Monkey.new(120,100)

m.height #=> 120
m.height = 160
m.height #=> 160

Методы .define_method, .instance_variable_set и .instance_variable_get являются одними из наиболее популярных методов в метапрограммировании на Ruby. Метод .define_method используется для объявления методов, он получает в качестве аргумента имя метода, который необходимо создать и блок кода в который помещен код метода. Использование .define_method должно быть оправдано, поскольку, во-первых, стандартный синтаксис с def более лаконичный, а во-вторых методы определенные с помощью .define_method создаются медленнее, чем методы объявленные с помощью def, что снижает производительность приложения.

.eval в метапрограммировании
.eval — очень интересный и очень мощный метод, который принимает строку кода и выполняет ее как код, простой пример:

eval ("puts \"rubydev.ru\"") # rubydev.ru

Не смотря на свою мощь, .eval применяется достаточно редко из-за своей небезопасности. Вместо .eval рекомендуется использовать .class_eval, #instance_eval и .module_eval. Разница между этими методами заключается в ограниченном контексте выполнения, .class_eval выполняет код для текущего класса, .instance_eval для текущего экземпляра класса, .module_eval для модуля. Поскольку класс и модуль являются объектами, то и для них доступен метод .instance_eval.

module Mod
end

class Monkey
include Mod
end

dm = <<CODE
def hello
puts 'hello'
end
CODE

dm2 = <<CODE
def bye
puts 'bye'
end
CODE

dm3 = <<CODE
def rubydev
puts 'rubydev.ru'
end
CODE

m = Monkey.new(120, 80)

Monkey.instance_eval(dm)
Monkey.hello #hello

Monkey.class_eval(dm2)
#Monkey.bye #=> NoMethodError

m.instance_eval(dm)
m.hello # hello

Mod.instance_eval(dm)
Mod.hello # hello

Mod.module_eval(dm3)
#Mod.rubydev #=> NoMethodError
m.rubydev # rubydev.ru

Monkey.singleton_methods #=> [:hello]
Mod.singleton_methods #=> [:hello]
m.singleton_methods #=> [:hello]

#instance_eval всегда работает в метаклассе объекта, из-за чего методы определенные с помощью #instance_eval всегда определяются как методы класса или модуля, что на самом деле означают, что они определены в метакласе класса и метаклассе модуля. .class_eval всегда выполняется в контексте самого класса, именнопо этой причине объявляемые с его помощью методы являются методами экземпляра класса. .module_eval абсолютно аналогичен методу .class_eval, только вместо класса простором для его действия является модуль. Вам следует запомнить эти различия и использовать #instance_eval, .class_eval и .module_eval, вместо .eval.

Тема метапрограммирования будет более подробно раскрыта в отдельной главе RubyDev Ruby Tutorial.

Рекомендации

1. Пишите программы, которые моделируют реальный мир, в которых объекты взаимодействуют подобно объектам реального мира.

2. Старайтесь организовать структуру проекта таким образом, чтобы в каждом файле вашей программы содержался только один класс или модуль.

3. Воспринимайте класс как абстрактный тип данных, а не просто как хранилище кода.

Постовой:
Рекомендую почитать хорошую статью о

Лучшая благодарность автору — ваши комментарии и доброжелательная критика. Нашли ошибки — не ругайтесь, отписывайтесь в комментариях, будем разбираться ;-) Удачного изучения Ruby!

Tags: ,

Responses

  1. Валентин says:

    апреля 4, 2011 at 01:58 (#)

    зря так категорично про MonkeyPatching написали, очень полезная методика для расширения функционала базовых типов

    очень широко в рельсах используется для добавления всевозможных удобств в базовых классах

    вот новички прочтут и не будут использовать, будут писать костыли :(

  2. says:

    апреля 6, 2011 at 11:23 (#)

    А почему вдруг нельзя менять значение константы-то? Еще как можно.

  3. admin says:

    апреля 6, 2011 at 17:05 (#)

    Иван, спасибо, поправил. Считал, что как и в большинстве ЯП константы хранят неизменяемые значения, интересно, почему в Ruby имеется такая возможность?

  4. Sashich says:

    мая 13, 2011 at 08:29 (#)

    «variable – локальная переменная, она доступна только в той области видимости, где была определена, а также во всех вложенных областях видимости.»

    По умолчанию в ruby отсутствует Inner Scope, как в Java или C#. Чтобы он появился нужно использовать технику Flattening the Scope.

    Пример:

    v1 = 1
    class MyClass
      v2 = 2
      puts local_variables
      def my_method
        v3 = 3
        puts local_variables
      end
      puts local_variables
    end
    
    obj = MyClass.new
    obj.my_method
    obj.my_method
    puts local_variables
    
  5. Ruby says:

    января 9, 2012 at 02:27 (#)

    Cпасибо, мне понравился урок.

  6. says:

    марта 7, 2012 at 10:08 (#)

    Половины не понял, но спасибо :)

  7. admin says:

    марта 7, 2012 at 10:30 (#)

    savvinov, задавайте вопросы если что-то не ясно.

  8. Publius says:

    июля 18, 2012 at 15:13 (#)

    Про трёх китов весьма образно. Но самый толстый кит это всё же полиморфизм, а самый тощий — инкапсуляция. Тем более в Ruby, где это вообще чисто условное понятие. Суть инкапсуляции вовсе не в собирательстве данных и методов в одной структуре, а в обеспечении требуемого уровня доступа к ним. Атрибуты и методы public доступны всем объектам, protected только ближайшим наследникам, private только родному объекту. В Ruby можно легко поменять уровень доступа, унаследовав нужный класс. А вот с полиморфизмом всё сложней. Грамотно выстроенные полиморфные ссылки изменить можно только переписав практически весь код. Даже не буду пытаться объяснить в двух словах, что такое полиморфизм. Дам только наколку, чтобы попытаться понять — ответьте себе на такой вопрос: каким образом в Ruby создаётся массив, содержащий объекты разных классов (полиморфный массив)?

  9. Artem Pecherin says:

    мая 14, 2013 at 12:51 (#)

    <<Поскольку объект не может иметь методов, а методы объявляются для определенного одного объекта, а не для всех, то была введена такая концепция как метаклассы.

    Вы тут ошиблись.
    Поскольку объект не может иметь методов, а методы объявляются для всех объектов, а не для одного, то была введена такая концепция как метаклассы.

    нет?

  10. says:

    января 10, 2014 at 15:25 (#)

    Огромное спасибо автору за статьи по Руби.
    Появилась срочная необходимость освоить язык, но порядочной русской документации вот так и не нашел. Здесь все компактно, и красиво оформлено. Работы выполнена супер.

    Единственное замечание. Для оформления кода вы используете SyntaxHighlighter (version 3.0.83 (July 02 2010)) — не знаю почему но именно версия 3 глючит -> добавляет полосу вертикальной прокрутки (смотрел в Хроме), которую так и хочется покрутить, хотя там только одна пустая строка снизу.
    У себя на блоге я (и смотрю что многие так делают) в настройках плагина (WordPress) можно выставить версию 2 (version 2.1.382 (June 24 2010)) — она намного приятнее для пользователей.

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.