Lua : Statégie d'objet

Par lcirpaci, 14 février, 2025
Lua logo

Pourquoi parler de stratégie d'objet et non d'orienté objet en Lua ?

En Lua, contrairement à des langages comme Java, C++ ou Python, la programmation orientée objet (POO) n'est pas un paradigme natif intégré dans le langage. Lua ne propose pas de mécanismes de classes, d'héritage ou de polymorphisme comme ceux que l'on retrouve dans d'autres langages. Cependant, Lua permet de créer des structures et des comportements similaires à ceux d'un système orienté objet, mais cela nécessite l'utilisation de stratégies et de techniques spécifiques.

C'est là qu'intervient la notion de stratégie d'objet. Au lieu de disposer de classes prédéfinies et d'un système de types rigide, Lua offre une grande flexibilité en permettant aux développeurs de simuler l'orienté objet via des tables et des métatables. Ce modèle est moins contraignant, mais demande une approche plus manuelle et souvent plus créative. Par exemple, l'héritage n'est pas implémenté de manière systématique, mais il peut être simulé en manipulant les métatables pour relier des objets et partager des comportements. Une métatable permet de lier des méthodes à des objets, de gérer l'héritage ou même de personnaliser les opérations de base comme l'addition, l'accès aux champs d'une table ou tout autres comportements non visible.

Quel est la différence entre un objet lamda en Lua et un objet avec une stratégie ?

Le contexte d'utilisation est différente

Un objet lamda n'as pas vraiment de fonctionnalité très pouser. Voici un example d'un objet lamda

local compte = {}
compte.solde = 0
compte.depot = function(self,montant)
	self.solde = self.solde + montant
end

Ceci est une façon de faire très conventiel est très accesible pour un aprennant. Mais ne pouvons allez plus loin en intégrant une stratégie d'objet. Voici un example se basant sur le précédent.

local data, meta = {},{}
data.solde = 0
data.depot = function(self,montant)
	self.solde = self.solde + montant
end
local compte = setmetatable(data,meta)

Qu'est-ce qui se passe ici? j'ai juste créer un nouveau tableau vide et associer mon objet dans une fonction qui à deux paramètres... En effet, ici notre stratégie d'objet n'as rien encore d'implémenter, Mais qu'est-ce que l'on peut implémenter ?

Les meta méthodes / meta fonctions

Une meta méthode est simplement une méthode qui est utiliser en interne par notre stratégie d'objet. Autrement dit, ces une méthode qui permet d'agir selon un comportement précis. Nous pouvons les distinguer grâce à leur suffix qui commence par __. Dans notre example en haut, nous ont avons pas implémenter de meta méthode est ces pour cela que notre stratégie d'objet ne sert à rien. Autrement dit, si nous avons besoin d'un comportement précis selon si par exemple j'appelle mon objet comme un itérateur, un string ou autres, nous allons utiliser la statégie d'objet pour créer un meta objet, sinon nous allons créer un objet lamda.

Qu'est-ce qu'un objet meta ?

Un objet meta est simplement de resultat de la création d'un objet avec statégie de d'objet. En parle ici du résultat de la méthode setmetatable

Structure d'une meta table (objet meta)

la fonction setmetatable prend deux paramètres

  • table : table
    • la première table est une représentatio de notre objet comme celle en lamda. Autrement dit, il contient des clé qui joue le rôle des nom pour les propriété et les méthodes et des valeurs, sont contenu.

Commentaire

Nous pouvons faire comprendre qu'une méthode et une propriété est privées lorsque celui ci commence par un _

  • meta : table | metatable
    • Les meta sont un peu comme la table en haut, sauf qu'elle contient uniquement des meta méthodes. Nous pouvons les distinguer grâce à leur suffix qui commence par __

Ces quoi les meta méthode disponible.

Il y'en à plusieurs, mais voici les principales :

