Flutter Arquitectura Limpia [2] – Entidades y Casos de Uso

flutter arquitectura limpia 1
Compartir

Flutter Arquitectura Limpia TDD [2] Entidades y Casos de Uso

 

En la primera parte, aprendió los conceptos centrales de la arquitectura limpia en lo que respecta a Flutter. También creamos un montón de carpetas vacías para las capas de presentación, dominio y datos dentro de la aplicación Number Trivia que estamos creando. Ahora es el momento de comenzar a llenar esas carpetas vacías con código, usando TDD, por supuesto.

 

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

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!

¿Dónde empezar?

Siempre que esté creando una aplicación con una interfaz de usuario, primero debe diseñar la interfaz de usuario y la experiencia de usuario. He hecho esta tarea por ti y la aplicación se mostró en la parte anterior.

El proceso de codificación real ocurrirá desde las capas internas más estables de la arquitectura hacia afuera. Esto significa que primero implementaremos la capa de dominio comenzando con la Entidad. Sin embargo, antes de hacer eso, tenemos que agregar ciertas dependencias de paquetes a pubspec.yaml. No quiero molestarme con este archivo más tarde, así que completemos todo ahora.

 

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  # Service locator
  get_it: ^2.0.1
  # Bloc for state management
  flutter_bloc: ^0.21.0
  # Value equality
  equatable: ^0.4.0
  # Functional programming thingies
  dartz: ^0.8.6
  # Remote API
  connectivity: ^0.4.3+7
  http: ^0.12.0+2
  # Local cache
  shared_preferences: ^0.5.3+4

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^4.1.0

Entidades/Entity

¿Con qué tipo de datos funcionará la aplicación Number Trivia? Bueno, entidades de NumberTrivia, por supuesto. Para saber qué campos debe tener esta clase, tenemos que echar un vistazo a la respuesta de la API de Numbers. Nuestra aplicación funcionará con respuestas de URL de números concretos o aleatorios, por ejemplo, http://numbersapi.com/42?json.

response.json

{
  "text": "42 is the answer to the Ultimate Question of Life, the Universe, and Everything.",
  "number": 42,
  "found": true,
  "type": "trivia"
}

Solo nos interesan los campos de texto y número. Después de todo, el tipo siempre será “trivia” en nuestro caso y el valor de encontrado es irrelevante. Si no se encuentra un número, obtendremos la siguiente respuesta. Todavía está perfectamente bien mostrarlo en la aplicación.

not_found.json

{
  "text": "123456 is an unremarkable number.",
  "number": 123456,
  "found": false,
  "type": "trivia"
}

NumberTrivia es una de las pocas clases que no vamos a escribir de una manera basada en pruebas y es por una simple razón: no hay nada que probar. Se extiende Equatable para permitir comparaciones de valores fáciles sin toda la plantilla (que tendríamos que probar), ya que Dart solo admite la igualdad referencial por defecto.

number_trivia.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

class NumberTrivia extends Equatable {
  final String text;
  final int number;

  NumberTrivia({
    @required this.text,
    @required this.number,
  }) : super([text, number]);
}

Casos de Uso/Use Cases

Los casos de uso son donde se ejecuta la lógica de negocios. Claro, no habrá mucha lógica en la aplicación Number Trivia: todo lo que hará un UseCase es obtener datos de un repositorio. Vamos a tener dos de ellos: GetConcreteNumberTrivia y GetRandomNumberTrivia.

Flujo de Datos y Manejo de Errores

 

Sabemos que los casos de uso obtendrán entidades NumberTrivia de los repositorios y pasarán estas entidades a la capa de presentación. Entonces, el tipo devuelto por UseCase debería ser un Future <NumberTrivia> para permitir la asincronía, ¿verdad?

¡No tan rapido! ¿Qué hay de los errores? ¿Es la mejor opción dejar que las excepciones se propaguen libremente, teniendo que recordar atraparlas en otro lugar del código? No lo creo. En cambio, queremos capturar excepciones lo antes posible (en el Repositorio) y luego devolver los objetos de Falla de los métodos en cuestión.

