Saltar al contenido principal

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.

this

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)
info

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
info

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 sin nombre ni edad.

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.

Escribir en una propiedad privada desde fuera

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.

info

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 para div.
  • -: 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 !a y para not 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 this dentro: cuando los dos operandos son instancias, se usa el operador de la clase del de la derecha, y this es ese.
  • La instancia tiene que ir a la izquierda de un número: a * 2 llama a tu operador, pero 2 * a da ERR_TYPE_MISMATCH (Unsupported operand types for operator '*'.).
  • Sin su operator, los operadores aritméticos dan ERR_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 >= dan false con dos instancias distintas.
  • Declarar con operator un 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étodoQué haceDevuelve
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
  • nombre tiene que ser una cadena; si no, el error es ERR_TYPE_MISMATCH.
  • Las listas de methods() y properties() 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 ==.

No llames a tus métodos como estos

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.