Покорим Ruby вместе! Капля четвертая

Собираем капли дальше (1, 2, 3). В этот раз узнаем о реализации ООП в Руби.

Классы, объекты, методы


Вместо того, чтобы углубляться дальше в синтаксис Ruby, отставим циклы, типы, модули и др. — мы вернёмся к ним позже. Сейчас же мы посмотрим, как создавать классы, объекты и методы.

Как и в других объектных языках объекты определяются классом. Например этот класс определяет собак (о_0):

class Dog 
   def set_name( aName ) 
     @myname = aName 
   end 
end

Ну а чем вам собаки — не класс? ;) Смотрим код. Определение класса начинается с ключевого слова class и названия самого класса, которое обязано должно начинаться с большой буквы. В классе определен метод set_name (def...end), который будет называть каждый объект-собаку. Он берёт входящий аргумент aName и назначает его значение переменной @myname.

Переменные, начинающиеся с @, — это переменные экземпляра класса. Важно то, что значение такой переменной доступно только внутри экземпляра, к которому она принадлежит. Создадим два объекта (две конкретные собаки) с помощью метода new:

moya = Dog.new 
tvoya = Dog.new

Напоминаю и запоминаем: класс называем только с большой буквы, объект — только со строчной. Используем метод set_name, чтобы назвать собак:

moya.set_name( 'Lassie' ) 
tvoya.set_name( 'Rex' ) 

Как теперь вывести клички собак? Мы не можем «вытащить» переменную @name из объекта, так как внутренние данные объекта известны только ему (см. выше). Это основной принцип ООП: данные объекта приватны; это называется скрытие данных и является частью принципа инкапсуляции.

Для решения этой задачки просто добавим новый метод в класс, который будет выводить переменную:

def get_name 
   return @myname 
end

Слово return необзательно, Руби возвратит последнее полученное значение, однако писать его — хорошая привычка.

Заставим собаку лаять (для этого напишем еще один метод) и соберём всё в кучу:

class Dog    
   def set_name( aName ) 
     @myname = aName 
   end 
 
   def get_name 
     return @myname 
   end 
 
   def gav 
     return 'r-r-r-r!' 
   end 
end

dog1 = Dog.new 
dog1.set_name( 'Fido' ) 
puts(dog1.get_name) 
puts(dog1.gav) 

Представим, что у нас есть два класса (например, кошки и собаки), в каждом определено по объекту (конкретные кот и пёс) и методу с одинаковыми названиями («говорить», но кот мяукает, а пёс лает). Получается, что на один и тот же метод объекты реагируют по-разному. Возможность иметь несколько классов, содержащих одноименные методы, называется полиморфизм. Узнали… и забыли это грозное слово.

Атрибуты


Для нашей задачи есть другой подход — создадим необходимые методы с помощью атрибутов класса. Усложним поставленное задание и создадим зоопарк ;)

class Cat
   attr_accessor :name, :age, :gender, :color
end
class Dog
   attr_accessor :name, :age, :gender, :color
end
class Snake
   attr_accessor :name, :age, :gender, :color, :length
end

Рассмотрим вторую строку. Она предоставляет три атрибута для класса Cat. Каждая кошка имеет свою кличку, возраст, пол и окрас — и код создает эти атрибуты. _accessor означает «сделайте эти атрибуты доступными для создания и изменения».

Создадим объект и применим атрибуты:

cat_object = Cat.new
cat_object.name = "Pussy"
........................
puts cat_object.name


Наследование


Посмотрев на последний код с объявлением трёх классов, легко заметить, что они предельно похожи. Лишь у змеи дополнительно определена длина. Вспомним принципы программирования на Ruby из первой капли. Один из них: DRY (Don't repeat yourself), который означает, что мы должны писать необходимый код только один раз, а у нас сплошные повторения. Нам просто необходимо обратить внимание на одну из лучших возможностей ООП: наследование.

Наследование позволяет разным классам соотноситься друг с другом и группироваться по схожестям в структуре. В нашем примере коты, собаки, змеи — всё это питомцы (pets). Так почему бы нам не создать класс Pet, который будет «родительским» для остальных, которые, в свою очередь, будут наледовать общие для всех питомцев свойства? Пишем:

class Pet
   attr_accessor :name, :age, :gender, :color
end
class Cat < Pet
end
class Dog < Pet
end
class Snake < Pet
   attr_accessor :length
end

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

Эпилог


В прошлой капле мы с вами выяснили, что такое ООП, в этой — научились применять знания в Ruby, узнали несколько замудренных слов, познакомились с переменными экземпляра и атрибутами класса. Еще одна капля в наш стакан ;)

