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 Вася

Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 20

    +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
              Прошу прощения у всех за этот косяк. Дело в том, что я никогда и не пользовался инкапсуляцией, а этот костыль прикрутил прямо во время написания статьи, чтобы не убивать ни одного кита. Был не прав, сейчас думаю как исправить.
                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
                      Статья изменилась, просьба перечитать. Пофиксил инкапсуляцию.
                        0
                        Пример: inherit.lua
                        -------------------------------------------------------------
                        local Object = {}
                        
                        function Object:new(properties)
                        	properties = properties or {}
                        	setmetatable(properties, self)
                        	self.__index = self
                        	return properties
                        end
                        
                        function Object:inherit(properties)
                        	return self:new():new(properties)
                        end
                        -------------------------------------------------------------
                        local Person = Object:inherit(
                        {
                        	name = "default"
                        })
                        
                        function Person:get_name()
                        	return self.name
                        end
                        -------------------------------------------------------------
                        local Coconut = Person:inherit(
                        {
                        	cosplay = "maiden"
                        })
                        
                        function Coconut:get_name()
                        	return "sexy " .. self.name
                        end
                        
                        function Coconut:get_cosplay()
                        	return self.cosplay
                        end
                        -------------------------------------------------------------
                        person1 = Person:new()
                        print("person1: name=" ..	person1.name ..
                        	"\tget_name()=" ..	person1:get_name())
                        -------------------------------------------------------------
                        person2 = Person:new({name = "Human"})
                        print("person2: name=" ..	person2.name ..
                        	"\tget_name()=" ..	person2:get_name())
                        -------------------------------------------------------------
                        coconut1 = Coconut:new()
                        print("coconut1: name=" ..	coconut1.name ..
                        	"\tget_name()=" ..	coconut1:get_name() ..
                        	"\tcosplay=" ..		coconut1.cosplay ..
                        	"\tget_cosplay()=" ..	coconut1:get_cosplay())
                        -------------------------------------------------------------
                        coconut2 = Coconut:new({name = "Coconut", cosplay = "nurse"})
                        print("coconut2: name=" ..	coconut2.name ..
                        	"\tget_name()=" ..	coconut2:get_name() ..
                        	"\tcosplay=" ..		coconut2.cosplay ..
                        	"\tget_cosplay()=" ..	coconut2:get_cosplay())
                        -------------------------------------------------------------
                        

                        Вывод:
                        person1: name=default get_name()=default
                        person2: name=Human get_name()=Human
                        coconut1: name=default get_name()=sexy default cosplay=maiden get_cosplay()=maiden
                        coconut2: name=Coconut get_name()=sexy Coconut cosplay=nurse get_cosplay()=nurse

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

                        Самое читаемое