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

Объектно-ориентированное программирование (ООП) — очень популярная архитектура компьютерного программирования, которая проектирует приложения путём создания объектов и манипулирования ими.

Все следующие языки программирования поддерживают объектно-ориентированное программирование:

  • C++
  • Java
  • Objective-C
  • Smalltalk
  • C#
  • Ruby

Lua — легковесный скриптовый язык. Хотя в нём нет встроенных мощных объектно-ориентированных (ОО) возможностей, как в Java или C++, он очень гибок и позволяет реализовать объектно-ориентированное программирование с помощью некоторых приёмов.


Объектно-ориентированные возможности

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

  • Наследование: повторное использование и расширение существующего кода путём порождения новых классов, сокращение повторяющегося кода, повышение эффективности разработки и расширяемости.

  • Полиморфизм: одна и та же операция ведёт себя по-разному для разных объектов, поддерживает унифицированные вызовы интерфейсов и повышает гибкость и расширяемость.

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


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

Мы знаем, что объекты состоят из свойств и методов.

Классы в Lua можно смоделировать с помощьюtable + function.

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

В Lua самая базовая структура — это таблица; мы можем использовать таблицы для создания объектов.

ClassName = {}  -- 创建一个表作为类

Создавайте объекты с помощью метода new (или других имён) и инициализируйте свойства объекта.

function ClassName:new(...)
    local obj = {}  -- 创建一个新的空表作为对象
    setmetatable(obj, self)  -- 设置元表,使对象继承类的方法
    self.__index = self  -- 设置索引元方法
    -- 初始化对象的属性
    obj:init(...)  -- 可选:调用初始化函数
    return obj
end

Таблица — это самый базовый составной тип данных в Lua, и её можно использовать для представления свойств объекта.

Функция в Lua может использоваться для представления методов:

function ClassName:sayHello()
    print("Hello, my name is " .. self.name)
end

Используйте метод new для создания объектов и вызывайте методы класса через объект.

local obj = ClassName:new("Alice")  -- 创建对象
obj:sayHello()  -- 调用对象的方法

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

Таблицы не только имеют элементы данных, но также могут содержать функции-члены, подобные методам объекта:

Пример

-- Определение класса Person
Person = {name = "", age = 0}

-- Конструктор Person
function Person:new(name, age)
    local obj = {}  -- Создать новую таблицу как объект
    setmetatable(obj, self)  -- Установите метатаблицу, чтобы сделать его экземпляром Person
    self.__index = self  -- Установите метаметод индекса, чтобы он указывал на Person
    obj.name = name
    obj.age = age
    return obj
end

-- Добавьте метод: вывод личной информации
function Person:introduce()
    print("Меня зовут " .. self.name .. " и мне " .. self.age .. " лет.")
end

Пояснение к коду:

  • Person— это таблица с двумя свойствами: name и age; эти два свойства являются свойствами класса по умолчанию.
  • Person:new(name, age)— это конструктор, используемый для создания новых объектов Person.
  • local obj = {}Создайте новую таблицу в качестве объекта; setmetatable(obj, self) устанавливает метатаблицу, так что таблица становится экземпляром класса Person.
  • self.__index = selfУстановите метаметод index, чтобы obj мог получать доступ к свойствам и методам класса Person.
  • introduceявляется методом класса Person, который выводит имя и возраст объекта Person.

Вызов методов:

-- 创建一个 Person 对象
local person1 = Person:new("Alice", 30)

-- 调用对象的方法
person1:introduce()  -- 输出 "My name is Alice and I am 30 years old."

Простой пример

Следующий простой класс содержит три свойства: area, length и breadth. Метод printArea используется для вывода результата вычисления:

Пример

-- Определение класса Rectangle
Rectangle = {area = 0, length = 0, breadth = 0}

-- Конструктор для создания объектов Rectangle
function Rectangle:new(o, length, breadth)
  o = o or {}  -- Если объект не передан, создайте новую пустую таблицу
  setmetatable(o, self)  -- Установите метатаблицу, чтобы self наследовал методы Rectangle
  self.__index = self  -- Убедитесь, что методы и свойства могут быть найдены при обращении
  o.length = length or 0  -- Установите length, по умолчанию 0
  o.breadth = breadth or 0  -- Установить ширину, по умолчанию 0
  o.area = o.length * o.breadth  -- Вычислить площадь
  return o
end

-- Вывести площадь прямоугольника
function Rectangle:printArea()
  print("Площадь прямоугольника равна ", self.area)
end

Создание объектов

Создание объекта — это процесс выделения памяти для экземпляра класса; каждый класс имеет собственную память и разделяет общедоступные данные:

r = Rectangle:new(nil,10,20)

