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

flutterclean3
Compartir

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

 

Nuestra aplicación Number Trivia se está moviendo muy bien. En la parte anterior, creamos una entidad, un contrato de repositorio y el primer caso de uso: GetConcreteNumberTrivia utilizando un desarrollo basado en pruebas. Hoy, agregaremos otro caso de uso que descubrirá una buena oportunidad para refactorizar el código.

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!

 

 

Clases invocables

¿Sabías que en Dart, un método llamado call  que se puede ejecutar llamando a object.call () pero también a object ()? ¡Ese es el método perfecto para usar en los casos de uso! Después de todo, sus nombres de clase ya son verbos como GetConcreteNumberTrivia, por lo que usarlos como “métodos falsos” encaja perfectamente.

En el espíritu de TDD, primero modificaremos la prueba (get_concrete_number_test.dart) para que ya no llame al método de ejecución:

final result = await usecase(number: tNumber);

Y como el código ni siquiera se compila, podemos modificar la clase GetConcreteNumberTrivia de inmediato:

And since the code doesn’t even compile, we can modify the GetConcreteNumberTrivia class immediately:

Future<Either<Failure, NumberTrivia>> call({ ...

El código de prueba ahora debería pasar.

Añadiendo otro caso de uso

Además de obtener trivia para un número concreto, nuestra aplicación también obtendrá trivia para un número aleatorio. Esto significa que necesitamos otro caso de uso: GetRandomNumberTrivia. La API de números que estamos utilizando tiene en realidad un punto final de API diferente para números concretos y aleatorios, por lo que no generaremos el número nosotros mismos. De lo contrario, el código de generación de números se ejecutaría dentro de la capa de dominio precisamente en el caso de uso GetRandomNumberTrivia. Generar números es una lógica de negocios, después de todo.

 

Clase base de caso de uso

Como codificadores limpios, seguramente nos gusta cuando nuestro código tiene una interfaz predecible. Los métodos públicos y las propiedades de las clases que hacen básicamente lo mismo deberían tener nombres estandarizados.

Cuando se trata de casos de uso, cada uno de ellos debe tener un método de llamada. No importa si la lógica dentro del caso de uso nos da un NumberTrivia o envía un transbordador espacial a la Luna, la interfaz debe ser la misma para evitar cualquier confusión.

Una forma de imponer una interfaz estable de una clase es confiar en la “palabra del programador” implícita. Lamentablemente, los programadores no son conocidos por recordar cosas. ¡Diablos, incluso veo algunos de mis antiguos tutoriales porque ya me he olvidado de cómo hacer ciertas cosas que ya sabía!

Otra forma de evitar que una clase tenga un método de llamada y otra un método de ejecución es proporcionar una interfaz explícita (en el caso de Dart, una clase abstracta), de la cual es inolvidable derivar. Como una clase base de caso de uso, por ejemplo.

El siguiente código irá al núcleo / casos de uso, ya que esta clase se puede compartir en múltiples funciones de una aplicación. Y, por supuesto, no tiene sentido probar una clase abstracta, por lo que podemos escribirla de inmediato.

usecase.dart

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

import '../error/failure.dart';

// Parameters have to be put into a container object so that they can be
// included in this abstract base class method definition.
abstract class UseCase<Type, Params> {
  Future<Either<Failure, Type>> call(Params params);
}

// This will be used by the code calling the use case whenever the use case
// doesn't accept any parameters.
class NoParams extends Equatable {}

Extendiendo la clase base

Como puede ver, agregamos dos parámetros de tipo a la clase UseCase. Uno es para el tipo de retorno “sin error”, que en el caso de nuestra aplicación será la entidad NumberTrivia. El otro parámetro de tipo, Params, causará algunos cambios menores en el código en el caso de uso GetConcreteNumberTrivia ya presente.

Cada clase de extensión UseCase definirá los parámetros que se pasan al método de llamada como una clase separada dentro del mismo archivo. Mientras estamos en eso, extendamos también UseCase. Otros cambios relacionados con el funcionamiento de la clase se producirán solo después de actualizar la prueba: ¡estamos haciendo TDD!

get_concrete_number_trivia.dart

class GetConcreteNumberTrivia extends UseCase<NumberTrivia, Params> {
  ...
}

class Params extends Equatable {
  final int number;

  Params({@required this.number}) : super([number]);
}

Ahora sabemos que la llamada debe incluir un objeto Params, en lugar de que el entero sea el parámetro directamente. Entonces, debido a que escribimos una prueba en la parte anterior, podemos usarla para confiar en nuestro código. Todo lo que tenemos que hacer es:

  • Actualice la prueba para usar los parámetros.
  • No se compilará.

get_concrete_number_trivia_test.dart

...
test(
  'should get trivia for the number from the repository',
  () async {
    // arrange
    when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
        .thenAnswer((_) async => Right(tNumberTrivia));
    // act
    final result = await usecase(Params(number: tNumber));
    // assert
    expect(result, Right(tNumberTrivia));
    verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
    verifyNoMoreInteractions(mockNumberTriviaRepository);
  },
);
  • Actualiza el código de producción. Utilice los parámetros como parámetro para el método de llamada.
  • Ejecute la prueba: debe pasar, la confianza del código se va por todo lo alto.

get_concrete_number_trivia.dart

class GetConcreteNumberTrivia extends UseCase<NumberTrivia, Params> {
  ...
  @override
  Future<Either<Failure, NumberTrivia>> call(Params params) async {
    return await repository.getConcreteNumberTrivia(params.number);
  }
}
...

GetRandomNumberTrivia

Agregar este nuevo caso de uso ahora es muy simple: hemos acordado una interfaz que cada UseCase debe tener. Además, debido a la naturaleza simple de la aplicación Number Trivia, este nuevo caso de uso solo obtendrá datos del repositorio.

Una vez más, comenzamos escribiendo la prueba: cree un nuevo archivo en la carpeta “prueba /…/ usecases“. La mayor parte del código se copia de la prueba para el caso de uso anterior.

get_random_number_trivia_test.dart

import 'package:clean_architecture_tdd_prep/core/usecase/usecase.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_random_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() {
  GetRandomNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;

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

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

  test(
    'should get trivia from the repository',
    () async {
      // arrange
      when(mockNumberTriviaRepository.getRandomNumberTrivia())
          .thenAnswer((_) async => Right(tNumberTrivia));
      // act
      // Since random number doesn't require any parameters, we pass in NoParams.
      final result = await usecase(NoParams());
      // assert
      expect(result, Right(tNumberTrivia));
      verify(mockNumberTriviaRepository.getRandomNumberTrivia());
      verifyNoMoreInteractions(mockNumberTriviaRepository);
    },
  );
}

Of course, this test fails and the implementation of the GetRandomNumberTrivia is as follows:

get_random_number_trivia.dart

import 'package:dartz/dartz.dart';

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

class GetRandomNumberTrivia extends UseCase<NumberTrivia, NoParams> {
  final NumberTriviaRepository repository;

  GetRandomNumberTrivia(this.repository);

  @override
  Future<Either<Failure, NumberTrivia>> call(NoParams params) async {
    return await repository.getRandomNumberTrivia();
  }
}

La prueba ahora pasará y con eso, acabamos de implementar completamente la capa de dominio de la aplicación Number Trivia. En la siguiente parte, comenzaremos a trabajar en la capa de datos que contiene la implementación del Repositorio y las Fuentes de datos.

Aquí les comparto también el video.


Compartir

Entradas relacionadas

Deja tu comentario