Классы, абстрактные и статические методы

На прошлом уроке мы научились наследовать классы друг от друга. Сегодня вы ещё немного нового узнаете об объектно-ориентированном подходе. Мы расскажем о статических и абстрактных методах и научим вас расширять поведение базового (родительского) класса.

План урока

  1. Статические методы
  2. Абстрактные методы
  3. Блокнот v. 1.0
  4. Первая полезная программа на github!

Статические методы класса

Давайте вспомним нашу программу с мостами

puts "Река, нужно перекинуть мост"
sleep 1

puts "Создаём мост"
bridge = Bridge.new
sleep 1

puts "Открываем мост"
bridge.open
sleep 1

puts "Проехали мост, едем дальше..."
sleep 1

Обратите внимание на два метода bridge.open мы вызываем у экземпляра класса Bridge, а Bridge.new мы вызываем у самого класса Bridge.

Методы, которые вызываются у класса, без создания экземпляра, например вышеупомянутый new — это статические методы. Сейчас расскажем поподробнее.

Зачем нужны статические методы?

У нас есть класс, по которому, как по чертежу, мы можем создавать экземпляры, то есть объекты. А что если какая-то функция общая, то есть не зависит от конкретного объекта.

Например у нас есть класс Человек, можем создать экземпляр человек и у конкретного человека можно спросить, какой у него цвет глаз: человек.цвет_глаз. А что, если нам нужно узнать, сколько всего людей на планете Земля или, например, какой человек самый высокий? Никакой конкретный человек этого не знает (без ограничения общности считаем, что у нас нет переменной, которая указывает на человека с именем Анатолий Вассерман). Тогда нам нужно спросить об этом у самого класса, который заведует всеми людьми: Человек.сколько или Человек.самый_высокий.

А, например, чтобы узнать, сколько людей в конкретном городе, можно было бы методу сколько передать параметр: Человек.сколько("Москва").

Статический метод, также, как и обычный, может что-то возвращать. Например Bridge.new — статический метод, встроенный в руби, возвращающий экземпляр класса Bridge.

Также вам, наверное, знакомы статические методы Time.now и Date.parse.

Статические методы

Как создавать статические методы

Определить статический метод у класса очень просто. Достаточно просто написать перед названием определяемого метода кодовое слово self:

class MyClass
  def self.static_method
    puts "Я статический метод класса MyClass"
  end
end

MyClass.static_method

Выведет на экран:

Я статический метод класса MyClass

Обратите внимание ещё раз — мы не создавали экземпляр класса MyClass, мы вызвали метод самого класса.

Перегрузка методов

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

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

Пусть у нас есть класс Ресторан и у него определен метод бизнес_ланч: в него входит первое, второе и десерт.

Допустим, у нас также есть ребенок СушиБар < Ресторан. У него тогда автоматически тоже будет метод бизнес_ланч. Однако, если понадобится, в бизнес_ланч от СушиБара могут входить совсем другие блюда: например, роллы и мисо суп.

Абстрактные методы

Если реализация метода у родительского класса нас не интересует вовсе, то мы можем не писать её, а просто обозначить, что такой метод вообще есть. Тогда у каждого ребенка уже должна быть своя конкретная реализация.

В некоторых языках программирования, например, в Java, такие методы называют абстрактными.

Абстрактные методы

Блокнот версия v. 1.0

Давайте спроектируем наш блокнот из предыдущего урока и напишем недостающие методы.

Структура блокнота с классами

У класса Post будут:

  • статический метод post_types, который будет возвращать ассоциативный массив всех возможных детей этого класса (чтобы можно было спросить у пользователя, что он хочет создать);
  • метод create, который по переданному значению будет создавать нужного ребенка;
  • два абстрактных метода read_from_console и to_strings, которые будут реализованы у каждого ребенка;
  • и метод save, который будет только у родителя, и который будет использовать метод to_strings;
  • а также служебный метод file_path (который просто будет использоваться для определения, куда сохранять заметку).

post.rb:

