Menu
Coddy logo textTech

Расширение конструктора

Часть раздела Объектно-ориентированное программирование путешествия по Lua на Coddy — урок 33 из 70.

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

Этот метод заключается в использовании имени родительского класса для вызова его метода :new(), передавая self в качестве первого аргумента. Это создает базовый объект со всей родительской инициализацией, который вы затем настраиваете для дочернего класса.

local Vehicle = {}
Vehicle.__index = Vehicle

function Vehicle:new(speed)
    local obj = {speed = speed}
    setmetatable(obj, Vehicle)
    return obj
end

-- Car наследует от Vehicle
local Car = {}
Car.__index = Car
setmetatable(Car, {__index = Vehicle})

function Car:new(speed, brand)
    local obj = Vehicle.new(Vehicle, speed)  -- Вызов родительского конструктора
    obj.brand = brand                         -- Добавление поля, специфичного для дочернего класса
    setmetatable(obj, Car)                    -- Перепривязка к Car
    return obj
end

Обратите внимание на основные шаги в конструкторе дочернего класса: сначала мы вызываем Vehicle.new(Vehicle, speed), чтобы получить объект с уже установленным полем speed. Затем мы добавляем специфичное для дочернего класса поле brand. Наконец, мы меняем метатаблицу с Vehicle на Car, чтобы экземпляр сначала искал методы в дочернем классе.

Вы также можете использовать синтаксис с двоеточием при вызове конструктора родителя — Vehicle:new(speed) эквивалентно Vehicle.new(Vehicle, speed), так как в обоих случаях Vehicle передается в качестве первого аргумента. Обе формы здесь работают корректно, поэтому смело используйте ту, которую считаете более читабельной.

local myCar = Car:new(120, "Toyota")
print(myCar.speed)  -- Вывод: 120
print(myCar.brand)  -- Вывод: Toyota

Этот паттерн позволяет вашему коду оставаться DRY — общая логика инициализации находится в родителе, в то время как каждый дочерний элемент обрабатывает только свои уникальные атрибуты.

challenge icon

Задание

Легко

Давайте создадим иерархию существ, где дочерний класс расширяет конструктор своего родителя! Вы создадите родительский класс Animal и дочерний класс Bird, где конструктор Bird вызывает конструктор Animal для обработки общей инициализации перед добавлением своего собственного уникального атрибута.

Вы организуете свой код в трех файлах:

  • Animal.lua: Определите родительский класс с конструктором :new(name), который сохраняет имя животного. Включите метод :describe(), который выводит имя животного. Настройте стандартный паттерн класса с __index и верните класс.
  • Bird.lua: Определите дочерний класс, который наследуется от Animal. Конструктор вашего Bird :new(name, wingspan) должен вызывать родительский конструктор, используя Animal.new(Animal, name) для инициализации имени, затем добавлять поле wingspan к объекту и, наконец, повторно привязывать метатаблицу к Bird. Включите метод :info(), который выводит и имя, и размах крыльев в определенном формате.
  • main.lua: Подключите модуль Bird, прочитайте два входных значения (имя и размах крыльев), создайте экземпляр Bird и вызовите оба метода, чтобы продемонстрировать совместную работу унаследованной и специфичной для дочернего класса функциональности.

Вы получите два входных значения:

  1. Имя птицы
  2. Размах крыльев птицы (число)

В вашем основном файле создайте объект Bird с заданным именем и размахом крыльев. Затем вызовите :describe(), а после него :info() на отдельных строках.

Метод :describe() (унаследованный от Animal) должен выводить:

Animal: {name}

Метод :info() (определенный в Bird) должен выводить:

{name} has a wingspan of {wingspan}

Например, если входные данные — Eagle и 200, вывод должен быть следующим:

Animal: Eagle
Eagle has a wingspan of 200

Ключевым моментом здесь является то, что конструктор Bird не дублирует инициализацию имени — он делегирует эту работу конструктору Animal, а затем добавляет только то, что уникально для птиц. Это позволяет соблюдать принцип DRY в вашем коде при построении правильной цепочки наследования!

Попробуйте сами

-- main.lua: Точка входа

local Bird = require('Bird')

-- Чтение входных данных
local name = io.read()
local wingspan = tonumber(io.read())

-- TODO: Создайте экземпляр Bird с заданным name и wingspan

-- TODO: Вызовите :describe() для bird (унаследовано от Animal)

-- TODO: Вызовите :info() для bird (определено в Bird)
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование