Menu
Coddy logo textTech

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

Часть раздела Object Oriented Programming путешествия по Lua на Coddy — урок 33 из 70.

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

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

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) (или эквивалентно Vehicle:new(speed)), чтобы получить объект с уже установленным полем speed. Затем мы добавляем специфичное для дочернего класса поле brand. Наконец, мы меняем метатаблицу с Vehicle на Car, чтобы экземпляр сначала искал методы в дочернем классе.

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 в вашем коде при построении правильной цепочки наследования!

Шпаргалка

Чтобы вызвать конструктор родительского класса из дочернего класса, используйте имя родительского класса с точечным синтаксисом, передавая сам родительский класс в качестве первого аргумента:

local obj = ParentClass.new(ParentClass, parentArgs)

После вызова родительского конструктора добавьте поля, специфичные для дочернего класса, и перепривяжите метатаблицу к дочернему классу:

local Vehicle = {}
Vehicle.__index = Vehicle

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

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

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

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

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

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

local Bird = require('Bird')

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

-- TODO: Создать экземпляр Bird с заданным именем и размахом крыльев

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

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

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

Все уроки раздела Object Oriented Programming