Clases y objetos
Programación orientada a objetos
La programación orientada a objetos (POO) es un paradigma de programación que se enfoca en el uso de "objetos" como unidades básicas de programación. Un objeto es una instancia de una clase, que es una plantilla o molde para crear objetos.
Un objeto tiene dos partes principales: propiedades (también conocidas como variables de instancia) y métodos (también conocidos como funciones de instancia). Las propiedades representan las características de un objeto, mientras que los métodos representan las acciones que un objeto puede realizar.
La POO se basa en tres principios fundamentales: encapsulamiento, herencia y polimorfismo. El encapsulamiento se refiere a la ocultación de los detalles de implementación de un objeto, lo que permite que los objetos interactúen entre sí a través de una interfaz pública. La herencia permite que una clase herede las características y comportamientos de otra clase, mientras que el polimorfismo permite que un objeto tome diferentes formas dependiendo del contexto.
En JARU, se pueden crear clases utilizando la palabra clave class seguida del nombre de la clase. Dentro de la clase, se pueden definir propiedades (variables) y métodos (funciones) que son específicos para esa clase.
Declaración de clases
En JARU, las clases son una herramienta fundamental para la programación orientada a objetos. Una clase es un molde o plantilla para crear objetos que comparten características y comportamientos específicos.
La declaración de una clase en JARU se inicia con la palabra clave class seguida del nombre de la clase:
class Persona
// propiedades y métodos van aquí
end
Así se crea una clase llamada Persona. Dentro de ella puedes declarar propiedades y métodos.
Dentro de class solo pueden ir métodos (def), declaraciones private y operadores (operator). Una propiedad no se declara ahí con var, que no compila: se crea en el constructor con this, como se ve a continuación.
Declaración de propiedades
En JARU, las propiedades son valores asociados a una instancia de una clase. Estas propiedades se pueden crear mientras el programa se ejecuta, y leer y cambiar tanto desde dentro de la clase (con la palabra clave this) como desde fuera, con la sintaxis miObjeto.propiedad.
La palabra clave this en JARU se refiere a la instancia actual de un objeto en una clase. Se utiliza para acceder a las propiedades y métodos de la clase desde dentro de la misma.
La declaración de propiedades en JARU se realiza de manera dinámica, es decir, no es necesario declararlas previamente en la clase. El método init es una forma común de inicializar las propiedades en el momento de crear una nueva instancia de una clase.
class Persona
def init(nombre)
this.nombre = nombre
end
end
var p1 = Persona("Juan")
var p2 = Persona("María")
print(p1.nombre) // muestra Juan
print(p2.nombre) // muestra María
Las propiedades también pueden ser creadas y modificadas desde fuera de la clase:
class Persona
def init()
end
end
var juan = Persona()
juan.alias = "Krilin" // Crea la propiedad "alias" en el objeto "juan"
print(juan.alias)
Las propiedades están asociadas a una instancia de un objeto, no a la clase en sí. Esto quiere decir que cada vez que se crea una nueva instancia de una clase, se crea una copia independiente de sus propiedades asociada a la instancia.
Declaración de métodos
Los métodos son funciones que se definen dentro de una clase y tienen acceso a las propiedades de la misma. La sintaxis para declarar un método en JARU es la palabra clave def seguida del nombre del método, los parámetros entre paréntesis, y debe finalizar con end.
class Persona
def saludar()
print("¡Hola!")
end
end
Los métodos también pueden tener parámetros y retornar valores:
class Calculadora
def sumar(a, b)
return a + b
end
end
Los métodos pueden acceder a las propiedades de una clase usando this:
class Persona
def init()
this.nombre = ""
end
def saludar()
print("¡Hola! " + this.nombre)
end
end
Dentro de un método, las propiedades y los demás métodos de la instancia se nombran siempre con this.. Sin él, JARU busca una variable o una función con ese nombre, y el programa se para con ERR_NAME_NOT_FOUND (Undefined variable 'suma'.):
class Contador
def init()
this.n = 0
end
def suma()
this.n = this.n + 1
end
def sumaDos()
this.suma() // con this.: llama al método
this.suma()
end
end
var c = Contador()
c.sumaDos()
println(c.n) // 2
Los métodos son de las instancias: no se pueden llamar sobre la clase. Contador.suma() da ERR_TYPE_MISMATCH (This object type does not support method calls.).
Constructores
El constructor es un método especial llamado init() que se utiliza para inicializar un objeto de una clase en el momento de crearlo:
class NombreDeLaClase
def init()
// código de inicialización
end
end
El constructor se llama automáticamente cuando se crea un nuevo objeto de una clase. El nombre del constructor siempre debe ser init y debe estar dentro de la definición de la clase.
class Persona
def init()
this.nombre = "Sin nombre"
this.edad = 0
end
end
También es posible pasar parámetros al constructor:
class Persona
def init(nombre, edad)
this.nombre = nombre
this.edad = edad
end
end
Sobrecarga de constructores
En JARU se permite la sobrecarga del método constructor init, que se refiere a la capacidad de tener varias versiones de este método con diferentes argumentos:
class Persona
def init(nombre, edad)
this.nombre = nombre
this.edad = edad
end
def init(nombre)
this.nombre = nombre
this.edad = 0
end
end
var p1 = Persona("Juan", 25)
var p2 = Persona("María")
Las versiones se distinguen por el número de parámetros: al crear la instancia, JARU llama a la que tiene tantos parámetros como argumentos le pasas.
- Si no hay ninguna con ese número, el error es
ERR_INVALID_ARG_NUMBER(No constructor found for class 'Persona' that takes 3 arguments.). - Sin argumentos y sin un
init()sin parámetros, la instancia se crea igual, sin pasar por ningún constructor:Persona()da una persona sinnombreniedad.
Solo init admite varias versiones. Si declaras dos métodos con el mismo nombre, el segundo sustituye al primero.
Herencia de clases
La herencia en programación orientada a objetos es un mecanismo que permite a una clase (llamada clase hija o subclase) heredar las propiedades y métodos de otra clase (llamada clase padre o superclase).
En JARU, el concepto de herencia se implementa mediante el uso de los dos puntos ::
class Padre
def init()
this.propiedad1 = "Valor"
end
def metodo1()
print("Hola, soy el método 1 del padre")
end
end
class Hijo : Padre
def metodo2()
print("Hola, soy el método 2 del hijo")
end
end
En este ejemplo, la clase Hijo hereda de la clase Padre, por lo que tiene acceso a la propiedad propiedad1 y al método metodo1, y puede también tener sus propias propiedades y métodos.
La clase hija recibe todos los métodos públicos del padre, también sus constructores y sus operadores: si la hija no tiene un init con el número de argumentos que le pasas, se usa el del padre. Para cambiar lo que hace un método, declara en la hija otro con el mismo nombre.
super
super.metodo(...) llama a la versión del padre de un método, aunque la hija lo haya cambiado. Se usa sobre todo en el constructor de la hija, para que el del padre prepare su parte: super.init(...).
class Animal
def init(nombre)
this.nombre = nombre
end
def habla()
return "..."
end
def presenta()
return this.nombre + " dice " + this.habla()
end
end
class Perro : Animal
def init(nombre, raza)
super.init(nombre) // el constructor del padre pone el nombre
this.raza = raza
end
def habla()
return "guau"
end
def hablaComoAnimal()
return super.habla() // la versión del padre
end
end
var p = Perro("Rex", "galgo")
println(p.presenta()) // Rex dice guau
println(p.hablaComoAnimal()) // ...
println(p.raza) // galgo
super va siempre con un punto y el nombre de un método detrás, y solo en una clase que hereda de otra: en una clase sin padre, el compilador da el error Can't use 'super' in a class with no superclass.
Encapsulamiento
El encapsulamiento esconde los detalles de una clase: lo que es privado solo se usa desde sus métodos. En JARU, las propiedades privadas se declaran con private al principio de la clase (varias en la misma línea: private saldo, pin), y los métodos privados, con def private:
class Cuenta
private saldo
def init(inicial)
this.saldo = inicial
end
def ingresa(cantidad)
this.saldo = this.saldo + this.comision(cantidad)
end
def private comision(cantidad)
return cantidad - 1
end
def verSaldo()
return this.saldo
end
end
var c = Cuenta(100)
c.ingresa(11)
println(c.verSaldo()) // 110
Desde fuera de la clase:
- leer una propiedad privada da
ERR_ATTRIBUTE_NOT_FOUND(Undefined property or method 'saldo'.); - llamar a un método privado da
ERR_PERMISSION_DENIED(Cannot access private method 'comision' from class 'Cuenta'.).
Dentro de los métodos de la clase puedes leer y cambiar las propiedades privadas de cualquier instancia de esa clase, no solo las de this: otra.saldo. Los métodos privados, en cambio, solo se llaman sobre this: otra.comision(1) da ERR_PERMISSION_DENIED, también dentro de la clase. Las propiedades y los métodos privados no salen en properties(), methods() ni hasAttribute(), y copy() y clone() también los copian.
c.saldo = 5, fuera de la clase, no da error, pero no cambia la propiedad privada: crea otra, pública, con el mismo nombre. Después, c.saldo lee la pública, y los métodos de la clase siguen usando la privada.
En JARU no existe protected, y lo privado es solo de su clase: los métodos de una clase hija no llegan a las propiedades ni a los métodos privados del padre (ERR_ATTRIBUTE_NOT_FOUND). Lo que sí puede hacer la hija es llamar a los métodos públicos del padre que los usan.
Polimorfismo
El polimorfismo es uno de los pilares de la programación orientada a objetos que permite que un objeto de una clase pueda ser tratado como si fuera de otra clase relacionada. Esto significa que una misma acción puede tener distintos comportamientos en diferentes objetos, dependiendo de su clase.
Por ejemplo, imagina que tienes una clase llamada Animal y dos subclases, Gato y Perro. La clase Animal tiene un método llamado hacerRuido que se sobrescribe en las subclases Gato y Perro para emitir un sonido diferente. Cuando se crean objetos de cada subclase y se les llama a hacerRuido, cada objeto emite un sonido distinto, pero aun así pueden ser tratados como si fueran objetos de la clase Animal.
El polimorfismo permite que se utilice una misma acción o método con distintos objetos, y dependiendo de su clase, se realice una acción diferente. En el ejemplo de super, presenta() está en Animal, pero llama a this.habla(): con un Perro, se ejecuta el habla() del perro.
Sobrecarga de operadores
Una clase puede decidir qué hacen los operadores con sus instancias: a + b, -a, a == b... Cada operador se declara dentro de la clase con operator, el símbolo y sus operandos, y devuelve el resultado. El ejemplo EasyRender, que se instala con el IDE, lo usa para sus vectores.
class Vector2
def init(x, y)
this.x = x
this.y = y
end
operator +(a, b)
return Vector2(a.x + b.x, a.y + b.y)
end
operator -(a)
return Vector2(-a.x, -a.y)
end
operator *(v, k)
return Vector2(v.x * k, v.y * k)
end
operator ==(a, b)
return a.x == b.x and a.y == b.y
end
def texto()
return "(" + this.x.toString() + ", " + this.y.toString() + ")"
end
end
var a = Vector2(1, 2)
var b = Vector2(3, 4)
var suma = a + b
println(suma.texto()) // (4, 6)
var opuesto = -a
println(opuesto.texto()) // (-1, -2)
var doble = a * 2
println(doble.texto()) // (2, 4)
println(a == Vector2(1, 2)) // true
Se pueden sobrecargar estos operadores:
+,*y/, con dos parámetros. El de/vale también paradiv.-: con dos parámetros es la resta (a - b), y con uno, el signo menos (-a). Puedes declarar los dos.==,!=,<,<=,>y>=, con dos parámetros. Cada uno se declara aparte:!=no sale de==.!, con un parámetro: vale para!ay paranot a.|y&, con dos parámetros. EasyRender los usa para el producto escalar y el vectorial.
% (mod), ^, ~, <<, >>, and, or y los corchetes no se pueden sobrecargar.
- El operador recibe los dos operandos, el de la izquierda primero. No uses
thisdentro: cuando los dos operandos son instancias, se usa el operador de la clase del de la derecha, ythises ese. - La instancia tiene que ir a la izquierda de un número:
a * 2llama a tu operador, pero2 * adaERR_TYPE_MISMATCH(Unsupported operand types for operator '*'.). - Sin su
operator, los operadores aritméticos danERR_INVALID_OPERATION(Operator '+' is not defined for the right-hand operand's type.). Las comparaciones no dan error:==y!=miran si es la misma instancia, y<,<=,>y>=danfalsecon dos instancias distintas. - Declarar con
operatorun operador que no está en la lista, o con un número de parámetros que no le toca, no compila, y el error dice por qué: «the operator 'mod' can't be overloaded», «the operator '+' takes 2 parameters».
Métodos de las instancias
Toda instancia tiene estos métodos, sin declararlos:
| Método | Qué hace | Devuelve |
|---|---|---|
methods() | Lista los métodos públicos de su clase, también los heredados. Los constructores salen como init@N, con su número de parámetros. | Una lista de cadenas |
properties() | Lista sus propiedades públicas. | Una lista de cadenas |
hasAttribute(nombre) | Mira si tiene una propiedad pública o un método público con ese nombre. Los constructores ("init") y estos métodos no cuentan. | true o false |
setProperty(nombre, valor) | Crea o cambia la propiedad pública nombre. Sirve cuando el nombre está en una variable. | nil |
remove(nombre) | Quita la propiedad pública nombre. Si no existe, o si es un método, no hace nada. | nil |
copy() | Una copia superficial (ver abajo). | Una instancia nueva |
clone() | Una copia completa (ver abajo). | Una instancia nueva |
nombretiene que ser una cadena; si no, el error esERR_TYPE_MISMATCH.- Las listas de
methods()yproperties()salen en el orden en que la VM las guarda, que no tiene por qué ser el de la declaración.
class Jugador
def init(nombre)
this.nombre = nombre
this.vidas = 3
end
def saluda()
println("Hola, ", this.nombre)
end
end
var j = Jugador("Ana")
println(j.properties()) // [nombre, vidas]
println(j.hasAttribute("saluda")) // true
var campo = "nivel"
j.setProperty(campo, 7)
println(j.nivel) // 7
j.remove("nivel")
println(j.hasAttribute("nivel")) // false
println de una instancia escribe el nombre de su clase (Jugador instance). Y dos instancias son iguales con == solo si son la misma, salvo que la clase declare operator ==.
Si una clase declara un método remove, copy, clone, methods, properties, hasAttribute o setProperty, desde fuera de la clase se ejecuta el de la VM, no el tuyo: inventario.remove("espada") intentaría quitar la propiedad espada en vez de llamar a tu método. Desde dentro (this.remove(...)) sí se llama al tuyo. Usa otros nombres, como quita o duplica.
Copiar una instancia: copy() y clone()
Asignar una instancia a otra variable no la copia: las dos variables apuntan a la misma. Para tener otra:
copy()hace una copia superficial: la copia tiene sus propias propiedades, pero las listas, los mapas y las instancias que guardan se comparten con el original.clone()hace una copia completa: duplica también las listas, los mapas, los arrays, las instancias y los structs que haya dentro.
class Mochila
def init()
this.cosas = ["espada"]
end
end
var original = Mochila()
var copia = original.copy()
var clon = original.clone()
copia.cosas.append("escudo") // es la misma lista que la del original
clon.cosas.append("arco") // el clon tiene su propia lista
println(original.cosas) // [espada, escudo]
println(clon.cosas) // [espada, arco]
clone() no duplica los bitmaps, los sprites ni los demás objetos de los módulos: el clon usa los mismos que el original, y si mueves el sprite del clon, también se mueve el del original. Para tener otro sprite, créalo con Sprite.new(sprite) (ver Sprite).
clone() con objetos que se apuntan entre síSi dentro de lo que clonas hay un ciclo, como un hijo que guarda a su padre en una propiedad, clone() no termina nunca de copiar: la VM se cierra sin ningún mensaje, y try no lo captura. Clona solo lo que no tenga ciclos, o copia a mano lo que necesites. copy() no tiene este problema.
Todos los problemas conocidos de JARU, juntos, están en Problemas conocidos.