Aprende a separar el qué del cómo y serás más feliz con tu código

No saber separar el qué del cómo es uno de los mayores problemas que te vas a encontrar como desarrollador de software.

Da igual la tecnología con la que trabajes, da igual la parte de la aplicación que estés desarrollando, da igual si eres de QA o de producto o si haces consultoría, si estas escribiendo capa de presentación o datos.

Estos dos conceptos forman parte del desarrollo de software desde un punto de vista general.

Qué quiero hacer

El qué, es lo que quieres hacer o el problema a resolver.

Por ejemplo cuando el usuario realiza una acción y esto provoca una llamada a servidor, lo que quieres hacer sería avisar al usuario para indicar que se esta trabajando en recuperación de datos para que no tenga sensación de no respuesta en la app.

Es pura lógica, cuando se produce esta acción quiero avisar al usuario sin entrar en detalle de cómo se avisa.

Cómo lo hago

El cómo es el medio o herramienta que utilizo para resolver lo que quiero hacer o el problema.

Siguiendo el ejemplo anterior, el cómo sería como vamos a mostrar el aviso, usando un control del framework o un control de una librería de terceros.

Aquí no hay lógica sencillamente se hace algo utilizando un framework o librería.

Beneficios de separar estos conceptos

El qué generalmente se refiere a lógica pura, si tenemos esto abstraído de la tecnología, que sería el cómo, nos aporta beneficios que nos van a permitir ser más felices con nuestro código.

  1. Tenemos responsabilidades separadas que pueden evolucionar en paralelo
  2. Vamos a tener la oportunidad de hacer test contra la lógica del qué.
  3. Los cambios de tecnología son mucho menos arriesgados porque solo tenemos que cambiar la parte del cómo, la parte del qué sigue intacta. Es menos probable que se pierdan funcionalidades con los cambios.

Ejemplos

Vamos a ver una serie de ejemplos.

Lógica de Dominio

Es uno de los problemas más comunes el tener la lógica de dominio repartida por toda la aplicación.

Por ejemplo para realizar cualquier acción dentro de una aplicación necesitas datos, del tipo que sea.

De lo que se trata es de separar qué necesitas unos datos concretos de cómo los consigues, da igual si accedes a una base de datos, a un fichero del sistema o a un servicio web externo.

Otro ejemplo sería si tenemos un servicio http y en el GET de usuarios o en el POST para registrar si el username viene vació queremos devolver un bad request.

Es posible que acabemos mezclando estos dos conceptos:

public IHttpActionResult Get(string userName)  
{
    if (string.IsNullOrWhiteSpace(userName))
        return this.BadRequest("Invalid user name.");

    var user = this.repository.FindUser(userName.ToUpper());
    return this.Ok(user);
}

[HttpPost]
public IHttpActionResult Register(string candidate)  
{
    if (string.IsNullOrWhiteSpace(candidate))
        return this.BadRequest("Invalid user name.");

    ...
}

Aquí estamos mezclando el qué y el cómo.

Si separamos estos dos conceptos por un lado tendríamos que ante cualquiera de esas situaciones queremos que se genere un error en nuestro sistema, para conseguirlo es mejor crear un tipo UserName que se encargue de sus invariantes

public class UserName:SimpleValueObject<UserName>  
{
    public UserName(string value):base(value)
    {
        if (value == null)
            throw new ArgumentNullException("value");
        if (!IsValid(value))
            throw new ArgumentException("Invalid value.", "value");
    }

    private bool IsValid(string value)
    {
        if (string.IsNullOrEmpty(value))
            return false;

        return value.Trim() == value;
    }
}

Y por otro lado, en el servicio http se decide cómo se informa al usuario de ese error.

public IHttpActionResult Get(string candidate)  
{
    UserName userName;
    if (!UserName.TryParse(candidate, out userName))
        return this.BadRequest("Invalid user name.");

    var user = this.repository.FindUser(userName);
    return this.Ok(user);
}

[HttpPost]
public IHttpActionResult Register(string candidate)  
{
    UserName userName;
    if (!UserName.TryParse(candidate, out userName))
        return this.BadRequest("Invalid user name.");

    ...
}

Lógica de presentación

Tanto si utilizamos javascript en web como desarrollo móvil nativo existe lógica de presentación.
En presentación también es muy útil separar el qué del cómo.

Para la capa de presentación, existen patrones como MVP, MVC, MVVM que nos ayudan a conseguir esta separación de responsabilidades.

Imaginemos que tenemos una pantalla en Android y cuando el usuario realiza una acción, y se están pidiendo datos a servidor, queremos mostrar un aviso.

Aplicando el patrón Model View Presenter tendríamos una presenter con la lógica de la pantalla comunicándose con una abstracción de la vista, esto sería el qué.

Aquí es donde se toman las decisiones de presentación, si hay que mostrar un loader, un mensaje de error etc.. pero sin entrar en detalle de cómo se hace.

public class CompetitorDetailPresenter implements Presenter{

    public interface View {
        void renderImage(String image);
        void renderTitle(String title);
        void showLoading();
        void hideLoading();
        void showNetworkNotAvailableError();
        void showDataNotDoundError();

        boolean isReady();
    }

    private View view;
    private GetCompetitorDetailUseCase getCompetitorDetailUseCase;

    private Competitor competitor;
    private String competitorName;

    public void attachView(View view, String competitorName) {
        if (view == null) {
            throw new IllegalArgumentException("You can't set a null view");
        }
        this.view = view;
        this.competitorName = competitorName;
        loadCompetitor(competitorName);
    }

