ООП 101

Как разбить классную программу

Все плюсы и минусы вашего первого урока в Ruby

Это вторая статья из серии из четырех частей, посвященных основам объектно-ориентированного программирования на Ruby. Прочтите предыдущую статью здесь.

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

Азбука ООП

Классы и объекты

В Ruby все от 'abc' до 123 является объектом. Объект - это инкапсуляция состояния и поведения. Класс похож на схему состояния и поведения объекта, из чего он сделан и что может делать. Если класс - это план, то объект - это дом. Давайте определимся с нашим первым классом!

Мы можем определить класс Dog с помощью зарезервированного слова class.

Затем мы можем вызвать метод класса new на Dog, чтобы создать экземпляр класса и назначить этот экземпляр локальной переменной, например fluffy. Другими словами, мы можем создать экземпляр объекта fluffy из класса Dog.

Когда мы создаем экземпляр объекта, он запускает метод конструктора. Метод конструктора в Ruby определяется зарезервированным словом initialize.

Как только мы создаем экземпляр Dog объекта fluffy, вызывается метод initialize и выводит строку 'This object is initialized!'.

Переменные и методы экземпляра

Однако цель метода конструктора - инициализировать состояние объекта. Давайте изменим initialize метод именно для этого.

И снова мы создаем экземпляр Dog объекта fluffy. Но на этот раз мы передаем строку 'Fluffy' из метода new в метод initialize и присваиваем ее локальной переменной name. Внутри метода переменная @name с символом @ перед ней является переменной экземпляра. Переменные экземпляра связывают данные с нашими объектами и отслеживают информацию об их состоянии.

Мы назначаем локальную переменную name переменной экземпляра @name. Это означает, что переменная экземпляра @name указывает на строку 'Fluffy', а строка 'Fluffy' является частью состояния нашего объекта.

Мы создадим другой Dog объект с именем buddy и назовем его 'Buddy'.

Теперь fluffy и buddy являются объектами класса Dog. Тем не менее, у них есть отдельная копия переменной экземпляра @name, потому что переменные экземпляра имеют область действия на уровне объекта. Состояние объекта уникально для объекта, а переменные экземпляра отслеживают информацию об индивидуальном состоянии.

Хотя объекты одного класса не имеют общего состояния, у них одинаковое поведение. Мы можем представить, что оба fluffy и buddy могут лаять.

Это поведение определяется методом экземпляра. Методы экземпляра могут вызываться каждым объектом класса. Если мы вызываем метод экземпляра bark на fluffy, выводится строка 'Woof!'.

И если мы вызовем его на buddy, будет выведена такая же строка!

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

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

Мы снова вызовем его для обоих Dog объектов.

Лапочка! (Извините.)

Методы доступа

Методы доступа - еще один способ раскрыть информацию о состоянии объекта. Если мы хотим получить Dog имя объекта, мы можем создать метод, который будет возвращать его переменную экземпляра @name. Это называется методом получателя. По соглашению мы назовем метод после переменной экземпляра.

Наш name метод возвращает ссылку на переменную экземпляра @name. Если мы изменим ссылку, мы изменим переменную экземпляра.

Скорее всего, это не является нашим намерением! Но если это так, мы можем создать метод setter, чтобы переназначить переменную экземпляра @name, а не изменять ее. По соглашению мы назовем метод после переменной экземпляра, добавленной символом =.

В строке 21 мы должны ожидать использования метода name= следующим образом: fluffy.name=(fluffy.name.reverse). Но в Ruby мы можем использовать более естественный синтаксис присваивания, если мы условно назовем наши методы установки.

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

Однако методы занимают много места, если мы хотим только читать или писать над переменной экземпляра @name. И это только одна переменная экземпляра! Вместо этого Ruby дает нам возможность автоматически их генерировать.

Метод attr_accessor принимает символ в качестве аргумента, создает переменную экземпляра с тем же именем и дает нам доступ для чтения и записи к ней. Если нам нужен только доступ для чтения, мы можем использовать метод attr_reader. Если нам нужен только доступ на запись, мы можем использовать метод attr_writer.

