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