Благодарю Хью Колинборна и Питера Купера за замечательные примеры для наших капель. Коментарии и замечания как всегда принимаются! До встречи!
_________
Текст подготовлен в ХабраРедакторе
+40
10 января 2009, 22:07
51
MaxElc 47,9

комментарии (66)

+3
elfxf #
Спасибо за оперативность статей, ненавижу ждать следующих глав/частей/и т.д.
Желаю не потерять боевой заряд, и писать больше/лучше/чаще, ибо полезно, познавательно, и читаем =)
+1
MaxElc #
Главное, что хоть кто-то читает :)
+2
taskmgr #
Читает, читает ;-)
+2
VolCh #
Угу, читаем :)
0
VolCh #
class Dog
def set_name( aName )
@myname = aName
end
def get_name
return @myname
end
def gav
return 'r-r-r-r!'
end
end

dog1 = Dog.new
puts(dog1.get_name)
puts(dog1.gav)

выводит:
nil
r-r-r-r!

То есть одна из самых трудноотлавливаемых ошибок (неиницализированная переменная) в Ruby вообще за ошибку, или хотя бы достойной предупреждения, не считается?
Странно, что даже не получил никаких предупреждений, просто
0
MaxElc #
Ой, а я уже исправлять ринулся :))) Да, на самом деле просто nil
0
alexbaum #
Выполнил пример, получил предупреждение в IDE (Netbeas).
А вы в консоли выполняли? Версия руби 1.8?
0
VolCh #
Точно так, консоль ruby 1.8.7 (irb тоже не выдал предупреждений)

P.S. сейчас вот поковырялся с параметрам. если вызывать ruby -w file.rb то выдает предпреждение
0
VolCh #
>Возможность иметь несколько классов, содержащих одноименные методы, называется полиморфизм.

Вообще понятие полиморфизма намного шире, главное в нем, по-моему, что возможны следующая операция (псевдокод. как на Ryby написать, вроде еще «не проходили» массивы и итераторы по ним

pets = [new Dog, new Cat]
foreach pets as pet
{
print pet.get_name
}
+2
titton #
>как на Ryby написать

pets = [Dog.new, Cat.new]
pets.each {|pet| puts pet.name}

вот так ;)
0
VolCh #
В принципе если бы подумал и попробовал выполнить свой код, то догадался бы :)
0
x7c6 #
или
pets.each do |pet| puts pet.name end

обожаю руби за возможность решать задачу кучей способов^_^
0
Nuru #
Я как Java программист могу сказать вам по этому поводу, что это называется рефлексией) По крайней мере в Java. Полиморфизм поидее это:

Полиморфи́зм (в языках программирования) — взаимозаменяемость объектов с одинаковым интерфейсом. (с) Википедия. Так же полиморфизм возможет если объекты предки расриряют общий суперкласс.

Хотя в динамических языках программирования вполне может быть и по другому)
+2
alexbaum #
ИМХО, транслит в названиях переменных, функции, метода, класса — дурная привычка.

moya = Dog.new
tvoya = Dog.new

коробит знающих англ. язык и ничего не означает для не владеющих им.

moya.skazi-gav-gromko — нехорошо, имхо.
0
MaxElc #
OK, будем писать по-английски
0
alexbaum #
:) да я так, побурчал. Однако, спасибо за обратную связь, с вниманием читаю ваши статьи. Пример с собакой не из открытой книжки (ruby open book) по руби?
0
MaxElc #
Эта собака с переменными из The Book of Ruby — во многих книгах ООП показывают на животных и собаках в частности :)
+1
mholub #
зря вы просто побурчали, писать транслитом идентификаторы в коде это просто невообразимый пипец.

Вот жутчайший пример:
ru.wikibooks.org/wiki/Введение_в_язык_Scheme_для_школьников
0
DeTeam #
От первых строк глаза выпучились о_О как
Это ужасно.
0
uranik #
Ven lärnoy püki votik, vödastok plösenon fikulis.
0
4ekuct25 #
спасибо за очередную статью)
с нетерпением жду следующую каплю.
0
MpaK999 #
Отлично, слежу за всеми выпусками, так как параллельно уже почти прочел книжечку по Руби, всё четко и очень доступно, продолжайте. Была бы сила, плюсанул…
0
kalisha #
Шустрый ты парнишка. Я слаживаю твои капли в стакан ;)
0
fleshy #
ИМХО логичнее было бы использовать конструктор

class Dog
def initialize(dogName)
@name = dogName
end

def name
puts @name
end
end

тогда можно написать
rex = Dog.new(«rex»)
rex.name
0
VolCh #
За конструктор обеими руками «за», особенно раз руби по дефолту позволяет не инициализировать переменные, но вот переменные с присваиванием только в конструкторе как-то не тру, имхо. Больше на константы похожи, чем на переменные :)
0
fleshy #
само собой. просто я не стал весь класс переписывать полностью, а только показал пример на использование конструктора.
вот более полная версия

