Intercepción de mensajes con noSuchMethod

noSuchMethod es el mecanismo de “escape” de la máquina virtual de Dart cuando intentas acceder a un miembro que no existe en la jerarquía de clases de un objeto. En lugar de lanzar inmediatamente un NoSuchMethodError, la VM busca este método en la instancia. Si está implementado, la ejecución se desvía hacia él, recibiendo un objeto Invocation que encapsula todo el contexto de la llamada: el nombre del miembro (memberName como Symbol), si se trata de un getter, un setter o un método, y sus argumentos (tanto posicionales como nombrados).

Este método es la pieza fundamental para implementar patrones como el Proxy o los Mocks de forma dinámica. Por diseño, la VM permite que un objeto responda a mensajes que no están declarados en su código fuente, delegando esa responsabilidad al desarrollador. Para que el analizador estático de Dart te permita tratar a una clase como si implementara una interfaz sin tener que escribir todos sus métodos manualmente, debes declarar la firma como dynamic noSuchMethod(Invocation invocation). Esto engaña al compilador, indicándole que la clase es capaz de responder a cualquier contrato.

Debes usarlo cuando necesites construir objetos que actúen como intermediarios (proxies) o cuando estés desarrollando herramientas de testing que generen mocks en tiempo de ejecución. Si intentas usarlo para ocultar errores de diseño en tu lógica de negocio, estarás sacrificando la seguridad del sistema de tipos. Si implementas noSuchMethod pero no manejas correctamente los tipos de retorno, lo que parece un error de compilación se transformará en un error de tipo Runtime muy difícil de rastrear, especialmente en entornos AOT donde la depuración es más estricta.

import 'dart:core';

/// Una interfaz de base para nuestro ejemplo.
abstract class DataRepository {
  Future<String> fetchUser(int id);
  String get connectionName;
  void logAction(String action, {required String severity});
}

/// Un Mock que no implementa explícitamente los métodos de DataRepository,
/// pero "engaña" al compilador para que crea que lo hace.
class MockRepository {
  // Al declarar noSuchMethod como 'dynamic', el analizador nos permite
  // tratar esta clase como si fuera de cualquier tipo mediante un cast.
  @override
  dynamic noSuchMethod(Invocation invocation) {
    // Identificamos el método mediante el Symbol
    switch (invocation.memberName) {
      case #fetchUser:
        // Extraemos argumentos posicionales
        final id = invocation.positionalArguments.first as int;
        print('Mock: Simulando fetchUser para ID $id');
        return Future.value('Usuario de prueba #$id');

      case #connectionName:
        // Interceptamos un getter
        return 'In-Memory-Mock-DB';

      case #logAction:
        // Interceptamos un método con argumentos nombrados
        final action = invocation.positionalArguments.first;
        final severity = invocation.namedArguments[#severity];
        print('Mock Log [$severity]: $action');
        return null;

      default:
        // Si el método no es uno que nos interesa, lanzamos el error estándar
        throw NoSuchMethodError.noSuchMethod(
          this,
          invocation.memberName,
          'Método no implementado en MockRepository',
        );
    }
  }
}

void main() async {
  // Usamos 'as DataRepository' para tratar al mock como la interfaz real.
  // Esto funciona gracias a la firma 'dynamic' en noSuchMethod.
  final repo = MockRepository() as DataRepository;

  print('--- Iniciando ejecución ---');

  // Caso 1: Llamada a método asíncrono
  final user = await repo.fetchUser(101);
  print('Resultado: $user');

  // Caso 2: Acceso a getter
  print('Conectado a: ${repo.connectionName}');

  // Caso 3: Método con argumentos nombrados
  repo.logAction('Consulta realizada', severity: 'LOW');

  print('--- Fin de la ejecución ---');
}

Desglose del concepto

En el ejemplo anterior, la clase MockRepository no tiene implementado el método fetchUser, connectionName ni logAction. Sin embargo, al usar as DataRepository, el compilador confía en que el objeto responderá a esos mensajes.

El corazón de la magia es invocation.memberName. En Dart, los nombres de los métodos se convierten en Symbol (como #fetchUser). Esto nos permite usar un switch para decidir qué lógica ejecutar según el método llamado. En el caso de fetchUser, accedemos a invocation.positionalArguments, que es una List<dynamic>, para obtener el id.

Fíjate en la importancia del tipo de retorno en noSuchMethod. Como la firma es dynamic, podemos devolver un Future.value(...) para el método fetchUser y un String para el getter connectionName. Si hubiéramos declarado el método como void, el programa fallaría al intentar usar el resultado de fetchUser en un await, ya que el runtime intentaría tratar un Future inexistente como un objeto válido.

Cuando llamamos a repo.logAction, el mapa namedArguments contiene los parámetros que pasamos con la sintaxis key: value. Es vital usar el Symbol correspondiente (en este caso #severity) para recuperar el valor, de lo contrario, el mapa no encontrará la clave.

El error frecuente

Un error crítico ocurre al desajustar la naturaleza asíncrona de los métodos originales. Si el método original es Future<String> fetchUser(...), pero en tu noSuchMethod devuelves simplemente un String (sin envolverlo en un Future), el código compilará porque el tipo de retorno de noSuchMethod es dynamic. Sin embargo, al hacer await repo.fetchUser(101), el runtime intentará tratar ese String como un Future.

// ERROR SUTIL
@override
dynamic noSuchMethod(Invocation invocation) {
  if (invocation.memberName == #fetchUser) {
    return 'Soy un string, no un Future'; // Esto romperá el await en el caller
  }
}

// El error ocurrirá aquí:
// Un `String` no tiene el método `.then()` que espera el `await`
await repo.fetchUser(1); 

Este error suele lanzarse como un NoSuchMethodError: type 'String' has no method 'then', lo cual puede confundir si solo estás mirando la lógica interna del mock y no la firma del método original.

108

Dejar un comentario

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

Scroll al inicio