Les plus commun, voir obligatoires

  • __index(clé:String)
    • Il joue plusieurs rôles
      • Héritage
		local data, meta = {}, {}
		function data:info()
			print("je suis une info")
		end
		local message = setmetatable(data,meta)
		data, meta = {}, {}
		meta.__index = message --héritage de message
		local secretMessage = setmetatable(data,meta)
		secretMessage:info()
		je suis une info
	- Multi Héritage
	    local data, meta = {}
	    function data:A()
	        print("A methode")  
	    end
	    local A = setmetatable(data,meta)

	    data, meta = {}, {}
	    function data:B()
	        print("B methode")  
	    end
	    local B = setmetatable(data,meta)

	    data, meta = {}, {}
	    function meta:__index(cle)
	        return A[cle] or B[cle]	--insérer par clé les objets
	    end
	    local C = setmetatable(data,meta)
	
	    C:A()
	    C:B()
	
		A methode
		B methode
  • __newindex(clé:String,valeur:Any)
    • Il permet de faire du traitement lorsque nous utilisons un mutateur dans ces méthodes ou ces données

    Commentaire

    Si vous ne l'apeller pas, vous pouvez manipuler vos méthode, propriété sans auucne protection. DANGER
	local Voiture = setmetatable({},{
	    __newindex = function (self,cle,valeur)
	        print(cle .. " à était détecter")
	    end
	})
	Voiture.x = 1
	print(Voiture.x)
	Voiture.x = 2
	```
	```txt
	x à était détecter
	nil
	x à était détecter
pour accepter la saisie, il faut utiliser `rawset` qui permet de définir une données en surpassant le `__newindex`.
	local Voiture = setmetatable({},{
	    __newindex = function (self,cle,valeur)
	        print(cle .. "} à était détecter")
	        rawset(self,cle,valeur)
	    end
	})
	Voiture.x = 1
	print(Voiture.x)
	Voiture.x = 2
	x à était détecter
	1
Un autre cas sympa est la protection de la surdéfinition
	local Voiture = setmetatable({
	    _couleur = "",
	    Couleur = function(self,field)
	        if field then
	            rawset(self,"_couleur",field)
	        end
	        return self._couleur
	    end,
	    avancer = function(self) print("J'avance") end
	},{
	    __call = function(self)
	        return setmetatable({},{
	            __index = self,
	            __newindex = function(self,cle,valeur)
	                if not self[cle] then
	                    error("Vous ne pouvez pas ajouter la méthode/	Propriété {" .. cle .. "}")
	                end
	                if (rawget(self,cle)~=valeur) then
	                    error("Vous ne pouvez pas modifier la méthode {" .. cle .. "}")
	                end
	                rawset(self,cle,valeur)
	            end
	        })
	    end
	})
	local v1 = Voiture()
	v1:avancer()
	v1.avancer = 1
	Javance
	Vous ne pouvez pas modifier la méthode/Propriété {avancer}
pareil si je remplace `v1.avancer = 1` par `v1.reculer = 1`
	Vous ne pouvez pas ajouter la méthode/Propriété {reculer}
Se qui est également super dans cette example, ces que je protége le mutateur couleur en passant par un mutateur privée. Le mutateur privée utilise `rawset` qui détourne les restrivtions de `__newindex` Si je fait `v1._couleur = "bleu"`
	Vous ne pouvez pas modifier la méthode/Propriété {_couleur}
  • __call(...:Any?):Any?
    • Il permet de faire du traitement lorsque nous utilisons l'objet comme une fonction
      • Constructeur interne
		local data, meta = {}, {}
		function data:info() print("hello world") end
		function meta:__call()
		  return setmetatable({},{__index = self})
		end
		local Message = setmetatable(data,meta)
		local m1 = Message()
		m1:info()
		hello world
	- Constructeur interne avec protection des méthodes à l'usage d'instance uniquement
		local data, meta = {}, {}
		function data:info() self:_instanceSeulement()
		  print("hello world")
		end
		function data:_instanceSeulement() 
		  if ( not self._instance) then 
		    error("problème de context static ici") 
		  end 
		end
		function meta:__call()
		  return setmetatable({
		  _instance = true
		  },{
		  __index = self
		  })
		end
		local Message = setmetatable(data,meta)
		local m1 = Message()
		m1:info()
		Message:info()
		hello world
		problème de context static ici