Доступ к свойствам

Мы можем использовать точку.для доступа к свойствам класса:

print(r.length)

Доступ к функциям-членам

Мы можем использовать двоеточие:для доступа к функциям-членам класса:

r:printArea()

Память выделяется при инициализации объектов.

Полный пример

Ниже мы демонстрируем полный пример объектно-ориентированного программирования на Lua:

Пример

-- Определение класса Rectangle
Rectangle = {area = 0, length = 0, breadth = 0}

-- Конструктор для создания объектов Rectangle
function Rectangle:new(o, length, breadth)
  o = o or {}  -- Если объект не передан, создайте новую пустую таблицу
  setmetatable(o, self)  -- Установите метатаблицу, чтобы self наследовал методы Rectangle
  self.__index = self  -- Убедитесь, что методы и свойства могут быть найдены при обращении
  o.length = length or 0  -- Установите length, по умолчанию 0
  o.breadth = breadth or 0  -- Установить ширину, по умолчанию 0
  o.area = o.length * o.breadth  -- Вычислить площадь
  return o
end

-- Вывести площадь прямоугольника
function Rectangle:printArea()
  print("Площадь прямоугольника равна ", self.area)
end

-- Запустите пример:
local rect1 = Rectangle:new(nil, 5, 10)  -- Создать прямоугольник с длиной 5 и шириной 10
rect1:printArea()  -- Вывод "Площадь прямоугольника равна 50"

local rect2 = Rectangle:new(nil, 7, 3)  -- Создать прямоугольник с длиной 7 и шириной 3
rect2:printArea()  -- Вывод "Площадь прямоугольника равна 21"

Выполнение вышеуказанной программы дает следующий вывод:

矩形面积为      50
矩形面积为      21

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

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

Наследование в Lua реализуется путём установки метатаблицы подкласса.

Мы можем создать новую таблицу и установить в качестве её метатаблицы родительский класс.

В следующем примере класс Square унаследует свойства и методы класса Rectangle и внесёт изменения на этой основе.

-- Определение класса Rectangle
Rectangle = {area = 0, length = 0, breadth = 0}

-- Конструктор для создания объектов Rectangle
function Rectangle:new(o, length, breadth)
  o = o or {}  -- Если объект не передан, создайте новую пустую таблицу
  setmetatable(o, self)  -- Установите метатаблицу, чтобы self наследовал методы Rectangle
  self.__index = self  -- Убедитесь, что методы и свойства могут быть найдены при обращении
  o.length = length or 0  -- Установите length, по умолчанию 0
  o.breadth = breadth or 0  -- Установить ширину, по умолчанию 0
  o.area = o.length * o.breadth  -- Вычислить площадь
  return o
end

-- Вывести площадь прямоугольника
function Rectangle:printArea()
  print("Площадь прямоугольника равна ", self.area)
end

-- Определить класс Square, наследующий от класса Rectangle
Square = Rectangle:new()  -- Square наследует класс Rectangle

-- Переопределить конструктор (все стороны квадрата равны)
function Square:new(o, side)
  o = o or {}  -- Если объект не передан, создайте новую пустую таблицу
  setmetatable(o, self)  -- Установите метатаблицу, чтобы self наследовал методы Rectangle
  self.__index = self  -- Убедитесь, что методы и свойства могут быть найдены при обращении
  o.length = side or 0  -- Установить длину стороны
  o.breadth = side or 0  -- Ширина и длина квадрата равны
  o.area = o.length * o.breadth  -- Вычислить площадь
  return o
end

-- Запустите пример:
local rect = Rectangle:new(nil, 5, 10)  -- Создать прямоугольник с длиной 5 и шириной 10
rect:printArea()  -- Вывести "Площадь прямоугольника равна 50"

local square = Square:new(nil, 4)  -- Создать квадрат с длиной стороны 4
square:printArea()  -- Вывести "Площадь прямоугольника равна 16"

RectangleКласс: по-прежнему базовый класс Rectangle, имеющийlength、breadthиareaсвойства, а также методы для вычисления и вывода площади.

SquareКласс наследует отRectangle:SquareКласс используетRectangle:new()наследоватьRectangleметоды и свойства класса. Поскольку длина и ширина квадрата равны, мыSquare:newпереопределили конструктор в методе, устанавливаяlengthиbreadthустановлено в одно и то же значение (т.е.side)。

Переопределение конструктора:Square:new(o, side)Когда метод создаёт объект Square, он использует переданную длину стороныsideчтобы инициализироватьlengthиbreadthсвойства и вычисляет площадь.

Результат выполнения:

矩形面积为  50
矩形面积为  16

Переопределение функций

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