В этом случае у нас есть name метод получения, name= метод установки и переменная экземпляра @name, которая инициализируется строкой при выполнении нашего initialize метода. (В противном случае неинициализированная переменная экземпляра указывает на nil.)

Мы можем вызывать наши методы из определения класса. Вместо того, чтобы напрямую ссылаться на переменную экземпляра @name в нашем bark методе, мы вызовем метод name для ее получения.

Удаляя символ @, мы обращаемся к методу получения name, а не к переменной экземпляра @name. Вспомните, когда мы хотели переформатировать имя fluffy каждый раз, когда мы его извлекали. Теперь это отразится на bark. И если бы мы захотели переформатировать его снова, нам нужно было бы внести изменения только в одном месте.

Точно так же мы можем вызвать метод установки name=, чтобы получить и переназначить переменную экземпляра @name. Но сначала мы добавим переменную экземпляра, чтобы отслеживать вес наших Dog объектов.

Затем мы создадим change_info метод для одновременного переназначения переменных экземпляра @name и @weight.

Мы позвоним на fluffy.

Оно работает! Но вместо того, чтобы ссылаться на переменные экземпляра непосредственно в change_info методе, давайте вызовем наши методы установки.

И мы снова вызовем change_info метод на fluffy.

Ой-ой! Метод больше не работает!

Это потому, что внутри него мы инициализируем локальные переменные name и weight вместо того, чтобы вызывать методы установки name= и weight=. Чтобы устранить неоднозначность, мы должны четко указать, на что мы ссылаемся. Один из способов - добавить к нашим методам установки self.

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

Если мы вызываем метод change_info на fluffy, self относится к fluffy внутри change_info, а self.name= совпадает с fluffy.name=.

Модификаторы доступа

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

С другой стороны, мы можем определить частные методы, чтобы ограничить доступ к информации. Мы приватизируем методы доступа для @name и @weight, переместив их под модификатор доступа private.

Ранее мы могли вызывать методы получения name и weight в любом месте программы. Теперь, когда они являются закрытыми, если мы вызовем их вне класса, который они определены, возникает NoMethodError.

Но что произойдет, если мы вызовем bark вне класса, который он определил? Хотя это общедоступный метод, он вызывает частный name метод получения.

Успех! Хотя мы вызываем bark вне класса, который он определил, он вызывает name внутри определения класса. Доступ к частным методам может осуществляться методами того же класса, поэтому мы можем использовать общедоступные методы для неявного вызова их вне определения класса. Но даже внутри мы не можем их явно вызвать.

Точно так же, как мы не можем вызвать name на fluffy, мы не можем вызвать его на self, поскольку self явно ссылается на вызывающий объект. Это означает, что мы также не можем вызвать частный метод для другого объекта того же класса, поэтому данные инкапсулируются не только извне определения класса, но и других объектов внутри.

Но помните, что если мы не добавляем метод установки с self, мы вместо этого инициализируем локальную переменную. Что произойдет, если метод установки является частным?

Давайте вызовем общедоступный метод change_info, который вызывает частные методы установки name= и weight= на self. Метод bark - единственный способ получить доступ к @name вне определения класса, поэтому мы будем вызывать его до и после вызова change_info. И мы обновим его, чтобы у нас тоже был доступ к @weight.

В отличие от случая, когда мы явно вызываем метод получения name в bark, ошибки не возникает. Мы можем изменить имя и вес fluffy. Таким образом, даже когда метод установки является частным, мы должны явно вызвать его, чтобы исключить неоднозначность инициализации локальной переменной. Это единственное исключение!

Хотя частные методы инкапсулируют данные вне определения класса и других объектов класса, иногда мы хотим инкапсулировать их только из первого. Чтобы сделать его доступным в противном случае, мы можем определить защищенные методы с модификатором доступа protected.

