Однажды судьба свела меня с ней. С первого взгляда я был ослеплен и долгое время не мог отвести от нее взгляд. Шло время, но она не переставала меня удивлять, иногда казалось, что я изучил ее вдоль и поперек, но она снова переворачивала все мои представления. Ее гибкости не было предела, а потом я узнал, что она умеет еще и… ООП!

Как-то я всерьез занялся покорением ООП в lua. И все, что я находил в интернете по этой теме, было вырвиглазными нагромождениями кода с обилием нижних подчеркиваний, которые никак не вписывались в элегантность этого языка. Поэтому я решил искать простое решение.

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

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


class Person
--класс
Person= {}
--тело класса
function Person:new(fName, lName)

    -- свойства
    local obj= {}
        obj.firstName = fName
        obj.lastName = lName

    -- метод
    function obj:getName()
        return self.firstName 
    end

    --чистая магия!
    setmetatable(obj, self)
    self.__index = self; return obj
end

--создаем экземпляр класса
vasya = Person:new("Вася", "Пупкин")

--обращаемся к свойству
print(vasya.firstName)    --> результат: Вася

--обращаемся к методу
print(vasya:getName())  --> результат: Вася


Как видите, все очень просто. Если кто-то путается где ставить точку, а где двоеточие, правило следующее: если обращаемся к свойству — ставим точку (object.name), если к методу — ставим двоеточие (object:getName()).

Дальше интереснее.

Как известно, ООП держится на трех китах: наследование, инкапсуляция и полиморфизм. Проведем «разбор полетов» в этом же порядке.

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


Допустим, нам нужно создать класс унаследованный от предыдущего (Person).

class Woman
Woman = {}
--наследуемся
setmetatable(Woman ,{__index = Person}) 
--проверяем
masha = Woman:new("Марья","Ивановна")
print(masha:getName())  --->результат: Марья


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

extended()
function extended (child, parent)
    setmetatable(child,{__index = parent}) 
end


Теперь наследование классов выглядит куда красивее:

class Woman
Woman = {};
--наследуемся
 extended(Woman, Person)
--проверяем
masha = Woman:new("Марья","Ивановна")
print(masha:getName())  --->результат: Марья


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


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

class Person
Person = {}
function Person:new(name)
    local private = {}
        --приватное свойство
        private.age = 18

    local public = {}
        --публичное свойство
        public.name = name or "Вася"   -- "Вася" - это значение по умолчанию 
        --публичный метод
        function public:getAge()
            return private.age
        end

    setmetatable(public,self)
    self.__index = self; return public
end

vasya = Person:new()

print(vasya.name)          --> результат: Вася 

print(vasya.age)           --> результат: nil

print(vasya:getAge())     --> результат: 18


Видите? Все почти так же как вы и привыкли.

Полиморфизм


Тут все еще проще.

полиморфизм
Person = {}
function Person:new(name)
    local private = {}
        private.age = 18 

    local public = {}
        public.name = name or "Вася" 

        --это защищенный метод, его нельзя переопределить
        function public:getName()
            return "Person protected "..self.name
        end

        --это открытый метод, его можно переопределить
        function Person:getName2()
            return "Person "..self.name
        end

    setmetatable(public,self)
    self.__index = self; return public
end

--создадим класс, унаследованный от Person
Woman = {}
extended(Woman, Person)  --не забываем про эту функцию

--переопределим защищенный метод 
function Woman:getName()
    return "Woman protected "..self.name
end

--переопределим метод getName2()
function Woman:getName2()
    return "Woman "..self.name
end

--проверим
masha = Woman:new()

print(masha:getName())   --> Person protected Вася

print(masha:getName2())  --> Woman Вася


Итак, что мы тут сделали?
— создали класс Person, с двумя методами: getName() и getName2(), первый из них защищен от переопределения;
— создали класс Woman и унаследовали его от класса Person;
— переопределили оба метода в классе Woman. Первый не переопределился;
— получили профит!

Кстати, открытые методы можно определять так же и вне тела класса:
полиморфизм
Person = {}
function Person:new(name)
    local private = {}
        private.age = 18 

    local public = {}
        public.name = name or "Вася" 

        --это защищенный метод, его нельзя переопределить
        function public:getName()
            return "Person protected "..self.name
        end

    setmetatable(public,self)
    self.__index = self; return public
end

--это открытый метод, его можно 
function Person:getName2()
        return "Person "..self.name
end


А что делать, если нужно вызвать метод базового класса, который у нас переопределен? Это тоже делается легко!
Синтаксис таков: РодительскийКласс.Метод(сам_объект, параметры (если есть)).

class Woman
--создадим класс, унаследованный от Person
Woman = {}
extended(Woman, Person)  --не забываем про эту функцию

--переопределим метод setName
function Woman:getName2()
    return "Woman "..self.name
end

print(masha:getName2())  --> Woman Вася

--вызываем метод родительского класса
print(Person.getName2(masha)) --> Person Вася


Постскриптум


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

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

