Flutter Arquitectura Limpia [4] – Descripción general de la capa de datos y modelos

Flutter Arquitectura Limpia [4] – Descripción general de la capa de datos y modelos

 

flutterclean4

Si bien la capa de dominio es el centro seguro de una aplicación que es independiente de otras capas, la capa de datos es un lugar donde la aplicación se encuentra con el duro mundo exterior de las API y las bibliotecas de terceros. Consiste en fuentes de datos de bajo nivel, repositorios que son la única fuente de verdad para los datos y, finalmente, modelos.

Debo aclarar que el contenido original es de Resocoder,  lo que he hecho  es una traducción al español del contenido. Al final de este artículo está el video en inglés para que vayan ilustrándose mejor y ya tengan una claridad de lo expuesto

Curso TDD Arquitectura Limpia

001 draw
Esta publicación es solo una parte de una serie de tutoriales. ¡Vea todas las otras partes y aprenda a diseñar sus aplicaciones Flutter!

Yendo hacia afuera

001 onion

 

Es posible que haya notado que siempre comenzamos a trabajar desde las partes internas de la aplicación y nos dirigimos hacia las afueras. Después de todo, comenzamos con el dominio completamente independiente e incluso allí creamos la Entidad. Ahora, comenzaremos con el Modelo y solo luego continuaremos e implementaremos el Repositorio y las Fuentes de Datos de bajo nivel. ¿Por qué?

Toda la arquitectura limpia se basa en un principio básico: las capas externas dependen de las capas internas, como lo indican esas flechas verticales —> en la imagen a continuación.

 

CleanArchitecture

Por lo tanto, tiene sentido comenzar en el centro, ya que de lo contrario tendríamos dificultades para implementar los casos de uso, por ejemplo, si no tuviéramos ninguna entidad que se pueda devolver primero.

Todo esto es posible solo a través del poder de los contratos, que en el caso de Dart se implementan como clases abstractas. Piénselo, hemos implementado completamente la capa de dominio usando TDD y, sin embargo, todo de lo que dependen los casos de uso es solo una clase abstracta NumberTriviaRepository que hemos aplicado mocking con una implementación falsa todo el tiempo.

Descripción general de la capa de datos

Sabemos cómo se verá la interfaz pública de la implementación del repositorio (se define en el siguiente contrato de la capa de dominio).

number_trivia_repository.dart

abstract class NumberTriviaRepository {
  Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
  Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia();
}

Por supuesto, estos datos no pueden extraerse de la nada, por lo que antes de crear la implementación del Repositorio, primero tendremos que crear las Fuentes de datos remotos y locales.

Nuevamente, no necesitaremos crear sus implementaciones de inmediato. Será suficiente para hacer sus contratos, que deben cumplir, utilizando clases abstractas. Luego usaremos mock en estas fuentes de datos mientras escribimos pruebas para el repositorio en las partes posteriores de este curso. Pero primero…

 

data layer diagram

La necesidad de los modelos

002 network

Los tipos de retorno de método de las fuentes de datos serán muy similares a los del repositorio pero con dos ENORMES diferencias. No van a devolver los errores “en línea” mediante el uso de fallas, sino que lanzarán excepciones. Además, en lugar de devolver entidades NumberTrivia, van a devolver objetos NumberTriviaModel.

Todo esto sucede porque las fuentes de datos están en el límite absoluto entre el mundo agradable y acogedor de nuestro propio código y el mundo exterior aterrador de las API y las bibliotecas de terceros.

Los modelos son entidades con alguna funcionalidad adicional agregada en la parte superior. En nuestro caso, esa será la capacidad de ser serializado y deserializado hacia / desde JSON. La API responderá con datos en formato JSON, por lo que debemos tener una forma de convertirlo en objetos Dart.

Algunos puristas dicen que los modelos deberían contener solo algunos campos adicionales (ID de una base de datos …) y no ninguna lógica de conversión, que debería colocarse en clases Mapper separadas.

Todo depende de las circunstancias: no tenemos campos adicionales para agregar y creo que el código de conversión dentro de un modelo es más fácil de usar.

Es posible que haya notado que NumberTriviaModel contendrá alguna lógica de conversión JSON. Esta palabra debería comenzar a encender luces rojas en su cabeza porque significa emplear nuevamente el desarrollo basado en pruebas.

