Часть 1: Введение в ООП

Объектно-ориентированное программирование (ООП) - это парадигма программирования. Это позволяет нам представлять сложности реального физического мира в цифровом виде. Языки, поддерживающие некоторый уровень объектной ориентации, распространены повсеместно - они включают Ruby, Python, Javascript, PHP, C ++, C #, Java, Rust, Kotlin, Go и многие другие.

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

По своей сути ООП состоит из классов и объектов, созданных из этих классов.

Классы определяют атрибуты и поведение

Класс - это план или шаблон для объектов. Это концепция или идея вещи, а объект - одно физическое проявление этой концепции. Например, возьмем концепцию книги. Представьте себе одно из них. Что ты видишь? Другими словами, что делает книгу книгой?

У книги есть атрибуты

  • Некоторое потенциально увлекательное название
  • Некоторые авторы, написавшие это
  • Некоторое количество страниц
  • Какой-то переплет в мягкой или твердой обложке
  • Обложка какого-то цвета
  • Некоторый измеримый вес

Это все характеристики - они описывают книгу. Переменные экземпляра - это атрибуты отдельной книги (например, название Гарри Поттер и Тайная комната, номер страницы 432, весом 0,74 фунта). Суммарное объединение всех переменных экземпляра отдельной книги составляет состояние этой конкретной книги.

С книгой можно выполнять определенные действия

  • Это можно прочитать
  • Его можно забрать
  • Это можно отложить
  • Его можно бросить
  • Страницы можно вырывать
  • Заметки можно писать на полях

В ООП действия, которые вы можете выполнять с объектом, определены в определении класса. Они называются методами экземпляра и могут вызываться для любого объекта, созданного из класса.

Сделав шаг назад, класс диктует возможные атрибуты или характеристики объектов, которые из него сделаны.

Давайте сосредоточимся на трех ключевых атрибутах - заголовке, авторе и количестве страниц, чтобы создать Book класс ниже.

class Book
  def initialize(title, author, pages)
    @title = title 
    @author = author
    @pages = pages
  end 
end

В нашем простом определении класса Book есть специальный метод экземпляра initialize. Это метод конструктора Ruby. Он запускается каждый раз, когда метод класса ::new вызывается для класса Book. Обратите внимание, что initialize - это частный метод экземпляра, что означает, что его нельзя вызывать вне определения класса. Кроме того, если метод инициализации определен для приема параметров, методу ::new должны быть переданы аргументы (если только параметры, определенные в определении класса, не имеют параметров по умолчанию).

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

Вы могли заметить переменные с префиксом @. Переменные экземпляра обозначаются знаком @ (также известным как сигил). Эти переменные имеют область видимости на уровне объекта - они доступны для всех методов экземпляра, определенных в классе, и каждый объект, созданный из класса, имеет свою собственную копию переменной, которая может содержать собственное значение.

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

Book.new('Triumph of the City', 'David Glaeser', 338)

Мы создали или инстанциировали один объект Book - один экземпляр класса Book. Этот book имеет набор переменных экземпляра - @title, @author и @pages. Переменные экземпляра этого конкретного экземпляра класса привязаны к значениям, которые мы передаем при вызове метода ::new. Другими словами, этот Book объект имеет уникальное состояние, состоящее из всех объектов, связанных с его переменными экземпляра.

Мы также можем присвоить этот объект локальной переменной для хранения.

triumph_of_the_city = Book.new('Triumph of the City', 'David Glaeser', 338)

Как мы можем получить доступ к данным, хранящимся в объекте? Как мы можем узнать количество страниц в нашем Book объекте? Прямо сейчас - наши возможности ограничены. Если вы хотите получить более глубокие знания о метапрограммировании Ruby, изучите Object#instance_variables и Object#instance_variable_get.

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

class Book
  def initialize(title, author, pages)
    @title = title 
    @author = author
    @pages = pages
  end
def title
    @title
  end
end

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

triumph_of_the_city.title

Это возвращает название книги.

