Lua, ООП и ничего лишнего

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

Как-то я всерьез занялся покорением ООП в 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
  • +2
    Неделя Lua ООП прямо. А по общему числу постов на эту тему на Хабре хоть сборник собирай.
    • +4
      Woman = {};
      --наследуемся
      extended(Woman, Person)

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

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

      Чтобы действительно не путать, лучше запомнить смысл двоеточия — это просто синтаксический сахар, который неявно добавляет к функции первый аргумент self. Можно было бы использовать точку, но тогда пришлось бы явно добавлять аргумент самим, чтобы иметь доступ к объекту, для которого вызван метод (документация).
      • 0
        Примерно так:
        function extend(parent)
            local child = {}
            setmetatable(child,{__index = parent})
            return child
        end
        
        • +2
          Лучший, на мой взгляд вариант — наследование от базового класса Object с предопределенными функциями new и extend.
      • +5
        Не понимаю плясок с бубном вокруг инкапсуляции и приватных полей в скриптовых языках. Нафига это там нужно, непонятно.
        Без этой ерунды, ООП в lua добавляется буквально одной функцией.
        • +2
          Кроме того, private на самом деле еще более public, чем public, т.к. доступен всем без исключения, что, собственно, функция setName и демонстрирует. Но все еще хуже. Попробуйте создать два класса с приватными полями. Будите сильно удивлены (это я автору).
          • 0
            какая клевая и класическая для Lua бага ))
            • 0
              Прошу прощения у всех за этот косяк. Дело в том, что я никогда и не пользовался инкапсуляцией, а этот костыль прикрутил прямо во время написания статьи, чтобы не убивать ни одного кита. Был не прав, сейчас думаю как исправить.
        • +3
          Похоже и мне теперь нужно написать про ООП в Lua. Мой метод не использует setmetatable() и экономит ресурсы.
          Кстати у вас глобальные переменные.
          • 0
            Я когда-то делал парсер для текстовых игр на 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 — создание нового объекта с глобальным именем, указанным в кавычках
            в скобках можно указать, от кого наследуемся (список)
            в фигурных скобках конструктор
            • 0
              Спасибо, что поделились опытом.
              Я давно присматриваюсь к Lua.
              V-REP скриптуется на Lua — роботы-Lua)
              • +1
                Learn Lua in 15 minutes — может, будет полезно для быстрого старта.
                • 0
                  А лучше прочитать Иерусалимски — Programming in Lua. Удивительно хорошо написана и легко читается.
                  • 0
                    Спасибо, кстати первая редакция книги доступна online
                • 0
                  Если кто не пробовал, рекомендую посмотреть на Squirrel
                  www.squirrel-lang.org
                  Очень похоже на Lua, но ООП без костылей.
                  • 0
                    белка хороша, но слишком редка. Я видел всего один игровой движок (а луа я использую именно в них), в котором был squirrel, да и тот умер.
                    К тому же, луа быстрее.
                    • 0
                      Lua быстрее, но имхо это далеко не везде критично во встроенном скриптовом интерпретаторе.
                      Кстати, для белки еще есть отличный биндинг SqPlus.
                  • +1
                    Статья изменилась, просьба перечитать. Пофиксил инкапсуляцию.

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