marp |
---|
- Objetos
- Aspectos
- Contratos
- Funciones
- Eventos
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...
}
Cohesion refers to the degree to which the elements inside a module belong together
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)
- Baja cohesión
- Alta variabilidad no bien tratada --> poca flexibilidad
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};
}
- ¿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.?
- Elevada complejidad
- Alta variabilidad no bien tratada --> poca flexibilidad, mala reutilización
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...
}
- 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
- Muchas dependencias --> acoplamiento
- Poca flexibilidad
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();
}
- 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
- 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
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
Reducir el acoplamiento usando módulos o componentes con distintas responsabilidades, agrupados en bibliotecas
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
-
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
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
}
}
- ¿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
}
}
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
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 claseAccount
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
}
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