Курс по Ruby+Rails. Часть 2. Объектно-ориентированное программирование

от автора

В этой лекции мы рассмотрим объектно-ориентированный стиль в Ruby: поговорим об объектах, классах и модулях, а также вспомним три принципа объектно-ориентированного программирования.

Основные свойства Ruby как чистого ОО-языка таковы:

  • Любая сущность — объект, исключений не существует.

  • Примитивных типов не существует.

  • Любая функция — это метод какого-либо объекта. Инфиксные операторы (например, + - / *) — не исключение.

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

  • Поведение объектов может программно изменяться в их жизненном цикле.

Объекты

Поговорим об объектах. Мы только что проговорили, что всё в Ruby является объектом. Например, число 42, которое вы видите — объект. Давайте это проверим. Вызовем какой-нибудь метод у этого числа. Допустим, метод class. И получим класс этого объекта:

pry(main)> 42 => 42  pry(main)> 42.class => Integer  pry(main)> Integer.class => Class

Попробуем посмотреть все доступные методы у объекта 42. Среди них увидим математические операторы, которые можем использовать:

pry(main)> 42.methods => [  # ...  :%,  :&,  :*,  :+,  :-,  :/,  # ... ]  pry(main)> 42.method(:+) => #<Method: Integer#+(_)>

Давайте убедимся в том, что математические операции — это тоже методы. Умножим 2 на 2. Вот сокращённая форма записи, где мы опустили некоторые знаки. Эта упрощённая запись будет эквивалентна следующей: у объекта 2 вызывается метод «умножение», и в качестве аргумента передаётся двойка. Оба выражения вернут один и тот же результат:

pry(main)> 2 * 2 => 4 pry(main)> 2.*(2) => 4

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

Классы

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

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

class DeepThought   def initialize     @answer = 42   end    def answer_for(question)     sleep 10      puts "The Answer about life, universe and everything, including '#{question}' is #{@answer}"   end end  supercomputer = DeepThought.new supercomputer.answer_for('To be or not to be?')  #> The Answer about life, universe and everything, including 'To be or not to be?' is 42

Прежде, чем продолжить, поговорим о собственном поведении класса. У класса есть методы, константы и переменные самого класса. Их мы можем использовать без создания нового экземпляра класса. Посмотрим на код: на второй строчке вы видите константу класса, на третьей — переменная класса. В конце класса находится конструкция class << self. Методы, которые описаны внутри этого блока, являются методами самого класса:

class DeepThought   ANSWER = 42   @@answer_template = 'Umm...'    attr_reader :answer, :seconds_to_sleep    def initialize(seconds_to_sleep)     @answer = ANSWER     @seconds_to_sleep = seconds_to_sleep   end    def answer_for(question)     sleep(seconds_to_sleep)      puts expand_answer(question)   end    def expand_answer(question)     "#{@@answer_template.gsub(/$q/, question)} #{answer}"   end    class << self     def answer_template=(string)       @@answer_template = string     end   end end

Обратите внимание на метод answer_template. В конце его имени стоит знак =. Он напрямую изменяет переменную класса. Такие методы называются сеттерами. Посмотрите на вызов метода attr_reader, — так создаются геттеры, — методы, возвращающие значения одноименных переменных экземпляра.

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

DeepThought::ANSWER #> 42

Теперь снова создадим экземпляр класса и вызовем у него какой-нибудь метод. Затем у самого класса вызовем сеттер и изменим нужную нам переменную класса. После этого снова вызовем метод экземпляра. Мы получили новый результат, так как изменилось определённое свойство класса в целом:

supercomputer = DeepThought.new(10) #> #<DeepThought:0x00007fece5c12418 @seconds_to_sleep=10>  supercomputer.answer_for('To be or not to be?') #> Umm... 42  DeepThought.answer_template =   "The Answer about life, universe and everything, including $q is" supercomputer.answer_for('To be or not to be?') #> The Answer about life, universe and everything, including $q is 42

Модули

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

module SupercomputersPlanet   PLANET = "Magrathea"    class DeepThought   # ...   end    def make_deep_thought     puts "Making Deep Thought of #{PLANET}"     DeepThought.new   end end

Посмотрим на примеры: мы можем получить список всех констант. Также можем попробовать создать экземпляр модуля, но в этом месте получим исключение. Можем попробовать прочитать константу, однако её нет в глобальной области видимости, константа описана только внутри модуля. Здесь же мы получим исключение. В конце мы явно указываем, к какому модулю хотим обратиться, и получаем нужное значение:

SupercomputersPlanet.class #> Module  SupercomputersPlanet.constants #> [:PLANET, :DeepThought]  SupercomputersPlanet.new #> NoMethodError: undefined method `new' for SupercomputersPlanet:Module  PLANET #> NameError: uninitialized constant PLANET  SupercomputersPlanet::PLANET #> "Magrathea"

Теперь попробуем создать экземпляр класса, определённого внутри пространства имён нашего модуля. На первой строчке получаем исключение. Это произошло потому, что мы не указали, что хотим инстанциировать класс из конкретного namespace:

DeepThought.new #> NameError: uninitialized constant DeepThought  SupercomputersPlanet::DeepThought.new #> #<SupercomputersPlanet::DeepThought:0x00007fe16bae4490>  SupercomputersPlanet.make_deep_thought #> NoMethodError: undefined method `make_deep_thought' for SupercomputersPlanet:Module

На второй строчке всё уже успешно. На третьей строчке мы попытаемся вызвать метод у этого модуля, но получим ошибку. Попробуем её исправить — опишем метод уровня класса внутри модуля. Теперь всё работает:

module SupercomputersPlanet   PLANET = "Magrathea"    class DeepThought   # ...   end    def self.make_deep_thought     puts "Making Deep Thought of #{PLANET}"     DeepThought.new   end end  SupercomputersPlanet.make_deep_thought #> Making Deep Thought of Magrathea #> #<SupercomputersPlanet::DeepThought:0x00007fdc389159d0>

3 основных принципа объектно-ориентированного программирования: инкапсуляция, наследование, полиморфизм. 

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

class DeepThought   ANSWER = 42    attr_reader :seconds_to_sleep   private :seconds_to_sleep    def initialize(seconds_to_sleep)     @seconds_to_sleep = seconds_to_sleep   end    def answer_for(question)     sleep(seconds_to_sleep)     puts full_answer_text(question)   end    private    def full_answer_text(question)     <<~TXT.gsub(/\n/, ' ')       The Answer about life, universe and everything,       including '#{question}' is #{answer}"     TXT   end    def answer     ANSWER   end end  DeepThought.new(5).answer #> NoMethodError: private method `answer' called ...  DeepThought.new(5).seconds_to_sleep #> NoMethodError: private method `seconds_to_sleep' called ...  DeepThought.new(5).answer_for("Foo?") #> The Answer about life, universe and everything, including 'Foo?' is 42"

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

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

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

class Animal   def breathe     puts "inhale and exhale"   end end  class Cat < Animal   def speak     puts "Meow"   end end  nyan = Cat.new nyan.breathe nyan.speak

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

Отдельного упоминания заслуживает механизм «примесей». Он позволяет собирать нужное поведение не только в классах, но и в модулях, которые можно затем включать в нужные нам классы. Определённые во включаемых модулях методы становятся методами экземпляра или класса в классе-«получателе»:

class LifeForm   def breathe     # all lifeforms breathe   end end  class Animal < LifeForm   include AnimalCellsBiochemistry   include AnimalGenetics   include AnimalMetabolism   include AnimalMotion   include AnimalSounds end  class Cat < Animal   # это поведение — DSL из модуля AnimalSounds   animal_sound "Meow" end

Вернёмся к примеру с иерархией классов. У нас есть класс Animal, есть класс Cat, который является его наследником. Есть ещё класс японской кошки — наследник класса Cat:

class Animal   def speak     puts 'The animal sound'   end end  class Cat < Animal   def speak     puts 'The cat speaks:'     super   # super вызывает одноимённый метод родителя   end end  module JapaneseCatSpeak   def speak     puts 'Nyaaaaa!'   end end  class JapaneseCat < Cat end  JapaneseCat.new.speak #> The cat speaks: #  The animal sound

Создадим экземпляр японской кошки и вызовем у неё один единственный метод. Но сейчас кошка мяукает по-английски, а должна по-японски. Давайте это сделаем. Создадим отдельный модуль и «подмешаем» его в нужный класс с помощью ключевого слова include. Теперь наша кошка мяукает правильно:

class JapaneseCat < Cat   include JapaneseCatSpeak    def speak     puts 'The cat speaks:'     super   # теперь родитель для этого метода — модуль JapaneseCatSpeak   end end  JapaneseCat.new.speak #> The cat speaks: #  Nyaaaaa!

Посмотрим на механизм примесей модулей внимательней. Есть некоторый модуль, у в котором определён метод, возвращающий массив. Подмешаем этот модуль в наш класс и попробуем вызвать этот метод на уровне класса. Получаем ошибку:

module CatsCommons   def features     %i[ears legs tail]   end end  class Cat   include CatsCommons end  Cat.features #> NoMethodError: undefined method `features' for Cat:Class  Cat.new.features #> [:ears, :legs, :tail]

Давайте попробуем создать экземпляр класса и вызовем метод на нём. Всё работает корректно. Однако изначально я хотел, чтобы метод вызывался на уровне класса. Исправим это поведение. Для этого используем ключевое слово eхtend:

class Cat   extend CatsCommons end  Cat.features #> [:ears, :legs, :tail]

С его помощью мы подмешиваем содержимое модуля не в экземпляр класса, а в сам класс. 

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

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

В Ruby тождественны понятие «тип» и «поведение». За счёт динамической природы языка мы можем изменять экземпляр класса непосредственно. На примере у нас есть модуль и класс. Создадим экземпляр класса, опишем в модуле новый метод для него и подмешаем модуль непосредственно в экземпляр. Проверим — всё работает корректно:

module ExplicitCatThings   def get_in_the_way     "I'm on the laptop!"   end end  class Cat   def speak     'Meow!'   end end  cat = Cat.new def cat.jump   'Weeeee!!! Rumble-rumble!' end cat.extend ExplicitCatThings  cat.speak #> "Meow!" cat.jump #> "Weeeee!!! Rumble-rumble!" cat.get_in_the_way #> "I'm on the laptop!"

Такой подход называется «monkey patching» — во время работы программы, после создания экземпляра мы меняем его поведение. Язык позволяет нам такую гибкость, но это считается не особенно хорошей практикой, поэтому старайтесь её избегать. Из-за «monkey patching» бывает сложно анализировать работу программы и отслеживать, где и как изменяются свойства классов и объектов.

Это был обзор основного стиля программирования в Ruby — объектно-ориентированного. Готовим для вас следующую лекцию, всего хорошего!


ссылка на оригинал статьи https://habr.com/ru/articles/691858/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *