19 de diciembre de 2020

Un return vs varios returns

La pregunta sobre si es mejor un sólo return o de dónde viene esta idea es muy habitual en los foros de programación. Hay varios puntos de vista a este respecto como es de suponer y se pueden ver en una de las preguntas con más actividad al respecto.

En la respuesta aceptada habla que en realidad viene de la programación estructurada y cuenta el ejemplo con FORTRAN.

Una razón que considero importante, y es la que más me convence, es uno de los comentarios a esta respuesta:

"This doesn't just apply to assembly. There was also some research done at Microsoft (on C codebases) that found multiple returns contributed to higher bug frequency. See: amazon.com/Writing-Solid-Code-20th-Anniversary/dp/1570740550/…"

Lo cuál ya es un fundamento basado en la experiencia y no sólo en opiniones subjetivas de qué lee mejor cada uno.

Él resultado de ese estudio podría explicarse con que si hace falta varios returns es posible que estés en un God Object y tengas que dividir funcionalidad para seguir SOLID (principio de responsabilidad única). Por eso usando buenas prácticas es raro que encuentres un caso para varios returns o no se solucione simplemente con el operador ternario ?:

Pero hay una excepción que me parece razonable: la cláusula de guarda (Guard Clause), la cual tiene sentido.

Éste sería un ejemplo con tres cláusulas.

double getPayAmount() {
    if (isDead)      return deadAmount();
    if (isSeparated) return separatedAmount();
    if (isRetired)   return retiredAmount();

    return normalPayAmount();
};

Y seguiría la verdadera recomendación que es minimizar los returns.

Por último está la idea extremista de que en efecto sólo haya un return, pero porque en POO sólo deberían utilizarse las palabras reservadas new y return y el resto debe ser todo manejado por objetos:

public int max(int a, int b) {
  return new If(
    new GreaterThan(a, b),
    a, b
  );
}

Conclusión

En mi opinión, parece que una Guard Clause podría tener sentido ya que evita validaciones externas al método y es lo primero que se vería en una revisión, manteniendo un único return más al final. En el resto de casos sería mejor evitar varios returns por los datos del estudio citado justo antes y porque parece que en general es más fácil de mantener (depurar y hacer cambios en el código) a costa de ser menos verbose que parece el principal argumento en contra de un único return.

Como es un tema más bien de debate, te invito a que pongas tu punto de vista en los comentarios.

11 de diciembre de 2020

Wildcards

Para terminar esta introducción a genéricos nos faltaría ver las wildcards que nos permiten poder definir un tipo variable usando como identificador del parámetro el carácter "?".

Es muy habitual verlo en la firma de los métodos para admitir cualquier Collection que admita un tipo de elemento o cualquiera de los subtipos de ese elemento, ya que Collection<Vehiculo> no es asignable a Collection<Coche>. En este ejemplo vamos a definir un método que admita esos dos tipos de colecciones:

private static void imprimirFlota(Collection<? extends Vehiculo> vehiculos) {
    vehiculos.forEach(System.out::println);
}

Su utilidad la vemos a continuación:

Collection coleccionRaw = Arrays.asList(new Coche(), new CocheProducto("", "", 10000.0f));
Collection<Vehiculo> vehiculos = coleccionRaw;
//  Collection<Coche> coches = vehiculos; // Error
imprimirFlota(vehiculos);
Collection<Coche> coches = coleccionRaw;
imprimirFlota(coches);
NOTA: Puede parecer que lo mejor es no definir el tipo ya que "no tiene el problema" que tenemos si lo definimos. Eclipse nos dará un warning tanto al declarar la variable (dirá que debemos declararlo) como al asignarla (nos advierte que no se puede checkear la seguridad de tipo). En resumen, hay que definirlo por seguridad de nuestro código en tiempo de compilación y porque de lo contrario no tendremos acceso a los miembros más allá de los pertenecientes a Object.

Sin tener más conocimiento sobre genéricos no recomiendo usar las wildcards en otro contexto ya que lo más normal es que tengamos muchos problemas.

Hay más conceptos sobre genéricos, de hecho hay libros enteros sólo sobre este tema con términos como type erasure, pero que no se van a ver en esta introducción de fundamentos, aunque hay que tenerlos presentes pues no conocerlos puede darnos quebraderos de cabeza (lo típico que te dice que no se puede implementar dos veces la misma interface; prueba a implementar Comparable<Coche> en la clase Coche y te encontrarás con el mensaje: "The interface Comparable cannot be implemented more than once with different arguments: Comparable<Vehiculo> and Comparable<Coche>" que lo causa el type erasure).