Методы экземпляра, которые обращаются к переменным экземпляра, называются методами getter, а те, которые позволяют пользователю изменять переменные экземпляра, называются методами setter. Напишем еще несколько геттеров и сеттер-метод.

class Book
  def initialize(title, author, pages)
    @title = title 
    @author = author
    @pages = pages
  end
def title
    @title
  end
def author
    @author
  end
def pages
    @pages
  end
def pages=(new_pages)
    @pages = new_pages
  end 
end

Как видите, теперь у нас есть методы получения для title, author и pages. У нас также есть метод установки для pages. Это может показаться странным. В Ruby принято использовать = в конце имени метода установки. Это похоже на методы предиката, заканчивающиеся на ?.

Установщик принимает один аргумент - новое значение, которое пользователь хочет присвоить переменной экземпляра. С синтаксическим сахаром Ruby вызвать метод установки для объекта за пределами определения класса очень просто! Допустим, мы вырываем одну из страниц из нашей книги.

triumph_of_the_city.pages = 337

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

triumph_of_the_city.pages=(337)

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

  • Мы определили класс. Этот класс является шаблоном для всех объектов, которые создаются из него. В классе мы диктуем возможные атрибуты и возможное поведение, которое будут иметь объекты, созданные из этого класса.
  • Мы создали экземпляр объекта Book и дали ему несколько уникальных значений для переменных экземпляра.
  • Мы определили несколько методов получения для доступа к переменным экземпляра путем вызова метода для объектов вне определения класса.
  • Мы определили метод установки, который можно использовать для повторного присвоения переменной экземпляра pages новому значению.

Давайте создадим еще один объект Book и свяжем его с локальной переменной.

walkable_city = Book.new('Walkable City Rules', 'Jeff Speck', 290)

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

* В этой статье мы не будем говорить о attr_* методах - сокращенном варианте Ruby для создания геттеров и сеттеров.

Методы экземпляра дают функциональность

Прямо сейчас у нас есть два Book объекта. Мы можем создать новую книгу, передав аргументы в вызов метода ::new класса Book. Давайте подумаем о функциях, доступных для любых Book объектов.

Поскольку мы написали методы экземпляра получателя для title, author и pages, мы можем получить доступ к значениям каждой из этих переменных экземпляра извне класса. Мы также написали метод установки для pages, который позволяет нам изменять значение, привязанное к @pages в любом Book объекте, на любое желаемое значение. У нас есть доступ для чтения и записи для pages переменной экземпляра, а у нас есть read-only доступ к title и author.

С помощью нашего метода установки для @pages мы можем изменить количество страниц. Фактически, пользователь нашей программы может даже установить количество страниц для объекта String из 'hello'.

walkable_city.pages = 'hello'

Присваивать String переменной, которая предназначена для представления количества страниц в книге, бессмысленно. Давайте наложим некоторые ограничения на то, что пользователь может делать с переменной экземпляра @pages. Вместо того, чтобы позволять пользователю устанавливать любое значение по своему усмотрению, давайте позволим ему выбрать только Integer объект.

def pages=(new_pages)
  if new_pages.instance_of?(Integer)
    @pages = new_pages
  else 
    puts 'Sorry, please enter a number'
  end 
end

Мы используем простое условие if-else, которое вызывает Object#instance_of? для объекта, привязанного к переданному в параметре new_pages. Вызову instance_of? мы передаем аргумент имени класса Integer. Это метод предиката, который вернет true или false, в зависимости от того, соответствует ли класс вызывающего объекта переданному аргументу. Если new_pages привязан к объектуInteger, мы указываем переменной экземпляра @pages на этот Integer. Если пользователь пытается передать объект любого другого типа, например String или Hash, мы выводим сообщение об ошибке на терминал и не присваиваем значение @pages.

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

Теперь, когда у нас есть небольшой, работающий Book класс, пара Book объектов с их собственными уникальными переменными экземпляра и общий набор методов (только методы получения и установки), мы можем взглянуть на основные идеи ООП и его основные преимущества. в качестве парадигмы программирования во второй статье этой серии здесь.