class Dog
def initialize(dogName)
@name = dogName
end

def getName
puts @name
end

attr_accessor :name
end

rex = Dog.new(«rex»)
rex.getName
>>rex
rex.name = «milo»
puts rex.name
>>milo
0
VolCh #
Просто думаю многие ничего не знают о руби, кроме того, что написано в «каплях», и не зная, что, например, можно создавать методы вида name= и про аксессоры типа ридеров и райтеров (о чем я узнал из коментов :) ) первый ваш код выглядит каким-то ущербным :) в том смысле, что кажется, что отказавшись от attr_accesor мы теряем возможность писать rex.name = 'rex' и будем вынуждены пользоваться rex.set_name('rex');
0
fleshy #
кажется, что отказавшись от attr_accesor мы теряем возможность писать rex.name = 'rex' и будем вынуждены пользоваться rex.set_name('rex');
так и есть
0
VolCh #
Наверное не правильно выразился, имел в виду, что казалось будто attr_accessor единственный способ писать a.name=«rex»
+3
dsCode #
> def getName

лишний метод-алиас, поскольку attr_accessor :name создаст геттер (ридер) для @name;

Вообще, свои геттеры (ридеры) и сеттеры (райтеры) лучше описывать, когда установка/чтение проперти более сложное, нежели простое «верни прямое значение», «установи прямое значение». Для примитивных же reader'ов и writter'ов (и для их обобщающих accessor'ов) достаточно соответствующих классовых методов.

def a
#какие-то сложные вычисления возвращающие некоторое значение, а не просто «примитивный „return“
end

def a=
#тоже могут быть сложные вычисление перед установкой проверти, а не просто „примитивный =“
end

attr_reader :b # только для чтения (будет создан метод def b @b end)
attr_writer :c # только для записи (будет создан метод def c=(val) @c = val end)
attr_accessor :d # объединение двух предыдущих „примитивных ридера и райтера“
0
kronos #
def a=(value)
0
dsCode #
> (value)

ну, ествественно, писал просто «на коленке», торопился :)
0
Benderlidze #
def initialize(dogName) — экто конструтор класса получается?
0
fleshy #
конструктор здесь — метод initialize. он вызывается когда мы создаем новый экземпляр класса (Dog.new). Конструктор может быть без параметров, а может с параметрами, как в данном случае. можно определять несколько конструкторов, напр. с параметрами и без, с разным числом параметров и пр.
class Dog

def initialize #без параметров
@name = «rex» #по умолчанию собаку зовут Рекс
end

def initialize(dogName) #но если нам не нравится
@name = dogName #мы можем назвать ее и по-другому
end

<...>

end
0
VolCh #
Еще пример полиморфизма :)

А вопрос такой есть: как определяется конструктор — по имени initialize или по тому, что он первый описан, или я еще чего-то не доглядел?
0
fleshy #
по имени метода initialize, положение его в коде класса насколько я знаю не важно
0
dsCode #
> можно определять несколько конструкторов, напр. с параметрами и без, с разным числом параметров и пр.

Это что-то из мира С++ и Java. В Ruby второй метод initialize просто-напросто перезапишет первый initialize. В итоге, без параметров Вы не вызовите метод. А параметры по умолчанию можно в самом initialize передавать:

class A

  def initialize(a=10)
    @a = a
  end

end

a = A.new # @a = 10
b = A.new(20) # @a = 20


Можно и так:

def initialize(a=nil)
  @a = a || 10
end
0
fleshy #
хм, действительно… прошу прощения, попутал что-то
0
avz #
нда. Только начинает нравиться, как что-нибудь всплывает. То ++ нет, то конструктор только один
0
iv_s #
Отсутствие оверлоадинга методов — это ограничение динамических языков.
Хотите два конструктора — используйте переменное число параметров.

class Test
    def initialize *args
        if args.size == 1
            puts "first constructor"
        elsif args.size == 2
            puts "second constructor"
        else
            raise "Required 1 or 2 parameters"
        end
    end
end
Test.new("asdf") #=> first constructor"
Test.new("asdf","asdf") #=> "second constructor"
Test.new() #=> `initialize': Required 1 or 2 parameters (RuntimeError)

Такой же код для перегрузки методов.
0
avz #
Я понимаю — выкрутиться можно. Это хорошо.
Но вот смотрите например в C#.

  class SimpleParser
  {
    public SimpleParser()
    {
    }

    public SimpleParser(String fileName)
    {
    }

    public SimpleParser(Stream stream)
    {
    }

    public SimpleParser(BinaryReader reader)
    {
    }

  }