Genéricos (desde Java 5) al igual que streams o lambdas (desde Java 8) son grandes avances pero quedan fuera del alcance que se pretende en esta parte de fundamentos. Puedes encontrar más tutoriales en la página oficial de Oracle.

Limites en genéricos (boundary)

Cuando necesitamos limitar las opciones de un tipo variable podemos establecer los límites adecuados en la declaración del parámetro tipo. En nuestro ejemplo de Identificable vamos a obligar que el tipo T implemente Comparable<T>. El código quedaría así:

public interface Identificable<T extends Comparable<T>> {

    T getId();

}

De esta forma sólo va a poder admitirse tipos que se encuentren dentro de los límites. Podemos probarlo intentando asignar a T un tipo que no sea comparable (aunque sea absurdo prueba con ProductExterno por ejemplo).

Ésto nos permite contar con otros miembros garantizados, en este caso podemos usar el método compareTo(Identificable<T> identificable) y podríamos definir un orden natural para todos los identificables usando el método getId(). El código definitivo sería:

public interface Identificable<T extends Comparable<T>> extends Comparable<Identificable<T>> {

    T getId();

    @Override
    default int compareTo(Identificable<T> identificable) {
        return getId().compareTo(identificable.getId());
    }
}
NOTA: Para que fuera correcto tendríamos que quitar la implementación de Comparable<Vehiculo> en la clase Vehiculo por el concepto type erasure que se menciona en la siguiente entrada.

Los límites no se ciñen a un único tipo, puedes hacer una composición de ellos (pero que sea posible, no puedes decir que sea de más de una clase siendo que no existe la herencia múltiple). En este caso voy a definir un tipo T en un método para que admita Identificable<Long> y Arrancable:

public static void main(String[] args) {
    admiteCoche(new Coche());
}

private static <T extends Identificable<Long> & Arrancable> void admiteCoche(T identificableArrancable) {
    System.out.println(identificableArrancable.getId());
    identificableArrancable.arrancar();
}

En este caso admitirá Coche, pero no Moto (ya que no es Identificable). La definición del tipo parámetro debe hacerse antes del tipo retorno ya que podría usarse T para el retorno y debería estar definido ya en ese caso.

Cuando en el límite quiero añadir una clase debo poner la clase la primera y luego irían todas las interfaces que hagan falta (intenta hacerlo al revés). Éste es un ejemplo pidiendo un Coche y Comerciable, con lo que sólo admitirá un CocheProducto:

private static  <T extends Coche & Comerciable> void admiteCocheProducto(T cocheComerciable) {
    System.out.println(cocheComerciable.getPrecio() + "|" + cocheComerciable.getClass());
}

Como vemos siempre se define un tipo parámetro que termina definiéndose y, dentro de su scope se mantiene fijo (puede ser definido en una clase que implemente un tipo genérico o tomado de un parámetro en un método/constructor). Sin embargo existe la opción de no definir un tipo específico y dejar que se admita sin hacer la declaración de tipo parámetro. Ésto se conoce como wildcard y lo vemos en la siguiente entrada.

10 de diciembre de 2020

Tipo variable

Para rematar esta parte de fundamentos vamos a ver el tipo por referencia que nos falta que es el Tipo Variable. Lo hemos usado al utilizar interfaces como Collection o Comparable. Ahora vamos a aprender a definirlo. Como dice la documentación, el tipo variable es introducido por la declaración de un parámetro tipo en una clase, interface, método o constructor genérico.

Esto quiere decir que en nuestro código vamos a referirnos a un tipo por su identificador dentro de su ámbito (scope) pero sin definir qué tipo concreto es. Podemos aplicarle unos límites (por ejemplo decir que debe cumplir con una interface, heredar de una clase o una mezcla de ello) como veremos en la siguiente entrada.

En este caso voy a tomar como ejemplo un identificador para una clase. Puede que no tenga claro el tipo que quiero usar para identificarlo, podría ser un número, un String o cualquier otro. Al no tener claro el tipo podría verme bloqueado a no poder seguir programando, sin embargo desde que existen los tipos variables puedo hacer código genérico reutilizable para distintos tipos.

Así pues me voy a crear una interface genérica que refleje esto:

public interface Identificable<T> {

    T getId();

}

Como vemos se marca un tipo T que por defecto puede ser de cualquier tipo. La sintaxis es rodeándolo por los caracteres < y > siendo habitual usar la letra T. Si se necesitan varios parámetros de tipo habría que usar más letras (también podías usar un identificador distinto a una única mayúscula). Otros ejemplos típicos si hay que usar más de un parámetro tipo son usar R para tipos retorno, K para claves o V para valores (ver ejemplos en las interfaces funcionales de Java 8). Ahora podemos usarlo añadiendo un id a Coche. En mi caso voy a identificar los coches por la matrícula así que T sería un String. El código quedaría así:

public class Coche extends VehiculoConRuedas implements Identificable<String> {

    ...

    @Override
    public String getId() {
        return getMatricula();
    }

}

No obstante, podía plantearse escenarios muy habituales donde una matrícula no sería el identificador que se necesitaría. Podría cambiarlo rápidamente a un Long:

public class Coche extends VehiculoConRuedas implements Identificable<Long> {

    ...

    private Long id;

    @Override
    public Long getId() {
        return id;
    }

}

Como podemos observar, al cambiar la definición de T el método anteriormente implementado ya no sirve y debe devolver el nuevo tipo Long. Al ser tipos por referencia se pide que T sea un tipo por referencia, es decir, no puedo usar como tipo variable un tipo primitivo, si lo intentara me saldría un error y no compilaría.

No obstante muchas veces no sirve con dejar completamente abierto T si quiero implementar código ya que sólo podríamos contar con los miembros de Object. Por ejemplo con Collection o Comparable no pasa nada porque debe admitir todo, pero en nuestro ejemplo si quisiéramos implementar por defecto un orden natural usando T deberíamos limitarlo a un Comparable<T>. Cómo se hace lo vemos en la siguiente entrada.

Integrando con código externo

En las entradas anteriores hemos visto las diferencias entre clases e interfaces. Por ahora hemos utilizado tipos nativos de Java pero hay otras muchas librerías que nos hacen la vida más fácil sin tener que reinventar la rueda.

Sin entrar en frameworks concretos y con la idea puesta que nuestro código tendrá que ampliar sistemas que ya estén en producción, añadir librerías de terceros o incluso integrar distintos componentes ¿Cómo podría utilizar tipos de otra librería sobre los que no quiero/puedo modificar el código fuente para integrarlos con mi negocio? ¿Cómo podría usar funcionalidades de terceros que me son útiles para utilizarlos con los objetos de mi negocio?

Sin entrar en cómo añadir librerías que lo veremos más adelante, en esta entrada vamos a simular que tuviera una clase y una interface de un tercero y cómo las integro en mi código.

Supuesto

Imaginemos que ahora tengo que integrar mi código sobre vehículos con una librería que me llevase el tema de comercializarlos. La idea es buscar una librería abierta que tenga mucho apoyo de la comunidad, que se vea que se está manteniendo (ver fecha último commit) y que los "issues" que se van generando son atendidos.

En nuestro caso no vamos a buscar una para desviarnos del tema, pero vamos a simular una que estuviera diseñada para el uso de interfaces. Ésto sabemos que nos permitiría implementar esas interfaces en nuestro código para utilizar sus métodos. La desventaja de esto es que nuestro código estaría acoplado a esa librería así que hay que decidir si de verdad la queremos como dependencia o si queremos poder cambiarla fácilmente en un futuro.

Caso 1.- Usar una clase externa dentro de mi negocio

Vamos a partir de una clase Product externa definida así:

package com.github.commerce;

public class Product implements Merchantable {

  private String description;
  private float price;

  @Override
  public String getDescription() {
    return description;
  }

  @Override
  public float getPrice() {
    return price;
  }

  public Product(String description, float price) {
    this.description = description;
    this.price = price;
  }

}

Y una interface Merchantable definida así:

package com.github.commerce;

public interface Merchantable {
  String DEFAULT_CURRENCY = "€";

  static Double priceToDouble(Merchantable merchantable) {
    return new Double(merchantable.getPrice());
  }

  String getDescription();
  float getPrice();

  default String getString() {
    return getDescription() + " (" + getPrice()
             + DEFAULT_CURRENCY + ")";
  }

}
NOTA: Tanto Product y Merchantable, así como el resto de código externo, vamos a suponer que no sería modificable por nosotros. De hecho vendría compilada como una dependencia aunque aquí la añadamos a nuestro código porque no hemos visto cómo añadir dependencias todavía.

Por nuestra parte tenemos una nueva interface Comerciable:

package vehiculos;

public interface Comerciable {

  String getDescripcion();
  float getPrecio();

}

Y un par de clases para implementar Comerciable. Una para utilizar nuestro tipo Coche:

public class CocheProducto extends Coche implements Comerciable {

  private float precio;

  @Override
  public float getPrecio() {
    return precio;
  }

  public CocheProducto(String modelo, String color, float precio) {
    super(modelo, color);
    this.precio = precio;
  }

  @Override
  public String getDescripcion() {
    return getModelo();
  }

}

Y otra para aprovechar el código nuestro con Product:

public class ProductExterno extends Product implements Comerciable {

  @Override
  public String getDescripcion() {
    return getDescription();
  }

  @Override
  public float getPrecio() {
    return getPrice();
  }

  public ProductExterno(String description, float price) {
    super(description, price);
  }

  @Override
  public String toString() {
    return getString();
  }

}

Ahora juntamos el código en este ejemplo y todo funciona correctamente:

public class UsandoLibreria {

  public static void main(String[] args) {
    ProductExterno productoExterno = new ProductExterno("CocheRaro", 25000f);
    CocheProducto cocheProducto = new CocheProducto("Seat", "Blanco", 16000);
	
    System.out.println("Usando Comerciable");
    Collection<Comerciable> productos = Arrays.asList(
                productoExterno,
                cocheProducto
        );

    productos.forEach(System.out::println);
    System.out.println(getValorTotal(productos));

  }

  private static float getValorTotal(Collection<Comerciable> comerciables) {
    return (float)comerciables.stream().mapToDouble(UsandoLibreria::toDouble).sum();
  }

  private static Double toDouble(Comerciable comerciable) {
    return new Double(comerciable.getPrecio());
  }

}

Ésta es la salida:

Usando Comerciable
CocheRaro (25000.0€)
Placa null - Seat (Blanco), 4 ruedas
41000.0

Como vemos en el ejemplo, al estar nuestro diseño orientado a interfaces, podemos usar tanto nuestras propias clases como otras implementando los métodos de nuestras interfaces. En general se trata de usar siempre la mínima interface en nuestras firmas de métodos.

Caso 2.- Usar nuestros tipos en código de terceros

Ahora vamos a reutilizar código de otros con nuestros tipos. Para ello vamos a añadir a CocheProducto la interface Merchantable:

public class CocheProducto extends Coche implements Comerciable, Merchantable {

  ...

  @Override
  public String getDescription() {
    return getDescripcion();
  }

  @Override
  public float getPrice() {
    return getPrecio();
  }

  @Override
  public String toString() {
    return getString();
  }

}

Y lo utilizamos en nuestro main:

System.out.println("\nUsando Merchantable");
Collection<Merchantable> merchantables = Arrays.asList(
            productoExterno,
            cocheProducto
    );
merchantables.forEach(System.out::println);
System.out.println(merchantables.stream().mapToDouble(Merchantable::priceToDouble).sum());

La salida es:

Usando Merchantable
CocheRaro (25000.0€)
Seat (Blanco) (16000.0€)
41000.0

Podemos observar un mismo "toString()" tanto en la clase que hereda de Product como la que hereda de Coche.

El ejemplo es muy sencillo, pero demuestra las posibilidades de orientar nuestras implementaciones siguiendo estas buenas prácticas. Por supuesto, al ser ProductExterno un Product puede usar todas las funcionalidades de la librería origen. Al autolimitarnos con que el código externo no es modificable (incluso teniendo acceso al código fuente) vamos a estar preparados para que nuestro código pueda ir admitiendo las modificaciones que se hagan desde proyectos externos que arreglen vulnerabilidades o proporcionen mejoras. Es bueno por tanto buscar repositorios que tengan en cuenta asegurar la retrocompatibilidad como lo hace Java.

Sé que esta entrada puede resultar un poco más complicada de entender que las anteriores, pero ya es el último contenido que se ve de fundamentos. Evidentemente se pueden hacer muchas cosas sin seguir estas prácticas, pero si se siguen el día de mañana se agradecerá el esfuerzo. Tómatelo como una inversión necesaria.