Полный код
function extended (child, parent)
    setmetatable(child,{__index = parent}) 
end

Person = {}
function Person:new(name)
	
    local private = {}
        private.age = 18 

    local public = {}
        public.name = name or "Вася" 


        --это защищенный метод, его нельзя переопределить
        function public:getName()
            return "Person protected "..self.name
        end

        --этот метод можно переопределить
        function Person:getName2()
            return "Person "..self.name
        end
    setmetatable(public,self)
    self.__index = self;
     return public
end

--создадим класс, унаследованный от Person
Woman = {}
extended(Woman, Person)  --не забываем про эту функцию

--переопределим метод setName
function Woman:getName2()
    return "Woman "..self.name
end

masha = Woman:new()
print(masha:getName2())  --> Woman Вася

--вызываем метод родительского класса
print(Person.getName2(masha)) --> Person Вася

Комментарии (19)


  1. AterCattus
    01.06.2015 17:00
    +2

    Неделя Lua ООП прямо. А по общему числу постов на эту тему на Хабре хоть сборник собирай.


  1. gcc
    01.06.2015 20:52
    +4

    Woman = {};
    --наследуемся
    extended(Woman, Person)

    Было бы еще прикольнее писать Woman = extend(Person)

    Если кто-то путается где ставить точку, а где двоеточие, правило следующее: если обращаемся к свойству — ставим точку (obj.name), если к методу — ставим двоеточие (obj:getName()).

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


    1. Keyten
      02.06.2015 00:41

      Примерно так:

      function extend(parent)
          local child = {}
          setmetatable(child,{__index = parent})
          return child
      end
      


      1. dannote
        02.06.2015 04:32
        +2

        Лучший, на мой взгляд вариант — наследование от базового класса Object с предопределенными функциями new и extend.


  1. stepanp
    01.06.2015 21:24
    +5

    Не понимаю плясок с бубном вокруг инкапсуляции и приватных полей в скриптовых языках. Нафига это там нужно, непонятно.
    Без этой ерунды, ООП в lua добавляется буквально одной функцией.


    1. Mingun
      01.06.2015 22:14
      +2

      Кроме того, private на самом деле еще более public, чем public, т.к. доступен всем без исключения, что, собственно, функция setName и демонстрирует. Но все еще хуже. Попробуйте создать два класса с приватными полями. Будите сильно удивлены (это я автору).


      1. Bagobor
        02.06.2015 00:16

        какая клевая и класическая для Lua бага ))


      1. ant00N Автор
        02.06.2015 08:58

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


        1. ant00N Автор
          12.06.2015 20:36

          пс. исправлено!


  1. Lerg
    01.06.2015 23:04
    +3

    Похоже и мне теперь нужно написать про ООП в Lua. Мой метод не использует setmetatable() и экономит ресурсы.
    Кстати у вас глобальные переменные.


  1. barabanus
    01.06.2015 23:57

    Я когда-то делал парсер для текстовых игр на Lua, получилось что-то вроде этого:

    sixdays:new "room" ()       { room = true }
    sixdays:new "player" ()     : moveto( room )
    sixdays:new "apple_red" ()  { name = "red apple" }      : moveto( room )
    sixdays:new "apple_green" (){ name = "green apple" }    : moveto( room )
    sixdays:new "table_red" ()  { name = "red table" }      : moveto( room )
    sixdays:new "table_green" (){ name = "green table" }    : moveto( room )
    

    new — создание нового объекта с глобальным именем, указанным в кавычках
    в скобках можно указать, от кого наследуемся (список)
    в фигурных скобках конструктор


  1. nemilya
    02.06.2015 00:51

    Спасибо, что поделились опытом.
    Я давно присматриваюсь к Lua.
    V-REP скриптуется на Lua — роботы-Lua)


    1. to_climb
      02.06.2015 09:44
      +1

      Learn Lua in 15 minutes — может, будет полезно для быстрого старта.


    1. evocatus
      02.06.2015 15:00

      А лучше прочитать Иерусалимски — Programming in Lua. Удивительно хорошо написана и легко читается.


      1. nemilya
        02.06.2015 15:45

        Спасибо, кстати первая редакция книги доступна online


  1. GeckoPelt
    02.06.2015 08:50

    Если кто не пробовал, рекомендую посмотреть на Squirrel
    www.squirrel-lang.org
    Очень похоже на Lua, но ООП без костылей.


    1. ant00N Автор
      02.06.2015 09:58

      белка хороша, но слишком редка. Я видел всего один игровой движок (а луа я использую именно в них), в котором был squirrel, да и тот умер.
      К тому же, луа быстрее.


      1. GeckoPelt
        02.06.2015 10:59

        Lua быстрее, но имхо это далеко не везде критично во встроенном скриптовом интерпретаторе.
        Кстати, для белки еще есть отличный биндинг SqPlus.


  1. ant00N Автор
    03.06.2015 07:15
    +1

    Статья изменилась, просьба перечитать. Пофиксил инкапсуляцию.