    public void loadCompetitor(String competitorName)
    {
        view.showLoading();

        getCompetitorDetailUseCase.execute(competitorName, new GetCompetitorDetailUseCase.Callback() {
            @Override
            public void onCompetitorDetailLoaded(Competitor retrievedCompetitor) {

                competitor = retrievedCompetitor;

                if (view.isReady()) {
                    view.hideLoading();
                    view.renderImage(competitor.getMainImage());
                    view.renderTitle(competitor.getName());
                }
            }

            @Override
            public void onCompetitorDetailNotFound() {
                view.hideLoading();
                view.showDataNotDoundError();
            }

            @Override
            public void onNetworkError() {
                view.hideLoading();
                view.showNetworkNotAvailableError();
            }
        });

    }
}

El cómo se hace es responsabilidad de la vista.

public class CompetitorDetailFragment  
        extends Fragment
        implements CompetitorDetailPresenter.View {

    View rootView;

    @Inject
    public CompetitorDetailPresenter presenter;

    private RecyclerView recyclerView;

    public static final String COMPETITOR_NAME = "competitorName";

    private ProgressBar progressBarLoading;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        rootView = inflater.inflate(R.layout.competitor_detail, container, false);


        if (getArguments().containsKey(COMPETITOR_NAME)) {

            String competitorName = (String) getArguments().get(COMPETITOR_NAME);

            progressBarLoading = (ProgressBar) getActivity().findViewById(R.id.pb_loading);

            initializePresenter(competitorName);
        }

        return rootView;
    }

    private void initializePresenter(String competitorName)
    {
        presenter.attachView(this,competitorName);
    }

    @Override
    public void renderImage(String image) {
        Activity activity = this.getActivity();

        ImageView imgtoolbar = (ImageView) activity.findViewById(R.id.imgToolbar);

        if (imgtoolbar != null) {
            Picasso.with(activity.getApplicationContext()).load(image)
                    .into(imgtoolbar);
        }
    }

    @Override
    public void renderTitle(String name) {
        CollapsingToolbarLayout collapsingToolbar =
                (CollapsingToolbarLayout) getActivity().findViewById(R.id.toolbar_layout);
        collapsingToolbar.setTitle(name);

    }

    @Override
    public void showLoading() {
        progressBarLoading.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        progressBarLoading.setVisibility(View.GONE);
    }

    @Override
    public boolean isReady() {
        return isAdded();
    }
}

En la vista es donde se decide cómo se representan los datos, qué vistas utilizamos, librerías, animaciones, qué texto corresponde a cada mensaje de error etc..

Lógica de Tests

Los test son código igualmente y por lo tanto existen los mismos problemas.

Existen diferentes frameworks de test en cada tecnología, por ejemplo en Android existe Espresso donde podemos hacer un test como este.

    @Test
    public void show_competitorName_as_tile() {
        onView(withId(R.id.competitor_list))
                .perform(RecyclerViewActions.actionOnItemAtPosition(position,
                        RecyclerViewItemActions.clickChildViewWithId(
                                R.id.competitor_item_explore)));

        onView(allOf(isAssignableFrom(TextView.class), withParent(isAssignableFrom(Toolbar.class))))
                .check(matches(withText(expectedTitle)));
    }

Este tipo de test tienen varios problemas:

  • son frágiles, si modificamos la interfaz de usuario vamos a tener muchos test que modificar y solucionarlo requiere modificar todos los test que prueban la pantalla que hemos cambiado.
  • es difícil cambiar de tecnología, cambiar de Espresso a otro framework de test puede ser un autentico dolor de cabeza.

Existe un patrón para testing en web llamado Page Object pattern, para móvil recibe el nombre de Robot pattern a través del cual conseguimos evitar estos dos problemas.

Creamos unos objetos que son una representación abstracta de cada pantalla, donde expone propiedades y métodos que representan cómo se puede interactuar en la pantalla, encapsulado como se realizan estas acciones con el framework de test.

public class CompetitorListRobot {

    public CompetitorRobot navigateToCompetitor(int position) {
        onView(withId(R.id.competitor_list))
                .perform(RecyclerViewActions.actionOnItemAtPosition(position,
                        RecyclerViewItemActions.clickChildViewWithId(R.id.competitor_item_explore)));

        return new CompetitorRobot(this);
    }
}

public class CompetitorRobot {

    CompetitorListRobot competitorListRobot;

    public CompetitorRobot(CompetitorListRobot competitorListRobot)
    {
        this.competitorListRobot = competitorListRobot;
    }

    public CompetitorRobot verifyTitle(String expectedTitle) {
        onView(allOf(isAssignableFrom(TextView.class), withParent(isAssignableFrom(Toolbar.class))))
                .check(matches(withText(expectedTitle)));

        return this;
    }
}

En los test solo tenemos código que representa lo que queremos probar, no como lo probamos.

@Test
public void Show_CompetitorName_As_Tile() {  
   competitorListRobot
   .navigateToCompetitor(1)
   .verifyTitle(expectedTitle)
}

Si cambiamos de framework de test o el diseño de una pantalla solo tengo que cambiar las clases que encapsulan la comunicación con el framework de tests pero no los tests.

Lo normal es que haya más tests que clases que representan a una pantalla así que los beneficios saltan a la vista.

Conclusiones

Separar el qué del cómo es una práctica que cuesta asimilar pero cuando lo conseguimos, somos más felices con nuestro código porque podremos evolucionarlo con muchos menos dolores de cabeza.