Implementando el modelo con TDD

El archivo para el modelo en sí vivirá debajo de la carpeta de modelos para la función number_trivia. Como siempre, su prueba adjunta estará en la misma ubicación, solo en relación con la carpeta de prueba.

The file for the model itself will live under the models folder for the number_trivia feature. As always, its accompanying test will be at the same location, only relative to the test folder.

number trivia model

Dado que la relación entre el Modelo y la Entidad es muy importante, la probaremos para poder dormir bien por la noche.

number_trivia_model_test.dart

import 'package:clean_architecture_tdd_prep/features/number_trivia/data/models/number_trivia_model.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  final tNumberTriviaModel = NumberTriviaModel(number: 1, text: 'Test Text');

  test(
    'should be a subclass of NumberTrivia entity',
    () async {
      // assert
      expect(tNumberTriviaModel, isA<NumberTrivia>());
    },
  );
}

To make the above test compile and pass, the NumberTriviaModel will extend NumberTrivia and simply pass all the constructor parameters to the super class.

number_trivia_model.dart

import 'package:meta/meta.dart';

import '../../domain/entities/number_trivia.dart';

class NumberTriviaModel extends NumberTrivia {
  NumberTriviaModel({
    @required String text,
    @required int number,
  }) : super(
          text: text,
          number: number,
        );
}

Primero, la lógica de conversión que implementaremos será el método fromJson que debería devolver una instancia de NumberTriviaModel con los mismos datos que están presentes dentro de la cadena JSON.

No vamos a obtener la cadena JSON de la API de Números “en vivo”. En su lugar, crearemos un dispositivo que es solo un archivo JSON normal utilizado para las pruebas. Esto se debe a que queremos tener una cadena JSON predecible para probar, por ejemplo, ¿qué pasa si la API de Numbers está en mantenimiento? No queremos que ninguna fuerza externa se meta con los resultados de nuestras pruebas.

“Un dispositivo de prueba es algo que se usa para probar constantemente algún elemento, dispositivo o pieza de software”.
Wikipedia

 

Creando Fixtures/Accesorios

