Шрифт:
Интервал:
Закладка:
class Person
def name=(new_name)
@name = new_name
end
end
Теперь он будет вести себя так, как если бы имя было общедоступным свойством объекта. Макрос установки может создавать для вас эти методы, подобно макросу геттера, который мы только что видели:
class Person
setter name
end
Его также можно использовать с объявлением типа или начальным значением.
Нам часто необходимо предоставить переменную экземпляра как с помощью геттера, так и сеттера. Для этого у Crystal есть макрос свойств:
class Person
property name
end
Это то же самое, что написать следующее:
class Person
def name
@name
end
def name=(new_name)
@name = new_name
end
end
Как обычно, объявления типов или начальные значения могут использоваться для очень удобного синтаксиса. Существуют и другие полезные макросы, например макрос записи, как вы увидите далее в этой главе. В следующих главах вы также узнаете, как создавать собственные макросы для автоматизации генерации кода. Далее вы узнаете об основной концепции объектно-ориентированного программирования: классах, которые наследуются от других классов.
Наследование
Классы могут основываться на других классах, обеспечивая более специализированное поведение. Когда класс наследует от другого, он получает все существующие методы и переменные экземпляра и может добавлять новые или перезаписывать существующие. Например, давайте расширим ранее определенный класс Person:
class Person
property name : String
def initialize(@name)
end
end
class Employee < Person
property salary = 0
end
Экземпляр Employee может находиться в любом месте, где требуется экземпляр Person, поскольку, по сути, employee – это человек:
person = Person.new("Alan")
employee = Employee.new("Helen")
employee.salary = 10000
p person.is_a? Person # => true
p employee.is_a? Person # => true
p person.is_a? Employee # => false
В этом примере родительским классом является Person, а дочерним - Employee. Для создания иерархии классов можно создать несколько классов. При наследовании от существующего класса дочерний класс может не только расширять, но и переопределять части своего родительского класса. Давайте посмотрим на это на практике:
class Employee
def yearly_salary
12 * @salary
end
end
class SalesEmployee < Employee
property bonus = 0
def yearly_salary
12 * @salary + @bonus
end
end
В этом примере мы видим, что ранее определенный класс Employee повторно открывается для добавления нового метода. При повторном открытии класса не следует указывать его родительский класс (в данном случае Person). Метод yearly_salary добавляется к Employee, а затем создается новый специализированный тип Employee, наследуемый от него (и, в свою очередь, также наследуемый от Person). Добавляется новое свойство и переопределяется yearly_ salary, чтобы учесть его. Переопределение затрагивает только объекты типа SalesEmployee, но не объекты типа Employee.
При наследовании от класса и переопределении метода ключевое слово super может использоваться для вызова переопределенного определения из родительского класса. yearly_salary можно было бы записать следующим образом:
def yearly_salary
super + @bonus
end
Поскольку метод initialize используется для подготовки начального состояния объекта, ожидается, что он всегда будет выполнен раньше всего остального. Таким образом, общепринятой практикой является использование ключевого слова super для вызова конструктора родительского класса при наследовании от существующего класса.
Теперь, когда мы определили несколько классов и подклассов, мы можем воспользоваться еще одной мощной концепцией: объекты типа подкласса могут храниться в переменной, типизированной для хранения одного из его базовых классов.
Полиморфизм
SalesEmployee наследуется от Employee, чтобы определить более специализированный тип сотрудника, но это не меняет того факта, что сотрудник отдела продаж является сотрудником и может рассматриваться как таковой. Это называется полиморфизмом. Давайте посмотрим пример этого в действии:
employee1 = Employee.new("Helen")
employee1.salary = 5000
employee2 = SalesEmployee.new("Susan")
employee2.salary = 4000
employee2.bonus = 20000
employee3 = Employee.new("Eric")
employee3.salary = 4000
employee_list = [employee1, employee2, employee3]
Здесь мы создали трех разных сотрудников, а затем создали массив, содержащий их всех. Этот массив имеет тип Array(Employee), хотя в нем также содержится SalesEmployee. Этот массив можно использовать для вызова методов:
employee_list.each do |employee|
puts "#{employee.name}'s yearly salary is $#{employee. yearly_salary.format(decimal_places: 2)}."
end
Это приведет к следующему результату:
Elen's yearly salary is $60,000.00.
Susan's yearly salary is $68,000.00.
Eric's yearly salary is $48,000.00.
Как показано в этом примере, Crystal вызовет правильный метод, основанный на реальном типе объекта во время выполнения, даже если он статически типизирован как родительский класс.
Создание иерархии классов полезно не только для повторного использования кода, но и для обеспечения полиморфизма. Вы даже можете ввести в свою программу неполные классы, просто чтобы объединить похожие концепции. Некоторые из них должны быть абстрактными классами, как мы увидим далее.
Абстрактные классы
Иногда мы пишем иерархию классов, и не имеет смысла разрешать создавать объекты на основе некоторых из них, потому что они не представляют конкретных понятий. Сейчас самое время пометить класс как абстрактный. Давайте рассмотрим пример:
abstract class Shape
end
class Circle < Shape
def initialize(@radius : Float64)
end
end
class Rectangle < Shape
def initialize(@width : Float64, @height : Float64)
end
end
И круги, и прямоугольники - это разновидности фигур, и они могут быть поняты сами по себе. Но форма сама по себе является чем-то абстрактным и была создана для наследования. Когда класс является абстрактным, его создание в виде объекта запрещено:
a = Circle.new(4)
b = Rectangle.new(2, 3)
c = Shape.new # This will fail to compile; it doesn't make sense.
Абстрактный класс не только накладывает ограничения, но и позволяет нам описать характеристики, которые должен реализовывать каждый подкласс. Абстрактные классы могут иметь абстрактные методы, а методы без определений должны быть переопределены:
abstract class Shape
abstract def area : Number
end