Muy bien, recapitulemos. Los repositorios y los casos de uso devolverán los objetos NumberTrivia y Failure de sus métodos. ¿Cómo es posible algo así?

El tipo Either 

El paquete dartz, que hemos agregado como dependencia, trae programación funcional (FP) a Dart. No voy a fingir que soy un profesional de FP, al menos todavía no. Tampoco necesitas saber muchas cosas. Todo lo que nos interesa para un mejor manejo de errores es el tipo Either <L, R>.

Este tipo se puede usar para representar dos tipos al mismo tiempo y es perfecto para el manejo de errores, donde L es la falla y R es el número de trivia. De esta manera, las fallas no tienen su propio “flujo de error” especial como las excepciones. Se manejarán como cualquier otro dato sin usar try/catch. Dejemos los detalles de cómo trabajar con Either para cuando lo necesitemos en las siguientes partes de este curso.

Definiendo Fallos

Antes de que podamos proceder a escribir los casos de uso, primero tenemos que definir las fallas/Failures, ya que serán una parte del tipo de retorno Either. Las fallas se usarán en varias funciones y capas de la aplicación, así que creémoslas en la carpeta principal debajo de una nueva subcarpeta de errores.

Habrá una clase de falla abstracta base de la cual se derivará cualquier falla concreta, al igual que con las excepciones regulares y la clase de excepción base.

failures.dart

import 'package:equatable/equatable.dart';

abstract class Failure extends Equatable {
  // If the subclasses have some properties, they'll get passed to this constructor
  // so that Equatable can perform value comparison.
  Failure([List properties = const <dynamic>[]]) : super(properties);
}

Esto es suficiente por ahora, definiremos algunas fallas concretas, como ServerFailure, en las siguientes partes de este curso.

Contrato de Repositorio

Como esperamos recordar de la última parte, y como se indica en el diagrama anterior, un Repositorio, del que UseCase obtiene sus datos, pertenece tanto al dominio como a la capa de datos. Para ser más precisos, su definición (también conocida como contrato) está en el dominio, mientras que la implementación está en los datos.

Esto permite una independencia total de la capa de dominio, pero hay otro beneficio del que aún no hemos hablado: la . ¡Eso es correcto! La capacidad de prueba y la separación de las preocupaciones van muy bien juntas. Oh, la belleza de la buena arquitectura …

Escribir un contrato del Repositorio, que en el caso de Dart es una clase abstracta, nos permitirá escribir pruebas (estilo TDD) para los UseCases sin tener una implementación real del Repositorio.

 

Las pruebas sin implementación concreta de clases son posibles con mocking Un paquete popular para esto es el llamado mockito, que hemos agregado a nuestro proyecto como dev_dependency.

Entonces, ¿cómo será el contrato? Tendrá dos métodos: uno para getting concrete trivia, otro para getting random trivia y el tipo de retorno de estos métodos es Future<Either<Failure, NumberTrivia>>, ¡asegurando que el manejo de errores será muy fácil!

number_trivia_repository.dart
import 'package:dartz/dartz.dart';

import '../../../../core/error/failure.dart';
import '../entities/number_trivia.dart';

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

GetConcreteNumberTrivia

Aunque esta parte ya es bastante larga y está llena de información, no quiero dejarte colgado. Finalmente vamos a escribir algunas pruebas mientras implementamos el caso de uso GetConcreteNumberTrivia. En la siguiente parte, agregaremos el caso de uso GetRandomNumberTrivia, ¡así que definitivamente esté atento para eso!

Como es el caso con TDD, vamos a escribir la prueba antes de escribir el código de producción. Esto asegura que no agregaremos un montón de cosas que “no vamos a necesitar” y también tendremos la confianza de que nuestro código no se desmoronará como fichas de dominó.

 

Escribiendo el Test

En las aplicaciones Dart, los tests van a la carpeta de prueba tests folder y es una costumbre hacer que las carpetas de prueba asignen las carpetas lib. Creemos todos los root y también una carpeta llamada “usecases” en “dominio”.

Cree un nuevo archivo en la carpeta de prueba “usecases” llamada get_concrete_number_trivia_test.dart y, mientras lo hacemos, también una get_concrete_number_trivia.dart en la carpeta lib “usecases”.