# Базовый класс "Запись"
# Задает основные методы и свойства, присущие всем разновидностям Записи
class Post
  # Конструктор
  def initialize
    @created_at = Time.now # дата создания записи
    @text = [] # массив строк записи — пока пустой
  end

  # Набор известных детей класса Запись в виде массива классов
  def self.post_types
    [Memo, Task, Link]
  end
  # XXX/ Строго говоря этот метод self.types нехорош — родительский класс в идеале в коде
  # не должен никак зависеть от своих конкретных детей. Мы его использовали для простоты
  # (он адекватен поставленной задаче).
  #
  # В сложных приложениях это делается немного иначе: например отдельный класс владеет всей информацией,
  # и умеет создавать нужные объекты (т. н. шаблон проектирования "Фабрика").
  # Или каждый дочерний класс динамически регистрируется в подобном массиве сам во время загрузки программы.
  # См. подробнее книги о шаблонах проектирования в доп. материалах.

  # Динамическое создание объекта нужного класса из набора возможных детей
  def self.create(type_index)
    return post_types[type_index].new
  end

  # Вызываться в программе когда нужно считать ввод пользователя и записать его в нужные поля объекта
  def read_from_console
    # todo: должен реализовываться детьми, которые знают как именно считывать свои данные из консоли
  end

  # Возвращает состояние объекта в виде массива строк, готовых к записи в файл
  def to_strings
    # todo: должен реализовываться детьми, которые знают как именно хранить себя в файле
  end

  # Записывает текущее состояние объекта в файл
  def save
    file = File.new(file_path, "w:UTF-8") # открываем файл на запись

    for item in to_strings do # идем по массиву строк, полученных из метода to_strings
      file.puts(item)
    end

    file.close # закрываем
  end

  # Метод, возвращающий путь к файлу, куда записывать этот объект
  def file_path
    # Сохраним в переменной current_path место, откуда запустили программу
    current_path = File.dirname(__FILE__)

    # Получим имя файла из даты создания поста метод strftime формирует строку типа "2014-12-27_12-08-31.txt"
    # набор возможных ключей см. в документации Руби
    file_name = @created_at.strftime("#{self.class.name}_%Y-%m-%d_%H-%M-%S.txt")
    # Обратите внимание, мы добавили в название файла даже секунды (%S) — это обеспечит уникальность имени файла

    return current_path + "/" + file_name
  end
end

# PS: Весь набор методов, объявленных в родительском классе называется интерфейсом класса
# Дети могут по–разному реализовывать методы, но они должны подчиняться общей идее
# и набору функций, которые заявлены в базовом (родительском классе)

# PPS: в других языках (например Java) методы, объявленные в классе, но пустые
# называются абстрактными (здесь это методы to_strings и read_from_console).
#
# Смысл абстрактных методов в том, что можно писать базовый класс и пользоваться
# этими методами уже как будто они реализованы, не задумываясь о деталях.
# С деталями реализации методов уже заморачиваются дочерние классы.
#

Пора реализовать абстрактные методы у детей: статьи, ссылки и задачи.

link.rb:

class Link < Post
  def initialize
    # Вызовем одноимённый метод (initialize) родителя (Post) методом super
    super

    # А потом добавим то, что будет отличаться у ребёнка — поле @url
    @url = ''
  end

  def read_from_console
    # Мы полностью переопределяем метод read_from_console родителя Post

    # Попросим у пользователя адрес ссылки
    puts "Введите адрес ссылки"
    @url = STDIN.gets.chomp

    # И описание ссылки (одной строчки будет достаточно)
    puts "Напишите пару слов о том, куда ведёт ссылка"
    @text = STDIN.gets.chomp
  end

  def save
    # Метод save во многом повторяет метод родителя, но отличия существенны

    file = File.new(file_path, "w:UTF-8")
    time_string = @created_at.strftime("%Y.%m.%d, %H:%M")
    file.puts(time_string + "\n\r")

    # Помимо текста мы ещё сохраняем в файл адрес ссылки
    file.puts(@url)
    file.puts(@text)

    file.close

    # Напишем пользователю, что запись добавлена
    puts "Ваша ссылка сохранена"
  end
end

memo.rb:

