Flutter: Injeção de Dependência
Injeção de Dependência é um dos padrões atualmente usados em grandes projetos e muito se tem falado sobre isto, mas talvez nem todos saibam exatamente o que é!
Para aqueles que ainda não sabem o que significa:
Injeção de Dependência é uma técnica de programação onde um objeto ou função recebe como “parâmetro” outros objetos ou funções que necessita, ao invés de criá-los (instanciá-los) internamente.
Agora que voce já sabe pelo menos a “definição” de Injeção de Dependência, vem a pergunta:
“Mas por que cargas d’água precisamos usa-la? Qual a utilidade dela na vida real?”
Existem várias razões para aplicarmos este padrão, mas para mim a principal utilidade dela é: permitir testes unitários automatizados!
Vamos a um exemplo prático para entendermos melhor o processo!
Suponhamos que tenhamos uma aplicação que precisa acessar dezenas de “endpoints” dentro da sua api. Precisaremos criar dezenas de “repositórios”, mas vamos nos concentrar em apenas 1 deles por enquanto (e bem simplificado):
- AuthRepository: responsável em comunicar com o “auth” da sua api (login, logout, password-reset, etc…)
class AuthRepository {
Dio dio = Dio();
Future<User?> login(String email, String password) async {
try {
var response = await dio.post("http://localhost:3000/login", data: {"email": email, "password": password});
if (response.statusCode != 200) return null;
return User.fromJson(response.data);
} catch (e) {
return null;
}
}
}
Note nesta classe nós a criamos com a “dependência” da classe Dio, onde o estamos usando para fazer a comunicação com o nosso backend.
Logo na segunda linha da função login, disparamos um método desta classe que faz uma consulta a uma api externa (via post).
O teste desta classe seria então:
(obs: o teste está bem simplificado para efeito de ilustração do problema!)
void main() {
test('AuthRepository.login with correct credentials', () async {
AuthRepository authRepository = AuthRepository();
User? user = await authRepository.login("myemail@email.com", "123456");
expect(user!.email, "myemail@email.com");
});
test('AuthRepository.login with wrong credentials', () async {
AuthRepository authRepository = AuthRepository();
User? user = await authRepository.login("myemail@email.com", "wrong password");
expect(user, null);
});
}
E é bem aqui que as coisas começam a se complicar!!
Quando rodamos o teste unitário da nossa classe AuthRepository, ele irá disparar o método login e irá “bater” em uma api! E não importa se esta api for de “testes” ou de “produção”.
A idéia dos testes unitários é permitir que possamos validar o nosso código sem a necessidade de “bater” nos nosso endpoints externos! Isto se deve por várias questões, desde performance (uma vez que os testes unitários PRECISAM ser rápidos), até mesmo o controle extato que cada endpoint irá retornar.
Ou seja: mesmo que o endpoint seja “local”, não é aconselhável acionar a nossa api todas as vezes que formos rodar nossos testes unitários! (Os testes que vão acionar a api são os de INTEGRAÇÃO, e serão feitos em outro momento.)
Mas da forma como nós implementamos a “dependência” da classe Dio, estamos condicionamos a SEMPRE acionar a api e sem a possibilidade de substituí-la por um outro objeto “simulado”, onde poderíamos controlar o comportamento dele.
OK, certo… mas como resolver isso?
É aqui que vamos então aplicar o tal do bendito padrão de Injeção de Dependência!
A idéia principal por debaixo do capô da Injeção de Dependência é que a classe que “usa” a dependência (no caso a classe AuthRepository) não “saiba” necessariamente qual implementação da “comunicação” ela está utilizando!
Para isso primeiramente então devemos criar uma classe abstrata que irá servir de contrato para a nossa comunicação externa:
abstract class HttpGateway {
Future<Response> post(String path, {Object? data});
}
E então reescrevemos a nossa classe AuthRepository de forma a “solicitar” como dependência uma instância já criada da abstração:
class AuthRepository {
HttpGateway httpGateway;
AuthRepository(this.httpGateway);
Future<User?> login(String email, String password) async {
try {
var response = await httpGateway.post("http://localhost:3000/login", data: {"email": email, "password": password});
if (response.statusCode != 200) return null;
return User.fromJson(response.data);
} catch (e) {
return null;
}
}
}
Veja que agora a nossa classe AuthRepository recebe no construtor o objeto que implementa o HttpGateway como dependência (o objeto é INJETADO ao invés de ser CRIADO).
Basta agora criamos 2 implementações do nosso contrato HttpGateway:
- Uma implementação para ser usada “na aplicação”, onde nesta sim teremos a instancia interna do Dio:
class HttpGatewayDio implements HttpGateway {
Dio dio = Dio();
@override
Future<Response> post(String path, {Object? data}) => dio.post(path, data: data);
}
- e outra para ser usada “nos testes”, onde nesta podemos “simular” o que os métodos respondem:
class HttpGatewayFake implements HttpGateway {
@override
Future<Response> post(String path, {Object? data}) async => onPost(path, data:data);
Future<Response> Function(String path, {Object? data}) onPost = (String path, {Object? data}) => throw UnimplementedError();
}
E agora alteramos o nosso teste para “responder” o comportamento desejado da api!
Ou seja: não vamos precisar “consultar” em uma api! Basta simularmos a resposta que o HttpGateway deveria dar ao “consultar” a api.
Isto nos permite responder ao método “post” da forma que desejamos! E de forma extremamente rápida!
void main() {
test('AuthRepository.login with correct credentials', () async {
HttpGatewayFake httpGateway = HttpGatewayFake();
httpGateway.onPost = (String path, {Object? data}) async => Response(statusCode: 200, data: {"email": (data as Map<String, dynamic>)["email"]}, requestOptions: RequestOptions(path: path));
AuthRepository authRepository = AuthRepository(httpGateway);
User? user = await authRepository.login("myemail@email.com", "123456");
expect(user!.email, "myemail@email.com");
});
test('AuthRepository.login with wrong credentials', () async {
HttpGatewayFake httpGateway = HttpGatewayFake();
httpGateway.onPost = (String path, {Object? data}) async => Response(statusCode: 404, data: null, requestOptions: RequestOptions(path: path));
AuthRepository authRepository = AuthRepository(httpGateway);
User? user = await authRepository.login("myemail@email.com", "wrong password");
expect(user, null);
});
}
Pronto! Agora você já sabe usar Injeção de Dependência!
E principalmente… já sabe o porquê de usá-la!
A próxima etapa é criarmos um Injetor de Dependências!
Mas este é assunto para o próximo artigo!
See ya!