Interfaces implícitas y el poder de implements

En Dart, cada clase es, de forma intrínseca, una interfaz. No necesitas declarar una palabra clave interface para definir un contrato; basta con definir una clase y todos sus miembros públicos (métodos, getters y setters) se convierten automáticamente en la definición de una interfaz para cualquier otra clase. Cuando usas implements, le estás diciendo al compilador que quieres cumplir con ese contrato, pero que no te interesa la lógica interna de la clase original, sino solo su firma.

Esto funciona así para simplificar el lenguaje y permitir una composición de comportamientos mucho más fluida que en otros lenguajes donde las interfaces y las clases son entidades separadas. Debes usar implements cuando necesites crear un objeto que se comporte como otro (como en un mock para tests) o cuando quieras implementar múltiples contratos en una sola clase. Si te equivocas y usas implements cuando lo que querías era heredar lógica, terminarás teniendo que reescribir todo el código de la clase padre, porque implements no hereda implementación, solo la estructura. Además, si intentas implementar una interfaz pero olvidas definir uno de sus miembros, el compilador detendrá la ejecución antes de que el error llegue a producción.

// Un contrato abstracto para el almacenamiento de datos
abstract class Storage {
  void save(String key, String value);
  String get status;
}

// Un contrato para la gestión de logs
abstract class Logger {
  void log(String message);
}

// Implementación real: heredamos la lógica de una clase base si fuera necesario,
// pero aquí usaremos una clase concreta que implementa el contrato.
class DatabaseStorage implements Storage {
  @override
  void save(String key, String value) {
    print('Guardando en SQL -> $key: $value');
  }

  @override
  String get status => 'Conectado a la base de datos';
}

// Implementación para tests: usamos múltiples interfaces
// para que MockStorage se comporte como Storage y como Logger.
class MockStorage implements Storage, Logger {
  @override
  void save(String key, String value) {
    print('[MOCK] Guardando $key: $value en memoria');
  }

  @override
  void log(String message) {
    print('[MOCK LOG] $message');
  }

  @override
  String get status => 'Simulado (Memoria)';
}

// Usamos interface class [disponible desde Dart 3.0]
// Esto permite que otras personas implementen el contrato de SecureService
// desde otros paquetes, pero les prohíbe extender (heredar) su lógica
// interna, protegiendo nuestra implementación privada.
interface class SecureService {
  void authenticate() {
    print('Autenticando mediante protocolo seguro...');
  }
}

class ExternalAuth implements SecureService {
  @override
  void authenticate() {
    print('Autenticación externa personalizada');
  }
}

void main() {
  final db = DatabaseStorage();
  final mock = MockStorage();
  final auth = ExternalAuth();

  print('Estado DB: ${db.status}');
  db.save('user_1', 'Juan');

  print('Estado Mock: ${mock.status}');
  mock.save('session_id', 'abc-123');
  mock.log('Acción de test ejecutada');

  auth.authenticate();
}

Análisis de la implementación

En el ejemplo anterior, fíjate en DatabaseStorage. Aunque implementa Storage, su objetivo es proporcionar una lógica real de persistencia. Si usáramos extends Storage (suponiendo que Storage fuera una clase con lógica), estaríamos heredando el comportamiento; pero al usar implements, estamos obligados a usar @override para cada método.

El caso de MockStorage es clave para el testing. Al declarar class MockStorage implements Storage, Logger, estamos aplicando implementación múltiple. Es una de las mayores ventajas de este modelo: una clase puede cumplir con tantos contratos como necesitemos, algo que la herencia simple no permite. Aquí, MockStorage no hereda nada de Storage ni de Logger; simplemente “promete” que tendrá esos métodos.

Finalmente, observa SecureService. Gracias a la modificación interface class de Dart 3.0, hemos establecido una frontera clara. Si alguien intenta hacer class BadService extends SecureService desde otro archivo, el compilador lanzará un error. Esto es vital en librerías profesionales para evitar que los usuarios dependan de la implementación interna de una clase, obligándolos a usar el contrato mediante implements.

El error frecuente

Un error muy común ocurre cuando intentas usar implements para reutilizar código de una clase base en lugar de heredarlo.

class BaseService {
  void init() => print('Servicio inicializado');
  void execute() => print('Ejecutando...');
}

// ERROR: El desarrollador quería heredar 'init' y 'execute'
class WrongService implements BaseService {
  // El compilador dirá que faltan 'init' y 'execute'
  // Aunque los escribas, no tendrás la lógica de 'print' original.
}

Si tu intención es que WrongService tenga el comportamiento de BaseService, debes usar extends. Si usas implements, estás creando una clase desde cero que casualmente tiene los mismos nombres, pero sin la funcionalidad original.

44

Dejar un comentario

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

Scroll al inicio