Файлы
Справка
Телеграм чат начинающих программистов. Общаемся и помогаем друг другу
Если ссылка не открывается, можно найти нас в поиске по чатам @rubyrush
или
пойти другим путем
На прошлом уроке мы научились наследовать классы друг от друга. Сегодня вы ещё немного нового узнаете об объектно-ориентированном подходе. Мы расскажем о статических и абстрактных методах и научим вас расширять поведение базового (родительского) класса.
Давайте вспомним нашу программу с мостами
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, такие методы называют абстрактными.
Давайте спроектируем наш блокнот из предыдущего урока и напишем недостающие методы.
У класса 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
Итак, мы научились управлять классами как взрослые дядьки. В следующих уроках мы начинаем новую тему — хранение данных. Конечно же эта тема тоже важна для уважающего себя программиста, так как все не очень важные темы мы просто заранее выбросили из программы нашего курса.