Estructura de clases: campos, métodos y comportamiento

Una clase es el plano o molde que define la estructura y el comportamiento de los objetos que crearás. Imagina que es el diseño de una ficha técnica para una entidad de tu sistema. Dentro de una clase, definimos campos (los datos o propiedades que posee el objeto) y métodos (las acciones o funciones que el objeto puede realizar). En Dart, cada campo de instancia debe tener su tipo explícito declarado; no puedes confiar en la inferencia de tipos para las propiedades de una clase.

Esto funciona así porque el compilador necesita conocer la naturaleza de cada dato para gestionar la memoria de forma eficiente. Cuando instancias una clase, creas un objeto único que sigue esas reglas. Deberías usar clases siempre que necesites agrupar datos relacionados y lógica de negocio coherente, evitando tener variables sueltas que no tengan una relación clara entre sí. Si implementas mal la estructura —por ejemplo, olvidando definir cómo se comparan dos objetos o cómo se identifica su valor único—, romperás el contrato de integridad de las colecciones de Dart, lo que provocará comportamientos erráticos en Set o Map.

class CuentaBancaria {
  final String titular;
  final int id; // Inicialización directa
  double _saldo; // Inicialización en constructor (campo privado)
  late String _categoria; // Inicialización tardía (late)

  // El constructor usa 'this.titular' para asignar el parámetro al campo.
  // Esto evita el "shadowing" (sombreado), donde el parámetro tiene el mismo
  // nombre que el campo y el compilador no sabría a cuál te refieres.
  CuentaBancaria(this.titular, this._saldo, this.id);

  // Getter: permite leer el saldo (que es privado) sin modificarlo directamente.
  double get saldo => _saldo;

  // Método para asignar la categoría después de la creación del objeto.
  void establecerCategoria(String categoria) {
    _categoria = categoria.toUpperCase();
  }

  // Método de instancia para realizar un depósito.
  void depositar(double monto) {
    if (monto > 0) {
      _saldo += monto;
    }
  }

  // Getter con lógica para obtener una descripción completa.
  String get resumen => 'Titular: $titular | Saldo: \$$_saldo | Cat: $_categoria';

  // Sobrescribir toString para que el objeto sea legible al usar print().
  @override
  String toString() => 'Cuenta(id: $id, titular: $titular)';

  // Implementación de igualdad: dos cuentas son iguales si tienen el mismo ID.
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is CuentaBancaria && other.id == id;

  // Es obligatorio sobrescribir hashCode si sobrescribes operator ==.
  @override
  int get hashCode => id.hashCode;
}

void main() {
  final cuentaA = CuentaBancaria('Ana Martínez', 1500.0, 501);
  final cuentaB = CuentaBancaria('Juan Pérez', 50.0, 501); // Mismo ID que cuentaA
  final cuentaC = CuentaBancaria('Carlos Ruiz', 300.0, 502);

  // Configuración de la variable 'late'
  cuentaA.establecerCategoria('Premium');

  print(cuentaA.resumen);
  
  cuentaA.depositar(500.0);
  print('Saldo actualizado de ${cuentaA.titular}: \$${cuentaA.saldo}');

  // Comparación de objetos: cuentaA y cuentaB son "iguales" por su ID.
  print('¿Es cuentaA igual a cuentaB? ${cuentaA == cuentaB}');
  print('¿Es cuentaA igual a cuentaC? ${cuentaA == cuentaC}');
  
  // Uso del toString sobreescrito
  print('Representación rápida: $cuentaA');
}

Análisis del código

En el ejemplo anterior, la clase CuentaBancaria encapsula toda la lógica de una cuenta. Observa cómo titular e id son final, lo que significa que su valor no puede cambiar tras la construcción. El campo _saldo tiene un prefijo _, lo que en Dart significa que es privado a nivel de librería; no puedes acceder a él directamente desde otro archivo.

Para leer ese saldo, utilizamos un getter llamado saldo. Este es un método especial que se usa como si fuera una propiedad, permitiendo proteger el valor real _saldo de modificaciones accidentales desde el exterior. Por otro lado, el campo _categoria usa la palabra clave late. Esto le dice al compilador: “Sé que este campo no tiene un valor inicial ahora, pero te prometo que lo asignaré antes de que alguien intente leerlo”. Si intentaras llamar a cuentaA.resumen antes de ejecutar establecerCategoria, la aplicación lanzaría un error en tiempo de ejecución.

En el método depositar, el uso de this es implícito para los campos de la clase, pero en el constructor usamos this.titular para que Dart sepa que el parámetro debe asignarse a la propiedad de la clase, resolviendo la ambigüedad de nombres. Finalmente, la lógica de == y hashCode es crucial: hemos definido que la identidad de la cuenta reside en su id. Si dos objetos tienen el mismo id, Dart los considerará el mismo objeto en operaciones de comparación.

El error frecuente

Un error clásico al empezar con POO en Dart es sobrescribir el operador de igualdad == pero olvidar implementar get hashCode.

// ERROR: Sobrescribir == pero no hashCode
@override
bool operator ==(Object other) =>
    other is CuentaBancaria && other.id == id;

// ERROR: Se ha olvidado de implementar:
// @override
// int get hashCode => id.hashCode;

Si cometes este error, romperás las colecciones de Dart. Por ejemplo, si intentas añadir cuentaA y cuentaB (que tienen el mismo ID) a un Set, el Set no detectará que son iguales porque sus hashCode serán distintos. Esto resultará en un Set que contiene dos objetos que, según tu lógica de negocio, deberían ser el mismo, provocando duplicados inesperados y bugs muy difíciles de rastrear en producción.

42

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio