Инкапсуляция

Прежде чем переходить к инкапсуляции давай поговорим про то, как можем получать доступ к нашим атрибутам из вне. Возьмем тот же класс Person:

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  # создаем метод name, который будет нам возвращать атрибут name
  def name 
    @name 
  end

  # создаем метод, который позводлит нам менять значение атрибута name с помощью знака `=`
  def name=(new_name)
    @name = new_name
  end

  # создаем метод age, который будет нам возвращать атрибут age
  def age
    @age
  end

  # создаем метод, который позводлит нам менять значение атрибута age с помощью знака `=`
  def age=(new_age)
    @age = new_age
  end
end
mike = Person.new('Mike', 23)

puts mike.name # = Mike
puts mike.age # = 23

# тот самый метод name=(new_name)
mike.name = 'Mike Brown'
# тот самый метод age=(new_age)
mike.age = 21

puts mike.name # = Mike Brown
puts mike.age # = 21

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

Метод, который позволяет получать значение атрибута называется обычно Getter, а метод, который позволяет изменять это значение, называется Setter.

Ruby позволяем нам сильно сократить за счет attribute accessors. Существует 3 типа аксессоров:

  • attr_reader - создает невидимый метод для чтения атрибута

  • attr_writer - создает невидимый метод для записи атрибута

  • attr_accessor - создает невидимые методы записи и чтения атрибута

Давайте посмотрим как может выглядеть наш класс Person с использованием аксессоров:

class Person
  # Хоть мы и не создаем метод name, name=, age, age=, но мы все равно получаем такие методы при использовании нашего класса
  # Если бы мы использовали attr_reader :name, :age, значит у нас бы создался невидимый метод name и age для получения значений
  # Если бы мы использовали attr_writer :name, :age, значит у нас бы создался невидимый метод name= и age= для изменения значений
  # Обрати внимание, что мы передаем не просто названия наших атрибутов, а передаем символы с таким же названием как и у атрибутов
  attr_accessor :name, :age
  def initialize(name, age)
    @name = name
    @age = age
  end
end
mike = Person.new('Mike', 23)

# работает точно так же как и в случае с методами name и age, но уже за счет attr_accessor
# Такая же возможность у нас была, если бы мы использовали attr_reader
puts mike.name # = Mike
puts mike.age # = 23

# тот самый метод name=(new_name)
mike.name = 'Mike Brown'
# тот самый метод age=(new_age)
mike.age = 21

# работает точно так же как и в случае с методами name= и age=, но уже за счет attr_accessor
# Такая же возможность у нас была, если бы мы использовали attr_writer
puts mike.name # = Mike Brown
puts mike.age # = 21

Как мы видим количество написанного кода внутри нашего класса Person заметно снизилось.

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

Для того, чтобы в ruby защитить наш объект от нежелательного использования у нас есть такие инструменты как: attribute accessors, а также приватные методы и публичные.

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

Приватные методы - это методы, которыми мы можем пользоваться только внутри нашего класса, вызывая их из других методов, например. Обычно приватные методы создают, если нам нужно в разных публичных методах переиспользовать один и тот же кусок кода, а также для разбиения большого методы на более мелкие. Это позволяет снизить сложность прочтения кода вашего приложения.

class Car
  # Пользователь не должен самостоятельно менять значение этого атрибута, так как это может поломать логику нашей программы
  attr_reader :mileage # публичный метод для получения значения переменной mileage

  def initialize
    @mileage = 0
  end

  # пользователь может только добавлять километраж, но не может изменять его на свое усмотрение
  def drive(miles)
    @mileage += miles
  end

  def start
  
  end

  # для того, чтобы пользователь смог поехать на нашем автомобиле ему не нужно уметь самостоятельно запускать двигатель
  # У нас может быть метод запуска машины, который будет запускать сразу нескольких компонентов машины
  # но запускать только двигатель конечному пользователю уметь не нужно 
  private # все методы, определенные ниже, являются приватными

  def engine_start
    # действия, необходимые для запуска двигателя
  end

  # а также ему не нужно уметь его останавливать
  # У нас может быть метод остановки машины, который будет запускать остановку сразу нескольких компонентов машины
  # но останавливать только двигатель конечному пользователю уметь не нужно 
  def engine_stop
    # действия, необходимые для остановки двигателя
  end
end
car = Car.new
# метод drive публичный, значит мы можем им воспользоваться
car.drive(10)
# mileage имеет attr_reader, за счет которого мы можем получить значение километража
puts "Пробег: #{car.mileage} км"
# при этом если мы попробуем изменить километраж с помощью car.mileage = 10, то мы получим ошибку
# такое же поведение будет если мы попробуем вызвать метод engine_start или engine_stop

Возможно по началу будет тяжело в этом ориентироваться, но если каждый раз задавать вопрос себе А нужен ли публичный доступ для данного метода ? или А нужен ли acessor для изменения атрибута ? Не сломает ли он нашу программу, если пользователь захочет напрямую изменить его ?.

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

Часто можно услышать такое понятие как API и многие ошибочно считают, что это касается только сетевого взаимодействия, но API и инкапсуляция тесно между собой связаны. API - это итоговая реализация инкапсуляции нашей программы, которая выстраивает все способы взаимодейсвия с нашей программы. Сам Ruby тоже предоставляем нам API для разработки программ. Взять тот же String. Что такое строка для компьютера ? Это всего лишь набор байтов доступ к которым Ruby нам не дает. Вместо этого мы получаем набор методов для взаимодейсвия с нашей строкой, а также публичные доступы до определенных переменных на чтение, которые позволяют получить полезную информацию о нашей строке.

Задание animals

Внимание ! Это и последующие задания данного модуля с названием animals необходимо выполнять в одной и той же директории в корне модуля - 3_oop/animals

  • выставьте для вашего класса Cat те права доступа и акссесоры, которые по вашему мнению большего всего подходят.

  • Реализуйте логику подсчета суммарно съеденной еды, а также счетчик суммарно выпитой воды.

  • создайте экземпляр класса кота и 'замяйкайте' его до сметри.

  • создайте еще одие экзампляр класса кота и перекормите его до смерти.

Задание elevator

Создайте класс Elevator, эмулирующий работу пассажирского лифта. В классе создайте:

  • Переменные current_floor (текущий этаж), min_floor и max_floor (минимальный и максимальный этажи).

  • Конструктор с двумя параметрами min_floor и max_floor, сохраняющий эти параметры в соответствующих переменных класса.

  • Значение переменной current_floor изначально должно быть равно 1.

  • Метод get_current_floor, возвращающий текущий этаж, на котором находится лифт.

  • Метод move_down, перемещающий лифт на один этаж вниз (уменьшающий значение переменной current_floor на единицу).

  • Метод move_up, перемещающий лифт на один этаж вверх.

  • Метод move(floor), перемещающий лифт на заданный в параметре этаж, если он задан верно. Если параметр у метода задан неверно, ничего не делать и выводить в консоль сообщение об ошибке. Этот метод может перемещать лифт только последовательно, по одному этажу, с помощью циклов и методов move_up и move_down, и он должен выводить в консоль текущий этаж после каждого перемещения между этажами.

elevator =  Elevator.new(-3, 36)
loop do
  puts 'Введите номер этажа: '
  floor = gets.chomp.to_i
  elevator.move(floor)
end

Этот код поможет вам протестировать созданный класс Elevator: он будет создавать лифт и в консоли запрашивать этаж, на который нужно переместить лифт, после чего вызывать у него метод move с указанием полученного из консоли этажа. Запустите получившийся код и убедитесь, что он работает корректно.

Дополнительный материал

Last updated