Los archivos de prueba escritos con TDD siempre se asignan a archivos de producción y agregan una “prueba” al final de su nombre.

Configuremos la prueba primero. Sabemos que el caso de uso debe obtener sus datos del NumberTriviaRepository. Le aplicaremos mockito, ya que solo tenemos una clase abstracta para él y también porque mockito nos permite verificar, entre otras cosas, si se ha llamado a un método.

get_concrete_number_trivia_test.dart

import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

Para operar con esta instancia de NumberTriviaRepository, el caso de uso GetConcreteNumberTrivia lo hará pasar a través de un constructor. Las pruebas en Dart tienen un método útil llamado setUp que se ejecuta antes de cada prueba individual. Aquí es donde crearemos una instancia de los objetos.

TEN EN CUENTA que el código que estamos escribiendo estará lleno de errores; ni siquiera tenemos una clase GetConcreteNumberTrivia todavía.

get_concrete_number_trivia_test.dart

import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

void main() {
  GetConcreteNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;

  setUp(() {
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
  });
}

Aunque todavía no hemos escrito ninguna prueba, ahora es un buen momento para comenzar a escribir el código de producción. Queremos hacer un esqueleto para la clase GetConcreteNumberTrivia, para que el código de configuración anterior esté libre de errores.

get_concrete_number_trivia.dart

import '../repositories/number_trivia_repository.dart';

class GetConcreteNumberTrivia {
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);
}

Ahora llega el momento de escribir la prueba real. Dado que la naturaleza de nuestra aplicación Number Trivia es simple, no habrá mucha lógica en el caso de uso, en realidad, no habrá lógica real en absoluto. Solo obtendrá datos del Repositorio.

Por lo tanto, la primera y única prueba garantizará que realmente se llame al Repositorio y que los datos simplemente pasen sin cambios a través del Caso de uso.

get_concrete_number_trivia_test.dart

import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

void main() {
  GetConcreteNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;

  setUp(() {
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
  });

  final tNumber = 1;
  final tNumberTrivia = NumberTrivia(number: 1, text: 'test');

  test(
    'should get trivia for the number from the repository',
    () async {
      // "On the fly" implementation of the Repository using the Mockito package.
      // When getConcreteNumberTrivia is called with any argument, always answer with
      // the Right "side" of Either containing a test NumberTrivia object.
      when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
          .thenAnswer((_) async => Right(tNumberTrivia));
      // The "act" phase of the test. Call the not-yet-existent method.
      final result = await usecase.execute(number: tNumber);
      // UseCase should simply return whatever was returned from the Repository
      expect(result, Right(tNumberTrivia));
      // Verify that the method has been called on the Repository
      verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
      // Only the above method should be called and nothing more.
      verifyNoMoreInteractions(mockNumberTriviaRepository);
    },
  );
}

Cuando lo piensas, la prueba anterior se lee como documentación, incluso sin todos mis comentarios. Ejecutar la prueba ahora no tiene sentido, ya que incluso hay errores de compilación, por lo que podemos saltar a la implementación.

Todo lo que necesitamos agregar al caso de uso GetConcreteNumberTrivia es la siguiente función que hará todo lo prescrito por la prueba.

get_concrete_number_trivia.dart

import 'package:dartz/dartz.dart';
import 'package:meta/meta.dart';

import '../../../../core/error/failure.dart';
import '../entities/number_trivia.dart';
import '../repositories/number_trivia_repository.dart';

class GetConcreteNumberTrivia {
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);

  Future<Either<Failure, NumberTrivia>> execute({
    @required int number,
  }) async {
    return await repository.getConcreteNumberTrivia(number);
  }
}

Cuando ejecute la prueba (si no sabe cómo, consulte su documentación IDE), ¡pasará! Y con eso acabamos de escribir el primer caso de uso de la aplicación Number Trivia usando TDD.

En la siguiente parte, vamos a refactorizar el código anterior, crear una clase base UseCase para hacer que la aplicación sea fácilmente extensible y agregar un caso de uso GetRandomNumberTrivia.

 

Aquí les comparto también el video.


Compartir

Entradas relacionadas

Deja tu comentario