Les itérateurs

  • __ipairs() : next, iteration:table, nil
    • Il permet d'indiquer quoi retrouner lorsque nous utilisons ll 'objet comme un itérateur
  • __len():Any idéalement un nombre
    • Il permet de retourne une valeur lorsque l'opérateur length est appeler sur l'objet
		local data, meta = {}, {}
		data.items = {"agenda", "crayon", "étuie"}
		function meta:__pairs() return next, 
			self.items, 
			nil 
		end
		function meta:__len() return #self.items end
		local Gestionnaire = setmetatable(data,meta)

		print("Nombre d'items dans le Gestionnaire",
		#Gestionnaire,
		"items")
		for k,v in pairs(Gestionnaire) do
		  print(k,v)  
		end
		Nombre d'items dans le Gestionnaire	3	items
		1	agenda
		2	crayon
		3	étuie

Autres

  • __tostring():String
    • Il permet de reourner un string lorsque nous appelons l'objet dans se context
  • __concat(left:String|self,right:String|self)
    • Il permet le traitement lorsque l'objet est utilisé dans un concaténation.
	local data, meta = {}, {}
	data.message = "je suis un message secret"

	function meta:__tostring() 
	    return self.message
	end
	meta.__concat = function(left,right)
	  if (type(left)=="string") then
	    return left .. right.message  
	  end
	  return left.message .. right
	end

	local Message = setmetatable(data, meta)

	print(Message .. "x")
	print("x" .. Message)
	print(Message)
#### Avertissement
Tous les meta méthodes qui sont un context left et right, vous êtez **obliger** d'utiliser le méta méthode sous forme de pointeur. Sinon cela ferais un peut bizzare
	local data, meta = {}, {}
	data.message = "je suis un message secret"
	function meta:__concat(right)
	  if (type(self)=="string") then
	    return self .. right.message  
	  end
	  return self.message .. right
	end

	local Message = setmetatable(data, meta)

	print(Message .. "x")
	print("x" .. Message)
suivez le même modèle pour les autres en bas

Des opérateurs mathématiques

  • __add(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans une addition
  • __sub(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans une soustraction
  • __div(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans une division
  • __mul(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans une multiplication
  • __pow(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans un exposant
  • __mod(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans un modulo
  • __idiv(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans une division unairie (//)
  • __band(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans un et
  • __bor(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans un où
  • __bxor(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans un xor
  • __shl(left:number|self,right:number|self)
  • permet de faire du traitement lorsque l'objet est dans un shift left (<<)
  • __shr(left:number|self,right:number|self) permet de faire du traitement lorsque l'objet est dans un shift right (>>)

Des opérateurs de comparaison

  • __eq(left:self,right:other):Boolean permet de faire du traitement lorsque l'objet est dans un contexat d'égalité avec un autre objet du même type
  • __lt(left:self,right:other):Boolean permet de faire du traitement lorsque l'objet est dans un context plus petite à un autre objet du même type
  • __le(left:self,right:other):Boolean permet de faire du traitement lorsque l'objet est dans une context plus petit ou égale à un autre objet du même type
Commentaires

Il n'y pas pas les opérateurs > et >=, car cela donne le même résultat de faire a > b et b > a. Cela permet d’économiser du travail et d’éviter de redéfinir des opérations redondantes.

Note API

Lorsque que vous regarder la documentation des meta méthode, le premier argument est toujours la référence de l'objet, bien que cela sois contre nature. Example dans __add(a,b) a est toujours référer à self

Formatage de code

Personellement, je préfère utiliser un syntaxe d'implémentation que de pointeur

--example syntaxe pointeur
local compte = {}
compte.depot = function(self,montant)
	self.solde = self.solde + montant
end

ici ont est obliger d'injecter le context de l'objet à savoir self

à la place,

local compte = {}
function compte:depot(montant)
	self.solde = self.solde + montant
end

self est directement injecter comme référant à l'objet compte. cela est de même avec un meta objet.

Références

Étiquettes

Commentaires2

mgacemi

il y a 2 mois 1 semaine

Ton article est très complet et offre une excellente introduction aux stratégies d’objet en Lua. J’aime particulièrement la manière dont tu expliques pourquoi Lua ne propose pas nativement la programmation orientée objet et comment on peut contourner cette limitation avec les métatables.

Une question : Y a-t-il des cas où l'utilisation des métatables pourrait être moins avantageuse qu'une autre approche en Lua ?

Ces surtout si ont à besoin d'un comportement précis. SI tu fait un objet genre compteur, sa sert à rien :), mais cela ne fait pas de mal