Защищенные методы похожи на частные методы вне того класса, в котором они определены, но внутри они похожи на общедоступные методы.

Это означает, что мы можем вызывать a_protected_method только из Test, но мы можем явно вызывать его из a_public_method.

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

Если мы вызовем метод new_friend на fluffy и предоставим другой объект Dog в качестве аргумента, мы можем явно вызвать метод получения name на обоих, поскольку name теперь защищен и доступен для всех объектов класса Dog.

И мы по-прежнему не можем назвать это вне определения класса Dog!

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

Переменные и методы класса

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

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

Мы создадим новый Dog класс с помощью метода класса.

В нашем определении класса self относится к Dog, а self.total_number_of_dogs то же самое, что и Dog.total_number_of_dogs. Переменная @@number_of_dogs с двумя символами @ перед ней является переменной класса. Переменная класса s отслеживает информацию, относящуюся к классу в целом, а не к конкретным объектам.

Мы инициализируем переменную класса @@number_of_dogs равным 0. Когда мы создаем экземпляр нового объекта Dog, запускается метод initialize и увеличивает переменную класса @@number_of_dogs на 1. Если мы хотим получить @@number_of_dogs, мы можем вызвать метод класса self.total_number_of_dogs для Dog класса.

Давай попробуем.

Таким образом, мы видим, что переменные класса имеют область видимости на уровне класса. Их можно инициализировать где угодно (но следует определять вне методов экземпляра), и они доступны как в методах класса, так и в методах экземпляра.

Поскольку переменные класса относятся к классу в целом, все объекты класса совместно используют одну копию переменной класса. Когда мы увеличиваем @@number_of_dogs, его новое значение отражается в классе Dog и в каждом из его объектов.

Теперь мы знаем нашу азбуку ...

Давайте свяжем то, что у нас есть, с основными концепциями, которые мы обсуждали в предыдущей статье. Сначала мы объединим два Dog класса в один.

Пока мы не будем беспокоиться о наследовании, но рассмотрим, как простой класс, подобный этому, демонстрирует абстракцию, инкапсуляцию и полиморфизм. Следующие соображения ни в коем случае не являются исчерпывающими!

1) Абстракция

В последний раз, когда мы запускали наш код, мы вызвали метод self.total_number_of_dogs в классе Dog, чтобы узнать, сколько было объектов этого класса. Мы можем использовать его, не запоминая, что переменная класса @@number_of_dogs увеличивается каждый раз, когда мы создаем экземпляр объекта Dog, или даже понимаем, что такое переменные класса и какова их область видимости.

То же самое и с методом change_info.

Если мы помним, как его использовать, мы можем легко изменить имя и вес fluffy, не запоминая, как он определен или реализован.

Тем не менее, определение метода выглядит довольно простым. Но переназначаем ли мы переменные экземпляра @name и @weight нашего объекта? Если да, то напрямую или через методы установки? Разве синтаксис вызова метода не должен быть похож на self.name=(name)? И вообще, что такое self?

Надеюсь, мы сможем ответить на эти вопросы сейчас, даже если бы мы не реализовали change_info метод. Дело в том, что всегда сложнее, чем кажется на первый взгляд. Мы можем выбрать любую «простую» часть нашего кода и углубиться все ниже и ниже в ее уровни абстракции.

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

Внутри нашего change_info метода мы используем self для явного вызова name= и weight=, методов, инкапсулированных вызывающим объектом. Методы переназначают @name и @weight, состояние, которое инкапсулируется в переменные экземпляра одним и тем же объектом.

Мы работали с модификаторами доступа, которые инкапсулируют методы и состояние по-разному. Мы можем сделать вывод, что состояние всегда является частным, и контролируем доступ к нему, предоставляя открытый интерфейс для взаимодействия с ним с помощью таких методов, как name= и weight=.

3) Полиморфизм

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

Спасибо, Далее

В следующей статье мы закрепим наши знания и напишем классическую программу по наследству: