Caso 1 - Recorrido de listas¶
Ocultación de la implementación¶
Versión inicial: Lista v0.1¶
Abstracción: La clase abstracta List<T>
diferencia entre el qué y el cómo: Qué hace la lista vs. cómo se almacenan los elementos
Criticar la implementación siguiente:
public abstract class List<T> {
public void addFirst(T value) { ... };
public void removeFirst() { ... };
public void addLast(T value) { ... };
public void removeLast() { ... };
public T first() { ... };
public T last() { ... };
public boolean isEmpty() { ... };
public int length() { ... };
public List<T> clone() { ... };
public boolean isEqualTo(List<T>) { ... };
public abstract void traverse();
// etc...
}
Cohesión¶
Cohesion refers to the degree to which the elements inside a module belong together
Críticas a Lista v0.1¶
List<T>
aglutina más de una responsabilidad: almacenar y recorrer. Implementación no cohesionada- ¿Y si hay distintas implementaciones de
traverse()
? Si implementamos varias versiones de la lista, introducimos más dependencias (acoplamiento)
Problemáticas de Lista v0.1¶
- Baja cohesión
- Alta variabilidad no bien tratada → poca flexibilidad
Implementación alternativa: Lista v0.2¶
Delegar funcionalidad hacia las subclases (vía herencia).
Criticar la implementación:
class ListForward<T> extends List<T> {
//...
public void traverse() { // recorrer hacia adelante };
}
class ListBackward<T> extends List<T> {
//...
public void traverse() { // recorrer hacia atras};
}
Críticas a Lista v0.2¶
- ¿Qué operación hace
traverse()
con cada elemento individual (imprimir, sumar, etc.)? ¿Hay que especializar de nuevo para cada tipo de operación? - ¿Y si hay que especializar de nuevo el recorrido: sólo los pares, sólo los impares, etc.?
Problemáticas de Lista v0.2¶
- Elevada complejidad
- Alta variabilidad no bien tratada → poca flexibilidad, mala reutilización
Implementación alternativa: Lista v0.3¶
Ampliamos la interfaz...
public interface List<T> {
public void addFirst(T value);
public void removeFirst();
public void addLast(T value);
public void removeLast();
public T first();
public T last();
public boolean isEmpty();
public int length();
public List<T> clone();
public boolean isEqualTo(List<T>);
public void traverseForward();
public void traverseBackWard();
public void traverseEvens(); //pares
public void traverseOdds(); //impares
// etc...
}
Críticas a Lista v0.3¶
- Si hay que cambiar la operación básica que hace
traverse()
con cada elemento (imprimir, sumar, etc.), ¿cuántos métodos hay que cambiar? Hay muchas dependencias - Cuanto más variedad de recorridos (la interfaz es mayor), menos flexibilidad para los cambios. Implementación poco flexible
Problemáticas de Lista v0.3¶
- Muchas dependencias → acoplamiento
- Poca flexibilidad
Implementación alternativa: Lista v0.4¶
Delegar hacia otra clase
public interface List<T> {
void addFirst(T value);
void removeFirst();
void addLast(T value);
void removeLast();
T first();
T last();
boolean isEmpty();
int length();
List<T> clone();
boolean isEqualTo(List<T>);
Iterator<T> iterator();
}
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Ventajas¶
- Mayor cohesión: Las responsabilidades están ahora separadas:
List
almacena,Iterator
recorre.List
está más cohesionada - Uso de delegación: la responsabilidad de recorrer se ha delegado hacia otro sitio
Ocultar la implementación¶
- Cohesión: módulos auto-contenidos, independientes y con un único propósito
- Acoplamiento: minimizar dependencias entre módulos
- Abstracción: diferenciar el qué y el cómo
- Modularidad: clases, interfaces y componentes/módulos
Alta cohesión, bajo acoplamiento¶
Cuando los componentes están aislados, puedes cambiar uno sin preocuparte por el resto. Mientras no cambies las interfaces externas, no habrá problemas en el resto del sistema
-- Eric Yourdon
Modularidad¶
Reducir el acoplamiento usando módulos o componentes con distintas responsabilidades, agrupados en bibliotecas
Técnicas de ocultación¶
Hay diversas técnicas para ocultar la implementación...
- Encapsular: agrupar en módulos y clases
- Visibilidad:
public
,private
,protected
, etc. - Delegación: incrementar la cohesión extrayendo funcionalidad pensada para otros propósitos fuera de un módulo
- Herencia: delegar en vertical
- Polimorfismo: ocultar la implementación de un método, manteniendo la misma interfaz de la clase base
- Interfaces: usar interfaces bien documentadas
Herencia: generalización y especialización¶
-
Reutilizar la interfaz
- Clase base y derivada son del mismo tipo
- Todas las operaciones de la clase base están también disponibles en la derivada
-
Redefinir vs. reutilizar el comportamiento
- Overriding (redefinición): cambio de comportamiento
- Overloading (sobrecarga): cambio de interfaz
-
Herencia pura vs. extensión
- Herencia pura: mantiene la interfaz tal cual (relación es-un)
- Extensión: amplía la interfaz con nuevas funcionalidades(relación es-como-un). Puede causar problemas de casting.
When you inherit, you take an existing class and make a special version of it. In general, this means that you’re taking a general-purpose class and specializing it for a particular need. [...] it would make no sense to compose a car using a vehicle object —a car doesn’t contain a vehicle, it is a vehicle. The is-a relationship is expressed with inheritance, and the has-a relationship is expressed with composition.
-- Bruce Eckel
Overriding¶
En general, en un lenguaje OO es posible sobreescribir o redefinir (override) los métodos heredados de una superclase. En algunos lenguajes es obligatorio (en otros es recomendado) especificar explícitamente cuándo un método es redefinido.
Ejemplo en Scala¶
class Complejo(real: Double, imaginaria: Double) {
def re = real
def im = imaginaria
override def toString() =
"" + re + (if (im < 0) "" else "+") + im + "i"
}
Ejemplo en Java¶
¿Qué sucede si no ponemos @Override
a los métodos redefinidos?
Disclaimer
Este ejemplo en Java es realmente la implementación de un diseño incorrecto, pues hay una doble dependencia entre las clases Real
y Complejo
. La frontera entre Diseño e Implementación queda aquí un poco difusa.
class Real {
double re;
public Real(double real) {
re = real;
}
public double getRe() { return re; }
/* Probar a comentar el siguiente método y mantener el
Override de Complejo::sum(Real other) */
public Real sum(Real other) {
return new Real(re + other.getRe());
}
/* Probar a comentar el siguiente método y mantener el
Override de Complejo::sum(Complejo other) */
public Complejo sum(Complejo other) {
return new Complejo( re + other.getRe(), other.getIm() );
}
public String toString() {
return String.format("%.1f", re);
}
}
class Complejo extends Real {
double im;
public Complejo(double real, double imaginaria) {
super(real);
im = imaginaria;
}
@Override
public Complejo sum(Real other) {
return new Complejo( re + other.getRe(), im );
}
@Override
public Complejo sum(Complejo other) {
return new Complejo( re + other.getRe(), im + other.getIm() );
}
public Double getIm() { return im; }
public String toString() {
return String.format("%.1f", re) + ((im < 0)? "" : "+") + String.format("%.1f", im) + "i";
}
}
public class MyClass {
public static void main(String args[]) {
Real r = new Real(7.6);
Complejo c = new Complejo(1.2, 3.4);
System.out.println("Número real: " + r);
System.out.println("Número complejo: " + c);
System.out.println("Número complejo: " + c.sum(r) );
System.out.println("Número complejo: " + r.sum(c) );
}
}
@Override
, podemos llegar a confundirnos y hacer sobrecarga cuando queríamos haber hecho sobreescritura.
Casting¶
Ejemplo: Aventura v0.1¶
public class PersonajeDeAccion {
public void luchar() {}
}
public class Heroe extends PersonajeDeAccion {
public void luchar() {}
public void volar() {}
}
public class Creador {
PersonajeDeAccion[] personajes() {
PersonajeDeAccion[] x = {
new PersonajeDeAccion(),
new PersonajeDeAccion(),
new Heroe(),
new PersonajeDeAccion()
};
return x;
}
}
public class Aventura {
public static void main(String[] args) {
PersonajeDeAccion[] cuatroFantasticos = new Creador().personajes();
cuatroFantasticos[1].luchar();
cuatroFantasticos[2].luchar(); // Upcast
// En tiempo de compilacion: metodo no encontrado:
//! cuatroFantasticos[2].volar();
((Heroe)cuatroFantasticos[2]).volar(); // Downcast
((Heroe)cuatroFantasticos[1]).volar(); // ClassCastException
for (PersonajeDeAccion p: cuatroFantasticos)
p.luchar; // Sin problema
for (PersonajeDeAccion p: cuatroFantasticos)
p.volar; // El 0, 1 y 3 van a lanzar ClassCastException
}
}
Críticas a Aventura v0.1¶
- ¿De qué tipos van a ser los personales de acción? → problema de downcasting
- Hay que rediseñar la solución por ser insegura
interface SabeLuchar {
void luchar();
}
interface SabeNadar {
void nadar();
}
interface SabeVolar {
void volar();
}
class PersonajeDeAccion {
public void luchar() {}
}
class Heroe
extends PersonajeDeAccion
implements SabeLuchar,
SabeNadar,
SabeVolar {
public void nadar() {}
public void volar() {}
}
public class Aventura {
static void t(SabeLuchar x)
{ x.luchar(); }
static void u(SabeNadar x)
{ x.nadar(); }
static void v(SabeVolar x)
{ x.volar(); }
static void w(PersonajeDeAccion x)
{ x.luchar(); }
public static void main(String[] args)
{
Heroe i = new Heroe();
t(i); // Tratar como un SabeLuchar
u(i); // Tratar como un SabeNadar
v(i); // Tratar como un SabeVolar
w(i); // Tratar como un PersonajeDeAccion
}
}
Uso correcto de la herencia¶
Hay dos formas de contemplar la herencia:
-
Como tipo:
- Las clases son tipos y las subclases son subtipos
- Las clases satisfacen la propiedad de substitución (LSP, Liskov Substitution Principle): toda operación que funciona para un objeto de la clase C también debe funcionar para un objeto de una subclase de C
-
Como estructura:
- La herencia se usa como una forma cualquiera de estructurar programas
- Esta visión es errónea, pues provoca que no se satisfaga la propiedad LSP
Ejemplo: herencia como estructura¶
class Account {
float balance;
float getBalance() { return balance; }
void transferIn (float amount) { balance -= amount; }
}
class VerboseAccount extends Account {
void verboseTransferIn (float amount) {
super.transferIn(amount);
System.out.println("Balance: "+balance);
};
}
class AccountWithFee extends VerboseAccount {
float fee = 1;
void transferIn (float amount) { super.verboseTransferIn(amount-fee); }
}
- Todos los objetos a de la clase
Account
deben cumplir que si b=a.getBalance() antes de ejecutar a.transferIn(s) y b´=a.getBalance() después de ejecutar a.transferIn(s), entonces b+s=b´. - Sin embargo, con la estructura
AccountWithFee
<VerboseAccount
<Account
, un objeto de tipoAccountWithFee
no funciona bien cuando se contempla como un objetoAccount
. Considérese la siguiente secuencia:
void f(Account a) {
float before = a.getBalance();
a.transferIn(10);
float after = a.getBalance();
// Suppose a is of type AccountWithFee:
// before + 10 != after !!
// before + 10-1 = after
}
Polimorfismo¶
Fenómeno por el que, cuando se llama a una operación de un objeto del que no se sabe su tipo específico, se ejecuta el método adecuado de acuerdo con su tipo.
El polimorfismo se basa en:
-
Enlace dinámico: se elige el método a ejecutar en tiempo de ejecución, en función de la clase de objeto; es la implementación del polimorfismo
-
Moldes (casting)
- Upcasting: Interpretar un objeto de una clase derivada como del mismo tipo que la clase base
- Downcasting: Interpretar un objeto de una clase base como del mismo tipo que una clase derivada suya