ООП в Ruby — Классы, Объекты, Взаимодействия
Один Класс. Несколько Методов. Все понятно
Маленький Класс. Один Метод
новички в ООП обычно довольно легко понимают концепцию класс-объект на примере одного класса с методами, которые вызываются на объекте
к примеру напишем простой класс Car:
# ./car.rbclass Car
def beep
puts 'beep beep'
end
end
переходим в irb:
$ irb -r ./car.rb
2.5.1 :001 > car = Car.new
=> #<Car:0x00000000012c1900>
2.5.1 :002 > car.beep
beep beep
=> nil
2.5.1 :003 >класс загружен, объект создан, метод вызван у объекта, результат совпадает с ожиданиями… отлично!
Метод initialize
также обычно не вызывает сложностей в понимании и некоторое усложнение класса
например:
в классе может быть метод initialize, который принимает аргументы и присваивает их значения инстанс-переменным
# ./car.rbclass Car
def initialize(number)
@number = number
enddef beep
puts 'beep beep'
end
end
теперь при инициализации объекта мы должны передать в аргументах метода .new номер и мы можем увидеть в irb, что этот номер присвоен соответствующей переменной объекта класса Car
$ irb -r ./car.rb
2.5.1 :001 > car = Car.new(123)
=> #<Car:0x0000000000e99b70 @number=123>
2.5.1 :002 >опять же все понятно и прозрачно
Добавление getter и setter
можно добавить еще один аттрибут и пару геттер-сеттер к нему
можно даже устанавливать в методе initializeзначение по умолчанию для этого нового аттрибута
пусть автомобиль имеет аттрибут “цвет” и пусть при создании нового автомобиля его цвет устанавливается в значение “белый”
при этом у автомобиля есть методы, с попощью которых можно узнать цвет или поменять его (как в примере ниже):
# ./car.rbclass Car
def initialize(number)
@number = number
@color = 'white'
enddef beep
puts 'beep beep'
enddef color=(color)
@color = color
enddef color
@color
end
end
и в irb мы можем проверить нашу новую функциональность:
$ irb -r ./car.rb
2.5.1 :001 > car = Car.new(123)
=> #<Car:0x0000000001cad4a0 @number=123, @color="white">
2.5.1 :002 > car.color
=> "white"
2.5.1 :003 > car.color = 'red'
=> "red"
2.5.1 :004 > car.color
=> "red"
2.5.1 :005 >все работает, как и было задумано
геттер и сеттер, чтобы не быть многословными можно переписать с помощью встроенного метода Руби:
# ./car.rbclass Car
attr_accessor :colordef initialize(number)
@number = number
@color = 'white'
enddef beep
puts 'beep beep'
end
end
и все работает, как прежде:
$ irb -r ./car.rb
2.5.1 :001 > car = Car.new(123)
=> #<Car:0x0000000001628c10 @number=123, @color="white">
2.5.1 :002 > car.color
=> "white"
2.5.1 :003 > car.color = 'red'
=> "red"
2.5.1 :004 > car.color
=> "red"
2.5.1 :005 >Методы принимающие аргументы
немного усложним )
предположим по заданию наш автомобиль должен обладать следующими качествами:
- “Имеет номер, который указывается при создании”
- “Имеет цвет, при создании цвет устанавливается в значение ‘white’”
- “Может выводить в консоли ‘beep beep’”
- “Может возвращать цвет, может менять цвет”
- “Знает имена, допущенных к управлению водителей”
- “Может добавлять имена водителей в список имен водителей”
- “Может возвращать список имен водителей”
- “Может выводить список имен водителей”
мы здесь должны добавить функциональность, связанную с именами водителей
в коде это может выглядеть вот так:
# ./car.rbclass Car
attr_accessor :color
attr_reader :drivers_namesdef initialize(number)
@number = number
@color = 'white'
@drivers_names = []
enddef beep
puts 'beep beep'
enddef add_driver(name)
@drivers_names << name
enddef show_drivers
@drivers_names.each { |name| puts name }
end
end
что нового в нашем классе ?
- чтобы выполнить пункт ТЗ “Знает имена, допущенных к управлению водителей” в классе
Carпоявилась еще одна инстанс-переменная и ридер (геттер) для нее
эта переменная инициализируется со значением “пустой массив” - в качестве реализации пункта “Может добавлять имена водителей в список имен водителей” добавился метод
add_driver(name), который добавляет элементы в массив, храняшийся в@drivers_names - наконец метод
show_driversреализует пункт “Может выводить список имен водителей”
все работает, как и было задумано, в чем можно легко убедиться зайдя в irb:
$ irb -r ./car.rb
2.5.1 :001 > car = Car.new(123)
=> #<Car:0x0000000001b789e0 @number=123, @color="white", @drivers_names=[]>
2.5.1 :002 > car.add_driver('Bill')
=> ["Bill"]
2.5.1 :003 > car.drivers_names
=> ["Bill"]
2.5.1 :004 > car.show_drivers
Bill
=> ["Bill"]
2.5.1 :005 > car.add_driver('Mark')
=> ["Bill", "Mark"]
2.5.1 :006 > car.drivers_names
=> ["Bill", "Mark"]
2.5.1 :007 > car.show_drivers
Bill
Mark
=> ["Bill", "Mark"]
2.5.1 :008 >далее начинается часть, на которой у многих новичков возникают вопросы
Взаимодействие двух классов
Классы взаимодействуют между собой с помощью объектов
чтобы понять, как объекты разных классов взаимодействуют друг с другом в ООП парадигме, сделаем для начала две вещи:
первое:
давайте добавим класс Driver
и пусть задание (ТЗ) для него звучит следующим образом
класс Водитель:
- “Имеет имя, которое указывается при создании”
- “Может возвращать имя”
второе:
изменим ТЗ для класса Car, чтобы оно теперь выглядело как описано ниже
класс Автомобиль:
- “Имеет номер, который указывается при создании”
- “Может возвращать номер”
- “Имеет цвет, при создании цвет устанавливается в значение ‘white’”
- “Может возвращать цвет, может менять цвет”
- “Может выводить в консоли ‘beep beep’”
- “Знает допущенных к управлению водителей”
- “Может добавлять водителей в список водителей”
- “Может возвращать список водителей”
- “Может выводить список имен водителей”
начнем мы с изменения уже существующего класса Car
что изменилось в ТЗ?
- “Может возвращать номер” означает, что нужно добавить метод, который возвращает значение инстанс-переменной, т.е. геттер
это легко реализуемо добавлением в класс одной строчки:
attr_reader :number
- “Знает допущенных к управлению водителей” vs. “Знает имена, допущенных к управлению водителей”…
что означает это почти незаметное изменение в ТЗ?
оно означает, что теперь мы должны хранить в инстанс-переменной объектаcar* не строки, а что-то другое… и это “другое” - это объект классаDriver
другими словами: объект классаCarдолжен хранить в своей переменной объекты классаDriver
эта модификация не требует больших изменений, разве что переименования переменнойdrivers_namesвdrivers(для простоты чтения и понимания кода): - вместо
attr_reader :drivers_names
- теперь стоит написать
attr_reader :drivers
- и аналогичным образом поменять имя переменной в
initialize: c@drivers_names, на@drivers
поменяв имя ридера мы также выполнили пункт “Может возвращать список водителей” - “Может добавлять водителей в список водителей” vs. “Может добавлять имена водителей в список имен водителей”
здесь изменения следуют той же логике, что и в предыдущем пункте — мы просто правим метод так, чтобы речь уже шла не об операциях со строкой, а об операциях с объектом классаDriver
вместо
def add_driver(name) @drivers_names << name end
- напишем
def add_driver(driver) @drivers << driver end
- Следуя той же логике меняем метод
show_drivers, теперь он будет выглядеть так:
def show_drivers @drivers.each { |driver| puts driver.name } end
- здесь стоит обратить внимание на следующий момент, который нередко бывает не совсем ясен студентам
если раньше мы перебирали элементы массива, являвшиеся строками и выводили их по очереди, то в новом методе мы перебираем элементы, каждый из которых — объект классаDriver
этих объектов не существует в природе на момент написания классаCar
мы даже возможно еще не начинали писать классDriver**, но у нас есть ТЗ и в ТЗ сказано:
- у объектов класса
Driverдолжнен быть аттрибут “имя” - и вторым пунктом ТЗ: “Может возвращать имя”
- именно поэтому мы и используем в блоке итератора вызов
driver.name
* (замечание к пункту 2) название переменной, хранящей объект может быть любым, речь идет об оъектах класса Car
** (замечание к пунтку 4) при написании кода можно пойти разными путями:
1) если держать какие-то свойства класса в уме, пока пишешь другой класс сложно, то можно писать два класса параллельно, и двигаться по пунктам
2) можно, предвосхищая написание класса, уже на этом этапе использовать его описанные в ТЗ аттрибуты и методы
со всеми исправлениями класс Car будет выглядеть следующим образом:
# ./car.rbclass Car
attr_accessor :color
attr_reader :drivers, :numberdef initialize(number)
@number = number
@color = 'white'
@drivers = []
enddef beep
puts 'beep beep'
enddef add_driver(driver)
@drivers << driver
enddef show_drivers
@drivers.each { |driver| puts driver.name }
end
end
перейдем к классу Driver
согласно ТЗ он должен выглядеть так:
# ./driver.rbclass Driver
attr_reader :namedef initialize(name)
@name = name
end
end
в этом коде реализованы пункты
- “Имеет имя, которое указывается при создании”
- “Может возвращать имя”
проверим в irb:
$ irb -r ./driver.rb -r ./car.rb
2.5.1 :001 > driver1 = Driver.new 'Bill'
=> #<Driver:0x0000000002801090 @name="Bill">
2.5.1 :002 > driver1.name
=> "Bill"
2.5.1 :003 > driver2 = Driver.new 'Mark'
=> #<Driver:0x00000000027f8800 @name="Mark">
2.5.1 :004 > driver2.name
=> "Mark"
2.5.1 :005 > car1 = Car.new 123
=> #<Car:0x0000000002724de8 @number=123, @color="white", @drivers=[]>
2.5.1 :006 > car1.beep
beep beep
=> nil
2.5.1 :007 > car1.color
=> "white"
2.5.1 :008 > car1.drivers
=> []
2.5.1 :009 > car1.add_driver(driver1)
=> [#<Driver:0x0000000002801090 @name="Bill">]
2.5.1 :010 > car1.drivers
=> [#<Driver:0x0000000002801090 @name="Bill">]
2.5.1 :011 > car1.add_driver(driver2)
=> [#<Driver:0x0000000002801090 @name="Bill">, #<Driver:0x00000000027f8800 @name="Mark">]
2.5.1 :012 > car1.drivers
=> [#<Driver:0x0000000002801090 @name="Bill">, #<Driver:0x00000000027f8800 @name="Mark">]
2.5.1 :013 > car1.show_drivers
Bill
Mark
=> [#<Driver:0x0000000002801090 @name="Bill">, #<Driver:0x00000000027f8800 @name="Mark">]
2.5.1 :014 >все методы работают именно так, как и ожидалось
Чуть больше взаимодействий
но давайте вернемся к реальности )))
автомобили ничего не знают про своих владельцев!
изменим ТЗ так, чтобы переложить бремя знания с автомобиля, на водителя
класс Автомобиль:
- “Имеет номер, который указывается при создании”
- “Может возвращать номер”
- “Может иметь на двери надпись”
- “Может устанавливать надпись на двери в значение равное имени владельца”
- “Может возвращать надпись на двери”
- “Имеет цвет, при создании цвет устанавливается в значение ‘white’”
- “Может возвращать цвет, может менять цвет”
- “Может выводить в консоли ‘beep beep’”
класс Водитель
- “Имеет имя, которое указывается при создании”
- “Может возвращать имя”
- “Может покупать и владеть автомобилями”
- “При покупке нового автомобиля наносит на его дверь свое имя”
- “Может возвращать список автомобилей, которыми владеет”
мы убрали из класса Автомобиль все пункты, связанные со знаниями о водителях, и добавили автомобилю необязательный аттрибут “надпись на двери”…
“может устанавливать”, “может возвращать” очевидно озаначают необходимость добавить соотвествующие методы
# ./car.rbclass Car
attr_accessor :color
attr_reader :door_title, :numberdef initialize(number)
@number = number
@color = 'white'
enddef beep
puts 'beep beep'
enddef change_door_title(driver)
@door_title = driver.name if driver.cars.include?(self)
end
end
- в методе
change_door_titleмы учли требование пункта “Может устанавливать надпись на двери в значение равное имени владельца”
благодаря проверке, касающейся “владения автомобилем”, надпись на двери изменится тогда и только тогда, когда переданный в метод объект (водитель) является владельцем текущего авто (внутри метода переменнаяselfссылается на объект классаCar, у которого этот метод был вызван)
другими словами:
автомобиль у которого вызван методchange_door_titleдолжен находится в списке@carsу водителя, имя которого мы хотим нанести на дверь - что может означать следующий пункт в классе Водитель: “Может покупать и владеть автомобилями” ?
он должен означать, что объект классаDriver, видимо должен хранить инстанс-переменную@cars(“владеть автомобилями”) и в классе должен быть методbuy_car, который будет добавлять автомобили в список автомобилей, которыми “владеет водитель”
здесь нужно обратить внимание на то, что в списке, т.е. в переменной, которая хранит в себе коллекцию (массив в нашем случае), должны храниться не номера автомобилей, а объекты классаCar(точнее ссылки на эти объекты) - как легко догадаться строчка “Может возвращать список автомобилей, которыми владеет” должна означать необходимость геттера для переменной
@cars
с учетом этих пунктов, класс нужно переписать так:
# ./driver.rbclass Driver
attr_reader :name, :carsdef initialize(name)
@name = name
@cars = []
enddef buy_car(car)
@cars << car
end
end
теперь посмотрим на фразу “При покупке нового автомобиля наносит на его дверь свое имя”… она должна означать, что в методе buy_car (в классе Driver) объект класса Car, переданный в метод, должен быть изменен
а именно, должно измениться содержание инстанс-переменной @door_title
для этого у нас в классе Car есть метод change_door_title
получается, что метод buy_car нужно дополнить следующим образом:
def buy_car(car)
@cars << car
car.change_door_title(driver)
endно откуда возьмется переменная driver ?
можно конечно передать объект в аргументах, вот так: driver.buy_car(car, driver)
однако такое решение будет хоть и работающим, но не верным
вспомним, что при вызове вида driver.buy_car(car) внутри метода buy_car на объект driver (тот самый у которого вызван метод) ссылается переменная self
тогда метод можно переписать следующим образом:
def buy_car(car)
@cars << car
car.change_door_title(self)
endпосле редактирования, класс у нас выглядит следующим образом:
# ./driver.rbclass Driver
attr_reader :name, :carsdef initialize(name)
@name = name
@cars = []
enddef buy_car(car)
@cars << car
car.change_door_title(self)
end
end
проведем испытания в irb
создаем автомобили
$ irb -r ./driver.rb -r ./car.rb
2.5.1 :001 > car1 = Car.new 123
=> #<Car:0x0000000000b80628 @number=123, @color="white">
2.5.1 :002 > car2 = Car.new 345
=> #<Car:0x000000000088a720 @number=345, @color="white">создаем водителей
2.5.1 :003 > bill = Driver.new 'Bill'
=> #<Driver:0x00000000008861c0 @name="Bill", @cars=[]>
2.5.1 :004 > mark = Driver.new 'Mark'
=> #<Driver:0x0000000000b7c1e0 @name="Mark", @cars=[]>проверяем надпись у первого авто и список авто у Билла
2.5.1 :005 > car1.door_title
=> nil
2.5.1 :006 > bill.cars
=> []надписи нет, автомобилей тоже )
Билл покупает первый автомобиль и на двери авто появляется его имя
2.5.1 :007 > bill.buy_car(car1)
=> "Bill"
2.5.1 :008 > bill.cars
=> [#<Car:0x0000000000b80628 @number=123, @color="white", @door_title="Bill">]
2.5.1 :009 > car1.door_title
=> "Bill"пытаемся изменить надпись на двери автомобиля Билла
2.5.1 :010 > car1.change_door_title(mark)
=> nil
2.5.1 :011 > car1.door_title
=> "Bill"безуспешно — надпись не поменялась, потому что Марк не является владельцем автомобиля car1
Марк покупает авто car2
2.5.1 :012 > mark.cars
=> []
2.5.1 :013 > car2.door_title
=> nil
2.5.1 :014 > mark.buy_car(car2)
=> "Mark"
2.5.1 :015 > mark.cars
=> [#<Car:0x000000000088a720 @number=345, @color="white", @door_title="Mark">]
2.5.1 :016 > car2.door_title
=> "Mark"все работает именно так, как и было задумано!
