Clases, objetos, métodos y herencia
Antes: Manejo de errores y excepciones. Después: Concurrencia, asincronía y canales. Referencia técnica: Sistema de tipos, Catálogo nativo.
¿Qué vas a aprender aquí?
A medida que una aplicación crece, tener variables dispersas por un lado y funciones sueltas por otro puede volverse caótico:
- Puedes tener una variable $usuario_nombre, otra $usuario_email, otra $usuario_rol.
- Si tienes 100 usuarios, ¿cómo mantienes unidos los datos de cada uno con las operaciones que les corresponden (como autenticarse, cambiar contraseña o enviar notificación)?
La Programación Orientada a Objetos (POO) resuelve este problema empaquetando datos y comportamientos relacionados en una sola unidad conceptual.
En esta guía aprenderás:
1. Qué es una clase (el plano de diseño) y qué es un objeto o instancia (la entidad real).
2. Cómo definir propiedades (atributos) y métodos (funciones de la clase).
3. Cómo inicializar objetos con constructores e Init.
4. El rol de la variable especial $this.
5. Los niveles de visibilidad y encapsulación: public, protected y private.
6. Miembros estáticos y el operador de resolución de ámbito (::).
7. Cómo reutilizar y especializar código mediante herencia (extends), interfaces (implements) y clases abstractas (abstract).
8. Clases genéricas (<T>), record inmutables y el operador de navegación segura ante nulos (?->).
1. Clases y Objetos: El plano y la casa
Para entender la orientación a objetos, la mejor analogía es la arquitectura: - Una clase es el plano arquitectónico: describe qué habitaciones tendrá la casa, cuántas puertas y qué funciones tiene. El plano no ocupa un terreno físico ni puedes vivir en él. - Un objeto (o instancia) es la casa física real construida en un terreno a partir de ese plano. Puedes construir diez casas a partir del mismo plano; pintar una casa de azul no cambia el color de las demás.
Veamos un ejemplo mínimo con un contador:
public class Contador {
public int $valor = 0
public func incrementar(): int {
$this->valor = $this->valor + 1
return $this->valor
}
}
var $contador = new Contador()
print($contador->incrementar())
print($contador->incrementar())
Salida en la terminal:
1
2
¿Qué elementos componen este código?
public class Contador: Declara una clase pública llamadaContador. En Joss, las clases a nivel de archivo requieren un modificador de visibilidad (publicoprivate).public int $valor = 0: Es una propiedad (un dato que cada instancia deContadorrecordará).public func incrementar(): int: Es un método (una función que le pertenece a la clase y que puede manipular sus propiedades).$this: Es una palabra reservada que significa “este objeto en particular”. Cuando ejecutas$this->valor, estás accediendo a la propiedad$valorde la instancia que está ejecutando el método.new Contador(): La palabra clavenewcrea una nueva instancia real en la memoria.$contador->incrementar(): El operador flecha->se utiliza para acceder a propiedades y métodos de una instancia.
2. Inicialización de objetos: Constructores
Cuando creas un objeto, casi siempre necesitas configurarlo con datos iniciales (por ejemplo, el nombre de una persona o las credenciales de una base de datos).
En Joss puedes definir un método public func constructor(...) o un bloque Init(...):
public class Persona {
private string $nombre = ""
public func constructor(string $nombre) {
$this->nombre = $nombre
}
public func saludar(): string {
return "Hola, " . $this->nombre
}
}
var $persona = new Persona("Ada")
print($persona->saludar())
Salida en la terminal:
Hola, Ada
Al escribir new Persona("Ada"), Joss llama automáticamente al constructor entregándole el argumento "Ada", el cual queda guardado de forma segura dentro de la propiedad privada $this->nombre.
[!NOTE] Joss también admite la sintaxis de bloque
Init(string $nombre) { ... }. Los bloquesInitno llevan modificadores de visibilidad (publicniprivate). La forma histórica combinadaInit constructor(...)sigue siendo aceptada por compatibilidad pero emite el aviso de transiciónJOSS-DECL-008; prefierepublic func constructor(...)oInit(...).
Promoción de propiedades en el constructor (Constructor Property Promotion)
Para evitar tener que declarar la propiedad, recibir el parámetro y escribir $this->prop = $prop manualmente, Joss permite declarar la visibilidad (public, protected o private) y constancia (const) directamente en los parámetros de Init o del constructor. Joss creará y asignará la propiedad automáticamente:
public class Usuario {
Init(
public string $nombre,
public int $edad = 30
) {}
}
var $u = new Usuario("Ada")
print($u->nombre)
print($u->edad)
Salida en la terminal:
Ada
30
También es posible declarar propiedades constantes promovidas con public const Tipo $campo para protegerlas frente a reasignaciones posteriores.
3. Encapsulación y modificadores de visibilidad
La encapsulación es el principio de proteger los datos internos de un objeto para evitar que código externo los modifique de forma incorrecta o corrupta.
Joss ofrece tres modificadores de visibilidad explícitos:
| Modificador | Dónde se puede acceder | Uso recomendado |
|---|---|---|
public |
Desde cualquier parte del programa (dentro de la clase, en subclases y desde código exterior). | Para la API pública del objeto: métodos que los usuarios de tu clase necesitan invocar. |
protected |
Solo dentro de la propia clase y dentro de las subclases que hereden de ella con extends. |
Para métodos y propiedades internas que las clases hijas necesitan especializar o consultar. |
private |
Únicamente dentro de la clase exacta donde fue declarada. Nadie más puede verla ni modificarla. | Para detalles de implementación íntimos (contraseñas, conexiones crudas, flags de estado). |
public class CuentaBancaria {
private decimal $saldo = 0.0m
public func depositar(decimal $monto): void {
($monto > 0.0m) ? {
$this->saldo = $this->saldo + $monto
}
}
public func obtenerSaldo(): decimal {
return $this->saldo
}
}
var $cuenta = new CuentaBancaria()
$cuenta->depositar(150.50m)
print($cuenta->obtenerSaldo())
Salida en la terminal:
150.5
Al hacer $saldo privado, nadie puede escribir $cuenta->saldo = -5000.0m desde afuera, garantizando que el dinero solo se modifique bajo las reglas del método depositar.
4. Miembros estáticos y el operador ::
No todas las propiedades o métodos le pertenecen a una casa individual; algunas operaciones pertenecen al concepto general de la clase o no requieren crear una instancia con new.
A estos elementos se les llama estáticos y se declaran con la palabra static (recuerda que static siempre acompaña a un modificador de visibilidad como public static):
public class Utilidades {
public static func limpiarTexto(string $t): string {
return trim($t)
}
}
var $limpio = Utilidades::limpiarTexto(" hola ")
print($limpio)
Salida en la terminal:
hola
Para invocar un método estático o leer una propiedad estática, no se utiliza ->, sino el operador de doble dos puntos ::. En Joss, las clases nativas del sistema (como Auth::user(), GranDB::table(), Router::get(), Cache::put()) son fachadas que se invocan habitualmente mediante ::.
5. Herencia con extends
La herencia permite crear una clase nueva basada en una clase existente, reutilizando todos sus métodos y propiedades públicas y protegidas sin tener que reescribirlos:
public class Mensaje {
public func texto(): string { return "hola" }
}
public class Aviso extends Mensaje {}
var $aviso = new Aviso()
print($aviso->texto())
Salida en la terminal:
hola
- La clase
Mensajees la clase base (o superclase). - La clase
Avisoes la clase derivada (o subclase). Avisohereda automáticamente el métodotexto()deMensaje.
[!TIP] Cuándo usar herencia vs cuándo usar composición: Usa herencia solo cuando exista una relación estricta de tipo “es un” (por ejemplo,
Gato extends AnimaloAdminUser extends User). Si solo quieres reutilizar una función utilitaria, no uses herencia; usa funciones o inyecta una clase de servicio.
6. Interfaces y Polimorfismo (interface e implements)
Cuando trabajas en aplicaciones modulares o arquitectura limpia, muchas veces quieres definir qué debe hacer un componente sin atarte a cómo lo hace.
Una interfaz es un contrato formal:
- Solo declara los prototipos de los métodos públicos (nombre, parámetros tipados y tipo de retorno) sin cuerpo.
- Una interfaz no se puede instanciar directamente con new.
- Cualquier clase que declare implements NombreInterfaz está obligada por el analizador semántico y el runtime a implementar todos los métodos prometidos con firmas compatibles.
- Una clase puede heredar de una clase base y a la vez implementar múltiples interfaces separadas por comas: public class MiClase extends Base implements I1, I2.
- Una interfaz puede extender una o varias interfaces: public interface IDerivada extends IBase1, IBase2.
Veamos un ejemplo de polimorfismo ejecutable:
public interface IFigura {
public func calcularArea(): int;
}
public class Rectangulo implements IFigura {
public int $ancho = 0
public int $alto = 0
public func constructor(int $ancho, int $alto) {
$this->ancho = $ancho
$this->alto = $alto
}
public func calcularArea(): int {
return $this->ancho * $this->alto
}
}
public class Cuadrado implements IFigura {
public int $lado = 0
public func constructor(int $lado) {
$this->lado = $lado
}
public func calcularArea(): int {
return $this->lado * $this->lado
}
}
public func imprimirArea(IFigura $figura): int {
return $figura->calcularArea()
}
var $r = new Rectangulo(5, 10)
var $c = new Cuadrado(6)
print(imprimirArea($r))
print(imprimirArea($c))
Salida en la terminal:
50
36
Ventajas del Polimorfismo con Interfaces:
- Desacoplamiento: La función
imprimirArea(IFigura $figura)no necesita saber si recibe unRectangulo, unCuadradoo cualquier figura futura; solo confía en que cumple con el contratoIFigura. - Validación estática exhaustiva: Si olvidas implementar un método de la interfaz en una clase o su firma no coincide, el analizador semántico emite
JOSS-DECL-004(oJOSS-SEM-001si un método implementado no espublic).
7. Clases y métodos abstractos (abstract)
Una clase abstracta (public abstract class) sirve como plantilla base para otras clases pero no puede ser instanciada directamente con new (intentarlo emite el diagnóstico JOSS-DECL-006).
Las clases abstractas pueden contener:
- Propiedades y métodos completos con implementación para ser heredados.
- Métodos abstractos (public abstract func nombre(...): Tipo) que carecen de cuerpo y obligan a las subclases concretas a implementarlos (omitirlos emite JOSS-DECL-005).
public abstract class Animal {
public abstract func hablar(): string
}
public class Perro extends Animal {
public func hablar(): string {
return "Guau!"
}
}
var $perro = new Perro()
print($perro->hablar())
Salida en la terminal:
Guau!
8. Comprobación de tipos e instancias: is e instanceof
Para verificar en tiempo de ejecución si un objeto pertenece a una clase concreta, hereda de una clase base o implementa una interfaz, utiliza los operadores equivalentes is o instanceof:
public interface IMovible {}
public class Auto implements IMovible {}
var $auto = new Auto()
print($auto is Auto)
print($auto is IMovible)
print($auto instanceof string)
Salida en la terminal:
true
true
false
También puedes usar is con tipos primitivos como int, string, bool, etc. (ej. $x is int).
9. Navegación segura contra nulos (?->)
Si una variable puede contener una instancia o ser null (tipo Persona?), intentar acceder a un método con -> sobre un valor nulo produciría un fallo.
Joss incluye el operador null-safe (?->):
public class Perfil {
public func saludo(): string {
return "Hola"
}
}
Perfil? $usuario = null
var $nombre = $usuario?->saludo()
print($nombre ?? "Sin usuario")
Salida en la terminal:
Sin usuario
Si $usuario es null, la llamada se cancela de forma silenciosa y segura, y $nombre recibirá simplemente null sin detener el programa.
10. Clases genéricas (class Contenedor<T>) y Records inmutables (record)
Clases genéricas
Joss admite parámetros de tipo formales <T> en la declaración de clases:
public class Caja<T> {
public T $contenido
public func constructor(T $valor) {
$this->contenido = $valor
}
}
El analizador semántico valida nominalmente los parámetros genéricos asegurando que Caja<int> no sea asignable a Caja<string>.
Records (public record Nombre(campos...))
Un record es una declaración concisa de clase orientada a datos inmutables con constructor y campos automáticos:
public record Coordenada(int $x, int $y)
var $c = new Coordenada(10, 20)
print($c->x)
print($c->y)
Salida en la terminal:
10
20
Los campos declarados en un record quedan sellados como constantes (const) tras la inicialización del objeto, impidiendo cualquier reasignación accidental (JOSS-CONST-001).
11. Ciclo de vida y autodestrucción inteligente por protección
En Joss, las clases se escriben de forma estándar sin sintaxis engorrosa. Internamente, el motor acopla un finalizador de ciclo de vida a cada instancia creada con new.
Cuando un objeto agota su ciclo de vida y queda sin referencias en el programa:
1. Destructor opcional: Si la clase define un método destructor(), destroy() o __destruct(), el motor lo ejecuta automáticamente de forma aislada y segura.
2. Cierre de recursos nativos: Si la instancia retenía recursos del sistema (archivos, canales de comunicación, streams), se cierran automáticamente previniendo fugas de descriptores.
3. Purga de memoria (Zeroization): Los campos internos de la instancia son vaciados y sanitizados para evitar que datos sensibles (tokens, contraseñas) persistan innecesariamente en memoria RAM.
4. Protección contra accesos zombi: La instancia queda marcada como destruida. Si algún puntero residual intenta leer o modificar sus miembros, el motor lanza un SecurityError protegiendo la integridad del sistema.
public class SesionSegura {
public string $token = "tok_12345"
public func constructor() {
print("Conexión activa")
}
public func destructor() {
print("Cerrando sesión de forma segura...")
}
}
var $s = new SesionSegura()
$s->destructor()
Salida en la terminal:
Conexión activa
Cerrando sesión de forma segura...
12. Errores comunes en POO con Joss
| Error | Causa | Solución |
|---|---|---|
Confundir -> con :: |
Escribir $objeto::metodo() o Clase->metodo(). |
Usa -> para instancias reales creadas con new y :: para llamadas estáticas a clases. |
| Intentar acceder a un miembro privado | $cuenta->saldo cuando es private. |
Crea un método público getter (como obtenerSaldo()) para consultar el valor. |
Olvidar new al instanciar |
var $p = Persona() en vez de var $p = new Persona(). |
La creación de instancias exige la palabra new. |
| Confundir una instancia con un map | Tratar un objeto como array asociativo ($objeto["campo"]). |
Los objetos usan flecha ($objeto->campo), los maps usan corchetes ($mapa["campo"]). |
| Incumplir contrato de interfaz | La clase declara implements pero le falta un método o sus parámetros no coinciden. |
Implementar todos los métodos de la interfaz con visibilidad public y tipos compatibles (JOSS-DECL-004). |
| Instanciar una clase abstracta | Hacer new ClaseAbstracta() directamente. |
Extiende la clase abstracta en una subclase concreta e instancia la subclase (JOSS-DECL-006). |
| Acceso a objeto destruido | Intentar leer o escribir un objeto tras ejecutarse su ciclo de destrucción. | Crear una nueva instancia válida en lugar de reutilizar un objeto ya invalidado (SecurityError). |
13. Ejercicios prácticos
Nivel 1 — Prueba rápida
Declara un public record Producto(string $nombre, decimal $precio). Instancia un producto "Teclado" con precio 45.50m e imprime su nombre y su precio usando ->.
Nivel 2 — Ejercicio guiado
- Crea una clase
public class Vehiculocon una propiedad protegidaprotected string $marca = ""inicializada enpublic func constructor(string $marca)y un métodopublic func obtenerMarca(): string. - Crea una clase derivada
public class Auto extends Vehiculoque tenga una propiedadpublic int $puertas = 4. - Instancia un
Auto("Toyota"), e imprime su marca (usandoobtenerMarca()) y su número de puertas en la consola.
Nivel 3 — Reto
- Define una interfaz
public interface INotificadorcon el métodopublic func enviar(string $mensaje): string;. - Crea dos clases que implementen
INotificador:NotificadorEmail: devuelve"[EMAIL] " . $mensaje.NotificadorSMS: devuelve"[SMS] " . $mensaje.
- Escribe una función
public func despacharAlerta(INotificador $canal, string $texto): voidque imprima el resultado de$canal->enviar($texto). - Pruébala pasándole una instancia de
NotificadorEmaily otra deNotificadorSMS.
Siguiente paso
Ahora que dominas clases, contratos de interfaces y objetos, estás listo para descubrir una de las capacidades más potentes de Joss: ejecutar tareas en paralelo, delegar operaciones a segundo plano y comunicar procesos mediante canales seguros.
Continúa con: Concurrencia, operaciones asíncronas, Future y Canales.