Перейти к основному содержимому

Объектно-ориентированное программирование в Lua

Разработчику Архитектору

Объектно-ориентированное программирование в Lua

Классы, объекты и элементы классов (свойства и методы)

Объектно-ориентированное программирование (ООП) — это парадигма программирования, которая организует код вокруг объектов, объединяющих данные и поведение. В языке Lua отсутствует встроенная поддержка классов как таковых, однако механизм таблиц и метатаблиц позволяет реализовать полноценную систему ООП, полностью соответствующую принципам объектной ориентации.

Класс — это шаблон или blueprint, определяющий структуру данных и набор функций, доступных для создания объектов. В Lua класс представляет собой обычную таблицу, содержащую поля для хранения свойств и методов. Таблица выступает в роли пространства имен, где хранятся определения всех элементов класса.

Объект — это конкретный экземпляр класса, созданный на основе шаблона. Объект обладает собственным состоянием, которое определяется значениями его свойств, и способностью выполнять действия через свои методы. Каждый объект в Lua является отдельной таблицой, наследующей структуру от родительского класса.

Свойство (поле) — это элемент данных, хранящийся внутри объекта или класса. Свойства описывают состояние объекта и могут иметь различные типы данных: числа, строки, таблицы, функции или другие объекты. В контексте ООП свойства часто называют атрибутами или членами класса.

Метод — это функция, связанная с объектом или классом, которая выполняет определенные действия над данными объекта. Методы определяют поведение системы и позволяют манипулировать состоянием объекта или взаимодействовать с другими частями программы. В Lua методы представляют собой функции, записанные внутри таблицы класса.

В языке Lua существует строгое различие между обращением к свойству и вызовом метода. Это различие реализуется через использование разных разделителей.

При обращении к свойству используется символ точки (.). Синтаксис выглядит следующим образом: object.name или class.property. Точка указывает интерпретатору Lua на необходимость получить значение конкретного поля из таблицы. Значение может быть любым типом данных, включая функцию, но при таком обращении функция не получает автоматического доступа к таблице-объекту.

При вызове метода используется символ двоеточия (:). Синтаксис выглядит следующим образом: object:methodName(). Двоеточие выполняет двойную функцию. Во-первых, оно вызывает функцию, записанную в таблице. Во-вторых, оно автоматически передает сам объект в качестве первого аргумента функции под именем self. Этот механизм позволяет методу работать с данными именно того объекта, который был указан перед двоеточием.

Различие между этими операторами критически важно для правильной работы кода. Использование точки вместо двоеточия при вызове метода приведет к ошибке, так как функция ожидает получить объект как первый параметр, но получит только ссылки на себя без контекста. Использование двоеточия при чтении свойства приведет к попытке вызвать функцию, что также вызовет ошибку выполнения.

-- Пример определения класса с использованием таблицы
local Person = {}

-- Свойство класса (общее для всех экземпляров, если не переопределено)
Person.species = "Homo sapiens"

-- Метод класса
function Person:speak()
print("Привет! Я говорю.")
end

-- Создание экземпляра (объекта)
local person1 = {
name = "Алексей",
age = 30
}

-- Привязываем методы класса к объекту через метатаблицу или явное копирование
setmetatable(person1, {__index = Person})

-- Обращение к свойству через точку
print(person1.name) -- Выведет: Алексей
print(Person.species) -- Выведет: Homo sapiens

-- Вызов метода через двоеточие
person1:speak() -- Выведет: Привет! Я говорю.
-- Эквивалентно person1.speak(person1), но синтаксис удобнее

В приведенном примере таблица Person выступает в роли класса. Поле species является свойством класса. Функция speak является методом. Объект person1 создается как новая таблица с уникальными свойствами name и age. Механизм setmetatable с полем __index связывает объект с классом, позволяя искать недостающие поля и методы в таблице родителя.

Когда выполняется выражение person1:name, интерпретатор сначала ищет поле name в самой таблице person1. Если оно найдено, возвращается его значение. Если поле не найдено, интерпретатор переходит к метатаблице объекта, проверяет наличие ключа __index и использует эту таблицу для поиска свойства.

Когда выполняется выражение person1:speak(), интерпретатор ищет функцию speak аналогичным образом. При нахождении функции она вызывается с передачей person1 в качестве первого аргумента. Внутри функции этот аргумент доступен под именем self.

function Person:greet()
-- self ссылается на объект person1
print(self.name .. " говорит: " .. self.age .. " лет")
end

