SOLID y Patrones de Diseño: Sesión 1 - Herencia, Interfaces y Clases Abstractas

Marzo 2021

Tipos de Datos

Una clase define
un Tipo de Dato

Con la herencia creamos una especialización
de un Tipo de Dato

SubTipo

La especialización
de un Tipo de Dato

SuperTipo

Una Tipo de Dato que
ha sido especializado

Un subtipo puede
agregar nuevos métodos,
o sobreescribir otros

Ejemplo

Servlets recibe y responde peticiones de clientes Web

Define un servlet
genérico independiente
de su protocolo

Provee una clase abstracta para ser extendida y crear un servlet HTTP

MyServlet

Clase que provee lógica específica de mi negocio

Árbol de herencia de Servlet*

*Un poco simplificado

Otro ejemplo

Árbol de Herencia de List*

*Un poco simplificado

La herencia NO
es un mecanismo
de reutilización
de código

Ejercicio

Vínculos

  • Tenemos un CMS
  • Varios Componentes incluyen vínculos
  • En un diálogo son 2 campos: texto y URL
@Test
@DisplayName("Vínculo Interno, empieza con /")
void invariantesVínculoInterno() {
  assertAll("Invariantes", 
    () -> assertEquals(URL_INTERNO, this.vínculoInterno.getUrl()),
    () -> assertEquals(TEXTO_VÍNCULO, this.vínculoInterno.getTexto()),
    () -> assertEquals(TARGET_VACÍO, this.vínculoInterno.getTarget())
  );
}
@Test
@DisplayName("Vínculo Externo, empieza con http")
void invariantesVínculoExterno() {
  assertAll("Invariantes", 
    () -> assertEquals(URL_EXTERNO, this.vínculoExterno.getUrl()),
    () -> assertEquals(TEXTO_VÍNCULO, this.vínculoExterno.getTexto()),
    () -> assertEquals(TARGET_BLANK, this.vínculoExterno.getTarget())
  );
}
@Test
@DisplayName("Invariantes de un Vínculo Email, contiene @")
void invariantesVínculoEmail() {
  assertAll("Invariantes", 
    () -> assertEquals(String.format("mailto:%s", EMAIL), this.vínculoEmail.getUrl()),
    () -> assertEquals(TEXTO_VÍNCULO, this.vínculoEmail.getTexto()),
    () -> assertEquals(TARGET_BLANK, this.vínculoEmail.getTarget())
  );
}
@Test
@DisplayName("Invariantes de un Vínculo Teléfono, empieza con +")
void invariantesVínculoTeléfono() {
  assertAll("Invariantes", 
    () -> assertEquals(String.format("tel:%s", TELÉFONO), this.vínculoTeléfono.getUrl()),
    () -> assertEquals(TEXTO_VÍNCULO, this.vínculoTeléfono.getTexto()),
    () -> assertEquals(TARGET_VACÍO, this.vínculoTeléfono.getTarget())
  );
}

Retos

  • La lógica de negocio debe estar centralizada
  • El código debe ser fácil de entender
  • El código debe ser fácil de modificar
<a href="${vínculo.url}" target="${vínculo.target}">${vínculo.texto}</a>

Nuestro ideal

¿Cómo lo resolverías?

Una posible solución

Vínculo

Representa cualquier vínculo

Vínculo Interno

  • Especialización de Vínculo
  • El URL empieza con "/"
  • Cargan en la misma pestaña

Vínculo Teléfono

  • Especialización de Vínculo Interno*
  • El URL empieza con "+"
  • Empieza con "tel:"

*Después veremos la mejor manera de lograrlo

Vínculo Externo

  • Especialización de Vínculo
  • Empiezan con "http"
  • Abren en una pestaña nueva

Vínculo Email

  • Especialización de Vínculo Externo*
  • Contiene "@"
  • Empieza con "mailto:"

*Después veremos la mejor manera de lograrlo

Herencia Singular

En Java, solo podemos heredar de una clase

Clases Final

Una clase que no puede
ser especializada.
Se les llama "hoja" (leaf)

Interface

Define un Tipo de Dato
sin importar su implementación

Método Abstracto

Un método sin implementación, solo define su firma

Interfaces

