Con el auge de los IOC Containers o inyectores dependencias en los últimos tiempos se ha estado haciendo un uso desproporcionado tanto de la inyección de dependencias o como de interfaces.
Muchas interfaces sin sentido provoca demasiado ruido en el código y entonces hay que seleccionar en qué casos nos esta aportando valor real y cuales muy poco.
Podemos llegar a una situación donde añadir una pequeña feature o corregir un bug requiere tocar por muchos ficheros haciendo pesada cualquier tipo de modificación.
Vamos a ver diferentes situaciones donde si creo un interface y donde no lo hago.
Inversión de dependencia para desacoplar
Para no incumplir la regla de dependencia que propone Uncle Bob, si necesitamos comunicarnos desde capas interiores de nuestro código con las exteriores necesitamos utilizar el principio de inversión de dependencia.
El principio inversión de dependencia nos permite desacoplar por ejemplo lógica de dominio de librerías de terceros o frameworks, en este tipo de escenarios tiene mucho sentido utilizar un interface.
package com.xurxodev.example.domain;
public interface ContactRepository {
//code
}
public class GetContactsUseCase {
private ContactRepository contactRepository;
public GetContactsUseCase(ContactsRepository contactsRepository){
this.contactRepository = contactRepository;
}
public List<Contacts> execute(){
List<Contacts> contacts = contactRepository.get();
return contacts;
}
}
Se define un interface, que establece como debe ser el repositorio, en el paquete de dominio que es desde donde se va a invocar y su implementación en el paquete de datos.
En este caso estamos desacoplando la lógica de dominio de la forma de acceder a los datos. De este modo si cambiamos algo en la forma a la que se accede o persisten los datos en nuestra capa de datos nuestro dominio no se ve afectado.
Otro ejemplo de utilizar un interface para desacoplar es cuando utilizamos el patrón Model View Presenter.
En este caso queremos desacoplar el presenter, que contiene la lógica de presentación, de la vista con la que se comunica.
De este modo el presenter no habla con la vista directamente sino con un interface definido en el propio paquete del presenter o incluso dentro del presenter mismo, ya que java nos permite hacer esto.
public class MoviesPresenter {
public interface MoviesView {
void showMovies(List<Movie> movies);
void clearMovies();
void showLoadingText();
void showTotalMovies(int count);
void showConnectionError();
boolean isReady();
}
GetMoviesUseCase getMoviesUseCase;
MoviesView view;
@Inject
public MoviesPresenter(GetMoviesUseCase getMoviesUseCase){
this.getMoviesUseCase = getMoviesUseCase;
}
public void attachView(MoviesView moviesView){
this.view = moviesView;
loadMovies();
}
private void loadMovies() {
loadingMovies();
getMoviesUseCase.execute(new GetMoviesUseCase.Callback() {
@Override
public void onMoviesLoaded(List<Movie> movies) {
showMovies(movies);
}
@Override
public void onConnectionError() {
view.showConnectionError();
}
});
}
private void loadingMovies() {
if (view.isReady()) {
view.clearMovies();
view.showLoadingText();
}
}
private void showMovies(List<Movie> movies) {
if (view.isReady()) {
view.showMovies(movies);
view.showTotalMovies(movies.size());
}
}
public void onRefreshAction(){
loadMovies();
}
}
Una vez más tener este desacoplamiento nos permite adaptarnos mejor a los cambios y podemos evolucionar como se representan los datos sin tener que modificar lógica de presentación.
Dos implementaciones
Otro escenario donde creo un interface es cuando voy a tener dos implementaciones.
En aplicaciones móviles es común tener varios origines de datos.
Por ejemplo si tenemos como origen de datos una API Rest y una base de datos local a modo de cache, lo normal es que queramos que quede encapsulado en la capa de datos cuando devuelve los datos de la API Rest y cuando de la base de datos local.
En esta situación debe existir un adaptador encargado de coordinar la comunicación con ambos orígenes de datos. El patrón repository puede servir perfectamente para esta tarea.
public interface MovieDataSource{
}
public class MovieDataRepository extends MovieRepository {
MovieDataSource localDataSource;
MovieDataSource remoteDataSource;
public Movie getMovieByKey(String movieKey){
Movie movie = localDataSource.get(movieKey);
if(movie == null) {
movie = remoteDataSource.get(movieKey);
localDataSource.addOrUpdate(movie);
}
return movie;
}
}
Invocando desde una capa exterior a una interior
Para clases que son invocadas desde una capa exterior no utilizo un interface porque las capas exteriores si pueden conocer una interior, aquí no se incumple la regla de dependencia.
Desacoplar en este punto no tiene un impacto tan beneficioso como en los otros casos porque si cambia el dominio, dependiendo de cada cambio, seguramente tenga que cambiar también las capas exteriores haya abstracción o no, porque el cambio será o que necesita más datos de entrada o devuelve más de salida y la capa exterior se va a tener que adaptar si o si.
Un ejemplo podría ser desde la capa de presentación, bien sea desde una vista o desde el presenter si utilizamos model view presenter, cuando invocamos un caso de uso o interactor, como en el ejemplo de MoviesPresenter de arriba.
Aplicar la inversión de dependencia se utiliza para no depender de frameworks y librerías pero un caso de uso es pura lógica de nuestro dominio, es perfectamente válido depender de ello.
Seguro que a alguien le surge una pregunta, ¿Como pruebo entonces el presenter de forma aislada creando diferentes situaciones mediante mocks que sustituyan al caso de uso?.
Bien pues aquí depende de tu estrategia de testing pero yo no suelo hacer test unitarios de presenters. De todas formas con alguna librería de mocking seguro que se puede inyectar la dependencia de un objeto de una capa inferior aunque no exista interface.
Suelo probar toda la capa de presentación mediante test de aceptación y lo que sustituyo mediante dobles de test son los repositorios para generar escenarios de prueba.
Invocando desde la misma capa
Este caso es muy similar al anterior, un ejemplo puede ser desde el repositorio invocar a un mapper para convertir modelos de datos a entidades de dominio.
Estos mapper yo no suelo abstraerlos con un interface por el mismo motivo que antes, creo que no aporta gran valor y si mucho ruido.
Conclusiones
Hemos visto en este artículo cuando suelo utilizar interfaces y cuando no.
He pasado por momentos donde abstraía prácticamente todo y los proyectos que creaba eran inmanejables.
Así que empece a utilizar interface solo en aquellos puntos que más valor me aportaban y para testing suelo elegir cuidadosamente la estrategia y me apoyo en librerías de mocks y de mock web server.
Cuando el único motivo para crear una interface es poder hacer test, si le das una vuelta a la estrategia de testing seguro que encuentras la forma de hacer unos test que te ayuden sin necesidad de comprometer la arquitectura de tu aplicación.