person1:greet() -- Выведет: Алексей говорит: 30 лет

Такой подход обеспечивает гибкость и мощь объектной модели Lua. Объекты могут иметь собственные уникальные свойства, при этом разделяя общую логику поведения с другими экземплярами того же класса. Система метатаблиц скрывает сложность реализации наследования и полиморфизма, предоставляя разработчику удобный синтаксический сахар.


Создание класса и экземпляра

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

Для создания базового класса достаточно объявить пустую таблицу и добавить в нее функции и переменные.

local MyClass = {}

MyClass.value = 0

function MyClass:init(val)
self.value = val
end

function MyClass:getValue()
return self.value
end

Таблица MyClass теперь содержит статическое поле value и два метода init и getValue. Однако, чтобы использовать этот шаблон для создания объектов, необходимо создать механизм, который позволит новым таблицам наследовать эти члены.

Создание экземпляра (объекта) происходит путем создания новой таблицы и назначения ей метатаблицы, указывающей на родительский класс. Ключевым механизмом здесь является поле __index в метатаблице. Когда программа пытается обратиться к полю объекта, которого там нет, Lua автоматически смотрит в таблицу, указанную в __index, и продолжает поиск там.

-- Создание экземпляра
local obj1 = setmetatable({}, {__index = MyClass})

-- Инициализация объекта
obj1:init(42)

-- Вызов метода
print(obj1:getValue()) -- Выведет: 42

В данном коде создаётся новая пустая таблица obj1. Ей присваивается метатаблица с полем __index, равным MyClass. При вызове obj1:init(42) интерпретатор ищет метод init в obj1. Не находя его там, он переходит к MyClass и находит функцию. Функция вызывается, и self внутри неё ссылается на obj1.

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

Реализация конструктора через метод new является наиболее распространенной практикой. Такой подход инкапсулирует процесс создания и инициализации объекта в одном месте.

local Rectangle = {}

Rectangle.width = 0
Rectangle.height = 0

function Rectangle:new(w, h)
local instance = setmetatable({}, {__index = self})
instance.width = w or 0
instance.height = h or 0
return instance
end

function Rectangle:getArea()
return self.width * self.height
end

-- Использование конструктора
local rect1 = Rectangle:new(10, 5)
print(rect1:getArea()) -- Выведет: 50

local rect2 = Rectangle:new() -- Использует значения по умолчанию
print(rect2:getArea()) -- Выведет: 0

В методе new создается новая таблица instance, которой назначается метатаблица со ссылкой на self (то есть на таблицу Rectangle). Затем происходит инициализация полей width и height. Если аргументы не переданы, используются значения по умолчанию. Функция возвращает готовый объект.

Альтернативный подход использует метод init как конструктор, который вызывается вручную после создания пустого объекта.

local Car = {}

function Car:new(model)
local instance = setmetatable({}, {__index = self})
return instance
end

function Car:init(model)
self.model = model
self.speed = 0
end

local myCar = Car:new("Toyota")
myCar:init("Toyota") -- Явный вызов инициализации

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

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

local Config = {
name = "Default",
version = 1.0,
settings = {debug = false}
}

function Module:new(config)
local instance = setmetatable({}, {__index = self})
for key, value in pairs(config or {}) do
instance[key] = value
end
return instance
end

local mod = Module:new({name = "Custom", debug = true})
print(mod.name) -- Custom
print(mod.debug) -- true

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

local SharedData = {count = 0}

local ObjA = setmetatable({Данные = SharedData}, {__index = MyParent})
local ObjB = setmetatable({Данные = SharedData}, {__index = MyParent})

ObjA.Данные.count = 10
print(ObjB.Данные.count) -- 10, так как обе таблицы ссылаются на один объект SharedData

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

function MyClass:new()
local instance = setmetatable({}, {__index = self})
instance.Данные = {} -- Новая таблица для каждого экземпляра
return instance
end

Система создания классов и объектов в Lua основана на динамической природе таблиц и метатаблиц. Это дает разработчику полный контроль над структурой объектов и позволяет адаптировать модель под любые нужды проекта.


Конструкторы

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

В Lua принято выделять два этапа работы конструктора: выделение структуры объекта и инициализация его состояния. Часто эти этапы объединяются в одну функцию, называемую new. Эта функция принимает параметры, необходимые для создания объекта, создает новую таблицу, настраивает её метатаблицу для наследования и возвращает готовый экземпляр.