Los métodos públicos deben ser abstractos

Interfaces

  • Collection
  • List, Set, Map
  • Servlet
  • Vínculo

Herencia Múltiple

Una clase puede implementar varias interfaces

Clases Abstractas

Permiten implementar métodos comunes de
las clases que implementan la misma interface

Las clases abstractas pueden contener métodos abstractos, para ser sobreescritos por
los subtipos

Clases Concretas

Una clase no abstracta,
se le llama concreta.

Las clases abstractas se usan entre las interfaces y las clases concretas

Recordemos

Métodos Final

Métodos que no pueden ser sobreescritos

Árbol de Herencia de Vínculo

public interface Vínculo {

  String getUrl();
  String getTexto();
  String getTarget();
}
public abstract class AbstractVínculo implements Vínculo {

  private final String url;
  private final String texto;

  public AbstractVínculo(final String url, final String texto) {
    this.url = Objects.requireNonNull(url);
    this.texto = Objects.requireNonNull(texto);
  }

  @Override
  public final String getUrl() {
    return url;
  }

  @Override
  public final String getTexto() {
    return texto;
  }
}
public abstract class AbstractVínculoInterno extends AbstractVínculo {

  public AbstractVínculoInterno(final String url, final String texto) {
    super(url, texto);
  }

  public final String getTarget() {
    return "";
  }
}
public final class VínculoInterno extends AbstractVínculoInterno {

  public VínculoInterno(final String url, final String texto) {
    super(url, texto);
  }
  
}
public final class VínculoTeléfono extends AbstractVínculoInterno {

  public VínculoTeléfono(final String url, final String texto) {
    super(String.format("tel:%s", url), texto);
  }

  public VínculoTeléfono(final String url) {
    this(url, url);
  }
}
public abstract class AbstractVínculoExterno extends AbstractVínculo {

  public AbstractVínculoExterno(final String url, final String texto) {
    super(url, texto);
  }

  public final String getTarget() {
    return "_blank";
  }
}
public final class VínculoExterno extends AbstractVínculoExterno {

  public VínculoInterno(final String url, final String texto) {
    super(url, texto);
  }

}
public final class VínculoEmail extends AbstractVínculoExterno {

  public VínculoEmail(final String url, final String texto) {
    super(String.format("mailto:%s", url), texto);
  }

  public VínculoEmail(final String url) {
    this(url, url);
  }
}

Recordemos

Tengan en cuenta

Modificadores
de Visibilidad

Traten de usar
el modificador más restrictivo que puedan

¡Cuidados con
la Herencia!

La herencia define una relación ES-UN

La Herencia
mal usada crea malos diseños

Ejemplo

public final class Coordenada {

  private final double latitud;
  private final double longitud;

  public Coordenada(double latitud, double longitud) {
    this.latitud = validoDentroDelRango(latitud, -90.0, 90.0);
    this.longitud = validoDentroDelRango(longitud, -180.0, 180.0);
  }

  public double getLatitud() {
    return latitud;
  }

  public double getLongitud() {
    return longitud;
  }
  
  // código
}

Necesitamos modelar
una ubicación

Solución ingenua

public final class Ubicación extends Coordenada {

  // más parámetros

  public Ubicación(double latitud, double longitud) {
    super(latitud, longitud);
    // más código
  }

}
Ubicación ubicación = new Ubicación(4.6056727, -74.0642803);
ubicación.latitud();
ubicación.longitud();

¡NO hagas esto!

La herencia genera un alto acoplamiento, no apta sin una relación ES-UN

Una Ubicación no ES-UNA Coordenada

¿Qué es un
mejor diseño?

public final class Ubicación {

  private final Coordenada coordenada;

  public Ubicación(double latitud, double longitud) {
    // reutilización del código de Coordenada
    this.coordenada = new Coordenada(latitud, longitud);
  }

  // delegación
  public double latitud() {
    return coordenada.getLatitud();
  }

  public double longitud() {
    return coordenada.getLongitud();
  }
}
Ubicación ubicación = new Ubicación(4.6056727, -74.0642803);
ubicación.latitud();
ubicación.longitud();

😊

Una Ubicación
TIENE-UNA Coordenada

Favorece la composición
sobre la herencia