Подкласс может по мере необходимости изменять или расширять поведение методов родительского класса.

В приведённом выше примере класс Square переопределяет конструктор класса Rectangle, тем самым изменяя способ инициализации объектов, в частности устанавливая длину и ширину прямоугольника в одно и то же значение, потому что характерная особенность квадрата — равные длины сторон.

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

-- Определить класс Animal
Animal = {name = "Unknown"}

-- Конструктор класса Animal
function Animal:new(o, name)
  o = o or {}  -- Если объект не передан, создайте новую пустую таблицу
  setmetatable(o, self)  -- Устанавливаем метатаблицу, чтобы объект наследовал методы Animal
  self.__index = self  -- Позволяет объекту обращаться к методам Animal
  o.name = name or "Unknown"  -- Устанавливаем name, значение по умолчанию — "Unknown"
  return o
end

-- Метод класса Animal: издавать звук
function Animal:speak()
  print(self.name .. " makes a sound.")
end


-- Определяем класс Dog, который наследует Animal
Dog = Animal:new()  -- Dog наследует класс Animal

-- Переопределяем конструктор класса Dog
function Dog:new(o, name, breed)
  o = o or {}  -- Если объект не передан, создайте новую пустую таблицу
  setmetatable(o, self)  -- Установить метатаблицу, чтобы объект наследовал методы от Dog и Animal
  self.__index = self  -- Разрешить объекту доступ к методам Dog
  o.name = name or "Неизвестно"
  o.breed = breed or "Неизвестно"
  return o
end

-- Переопределить метод sound класса Dog (переопределяя метод speak класса Animal)
function Dog:speak()
  print(self.name .. " лает.")
end


-- Создать объект Animal
local animal = Animal:new(nil, "Обычное животное")
animal:speak()  -- Вывод: "Обычное животное издаёт звук."

-- Создать объект Dog
local dog = Dog:new(nil, "Buddy", "Golden Retriever")
dog:speak()  -- Вывод "Buddy barks."
  • AnimalClass: определяет базовый классAnimal, который имеетnameсвойства иspeakметоды.speakМетод — это реализация по умолчанию, которая выводит "some animal makes a sound."

  • DogНаследование классовAnimal:DogКласс наследует отAnimal, и с помощьюDog:new()метод создаёт свои собственные экземпляры.

  • Переопределениеspeakметод: вDogкласс, переопределилspeakметод, изменяя его поведение с "издавать звук" родительского класса на "лай собаки". Это проявление переопределения методов. Подкласс (Dog) изменил родительский класс (Animal) поведение метода.

Результат выполнения:

Generic Animal makes a sound.
Buddy barks.

Полиморфизм

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

Пример

-- Определить "класс" (на самом деле таблицу)
Person = {}

-- Добавить конструктор в "класс"
function Person:new(name, age)
    local obj = {}  -- Создать новую таблицу как объект
    setmetatable(obj, self)  -- Установить метатаблицу, чтобы указать, что это экземпляр класса Person
    self.__index = self  -- Установите метаметод индекса, чтобы он указывал на Person
    obj.name = name
    obj.age = age
    return obj
end

-- Добавить методы
function Person:greet()
    print("Привет, меня зовут " .. self.name)
end

-- Определить подкласс Student, который наследуется от Person
Student = Person:new()

-- Подкласс переопределяет метод родительского класса
function Student:greet()
    print("Привет, я студент, и меня зовут " .. self.name)
end

local person2 = Person:new("Charlie", 25)
local student2 = Student:new("David", 18)

-- Полиморфизм: объекты разных типов вызывают один и тот же метод
person2:greet()  -- Вывод "Привет, меня зовут Charlie"
student2:greet()  -- Вывод "Привет, я студент, и меня зовут David"

Хотя person2 и student2 вызывают один и тот же метод greet, Lua вызывает соответствующую версию для каждого, потому что их типы различаются.

Результат выполнения:

Hello, my name is Charlie
Hi, I'm a student and my name is David

Другие концепции объектно-ориентированного программирования

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

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

Пример

-- Определить "класс" (на самом деле таблицу)
Person = {}

-- Добавить инкапсуляцию: скрыть свойства
function Person:new(name, age)
    local obj = {}
    setmetatable(obj, self)
    self.__index = self
    obj.name = name
    obj.age = age
    return obj
end

function Person:setName(name)
    self.name = name  -- Предоставить метод для изменения name
end

function Person:getName()
    return self.name  -- Предоставить метод для получения name
end

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

Абстракция

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

Пример

-- Показывать только интерфейс, а не детали реализации
function Person:showInfo()
    print("Name: " .. self.name)
    print("Age: " .. self.age)
end
Другие расширения