local BankAccount = {}

function BankAccount:new(ownerName, initialBalance)
local account = setmetatable({}, {__index = self})
account.owner = ownerName
account.balance = initialBalance or 0
account.transactions = {}
return account
end

function BankAccount:deposit(amount)
if amount > 0 then
self.balance = self.balance + amount
table.insert(self.transactions, {type = "credit", amount = amount})
end
end

function BankAccount:withdraw(amount)
if amount > 0 and self.balance >= amount then
self.balance = self.balance - amount
table.insert(self.transactions, {type = "debit", amount = amount})
return true
end
return false
end

-- Использование
local myAccount = BankAccount:new("Иван Иванов", 1000)
myAccount:deposit(500)
print(myAccount.balance) -- 1500

В примере выше функция BankAccount:new выступает в роли конструктора. Она принимает имя владельца и начальную сумму баланса. Если баланс не указан, устанавливается значение по умолчанию 0. Также создается пустая таблица для истории транзакций. Возвращаемый объект уже полностью готов к работе.

Конструкторы могут содержать логику валидации входных данных. Это позволяет предотвратить создание объектов в недопустимом состоянии.

local User = {}

function User:new(username, email)
if not username or #username < 3 then
error("Имя пользователя должно быть не короче 3 символов")
end
if not string.match(email, "@") then
error("Некорректный формат email")
end

local user = setmetatable({}, {__index = self})
user.username = username
user.email = email
user.isActive = true
return user
end

При попытке создать пользователя с коротким именем или неправильным email возникнет ошибка, что предотвратит появление некорректных данных в системе.

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

local DatabaseConnection = {}

function DatabaseConnection:new(host, port)
local conn = setmetatable({}, {__index = self})
conn.host = host
conn.port = port
conn.isConnected = false
conn:connect() -- Автоматическое подключение при создании
return conn
end

function DatabaseConnection:connect()
-- Имитация подключения
self.isConnected = true
print("Подключение к " .. self.host .. ":" .. self.port)
end

local db = DatabaseConnection:new("localhost", 3306)

В этом случае конструктор автоматически инициирует соединение, избавляя разработчика от необходимости вызывать метод подключения отдельно.

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

local Vehicle = {}

function Vehicle:new(type)
local vehicle = setmetatable({}, {__index = self})
vehicle.type = type
vehicle.fuelLevel = 0
return vehicle
end

function Vehicle:refuel(amount)
self.fuelLevel = self.fuelLevel + amount
end

local Car = {}
setmetatable(Car, {__index = Vehicle})

function Car:new(model, color)
-- Вызов конструктора родителя
local car = Vehicle.new(self, "Car")
car.model = model
car.color = color
car.wheels = 4
return car
end

local myCar = Car:new("Tesla Model S", "Red")
print(myCar.type) -- Car
print(myCar.model) -- Tesla Model S
print(myCar.wheels) -- 4

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

Конструкторы могут использовать паттерн прототипного наследования, когда новый объект создается путем клонирования существующего.

local Prototype = {}

function Prototype:new(baseObject)
local newObj = {}
for k, v in pairs(baseObject) do
newObj[k] = v
end
setmetatable(newObj, {__index = self})
return newObj
end

local template = {color = "blue", size = "L"}
local newItem = Prototype:new(template)
print(newItem.color) -- blue

Такой подход полезен при создании множества похожих объектов с небольшими отличиями.

Важным аспектом работы конструкторов является управление памятью. Поскольку Lua использует сборщик мусора, объекты, созданные в конструкторе, автоматически освобождаются, когда перестают использоваться. Однако, если конструктор открывает внешние ресурсы (файлы, сокеты, соединения БД), их необходимо закрывать в деструкторе или специальном методе очистки.

local FileHandler = {}

function FileHandler:new(filename, mode)
local handler = setmetatable({}, {__index = self})
handler.filename = filename
handler.file = io.open(filename, mode)
if not handler.file then
error("Не удалось открыть файл")
end
return handler
end

function FileHandler:close()
if self.file then
self.file:close()
self.file = nil
end
end

-- Использование
local f = FileHandler:new("test.txt", "w")
f:write("Hello")
f:close()

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


Наследование

Наследование — это механизм объектно-ориентированного программирования, позволяющий одному классу (дочернему или производному) заимствовать свойства и методы другого класса (родительского или базового). В Lua реализация наследования основана на использовании метатаблиц и ключевого поля __index. Этот механизм обеспечивает динамическое разрешение членов класса, позволяя объектам получать доступ к элементам, определенным в родительской таблице.

