10 de diciembre de 2020

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.

No hay comentarios:

Publicar un comentario

Compárteme

Entradas populares