Aunque la herencia
sigue siendo necesaria
para ciertos diseños

Otra recomendación

🤔

Si en el futuro VínculoInterno y VínculoTeléfono
divergen, podemos
tener un problema

Jerarquía Flexible

Al extender de una clase abstracta, rompemos el acoplamiento directo

En Java toda
clase debe ser
abstracta o final

Más sobre Interfaces

Interfaces pueden tener

  • Métodos públicos abstract
  • Métodos estáticos
  • Métodos default públicos
  • Métodos privados

"Companion Objects"

Es común tener una clase utilitaria para acompañar a un Tipo de Dato

"Companion Objects"

  • Object -> Objects
  • Collection -> Collections
  • Array -> Arrays
  • Path -> Paths
  • File -> Files
  • FileSystem -> FileSystems

¿Si ya tenemos una Interface por qué no
la usamos como Companion Object?

Por eso las
Interfaces permiten métodos estáticos

public interface Vínculo {

  String getUrl();
  String getTexto();
  String getTarget();

  public static Vínculo de(final String url, final String texto) {
    if (url.startsWith("/")) {
      return new VínculoInterno(url, texto);
    }

    if (url.startsWith("http")) {
      return new VínculoExterno(url, texto);
    }

    if (url.startsWith("+")) {
      return texto.isBlank() ? new VínculoTeléfono(url) : new VínculoTeléfono(url, texto);
    }

    if (url.contains("@")) {
      return texto.isBlank() ? new VínculoEmail(url) : new VínculoEmail(url, texto);
    }

    throw new AssertionError(String.format("URL con formato no reconocido: %s", url));
  }
}

Una vez publicada
una Interface ¿ya no se puede cambiar?

Para eso tenemos
métodos default

Si tenemos métodos default y métodos estáticos, para evitar repetición, mejor tener métodos private

Código Limpio

Nombres

  • ¡Nunca pongan una I antes del nombre de una Interfaz! Eviten: ICollection, IList, IServlet, IPath, IVínculo, etc.
  • No utilicen el sufijo Impl para la implementación de una Interfaz. Usen un nombre que indique qué tiene de especial esa Implementación: ArrayList, HttpServlet, LinkedHashMap, VínculoEmail, etc.
  • Si no hay nada "especial" de la implementación utiliza nombres como Base, Common, Simple, Generic, etc.

¡Vistazo al futuro!

¿Si yo creo una jerarquía con una interface / clase abstracta pública qué evita SubTipos Malisiosos?

😞

Sealed Clases

Me permite controlar
cómo se especializa
un Tipo de Dato

public sealed interface Vínculo permits AbstractVínculo {

  String getUrl();
  String getTexto();
  String getTarget();
}
public abstract sealed class AbstractVínculo implements Vínculo 
    permits AbstractVínculoInterno, AbstractVínculoExterno {

  private String url;
  private String texto;

  public AbstractVínculo(final String url, final String texto) {
    this.url = Objects.requireNonNull(url);
	this.texto = Objects.requireNonNull(texto);
  }

  @Override
  public final String getUrl() {
    return url;
  }

  @Override
  public final String getTexto() {
	return texto;
  }
}
public sealed abstract class AbstractVínculoInterno 
    extends AbstractVínculo permits VínculoInterno, VínculoTeléfono {

  public AbstractVínculoInterno(final String url, final String texto) {
    super(url, texto);
  }

  public final String getTarget() {
    return "";
  }
}
public sealed abstract class AbstractVínculoExterno 
    extends AbstractVínculo permits VínculoExterno, VínculoEmail {

  public AbstractVínculoExterno(final String url, final String texto) {
    super(url, texto);
  }

  public final String getTarget() {
    return "_blank";
  }
}

Las demás clases permanecen iguales

Una posible jerarquía
más controlada

Sealed Class

Second Preview en Java 16

SOLID y Patrones de Diseño: Sesión 1 - Herencia, Interfaces y Clases Abstractas

By Carlos Obregón

SOLID y Patrones de Diseño: Sesión 1 - Herencia, Interfaces y Clases Abstractas

Introducción a Principios SOLID y Patrones de Diseño: Herencia, Interfaces y Clases Abstractas en Java

  • 1,168