Процесс наследования в Lua начинается с создания дочерней таблицы, которая должна ссылаться на родительскую таблицу через метатаблицу. При этом родительская таблица становится точкой поиска для всех отсутствующих полей и методов дочернего объекта.

local Animal = {}

function Animal:new(name)
local animal = setmetatable({}, {__index = self})
animal.name = name
animal.age = 0
return animal
end

function Animal:speak()
print(self.name .. " издает звук")
end

-- Создание дочернего класса
local Dog = {}
setmetatable(Dog, {__index = Animal})

-- Переопределение конструктора для дочернего класса
function Dog:new(name, breed)
local dog = Animal.new(self, name) -- Вызов конструктора родителя
dog.breed = breed
return dog
end

function Dog:speak()
print(self.name .. " лает: Гав!")
end

-- Использование
local myDog = Dog:new("Рекс", "Лабрадор")
myDog:speak() -- Выведет: Рекс лает: Гав!
print(myDog.age) -- 0 (наследовано от Animal)

В примере выше таблица Dog наследуется от Animal через setmetatable(Dog, {__index = Animal}). Метод speak переопределяется в классе Dog, что демонстрирует способность дочерних классов изменять поведение родителей. При этом свойство age остается доступным, так как оно определено в родительском классе.

Метод Animal.new(self, name) вызывает конструктор родителя, передавая self (таблицу Dog) в качестве первого аргумента. Это гарантирует, что созданная метатаблица будет ссылаться на правильный класс, сохраняя цепочку наследования.

Наследование может быть многоступенчатым, когда класс наследуется от класса, который уже является потомком другого класса.

local Mammal = {}
setmetatable(Mammal, {__index = Animal})

function Mammal:new(name)
local mammal = Animal.new(self, name)
mammal.hasHair = true
return mammal
end