class Memo < Post
  def read_from_console
    # Метод, который спрашивает у пользователя, что он хочет написать в дневнике
    puts "Я сохраню всё, что ты напишешь до строчки \"end\" в файл."

    # Объявим переменную, которая будет содержать текущую введенную строку
    line = nil

    # Запустим цикл, пока не дошли до строчки "end",
    while line != "end" do
      # Читаем очередную строку и записываем в массив @text
      line = STDIN.gets.chomp
      @text << line
    end

    # Теперь удалим последний элемент из массива @text – там служебное слово "end"
    @text.pop
  end

  def save
    # Откроем файл для записи в режиме записи (write)
    # Файл не существует и будет создан
    file = File.new(file_path, "w:UTF-8")

    # Обратите внимание, что мы вызвали метод file_name, который определили выше
    # save и file_name — методы одного класса и поэтому могут использовать друг друга

    # Сперва запишем в блокнот дату и время записи и сделаем отступ
    # \r – специальный дополнительный символ конца строки для Windows
    time_string = @created_at.strftime("%Y.%m.%d, %H:%M")
    file.puts(time_string + "\n\r")

    # Затем в цикле запишем в файл строчку за строчкой массив @text
    for item in @text do
      # Метод puts добавляет перевод строки в конце, что нам и надо
      file.puts(item)
    end

    # Обязательно закрыть файл, чтобы сохранить все изменения
    file.close

    # Напишем пользователю, что запись добавлена
    puts "Ваша запись сохранена"
  end
end

task.rb:

# Подключим встроенный в руби класс Date для работы с датами
require 'date'

class Task < Post
  def initialize
    # Вызовем одноимённый метод (initialize) родителя (Post) методом super
    super

    # А потом добавим то, что будет отличаться у ребёнка — поле @due_date
    @due_date = ''
  end

  def read_from_console
    # Мы полностью переопределяем метод read_from_console родителя Post

    # Спросим у пользователя, что за задачу ему нужно сделать
    # Одной строчки будет достаточно
    puts "Что вам необходимо сделать?"
    @text = STDIN.gets.chomp

    # А теперь спросим у пользователя, до какого числа ему нужно это сделать
    # И подскажем формат, в котором нужно вводить дату
    puts "До какого числа вам нужно это сделать?"
    puts "Укажите дату в формате ДД.ММ.ГГГГ, например 12.05.2003"
    input = STDIN.gets.chomp

    # Для того, чтобы записть дату в удобном формате, воспользуемся методом parse класса Time
    @due_date = Date.parse(input)
  end

  def save
    file = File.new(file_path, "w:UTF-8")
    time_string = @created_at.strftime("%Y.%m.%d, %H:%M")
    file.puts(time_string + "\n\r")

    # Так как поле @due_date указывает на объект класса Date, мы можем вызвать у него метод strftime
    # Подробнее о классе Date читайте по ссылкам в материалах
    file.puts("Сделать до #{@due_date.strftime("%Y.%m.%d")}")
    file.puts(@text)

    file.close

    # Напишем пользователю, что задача добавлена
    puts "Ваша задача сохранена"
  end
end

Теперь есть чертежи, по которым можно строить любую запись. В основной программе — спросим пользователя, что он хочет создать, получив список возможных типов статическим методом Post.post_types.

Создаём объект нужного класса, основываясь на ответе с помощью метода create, а потом просто вызываем у созданного объекта его методы read_from_console и save (обратите внимание, нам совершенно не важно, что за класс у нас получился, т.к. мы используем абстрактные методы).

notepad.rb:

# Подключаем класс Post и его детей
require_relative 'post.rb'
require_relative 'memo.rb'
require_relative 'link.rb'
require_relative 'task.rb'

# Как обычно, при использовании классов программа выглядит очень лаконично
puts "Привет, я твой блокнот!"

# Теперь надо спросить у пользователя, что он хочет создать
puts "Что хотите записать в блокнот?"

# массив возможных видов Записи (поста)
choices = Post.post_types

choice = -1

until choice >= 0 && choice < choices.size # пока юзер не выбрал правильно
  # выводим заново массив возможных типов поста
  choices.each_with_index do |type, index|
    puts "\t#{index}. #{type}"
  end
  choice = gets.chomp.to_i
end

# выбор сделан, создаем запись с помощью стат. метода класса Post
entry = Post.create(choice)

# сейчас в переменной entry лежит один из детей класса Post, какой именно,
# определилось выбором пользователя, переменной choice.
# Но мы не знаем какой, и обращаемся с entry как с объектом класса Post, этого, оказывается, достаточно.

# Просим пользователя ввести пост (каким бы он ни был)
entry.read_from_console

# Сохраняем пост в файл
entry.save

puts "Ваша запись сохранена!"

Мы написали довольно непростую программу. Не забудьте положить её в репозиторий и отправить в свой аккаунт на github. Создайте на github-е новый репозиторий и назовите его, например notepad.

git add link.rb task.rb post.rb memo.rb notepad.rb
git commit -m "Version 1.0: Memo, Link and Task"
git remote add git@github.com:your_username/notepad.git
git push -u origin master

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