El contenido del fixture imitará la respuesta JSON de la API. Veamos cómo se ve la respuesta desde el punto final aleatorio (http://numbersapi.com/random/trivia?json).

response.json

{
 "text": "418 is the error code for \"I'm a teapot\" in the Hyper Text Coffee Pot Control Protocol.",
 "number": 418,
 "found": true,
 "type": "trivia"
}

Tenemos que asegurarnos de conocer todos los diferentes casos límite para una respuesta. Por ejemplo, el número no siempre tiene que ser un buen número entero. A veces, puede ser algo que Dart consideraría como un doble, aunque en realidad es solo un número entero.

response.json

{
 "text": "4e+185 is the number of planck volumes in the observable universe.",
 "number": 4e+185,
 "found": true,
 "type": "trivia"
}

Ahora tomaremos estas respuestas y las “congelaremos en su lugar” creando dos elementos fijos: trivia.json y trivia_double.json. Todos irán a una carpeta llamada fixtures/accesorios que está anidada dentro de la carpeta de prueba.

fixtures folder

Si bien estos archivos de dispositivo JSON deben tener los mismos campos que las respuestas reales, simplificaremos sus valores para que nos sea más fácil escribir pruebas con ellos.

trivia.json

{
  "text": "Test Text",
  "number": 1,
  "found": true,
  "type": "trivia"
}

Si bien la API respondió un número 4e + 185 (que es un doble a los ojos de Dart), podemos lograr lo mismo con un número 1.0: sigue siendo prácticamente un número entero, pero Dart lo manejará como un doble

trivia_double.json

{
  "text": "Test Text",
  "number": 1.0,
  "found": true,
  "type": "trivia"
}

Lectura de archivos de accesorios

Ahora hemos puesto respuestas JSON falsas en archivos. Para usar el JSON contenido dentro de ellos, tenemos que tener una manera de obtener el contenido de estos archivos como una cadena string. Para eso, vamos a crear una función de nivel superior llamada fixture dentro de un archivo fixture_reader.dart (va a la carpeta de fixture).

fixture_reader.dart

import 'dart:io';

String fixture(String name) => File('test/fixtures/$name').readAsStringSync();

fromJson

Con todos los accesorios en su lugar, finalmente podemos comenzar con el método fromJson. En el espíritu de TDD, escribiremos las pruebas primero. Como es una costumbre en Dart, fromJson siempre toma un Map <String, dynamic> como argumento y genera un tipo, en este caso, NumberTriviaModel.

number_trivia_model_test.dart

void main() {
  final tNumberTriviaModel = NumberTriviaModel(number: 1, text: 'Test Text');
  ...
  group('fromJson', () {
    test(
      'should return a valid model when the JSON number is an integer',
      () async {
        // arrange
        final Map<String, dynamic> jsonMap =
            json.decode(fixture('trivia.json'));
        // act
        final result = NumberTriviaModel.fromJson(jsonMap);
        // assert
        expect(result, tNumberTriviaModel);
      },
    );
  });
}

Como puede ver, hemos obtenido el mapa JSON del archivo accesorio trivia.json. ¡Esta prueba fallará, de hecho, no se compilará! Implementemos el método fromJson.

number_trivia_model.dart

class NumberTriviaModel extends NumberTrivia {
  ...
  factory NumberTriviaModel.fromJson(Map<String, dynamic> json) {
    return NumberTriviaModel(
      text: json['text'],
      number: json['number'],
    );
  }
}

Después de ejecutar la prueba, ¡esta pasa! Entonces, estamos bien, ¿verdad? Aún no. También tenemos que probar todos los casos límite, como cuando el número dentro del JSON se considerará un doble (cuando el valor es 4e + 185 o 1.0) Hagamos una prueba para eso.

number_trivia_model_test.dart

group('fromJson', () {
  ...
  test(
    'should return a valid model when the JSON number is regarded as a double',
    () async {
      // arrange
      final Map<String, dynamic> jsonMap =
          json.decode(fixture('trivia_double.json'));
      // act
      final result = NumberTriviaModel.fromJson(jsonMap);
      // assert
      expect(result, tNumberTriviaModel);
    },
  );
});

Oh no, ahora la prueba falla diciendo que “tipo ‘double’ no es un subtipo de tipo ‘int'”.  ¿podría ser porque el número de campo es de tipo int? ¿Qué pasa si explícitamente convertimos el doble en un int? ¿Funcionará entonces?

number_trivia_model.dart

...
factory NumberTriviaModel.fromJson(Map<String, dynamic> json) {
  return NumberTriviaModel(
    text: json['text'],
    // The 'num' type can be both a 'double' and an 'int'
    number: (json['number'] as num).toInt(),
  );
}
...

Claro, esto solucionó el error de conversión implícito y ahora la prueba pasa.

passing tests

toJson

Pasemos ahora al segundo método de conversión: a Json. Por convención, este es un método de instancia que devuelve un Map <String, dynamic>. Escribir la prueba primero …

number_trivia_model_test.dart

...
final tNumberTriviaModel = NumberTriviaModel(number: 1, text: 'Test Text');
...
group('toJson', () {
  test(
    'should return a JSON map containing the proper data',
    () async {
      // act
      final result = tNumberTriviaModel.toJson();
      // assert
      final expectedJsonMap = {
        "text": "Test Text",
        "number": 1,
      };
      expect(result, expectedJsonMap);
    },
  );
});
...

Before even running it, we’re again going to implement the toJson method to get rid of “method not present” errors.

number_trivia_model.dart

class NumberTriviaModel extends NumberTrivia {
  ...
  Map<String, dynamic> toJson() {
    return {
      'text': text,
      'number': number,
    };
  }
}

Y, por supuesto, la prueba pasa. Dado que no hay ningún caso límite cuando se convierte a JSON (después de todo, estamos controlando los tipos de datos aquí), esta única prueba es suficiente con el método toJson.

¿Qué sigue?

Ahora que tenemos el Modelo que se puede convertir a / desde JSON en su lugar, vamos a comenzar a trabajar en la implementación del Repositorio y los contratos de Fuente de Datos. ¡Suscríbete a continuación para convertirte en miembro de los desarrolladores de Flutter orientados al crecimiento y recibir correos electrónicos cuando salga un nuevo tutorial y más!

Aquí les comparto también el video.

Previous

Flutter Arquitectura Limpia [3] – Refactorización de capa de dominio

Análisis y Disputa de Datos – Conceptos y ejemplos básicos en Python

Next

Deja un comentario