4 de diciembre de 2020

Comparación de objetos (equals vs Comparable y Comparator)

Vimos que equals es un miembro de Object. Sirve para comparar dos objetos y obtener si son iguales/equivalentes.

Por otra parte existen las interfaces Comparable y Comparator que sirven también para comparar dos objetos, pero las usaremos para ordenar pues nos pueden decir cuan iguales/distintos son dos objetos midiéndolo con un entero (int). Las diferencias entre estas dos formas de comparar las podemos resumir en:
  1. Comparable/Comparator es una interfaz mientras que equals es un miembro de todos los objetos (todos tienen una implementación).
  2. Todos los objetos pueden comparar su igualdad (usar equals), pero sólo los objetos con Comparable implementada tienen lo que se llama un orden natural con el que ordenarse por defecto.
  3. Comparable/Comparator admiten un tipo variable T (el tipo que pueden comparar). La definición de este tipo variable afectara al parámetro de su único método a implementar: compareTo(T o)/compare(T o1, T o2).
La diferencia fundamental entre Comparable y Comparator es el dónde y el para qué de su funcionalidad:
  • Dónde:
    • Comparable necesita implementar el método compareTo(T o) como miembro del objeto que se va a comparar con el parámetro o, y son del mismo tipo T (teniendo en cuenta que los subtipos son del tipo de su supertipo).
    • Comparator necesita implementar el método compare(T o1, T o2) como miembro del objeto comparador que se encargará de comparar o1 con o2: el comparador es un objeto independiente que no tiene relación con o1 ni o2.
  • Para qué: Comparable va a establecer un orden natural para el tipo mientras que Comparator se puede usar para hacer una ordenación puntual, pudiendo tener varios comparadores para ordenar por distintos criterios.

equals nos dice si dos valores son iguales, Comparable/Comparator nos dice cuan iguales son y nos permite ordenar


¿Cómo funcionan?


El único método que debe implementar cada una debe devolvernos:
  1. un número negativo si o1 es menor que o2,
  2. cero si son iguales (no implica que o1.equals(o2) sea true) o
  3. positivo si es mayor.
De esta forma podemos medir cuan distintos son dos objetos y por tanto ordenarlos, lo cual es imposible con equals que devuelve un boolean.

Se recomienda encarecidamente que ambos métodos sean coherentes entendido como: o1.compareTo(o2)/compare(o1, o2) == 0 debería dar el mismo resultado que o1.equals(o2).

¿Cuándo usamos cada una?

Desde mi experiencia yo tiendo a generar comparadores antes que a implementar Comparable. Mis motivos son:
  1. Puedo ordenar un tipo incluso si no tengo acceso al código para implementar Comparable
  2. Puedo disponer de varios comparadores como constantes (static final) en un tipo para poder ordenarlos fácilmente, mientras que sólo puedo tener una implementación para Comparable
  3. Comparator es una interfaz funcional y puedes instanciarlos con lambdas desde Java 8. El código queda muy legible y compacto con lo que no es un problema crearlos como antiguamente que había que hacer un tipo sólo para eso.
Sólo se puede implementar Comparable una vez, si un supertipo ya lo tiene implementado habrá que sobrescribir el método compareTo si debe ser distinta implementación y las comparaciones dos-a-dos podrían salir inconsistentes (ver type erasure cuando se hable de genéricos).

El consejo definitivo es: haz un comparador a menos que el diseño de un código en concreto te obligue a implementar Comparable (por ejemplo que tenga que usarse el tipo como clave de un mapa ordenado o que un tipo genérico imponga la interface Comparable).

Si no estás obligado a usar Comparable mejor hazte un Comparator para tener el código de ordenamiento separado

Para ver el uso de Comparator termino la entrada añadiendo éste código al de la entrada anterior que ordena los vehículos por su color:

System.out.println("\nLista ordenada (por color):");
vehiculos.sort((v1, v2) -> v1.getColor().compareTo(v2.getColor()));
vehiculos.forEach(System.out::println);
Así queda más limpio y separado el código de vehículos y el de ordenación para un caso concreto. Si te fijas en los métodos de Comparator verás cómo han potenciado la interfaz desde Java 8 y lo fácil que es tener un comparador que ordene por orden inverso, usando una clave específica (Comparable) o que trate los valores null de forma distinta. Todo son facilidades.

Compárteme

Entradas populares