* This source code was highlighted with Source Code Highlighter.


При компиляции в месте создания объекта SimpleParser будет сразу сгенерирован вызов того конструктора, который нужен в зависимости от переданного (или не переданного вовсе) параметра. И в рантайме уже анализировать ничего не нужно. А это скорость

0
dsCode #
> А это скорость

А статические системы в любом случае быстрее динамических. С другой стороны, статические не обладают такой гибкостью, как динамические.
0
VolCh #
Руби, вроде как, язык интерпретируемый и по любому анализ в рантайме будет проходить, насколько я понимаю, что такое интерпретатор
0
avz #
Я просто надеюсь, что он предварительно какой-нибудь байт-код генерит.
В таком случае, будь в нем overload, проверка параметров проводилась бы один раз на этапе генерации байт-кода, а так при каждом создании объекта
0
iv_s #
Во-первых, в ветке Ruby 1.8 компилляции в байткод нет, только в 1.9.
Во-вторых, не в компиляции дело.
Ruby — это динамический язык, в динамических языках нет оверлоадинга.
И по скорости динамические языки всегда медленнее статических.
Если для ваших задач критична скорость — используйте тот же C#:)
0
tass #
если критична скорость то дотНЕТ вряд ли поможет…
0
dsCode #
Если более точно, что initialize — это инциализатор порожденного объекта (инстанса). Конструктор, именно в плане конструирования — это new (и то, new — это лишь оболочка для allocate (выделяет память) и initialize (инициализирует объект)).
НЛО прилетело и опубликовало эту надпись здесь
0
VolCh #
Вот, спасибо! А только хотел спросить, как реализуется внутренне конструкции a.name и a.name=при использовании attr_acсesor (вернее последняя, с первой-то все понятно), можно ли как-то изменить поведение, например валидацию провести в «сеттере» или же attr_acсesor является аналогом public переменных класса в других языках и способа контроля (простого) в присваивании не существует. А так сразу все понятно стало :)
0
dsCode #
Да, поскольку, инстанс-переменные являются настоящими пропертями, то, лучше бы, показать, что get_name и set_name — это прерогатива того, что работает не с виртуальными пропертями. В Руби же для таких целей есть полноценные аксессроры (ридеры, райтеры): name и name= (который, как было сказано выше, создаются автоматом в «примитивном виде» (установить простое значение / вернуть простое значение) классовыми методами attr_accessor, attr_reader и attr_writer)
0
MaxElc #
Благодарю! Я тоже весь в сомнениях был, а в книгах никаких подробностей :(
+1
DeTeam #
есть еще, по аналогии с attr_accessor:

attr_reader
attr_writer
0
DeTeam #
За статьи — большое спасибо.
0
iv_s #
или просто: attr
attr :asdf, true #тоже что и attr_accessor :asdf
attr :asdf, false #тожу что и attr_reader :asdf
–6
13hero #
И всё таки мне кажется что руби не такой православный как питон.
0
Crashus #
и всётаки мне кажется что топик не об этом
–3
13hero #
:0)
+3
VolCh #
А слабо такие же «капли» по питону писать? ;) А мы бы читали и то, и другое и сравнивали бы :)
+4
iv_s #
От заявлений о православности питона сразу хочеться холиварить:)
–1
13hero #
:-р
+1
Posigrade #
По рубишному соглашению, мультивордовые имена необходимо писать через underscore. @myname и особенно aName выбивают из колеи, делают код нечитаемым. Надо так: a_name, @my_name. Через капиталайзед, пишутся мультивордовые имена классов(e.g. MyClassName).

и вообще:
Table 2.1. Example variable and class names
----------------------------------------------------------+---------------
                           Variables                      | Constants and
                                                          | Class Names
----------------------------------------------------------+---------------
Local           | Global       | Instance     | Class     | 
----------------+--------------+--------------+-----------|
name            | $debug       | @name        | @@total   | PI
fish_and_chips  | $CUSTOMER    | @point_1     | @@symtab  | FeetPerMile
x_axis          | $_           | @X           | @@N       | String
thx1138         | $plan9       | @_           | @@x_pos   | MyClass
_26             | $Global      | @plan9       | @@SINGLE  | JazzSong
--------------------------------------------------------------------------
0
iv_s #
Не скажите откуда табличка?
+1
TiGR #
Змейка в качестве питомца (pet), это сильно :)

Тем не менее, спасибо за статьи. Сам брался как-то за руби, но толи книги не те попались, то ли голова тогда не соображала, мне он показался какой-то излишне мудрёный. А тут смотрю — вполне себе красиво, мне нравится :)
0
TheBits #
Автор как бы намекает нам: python.
НЛО прилетело и опубликовало эту надпись здесь

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.