function Mammal:nurse()
print(self.name кормит детенышей молоком")
end

local Cat = {}
setmetatable(Cat, {__index = Mammal})

function Cat:new(name)
local cat = Mammal.new(self, name)
cat.meow = true
return cat
end

function Cat:speak()
print(self.name мяукает: Мяу!")
end

local myCat = Cat:new("Барсик")
myCat:speak() -- Мяукает: Мяу!
myCat:nurse() -- Барсик кормит детенышей молоком
print(myCat.hasHair) -- true
print(myCat.age) -- 0

Цепочка наследования Cat -> Mammal -> Animal позволяет классу Cat использовать функциональность всех предков. Метод speak переопределяется на каждом уровне, обеспечивая специфичное поведение для каждого типа животного.

Динамическая природа Lua позволяет изменять поведение классов во время выполнения. Можно добавлять новые методы в родительский класс, и все наследники автоматически получат доступ к ним.

function Animal:move()
print(self.name движется")
end

myDog:move() -- Рекс движется
myCat:move() -- Барсик движется

Полиморфизм в сочетании с наследованием позволяет писать универсальный код, работающий с объектами разных типов, но имеющими общий интерфейс.

function makeSound(animal)
animal:speak()
end

makeSound(myDog) -- Рекс лает: Гав!
makeSound(myCat) -- Барсик мяукает: Мяу!
makeSound(myDog) -- Рекс лает: Гав!

Функция makeSound принимает любой объект, у которого есть метод speak, и вызывает его. Благодаря наследованию и полиморфизму, она работает корректно для всех типов животных.

Наследование также поддерживает множественное наследование через составление метатаблиц, хотя стандартный подход в Lua предполагает одноуровневое наследование.

local Flyable = {}
Flyable.fly = function(self)
print(self.name летит")
end

local Swimmable = {}
Swimmable.swim = function(self)
print(self.name плавает")
end

local Duck = {}
setmetatable(Duck, {__index = Flyable})
-- Добавление второго уровня наследования через ручное копирование или использование таблиц
for k, v in pairs(Swimmable) do
Duck[k] = v
end

local duck = Duck:new("Утка")
duck:flew() -- Утка летит
duck:swim() -- Утка плавает

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

Наследование в Lua предоставляет гибкую и мощную систему для построения иерархий классов. Оно позволяет повторно использовать код, расширять функциональность и создавать абстрактные модели, отражающие реальные отношения между объектами.


Инкапсуляция

Инкапсуляция — это принцип объектно-ориентированного программирования, который ограничивает прямой доступ к некоторым компонентам объекта, скрывая внутреннюю реализацию и предоставляя контролируемый интерфейс для взаимодействия. В Lua инкапсуляция достигается за счет использования локальных переменных, замыканий и правил соглашения об именах, так как язык не имеет жестких модификаторов доступа вроде private или public.

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

В Lua принято считать, что переменные, начинающиеся с подчеркивания (_), являются приватными и не предназначены для прямого доступа извне. Это соглашение помогает разработчикам идентифицировать внутренние данные, которые должны быть скрыты.

local BankAccount = {}

function BankAccount:new(initialBalance)
local privateData = {
balance = initialBalance or 0,
owner = "Unknown"
}

local publicInterface = setmetatable({}, {__index = self})

function publicInterface:getBalance()
return privateData.balance
end

function publicInterface:setOwner(name)
if name and #name > 0 then
privateData.owner = name
end
end

function publicInterface:deposit(amount)
if amount > 0 then
privateData.balance = privateData.balance + amount
end
end

function publicInterface:withdraw(amount)
if amount > 0 and privateData.balance >= amount then
privateData.balance = privateData.balance - amount
return true
end
return false
end

return publicInterface
end

local account = BankAccount:new(1000)
account:setOwner("Иван")
account:deposit(500)
print(account:getBalance()) -- 1500
-- Попытка прямого доступа к балансу невозможна, так как он скрыт в замыкании
-- print(account.privateData.balance) -- Ошибка или nil

В этом примере данные счета (balance и owner) хранятся в локальной таблице privateData, которая недоступна извне. Доступ к ним осуществляется только через методы getBalance, setOwner, deposit и withdraw. Эти методы содержат логику проверки, например, запрет на снятие средств, превышающих баланс.

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

local Counter = {}

function Counter:new(startValue)
local count = startValue or 0

local publicMethods = setmetatable({}, {__index = self})

function publicMethods:increment()
count = count + 1
end

function publicMethods:decrement()
count = count - 1
end

function publicMethods:getCount()
return count
end

return publicMethods
end

local counter = Counter:new(10)
counter:increment()
counter:increment()
print(counter:getCount()) -- 12
-- Прямое изменение count невозможно

Здесь переменная count находится в области видимости функции Counter:new. Даже если кто-то попытается изменить её напрямую, это не удастся, так как она не экспортируется в публичный интерфейс.

Инкапсуляция также позволяет внедрить логику валидации и аудита изменений.

local Student = {}

function Student:new(name)
local Данные = {
name = name,
grades = {},
lastModified = os.date()
}

local interface = setmetatable({}, {__index = self})

function interface:addGrade(grade)
if grade >= 0 and grade <= 100 then
table.insert(Данные.grades, grade)
Данные.lastModified = os.date()
end
end

function interface:getAverage()
local sum = 0
for _, g in ipairs(Данные.grades) do
sum = sum + g
end
return #Данные.grades > 0 and sum / #Данные.grades or 0
end

return interface
end

local student = Student:new("Анна")
student:addGrade(90)
student:addGrade(85)
student:addGrade(95)
print(student:getAverage()) -- 90

В этом примере метод addGrade проверяет допустимость оценки перед добавлением её в список. Метод getAverage вычисляет среднее значение, используя только легальные данные.

Инкапсуляция также полезна для управления состоянием объекта. Например, можно запретить изменение определенных полей после инициализации.

local Configuration = {}

function Configuration:new(key, value)
local config = {key = key, value = value, locked = false}

local public = setmetatable({}, {__index = self})

function public:setNewValue(newValue)
if self.locked then
error("Конфигурация заблокирована")
end
self.value = newValue
end

function public:lock()
self.locked = true
end

return public
end

local config = Configuration:new("theme", "dark")
config:setNewValue("light")
config:lock()
-- config:setNewValue("red") -- Ошибка: Конфигурация заблокирована

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


Полиморфизм

Полиморфизм — это способность объектов разных классов обрабатываться одинаковым образом через общий интерфейс, несмотря на различия во внутренней реализации. В языке Lua полиморфизм реализуется динамически благодаря наличию таблиц и метатаблиц, что позволяет функциям работать с любыми объектами, обладающими необходимыми методами.

В Lua полиморфизм не требует явного объявления интерфейсов или абстрактных классов. Достаточно, чтобы объекты имели методы с одинаковыми именами и сигнатурами. Интерпретатор Lua разрешает вызов этих методов в runtime, опираясь на фактическое наличие метода в объекте.

local Shape = {}

function Shape:new()
local shape = setmetatable({}, {__index = self})
return shape
end

function Shape:draw()
print("Рисуется фигура")
end

local Circle = {}
setmetatable(Circle, {__index = Shape})

function Circle:new(radius)
local circle = Shape.new(self)
circle.radius = radius
return circle
end

function Circle:draw()
print("Рисуется круг радиусом " .. self.radius)
end

local Square = {}
setmetatable(Square, {__index = Shape})

function Square:new(side)
local square = Shape.new(self)
square.side = side
return square
end

function Square:draw()
print("Рисуется квадрат со стороной " .. self.side)
end

local shapes = {Circle:new(5), Square:new(10), Shape:new()}

for _, s in ipairs(shapes) do
s:draw()
end

В этом примере массив shapes содержит объекты разных типов: Circle, Square и базовый Shape. Цикл перебирает все объекты и вызывает метод draw. Несмотря на разные реализации, каждый объект корректно выполняет свою версию метода. Это и есть проявление полиморфизма.

Рисуется круг радиусом 5
Рисуется квадрат со стороной 10
Рисуется фигура

Полиморфизм позволяет писать универсальный код, который не зависит от конкретных типов объектов. Функция может принимать любой объект, имеющий нужный метод, и выполнять над ним действие.

function renderAll(objects)
for _, obj in ipairs(objects) do
if obj.draw then
obj:draw()
end
end
end

renderAll(shapes)

Функция renderAll проверяет наличие метода draw в каждом объекте и вызывает его. Если метод отсутствует, объект пропускается. Это делает код устойчивым к добавлению новых типов фигур без необходимости изменения функции рендеринга.

Динамический полиморфизм в Lua также позволяет заменять поведение объектов во время выполнения.

local Button = {}

function Button:new(label)
local button = setmetatable({}, {__index = self})
button.label = label
button.action = function() print("Нажата кнопка: " .. label) end
return button
end

function Button:setAction(fn)
self.action = fn
end

local btn1 = Button:new("OK")
local btn2 = Button:new("Cancel")

btn2:setAction(function() print("Отмена действия") end)

btn1.action() -- Нажата кнопка: OK
btn2.action() -- Отмена действия

Метод action может быть изменен для любого экземпляра, что демонстрирует гибкость полиморфизма в Lua.

Полиморфизм также проявляется в перегрузке методов через проверку типов аргументов.

local Calculator = {}

function Calculator:new()
local calc = setmetatable({}, {__index = self})
return calc
end

function Calculator:calculate(a, b)
if type(a) == "number" and type(b) == "number" then
return a + b
elseif type(a) == "string" and type(b) == "string" then
return a .. b
else
error("Неподдерживаемые типы аргументов")
end
end

local calc = Calculator:new()
print(calc:calculate(5, 10)) -- 15
print(calc:calculate("Hello", " World")) -- Hello World

Функция calculate ведет себя по-разному в зависимости от типов переданных аргументов. Это пример ад-hoc полиморфизма, реализованного через проверку типов.

Полиморфизм в Lua упрощает архитектуру программ, позволяя создавать модульные и расширяемые системы. Разработчики могут добавлять новые классы, не затрагивая существующий код, если новые классы соответствуют ожидаемому интерфейсу.

local ReportGenerator = {}

function ReportGenerator:new()
local gen = setmetatable({}, {__index = self})
return gen
end

function ReportGenerator:generate(report)
report:start()
report:Данные()
report:endReport()
end

local TextReport = {}
setmetatable(TextReport, {__index = ReportGenerator})

function TextReport:start()
print("--- Начало текстового отчета ---")
end

function TextReport:Данные()
print("Текст отчета")
end

function TextReport:endReport()
print("--- Конец текстового отчета ---")
end

local PDFReport = {}
setmetatable(PDFReport, {__index = ReportGenerator})

function PDFReport:start()
print("[PDF] Старт")
end

function PDFReport:Данные()
print("[PDF] Данные")
end

function PDFReport:endReport()
print("[PDF] Финиш")
end

local generator = ReportGenerator:new()
generator:generate(TextReport:new())
generator:generate(PDFReport:new())

Функция generate работает с любыми отчетами, имеющими методы start, Данные и endReport. Добавление нового типа отчета (например, HTML) не потребует изменения кода генератора.

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


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).