- Variabilidad
- Acoplamiento
- Complejidad
- Robustez
- Reutilización
- Flexibilidad
- Ocultación
- Cohesión
- Ortogonalidad
- Delegación
- Herencia
- Polimorfismo
- Composición
- Inyección de dependencias
- Refactoring
- Calidad
- Mixins
- Anónimos y cierres
- Reflexión
- Metaprogramación
- Objetos
- Eventos
- Funcional
- Aspectos
- Contratos
- Ocultación de la implementación - Recorrido de una lista
- Delegación - Implementación de una orquesta
- Inyección de dependencias - Caballeros de la mesa redonda
- Código duplicado - Cálculo de nóminas
- Ortogonalidad con aspectos - Editor de figuras
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 -- E. Yourdon & L. Constantine
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
$\Rightarrow$ 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
$\Rightarrow$ 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
$\Rightarrow$ 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?
$\Rightarrow$ 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
AccountWihFee
<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
Criticar la solución siguiente (parte 1):
abstract class Instrumento {
public void tocar() { }
public static void afinarInstrumento(Instrumento i)
{
// Afinar en funcion del tipo de i
if (i instanceof Viento)
afinarViento(i);
else if (i instanceof Cuerda)
afinarCuerda(i);
// Probar que esta afinado
i.tocar();
}
public static void afinarViento(Viento i)
{ System.out.println("afinal soplido"); }
public static void afinarCuerda(Cuerda i)
{ System.out.println("afinar rasgado"); }
}
class Viento extends Instrumento {
public void tocar() { soplar(); }
public void afinar() { System.out.println("afinar soplido"); }
public void soplar() { System.out.println("soplar"); }
}
class Cuerda extends Instrumento {
public void tocar() { rasgar(); }
public void afinar() { System.out.println("afinar rasgado"); }
public void rasgar() { System.out.println("rasgar"); }
}
public class Orquesta {
ArrayList<Instrumento> instrumentos;
public Orquesta() {
instrumentos = new ArrayList<Instrumento>(3); }
public void tocar() {
for (int i=0; i<instrumentos.size(); i++)
instrumentos.get(i).tocar();
}
public static void main(String[] args) {
instrumentos.add(new Viento());
instrumentos.add(new Cuerda());
for (int i=0; i<instrumentos.size(); i++)
Instrumento.afinarInstrumento(
instrumentos.get(i));
tocar();
}
}
- Acoplamiento: método
static
- Cohesión: ubicación de
main
Usar polimorfismo. Seguir criticando la implementación...
class Orquesta {
ArrayList<Instrumento> instrumentos;
public Orquesta() {
instrumentos = new ArrayList<Instrumento>(3);
}
public void tocar() {
for (int i=0; i<instrumentos.size(); i++)
instrumentos.get(i).tocar();
}
public void afinar(Instrumento i) {
i.afinar(); // Metodo polimorfico
i.tocar(); // Prueba de que esta afinado
}
}
public class PruebaOrquesta {
public static void main(String[] args) {
Orquesta orquesta = new Orquesta();
orquesta.instrumentos.add(new Viento());
orquesta.instrumentos.add(new Cuerda());
orquesta.instrumentos.add(new Percusion());
for (int i=0; i<instrumentos.size(); i++)
orquesta.afinar(orquesta.instrumentos.get(i));
orquesta.tocar();
}
}
abstract class Instrumento {
public void tocar() { };
public void afinar() { };
}
class Viento extends Instrumento {
public void tocar() { soplar(); }
public void afinar() { System.out.println("afinar soplido"); }
public void soplar() { System.out.println("soplar"); }
}
class Cuerda extends Instrumento {
public void tocar() { rasgar(); }
public void afinar() { System.out.println("afinar rasgado"); }
public void rasgar() { System.out.println("rasgar"); }
}
class Percusion extends Instrumento {
public void tocar() { golpear(); }
public void afinar() { System.out.println("afinar golpeado"); }
public void golpear() { System.out.println("golpear"); }
}
- Encapsulación: método
add
- Encapsulación: visibilidad de
Orquesta::instrumentos
(en C++ seríafriend
) - Flexibilidad: la implementación
Orquesta::instrumentos
puede variar, pero no hay colección (agregado) en quien confíeOrquesta
por delegación.
Delegar las altas/bajas de Instrumento
en la colección (agregado) de Orquesta
:
class Orquesta {
protected ArrayList<Instrumento> instrumentos;
public Orquesta() {
instrumentos = new ArrayList<Instrumento>(3);
}
public boolean addInstrumento(Instrumento i) {
return instrumentos.add(i);
}
public boolean removeInstrumento(Instrumento i) {
return instrumentos.remove(i);
}
public void tocar() {
for (int i=0; i<instrumentos.size(); i++)
instrumentos.get(i).tocar();
}
public void afinar(Instrumento i) {
i.afinar();
i.tocar(); // Prueba de que esta afinado
}
}
public class PruebaOrquesta {
public static void main(String[] args) {
Orquesta orquesta = new Orquesta();
orquesta.addInstrumento(new Viento());
orquesta.addInstrumento(new Cuerda());
orquesta.addInstrumento(new Percusion());
for (int i=0; i<orquesta.instrumentos.size(); i++)
orquesta.afinar(orquesta.instrumentos.get(i));
orquesta.tocar();
}
}
- Acoplamiento:
PruebaOrquesta
conoce la implementación basada en unArrayList
de la colección de instrumentos de la orquesta. - Variabilidad: ¿La colección de instrumentos será siempre lineal?
Definir una interfaz para iterar en la colección de instrumentos:
class Orquesta {
protected List<Instrumento> instrumentos;
public Orquesta() {
instrumentos = new ArrayList<Instrumento>(3);
}
public boolean addInstrumento(Instrumento i) {
return instrumentos.add(i);
}
public boolean removeInstrumento(Instrumento i) {
return instrumentos.remove(i);
}
public void tocar() {
for (Iterator<Instrumento> i = instrumentos.iterator(); i.hasNext(); )
i.next().tocar();
}
public void afinar(Instrumento i) {
i.afinar();
i.tocar(); // Prueba de que esta afinado
}
}
public class PruebaOrquesta {
public static void main(String[] args) {
Orquesta orquesta = new Orquesta();
orquesta.addInstrumento(new Viento());
orquesta.addInstrumento(new Cuerda());
orquesta.addInstrumento(new Percusion());
for (Iterator<Instrumento> i = orquesta.instrumentos.iterator(); i.hasNext(); )
orquesta.afinar(i.next());
orquesta.tocar();
}
}
- Ocultación: el atributo
instrumentos
sigue sin ser privado.
Rehacemos la implementación, aprovechando que aparece una nueva versión del lenguaje (Java JDK 1.5) que permite iterar haciendo un for each sobre una colección que implemente la interfaz Iterable
.
Usando delegación + interfaces y el for each de Java 1.5:
Criticar...
class Orquesta {
private List<Instrumento> instrumentos;
public Orquesta() {
instrumentos = new ArrayList<Instrumento>(3);
}
public boolean addInstrumento(Instrumento i) {
return instrumentos.add(i);
}
public boolean removeInstrumento(Instrumento i) {
return instrumentos.remove(i);
}
public List<Instrumento> instrumentos() {
return instrumentos;
}
public void tocar() {
for (Instrumento i: instrumentos)
i.tocar();
}
public void afinar(Instrumento i) {
i.afinar();
i.tocar(); // Prueba de que esta afinado
}
}
public class PruebaOrquesta {
public static void main(String[] args) {
Orquesta orquesta = new Orquesta();
orquesta.addInstrumento(new Viento());
orquesta.addInstrumento(new Cuerda());
orquesta.addInstrumento(new Percusion());
for (Instrumento i: orquesta.instrumentos())
orquesta.afinar(i);
orquesta.tocar();
}
}
- Ocultación: la interfaz del método
instrumentos()
sigue expuesta: el cliente sabe que devuelve unaList
. - Hemos ocultado un poco la implementación de
instrumentos
(que es unaList
), pero ¿conviene saber que es unaList
? Quizá no hemos ocultado lo suficiente.
Nos quedamos sólo con lo que nos interesa de la Orquesta: que es una colección iterable.
Eliminamos lo que no nos interesa: el resto de elementos de la interfaz List
que explican la forma lineal de almacenar los instrumentos.
class Orquesta implements Iterable<Instrumento> {
private List<Instrumento> instrumentos;
public Orquesta() {
instrumentos = new ArrayList<Instrumento>(3);
}
public boolean addInstrumento(Instrumento i) {
return instrumentos.add(i);
}
public boolean removeInstrumento(Instrumento i) {
return instrumentos.remove(i);
}
public Iterator<Instrumento> iterator() {
return instrumentos.iterator();
}
public void tocar() {
for (Instrumento i: this)
i.tocar();
}
public void afinar(Instrumento i) {
i.afinar();
i.tocar(); // Prueba de que esta afinado
}
}
public class PruebaOrquesta {
public static void main(String[] args) {
Orquesta orquesta = new Orquesta();
orquesta.addInstrumento(new Viento());
orquesta.addInstrumento(new Cuerda());
orquesta.addInstrumento(new Percusion());
for (Instrumento i: orquesta)
orquesta.afinar(i);
orquesta.tocar();
}
}
Supongamos que queremos sustituir la implementación basada en una List
por otra (quizá más eficiente) basada en un Map
.
Nota: La interfaz java.util.Map
declara los métodos siguientes:
clear() void – Map
containsKey(Object key) boolean – Map
containsValue(Object value) boolean – Map
entrySet() Set – Map
equals(Object o) boolean – Map
get(Object key) Object – Map
getClass() Class<? extends Object> – Object
hashCode() int – Map
isEmpty() boolean – Map
keySet() Set – Map
notify() void – Object
notifyAll() void – Object
put(Object key, Object value) Object – Map
putAll(Map t) void – Map
remove(Object key) Object – Map
size() int – Map
toString() String – Object
values() Collection – Map
wait() void – Object
wait(long timeout) void – Object
wait(long timeout, int nanos) void – Object
Pero ¡Map
no implementa Iterable
!
Existe una cierta tensión proveedor-cliente en la frontera de la interfaz
- Los proveedores de packages y frameworks quieren ampliar aplicabilidad
- Los clientes quieren una interfaz centrada en sus necesidades particulares
Construimos un Map
y lo pasamos.
- Primera opción: Ninguno de los receptores deberá poder borrar algo del map. Pero ¡hay un
clear()
en elMap
! - Segunda opción: solo algunos tipos de objetos deben poderse guardar. Pero ¡los tipos de objeto a guardar no están restringidos en un
Map
!
¿La interfaz Map
es siempre satisfactoria? ¿seguro que no va a cambiar?
- JDK < 5.0:
Map sensors = new HashMap();
...
Sensor s = (Sensor)sensors.get(sensorId);
- JDK >= 5.0:
Map<Sensor> sensors = new HashMap<Sensor>();
...
Sensor s = sensors.get(sensorId);
Conclusión: Map<Sensor>
ofrece más de lo que necesitamos
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
//...
}
- La interfaz
Map
queda oculta - Filtramos los métodos que no nos sirven
- Más fácil de hacer evolucionar sin impacto en el resto de la aplicación
- El casting queda confinado en la clase Sensors, que es más seguro
Interfaces de frontera: No todo uso de Map
o interfaz de frontera debe quedar encapsulado. Sólo es un consejo para no pasar la interfaz con métodos que no vamos a necesitar.
Así que proponemos esta implementación de la Orquesta:
class Orquesta implements Iterable<Instrumento> {
private Instrumentos instrumentos;
public Orquesta() {
instrumentos = new Instrumentos(3);
}
public boolean addInstrumento(Instrumento i) {
return instrumentos.add(i);
}
public boolean removeInstrumento(Instrumento i) {
return instrumentos.remove(i);
}
public Iterator<Instrumento> iterator() {
return instrumentos.iterator();
}
public void tocar() {
for (Instrumento i: this)
i.tocar();
}
public void afinar(Instrumento i) {
i.afinar();
i.tocar(); // Prueba de que esta afinado
}
}
public class Instrumentos {
private List instrumentos;
public Instrumentos(int numero) {
instrumentos = new ArrayList<numero>();
}
public Instrumento addInstrumento(Instrumento i) {
return instrumentos.add(i);
}
public Instrumento removeInstrumento(Instrumento i) {
return instrumentos.remove(i);
}
}
public class PruebaOrquesta {
public static void main(String[] args) {
Orquesta orquesta = new Orquesta();
orquesta.addInstrumento(new Viento());
orquesta.addInstrumento(new Cuerda());
orquesta.addInstrumento(new Percusion());
for (Instrumento i: orquesta)
orquesta.afinar(i);
orquesta.tocar();
}
}
Esta implementación sí que podemos adaptarla más fácilmente para cambiar el List
por un Map
, pues la responsabilidad de ser iterable ha quedado confinada en Instrumentos
, que desacopla Orquesta
y la implementación elegida (List
, Map
, etc.) para la colección de instrumentos.
Los new
de PruebaOrquesta
siguen introduciendo dependencias de PruebaOrquesta
con respecto a los tipos concretos de Instrumento
.
Ver antes el apartado inyección de dependencias
A través de un fichero de configuración orquesta.xml
le indicamos los valores inyectables:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="trompeta"
class="Viento"/>
<bean id="violin"
class="Cuerda"/>
<bean id="tambor"
class="Percusion"/>
<bean id="viola"
class="Cuerda"/>
<bean id="cuarteto"
class="Orquesta">
<property name="instrumento1">
<ref bean="trompeta"/>
</property>
<property name="instrumento2">
<ref bean="violin"/>
</property>
<property name="instrumento3">
<ref bean="viola"/>
</property>
<property name="instrumento4">
<ref bean="tambor"/>
</property>
</bean>
</beans>
La inyección de la dependencia concreta la hace el contenedor (spring en este ejemplo):
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
public class PruebaOrquesta {
public static void main(String[] args) throws Exception {
BeanFactory factory =
new XmlBeanFactory(new FileInputStream("orquesta.xml"));
Orquesta orquesta =
(Orquesta) factory.getBean("cuarteto");
for (Instrumento i: orquesta)
orquesta.afinar(i);
orquesta.tocar();
}
}
Delegación en horizontal hacia otras clases cuya interfaz es bien conocida
- Los objetos miembro delegados son cambiables en tiempo de ejecución sin afectar al código cliente ya existente
- Alternativa más flexible que la herencia. Ejemplo:
Cola extends ArrayList
implica que una cola va a implementarse como unArrayList
para toda la vida, sin posibilidad de cambio en ejecución
-
Composición (delegación en horizontal)
- Sirve cuando hacen falta las características de una clase existente dentro de una nueva, pero no su interfaz.
- Los objetos miembro privados pueden cambiarse en tiempo de ejecución.
- Los cambios en el objeto miembro no afectan al código del cliente.
-
Herencia (delegación en vertical)
- Sirve para hacer una versión especial de una clase existente, reutilizando su interfaz.
- La relación de herencia en los lenguajes de programación suele ser estática (definida en tiempo de compilación) y no dinámica (que pueda cambiarse en tiempo de ejecución).
- Permite re-interpretar el tipo de un objeto en tiempo de ejecución.
interface Handler{
String toString();
int compareTo(Handler otro);
}
class IdentificadorNumerico implements Handler {
private int id;
IdentificadorNumerico (String id) throws NumberFormatException {
this.id = new Integer(id).intValue();
}
public String toString() {
return new Integer(id).toString();
}
public int compareTo(Handler otro) {
return toString().compareTo(otro.toString());
}
}
java.lang.Comparable
es una interfaz implementada por String
, File
, Date
, etc. y todas las llamadas clases de envoltura del JDK (i.e. Integer
, Long
, etc.)
- JDK 1.4:
public interface Comparable {
public int compareTo(Object o); //throws ClassCastException
}
- JDK 1.5:
public interface Comparable<T> {
public int compareTo(T o); //throws ClassCastException
}
-
Anticonmutativa:
sgn(x.compareTo(y)) = -sgn(y.compareTo(x))
-
Transitividad:
(x.compareTo(y)>0 and y.compareTo(z)>0)
$\Rightarrow$ x.compareTo(z)>0
-
x.compareTo(y)=0
$\Rightarrow$ sgn(x.compareTo(z))=sgn(y.compareTo(z))
$\forall$ z
-
Consistencia con
equals
(no obligatoria):(x.compareTo(y)=0)
$\Leftrightarrow$ (x.equals(y))
- Utilizando templates
- Delegar en
compareTo
yequals
del tipo de id envuelto (e.g.String
)
import java.util.*;
import java.io.*;
public final class BankAccount implements Comparable<BankAccount> {
private final String id;
public BankAccount (String number) {
this.id = number;
}
public String getId() { return id; }
@Override
public int compareTo(BankAccount other) {
if (this == other) return 0;
assert this.equals(other) : "compareTo inconsistent with equals.";
return this.id.compareTo(other.getId());
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof BankAccount)) return false;
BankAccount that = (BankAccount)other;
return this.id.equals(that.getId());
}
@Override
public String toString() {
return id.toString();
}
}
- No hay plantillas. La genericidad se consigue con
Object
. Hay que hacer casting. - Cuidado con
Boolean
que no implementaComparable
en JDK 1.4
import java.util.*;
import java.io.*;
public final class BankAccount implements Comparable {
private final String id;
public BankAccount (String number) {
this.id = number;
}
public String getId() { return id; }
public int compareTo(Object other) {
if (this == other) return 0;
assert (other instanceof BankAccount) : "compareTo comparing objects of different type";
BankAccount that = (BankAccount)other;
assert this.equals(that) : "compareTo inconsistent with equals.";
return this.id.compareTo(that.getId());
}
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof BankAccount)) return false;
BankAccount that = (BankAccount)other;
return this.id.equals(that.getId());
}
public String toString() {
return id.toString();
}
}
Cuando una clase hereda de una clase concreta que implementa Comparable
y le añade un campo significativo para la comparación, no se puede construir una implementación correcta de compareTo
. La única alternativa entonces es la composición en lugar de la herencia.
Una alternativa (no excluyente) a implementar Comparable
es pasar un Comparator
como parámetro (se prefiere composición frente a herencia):
-
Si
BankAccount
implementaComparable
:class BankAccountComparator implements java.util.Comparator<BankAccount> { public int compare(BankAccount o1, BankAccount o2) { return o1.compareTo(o2); } }
-
Si
BankAccount
no implementaComparable
:class BankAccountComparator implements java.util.Comparator<BankAccount> { public int compare(BankAccount o1, BankAccount o2) { return compare(o1.getId(), o2.getId()); } }
¿Qué ventajas tiene la opción que usa Composición frente a la que usa Herencia (estática)?
Tomado de Spring in Action
Añadir pruebas unitarias a la solución siguiente:
public class KnightOfTheRoundTable {
private String name;
private HolyGrailQuest quest;
public KnightOfTheRoundTable(String name) {
this.name = name;
quest = new HolyGrailQuest();
}
public HolyGrail embarkOnQuest() throws GrailNotFoundException {
return quest.embark();
}
}
public class HolyGrailQuest {
public HolyGrailQuest() {}
public HolyGrail embark() throws GrailNotFoundException {
HolyGrail grail = null;
// Look for grail
...
return grail;
}
}
¿Dónde está el acoplamiento?
import junit.framework.TestCase;
public class KnightOfTheRoundTableTest extends TestCase {
public void testEmbarkOnQuest() throws GrailNotFoundException {
KnightOfTheRoundTable knight =
new KnightOfTheRoundTable("CruzadoMagico");
HolyGrail grail = knight.embarkOnQuest();
assertNotNull(grail);
assertTrue(grail.isHoly());
}
}
-
Instanciación de
HolyGrail
-
Cada vez que se prueba
KnightOfTheRoundTable
, también se pruebaHolyGrailQuest
. -
No se puede pedir a
HolyGrailQuest
que se comporte de otra forma (v.g. devolver null o elevar una excepción)
public interface Knight {
Object embarkOnQuest() throws QuestFailedException;
}
public class KnightOfTheRoundTable implements Knight {
private String name;
private Quest quest;
public KnightOfTheRoundTable(String name) {
this.name = name;
quest = new HolyGrailQuest();
}
public Object embarkOnQuest() throws QuestFailedException {
return quest.embark();
}
}
public interface Quest {
abstract Object embark()
throws QuestFailedException;
}
public class HolyGrailQuest implements Quest {
public HolyGrailQuest() {}
public Object embark() throws QuestFailedException {
// Do whatever it means to embark on a quest
return new HolyGrail();
}
}
- El
Knight
aún recibe un tipo específico deQuest
- ¿Debe ser el caballero responsable de obtener un desafío?
public class KnightOfTheRoundTable implements Knight {
private String name;
private Quest quest;
public KnightOfTheRoundTable(String name) {
this.name = name;
}
public Object embarkOnQuest() throws QuestFailedException {
return quest.embark();
}
public void setQuest(Quest quest) {
this.quest = quest;
}
}
- El caballero no es el responsable de averiguar su misión.
- El caballero sólo sabe de su misión a través de la interfaz
Quest
. - El caballero recibe la misión (se le inyecta) a través de
setQuest()
- Puede asignársele cualquier implementación de
Quest
(HolyGrailQuest
,RescueDamselQuest
, etc.)
A través de un fichero de configuración XML le indicamos los valores inyectables:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="quest"
class="HolyGrailQuest"/>
<bean id="knight"
class="KnightOfTheRoundTable">
<constructor-arg>
<value>CruzadoMagico</value>
</constructor-arg>
<property name="quest">
<ref bean="quest"/>
</property>
</bean>
</beans>
La inyección de la dependencia concreta la hace el contenedor (spring en este ejemplo):
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
public class KnightApp {
public static void main(String[] args) throws Exception {
BeanFactory factory =
new XmlBeanFactory(new FileInputStream("knight.xml"));
KnightOfTheRoundTable knight =
(KnightOfTheRoundTable) factory.getBean("knight");
knight.embarkOnQuest();
}
}
También se puede inyectar la dependencia en el constructor.
import java.util.logging.Logger;
public class MyClass {
private final static Logger logger;
public MyClass(Logger logger) {
this.logger = logger;
// write an info log message
logger.info("This is a log message.")
}
}
Un contenedor de dependencias en el framework debe responsabilizarse de crear las instancias de Logger
e inyectarlas en su sitio (normalmente vía reflexión o introspección)
Los new
de PruebaOrquesta
de la versión v0.7 siguen introduciendo dependencias de PruebaOrquesta
con respecto a los tipos concretos de Instrumento
.
A través de un fichero de configuración orquesta.xml
de Spring le indicamos los valores inyectables:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="trompeta"
class="Viento"/>
<bean id="violin"
class="Cuerda"/>
<bean id="tambor"
class="Percusion"/>
<bean id="viola"
class="Cuerda"/>
<bean id="cuarteto"
class="Orquesta">
<property name="instrumento1">
<ref bean="trompeta"/>
</property>
<property name="instrumento2">
<ref bean="violin"/>
</property>
<property name="instrumento3">
<ref bean="viola"/>
</property>
<property name="instrumento4">
<ref bean="tambor"/>
</property>
</bean>
</beans>
La inyección de la dependencia concreta la hace el contenedor (spring en este ejemplo):
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
public class PruebaOrquesta {
public static void main(String[] args) throws Exception {
BeanFactory factory =
new XmlBeanFactory(new FileInputStream("orquesta.xml"));
Orquesta orquesta =
(Orquesta) factory.getBean("cuarteto");
for (Instrumento i: orquesta)
orquesta.afinar(i);
orquesta.tocar();
}
}
Estándar de Java (JSR 330) para describir las dependencias de una clase con anotaciones
public class MyPart {
@Inject private Logger logger;
// inject class for database access
@Inject private DatabaseAccessClass dao;
@Inject
public void createControls(Composite parent) {
logger.info("UI will start to build");
Label label = new Label(parent, SWT.NONE);
label.setText("Eclipse 4");
Text text = new Text(parent, SWT.NONE);
text.setText(dao.getNumber());
}
}
Esta clase sigue usando new
para ciertos elementos de la interfaz. Esto significa que no pensamos reemplazarlos ni siquiera para hacer pruebas.
Supongamos que queremos obtener un listado ordenado por fecha de creación de las cuentas bancarias.
¿Cómo afecta este cambio a la versión de BankAccount
ya implementada con JDK 1.5? Resolver mediante inyección de dependencias
BankAcccount.java
:
import java.util.*;
import java.io.*;
import java.time.*;
public final class BankAccount implements Comparable<BankAccount> {
private final String id;
private LocalDate creationDate;
private Comparator comparator;
public BankAccount(String number) {
this.id = number;
comparator = new BankAccountComparatorById();
}
public LocalDate getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDate date) {
this.creationDate = date;
}
public String getId() {
return id;
}
public void setComparator(Comparator cmp) {
comparator = cmp;
}
@Override
public int compareTo(BankAccount other) {
if (this == other)
return 0;
assert this.equals(other) : "compareTo inconsistent with equals.";
return comparator.compare(this, other);
}
@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (!(other instanceof BankAccount))
return false;
BankAccount that = (BankAccount) other;
return this.id.equals(that.getId());
}
@Override
public String toString() {
return id.toString();
}
}
BankAcccountComparatorById.java
:
import java.util.*;
class BankAccountComparatorById implements Comparator<BankAccount> {
public int compare(BankAccount o1, BankAccount o2) {
return o1.getId().compareTo(o2.getId());
}
}
BankAcccountComparatorByCreationDate.java
:
import java.util.*;
class BankAccountComparatorByCreationDate implements Comparator<BankAccount> {
public int compare(BankAccount o1, BankAccount o2) {
return o1.getCreationDate().compareTo(o2.getCreationDate());
}
}
Los decoradores de TypeScript son una forma de modificar programáticamente la definición de una clase.
La definición de una clase describe la forma de la clase, es decir, sus métodos y propiedades. Sólo cuando se instancie la clase, estas propiedades y métodos estarán disponibles.
Los decoradores permiten inyectar código en la definición real de una clase.
Pueden emplearse sobre:
- definiciones de clase
- definiciones de propiedades
- definiciones de funciones
- parámetros de métodos
Los decoradores de TypeScript se llaman atributos en C# y anotaciones en Java
Los decoradores de TypeScript son una característica experimental del compilador y se han propuesto como parte del estándar ECMAScript 7. Deben activarse modificando el parámetro experimentalDecorators
en tsconfig.json
:
{
"compilerOptions": {
"module": "commonjs",
"target": "es3",
"sourceMap": true,
"experimentalDecorators": true
},
"exclude": [
"node_modules"
]
}
function simpleDecorator(constructor: Function) {
console.log('simpleDecorator called.');
}
@simpleDecorator
class ClassWithSimpleDecorator {
}
¿Cuál es la salida del siguiente código TypeScript?
let instance_1 = new ClassWithSimpleDecorator();
let instance_2 = new ClassWithSimpleDecorator();
console.log(`instance_1: ${instance_1}`);
console.log(`instance_2 : ${instance_2}`);
simpleDecorator called.
instance_1 : [object Object]
instance_2 : [object Object]
¿Cuál es la salida del siguiente código TypeScript?
function simpleDecorator(constructor: Function) {
console.log('simpleDecorator called.');
}
function secondDecorator(constructor: Function) {
console.log('secondDecorator called.')
}
@simpleDecorator
@secondDecorator
class ClassWithMultipleDecorators {
}
let instance_1 = new ClassWithMultipleDecorators();
console.log(`instance_1: ${instance_1}`);
secondDecorator called.
simpleDecorator called.
instance_1 : [object Object]
- Los decoradores pueden aceptar parámetros
- Una factoría de decoradores es una función que devuelve el propio decorador.
function decoratorFactory(name: string) {
return function (constructor: Function) {
console.log(`decorator function called with: ${name}`);
}
}
@decoratorFactory('testName')
class ClassWithDecoratorFactory {
}
Salida:
decorator function called with: testName
- Decoradores de clases
- Decoradores de propiedades
- Decoradores de propiedades estáticas
- Decoradores de métodos
- Decoradores de parámetros
Nathan Rozentals: Mastering TypeScript, Packt Publishing, 2nd edition, 2017
En la siguiente implementación, ¿dónde hay código duplicado?
public class Empleado {
Comparable id;
String name;
public Empleado(String id, String name) {
this.id = id;
this.name = name;
}
public void print() {
System.out.println(id+" "+name);
}
}
public class Autonomo extends Empleado {
String vatCode;
public Autonomo(String id, String name, String vat) {
this.id = id;
this.name = name;
this.vatCode = vat;
}
public void print() {
System.out.println(id+" "+name+" "+vatCode);
}
}
public class Prueba {
public static void main(String[] args) {
Empleado e = new Empleado("0001","Enrique");
Empleado a = new Autonomo("0002","Ana","12345-A");
e.print();
a.print();
}
}
- Código duplicado en los constructores de las clases y subclases
- Refactorizar delegando hacia la superclase
- Requisito: los trabajadores autónomos cobran por horas (no tienen un salario fijo bruto)
- Incluimos el método
computeMonthlySalary
para el cálculo de la nómina mensual - ¿Están descohesionadas las clases?
public class Empleado {
Comparable id;
String name;
float yearlyGrossSalary;
public Empleado(String id, String name) {
this.id = id;
this.name = name;
}
float setSalary( float s ) { yearlyGrossSalary=s; }
public void print() {
System.out.print(id+" "+name);
}
public float computeMonthlySalary() {
return yearlyGrossSalary/12;
}
}
public class Autonomo extends Empleado {
String vatCode;
float workingHours;
public Autonomo(String id, String name, String vat) {
super(id,name);
this.vatCode = vat;
this.workingHours = 0.0;
}
public float computeMonthlySalary() {
return workingHours*Company.getHourlyRate()*(1.0+Company.getVatRate());
}
@Override
public void print() {
super.print();
System.out.print(" "+vatCode);
}
}
public class Prueba {
public static void main(String[] args) {
Empleado e = new Empleado("0001", "Enrique");
Empleado a = new Autonomo("0001", "Ana", "12345-A");
e.print(); System.out.println();
a.print(); System.out.println();
}
}
- ¿Todos los empleados deben tener un salario anual bruto? Los autónomos no...
- El método de cálculo del salario está descohesionado
public abstract class Empleado {
/* ... */
public abstract float computeMonthlySalary();
}
public class Plantilla extends Empleado {
float yearlyGrossSalary;
/* ... */
float setSalary( float s ) { yearlyGrossSalary=s; }
public float computeMonthlySalary() {
return yearlyGrossSalary/12;
}
}
public class Autonomo extends Empleado {
String vatCode;
float workingHours;
public Autonomo(String id, String name, String vat) {
super(id,name);
this.vatCode = vat;
this.workingHours = 0.0;
}
public void addWorkingHours(float workingHours){
this.workingHours += workingHours;
}
public float computeMonthlySalary() {
return workingHours*Company.getHourlyRate()*(1.0+Company.getVatRate());
}
@Override
public void print() {
super.print();
System.out.print(" "+vatCode);
}
}
public class Prueba {
public static void main(String[] args) {
Empleado e = new Plantilla("0001", "Pepe");
e.setSalary(25000.0);
Empleado a = new Autonomo("0001", "Ana", "12345-A");
a.addWorkingHours(30.0);
e.print(); System.out.println(" Salario: "+e.computeMonthlySalary()+" EUR");
a.print(); System.out.println(" Salario: "+a.computeMonthlySalary()+" EUR");
}
}
Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior [@Refactoring]
— M. Fowler, www.refactoring.com
A change made to the internal structure of the software to make it easier to understand and cheaper to modify without changing its observable behavior
- Pequeñas transformaciones
- Mantienen el sistema funcional
¿Cuál es la primera razón para hacer refactoring?
- Código duplicado
- Rutinas demasiado largas
- Bucles demasiado largos o demasiado anidados
- Clases poco cohesionadas
- Interfaz de una clase con un nivel de abstracción poco consistente
- Demasiados parámetros en una lista de parámetros
- Muchos cambios en una clase tienden a estar compartimentalizados (afectan solo a una parte)
- Muchos cambios requieren modificaciones en paralelo a varias clases
- Hay que cambiar jerarquías de herencia en paralelo
- Hay que cambiar muchas sentencias case en paralelo
- Etc.
Véase McConnell(2004): Code Complete
- Mantenimiento
- Cambios (no sólo a nivel de código)
- Trazabilidad
- Impuesta: No hay elección
- Inadvertida: No me he dado cuenta
- Impaciencia: No puedo esperar
- Simultaneidad: Ha sido otro
Copy and paste is a design error
– McConnell (1998)
La gestión del proyecto así nos lo exige. Algunos ejemplos:
- Representaciones múltiples de la información:
- un TAD para guardar elementos de distintos tipos;
- el esquema de una BD configurado en la BD y en el código fuente a través de un ORM
- Documentación del código:
- código incrustado en javadocs
- Casos de prueba:
- pruebas unitarias con jUnit
- Características del lenguaje:
- C/C++ header files
- IDL specs
- Generadores de código: para evitar duplicar representaciones múltiples de la información
- Herramientas de ingeniería inversa: para generar código a partir de un esquema de BD – v.g. jeddict para crear clases JPA, visualizar y modificar BDs y automatizar la generación de código Java EE.
- Plantillas: Tipos genéricos del lenguaje (Java, C++, TypeScript, etc.) o mediante un motor de plantillas – v.g. Apache Velocity template language (VTL)
- Metadatos: Anotaciones @ en Java, decoradores en TypeScript, etc.
- Herramientas de documentación (v.g. asciidoctor: inclusión de ficheros y formateo de código fuente).
- Herramientas de programación literaria
- Ayuda del IDE
Normalmente tiene origen en un diseño inapropiado.
Fuente de numerosos problemas de integración.
public class Line {
public Point start;
public Point end;
public double length;
}
¿Dónde está la duplicación?
Realmente length
ya está definido con start
y end
. ¿Mejor así...?
public class Line {
public Point start;
public Point end;
public double length() {
return start.distanceTo(end);
}
}
¿Es conveniente aplicar siempre DRY?
A veces se puede optar por violar DRY por razones de rendimiento.
Memoization: cachear los resultados de cómputos costosos
public class Line {
private boolean changed;
private double length;
private Point start;
private Point end;
public void setStart(Point p) { start = p; changed = true; }
public void setEnd(Point p) { end = p; changed = true; }
public Point getStart() { return start; }
public Point getEnd() { return end; }
public double getLength() {
if (changed) {
length = start.distanceTo(end);
changed = false;
}
return length;
}
}
La técnica de memoization es menos problemática si queda dentro de los límites de la clase/módulo.
Otras veces no merece la pena violar DRY por rendimiento: ¡las cachés y los optimizadores de código también hacen su labor!
All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation
Conviene aplicar el principio de acceso uniforme para que sea más fácil añadir mejoras de rendimiento (v.g. caching)
public class Line {
private Point Start;
private Point End;
private double Length;
public Point Start {
get { return Start; }
set { Start = value; }
}
public Point End {
get { return End; }
set { Start = value; }
}
public double Length {
get { return Start.distanceTo(End); }
}
}
- Los peligros del copy&paste
- "Vísteme despacio que tengo prisa" (shortcuts make for long delays). Ejemplos:
- Meter el
main
de Java en cualquier clase - Fiasco del año 2000
- Meter el
- No resoluble a nivel de técnicas de construcción
- Hace falta metodología, gestión de equipos + herramientas de comunicación
Dos componentes A y B son ortogonales (
- La base de datos debe ser ortogonal a la interfaz de usuario
- En un helicóptero, los mandos de control no suelen ser ortogonales
- Es más fácil escribir un componente pequeño y auto-contenido que un bloque muy grande de código. El tiempo de desarrollo y pruebas se reduce
- Se pueden combinar unos componentes con otros más fácilmente. Mayor reutilización.
- Si
$A \perp B$ , el componente A sirve para$m$ propósitos y B sirve para$n$ , entonces$A \cup B$ sirve para$m \times n$ propósitos. - La falta de cohesión perjudica la reutilización – v.g. ¿y si hay que hacer una nueva versión gráfica de una aplicación de línea de comandos? (los
System.out.println
pueden descohesionar)
- Defectos aislados, más fáciles de arreglar
- Menor fragilidad del sistema global, los problemas provocados por cambios en un área se limitan a ese área
- Más fácil de probar, pues será más fácil construir pruebas individuales de cada uno de sus componentes (e.g. mocking es más sencillo)
La ortogonalidad es aplicable a:
- la gestión de proyectos
- el diseño
- la codificación
- las pruebas
- la documentación
A nivel de diseño, los patrones de diseño y las arquitecturas como MVC facilitan la construcción de componentes ortogonales.
Técnicas de codificación para fomentar la ortogonalidad:
- Hacer refactoring
- Codificar patrones de diseño: strategy, template method, etc.
- Evitar datos globales y singletons: ¿qué pasaría si hubiera que hacer una versión multithreaded de una aplicación?
- Inyectar: pasar explícitamente el contexto (dependencia) como parámetro a los constructores
- Usar anotaciones (Java), decoradores (JavaScript) o atributos (C#)
- Desacoplar: Ley de Demeter—No hables con extraños
- Usar programación orientada a aspectos
Al pedir un servicio a un objeto, el servicio debe ser realizado de parte nuestra, no que nos devuelva un tercero con el que tratar para realizarlo
Ejemplo:
public boolean canWrite(User user) {
if (user.isAnonymous())
return false;
else {
return user.getGroup().hasPermission(Permission.WRITE);
}
}
Refactorización: definir un método User.hasPermission()
Pasar explícitamente el contexto (dependencia) como parámetro a los constructores de la clase
En el patrón de diseño strategy, pasar el contexto a la estrategia en su creación
public interface Knight {
Object embarkOnQuest() throws QuestFailedException;
}
public class KnightOfTheRoundTable implements Knight {
private String name;
private Quest quest;
public KnightOfTheRoundTable(String name, Quest quest) {
this.name = name;
this.quest = quest;
}
public Object embarkOnQuest() throws QuestFailedException {
return quest.embark();
}
public void setQuest(Quest quest) {
this.quest = quest;
}
}
public interface Quest {
abstract Object embark()
throws QuestFailedException;
}
Los métodos de un objeto solo deben hacer llamadas a métodos...
caso 1. propios caso 2. de objetos pasados como parámetros caso 3. de objetos creados por ellos mismos caso 4. de objetos declarados en el mismo método
class Demeter {
private A a;
private int func();
public void example (B b);
void example(B b) {
C c;
int f = func(); // (caso 1)
b.invert(); // (caso 2)
a = new A();
a.setActive(); // (caso 3)
c.print(); // (caso 4)
}
La ley de Demeter, ¿realmente ayuda a crear código más mantenible?
-
Pintar un gráfico con los datos registrados por una serie de grabadoras (
Recorder
) dispersas por el mundo. -
Cada grabadora está en una ubicación (
Location
), que tiene una zona horaria (TimeZone
). -
Los usuarios seleccionan (
Selection
) una grabadora y pintan sus datos etiquetados con la zona horaria correcta...public void plotDate(Date aDate, Selection aSelection) { TimeZone tz = aSelection.getRecorder().getLocation().getZone(); }
-
Multiplicidad de dependencias:
plotDate
$\dashrightarrow$ Selection
,Recorder
,Location
,TimeZone
. -
Si cambia la implementación de
Location
de forma que ya no incluye directamente unaTimeZone
, hay que cambiarplotDate
-
Añadir un método delegado
getTimeZone
aSelection
. AsíplotDate
no se entera de si laTimeZone
le llega desdeRecorder
o desde un objeto contenido enRecorder
.public void plotDate(Date aDate, TimeZone tz) { /* ... */ } plotDate(someDate, someSelection.getTimeZone());
Ahora
plotDate
$\dashrightarrow$ Selection
,TimeZone
, pero se han eliminado las restantes dependencias. -
Costes de espacio y ejecución de métodos wrapper que reenvían la petición al objeto delegado: violar la ley de Demeter para mejorar el rendimiento
-
Otros ejemplos de mejora del rendimiento: desnormalización de BBDD
- Usar metadatos (@tag) para propósitos específicos – v.g. persistencia de objetos, transacciones, etc.
- Aspect-Oriented Programming (AOP)
class Line implements FigureElement{
private Point p1, p2;
Point getP1() { return p1; }
Point getP2() { return p2; }
void setP1(Point p1) { this.p1 = p1; }
void setP2(Point p2) { this.p2 = p2; }
}
class Point implements FigureElement {
private int x = 0, y = 0;
int getX() { return x; }
int getY() { return y; }
void setX(int x) { this.x = x; }
void setY(int y) { this.y = y; }
}
Hay que actualizar la pantalla tras mover los objetos
Hay una colección de figuras que cambian periódicamente. Se deben monitorizar los cambios para refrescar el display.
class Line {
private Point p1, p2;
Point getP1() { return p1; }
Point getP2() { return p2; }
void setP1(Point p1) {
this.p1 = p1;
}
void setP2(Point p2) {
this.p2 = p2;
}
}
class Point {
private int x = 0, y= 0;
int getX() { return x; }
int getY() { return y; }
void setX(int x) {
this.x = x;
}
void setY(int y) {
this.y = y;
}
}
Implementamos MoveTracking
. ¿Qué dependencias aparecen?
-
Line
$\dashrightarrow$ MoveTracking
-
Point
$\dashrightarrow$ MoveTracking
Solo detecta el cambio de los extremos de una línea.
Line
MoveTracking
class Line {
private Point p1, p2;
Point getP1() { return _p1; }
Point getP2() { return _p2; }
void setP1(Point p1) {
this.p1 = p1;
MoveTracking.setFlag(); // añadido
}
void setP2(Point p2) {
this.p2 = p2;
MoveTracking.setFlag(); // añadido
}
}
class Point {
private int x = 0, y= 0;
int getX() { return x; }
int getY() { return y; }
void setX(int x) {
this.x = x;
}
void setY(int y) {
this.y = y;
}
}
class MoveTracking {
private static boolean flag = false;
public static void setFlag() {
flag = true;
}
public static boolean testAndClear() {�
boolean result = flag;
flag = false;
return result;
}
}
También detecta el cambio de coordenadas de un punto.
Line
MoveTracking
Point
MoveTracking
class Line {
private Point p1, p2;
Point getP1() { return p1; }
Point getP2() { return p2; }
void setP1(Point p1) {
this.p1 = p1;
MoveTracking.setFlag();
}
void setP2(Point p2) {
this.p2 = p2;
MoveTracking.setFlag();
}
}
class Point {
private int x = 0, y = 0;
int getX() { return x; }
int getY() { return y; }
void setX(int x) {
this.x = x;
MoveTracking.setFlag(); //añadido
}
void setY(int y) {
this.y = y;
MoveTracking.setFlag(); //añadido
}
}
class MoveTracking {
private static boolean flag = false;
public static void setFlag() {
flag = true;
}
public static boolean testAndClear() {
boolean result = flag;
flag = false;
return result;
}
}
Las colecciones de figuras son complejas. Las estructuras de objetos son jerárquicas y se producen eventos asíncronos:
La versión 2 hace que un cambio en cualquier elemento provoque un refresco de todas las figuras.
Mejor monitorizar las figuras que cambian...
Decidimos modificar la implementación: cambiar el método setFlag
por collectOne
, indicando la figura que se mueve.
class Line {
private Point p1, p2;
Point getP1() { return p1; }
Point getP2() { return p2; }
void setP1(Point p1) {
this.p1 = p1;
MoveTracking.collectOne(this); // modificado
}
void setP2(Point p2) {
this.p2 = p2;
MoveTracking.collectOne(this); // modificado
}
}
class Point {
private int x = 0, y = 0;
int getX() { return x; }
int getY() { return y; }
void setX(int x) {
this.x = x;
MoveTracking.collectOne(this); // modificado
}
void setY(int y) {
this.y = y;
MoveTracking.collectOne(this); // modificado
}
}
class MoveTracking {
private static Set movees = new HashSet();
public static void collectOne(Object o) {
movees.add(o);
}
public static Set getmovees() {
Set result = movees;
movees = new HashSet();
return result;
}
}
La no ortogonalidad de MoveTracking
con respecto a Line
y Point
hace que la solicitud de un cambio de implementación (el seguimiento de los cambios en las figuras para el refresco en pantalla) provoque un camnbio en los otros módulos (clases).
El cambio de implementación del seguimiento de los cambios para el refresco en pantalla ha dado lugar a modificaciones en todas las clases: Line
, Point
y MoveTracking
La programación orientada a aspectos (AOP) es un paradigma de programación cuyo objetivo es incrementar la modularidad (ortogonalidad) de las implementaciones mediante la separación de aspectos transversales (cross-cutting concerns).
- aspect = modularización de un aspecto de interés (concern) que afecta a varias clases o módulos
- joinpoint = especificación declarativa de un punto en la ejecución de un programa (por ejemplo, la ejecución de un método, el manejo de una excepción, etc.)
- advice = acción a tomar por la especificación de un aspecto dado en un determinado joinpoint.
- Interceptan la ejecución de un joinpoint. Hay una cadena de interceptores alrededor de cada joinpoint.
- Tipos de advice: after, before, around, etc.
- pointcut = predicado que define cuándo se aplica un advice de un aspecto en un jointpoint determinado. Se asocia un advice con la expresión de un pointcut y se ejecuta el advice en todos los joinpoint que cumplan la expresión del pointcut.
En el ejemplo anterior, las clases Line
y Point
no se ven afectadas:
class Line {
private Point p1, p2;
Point getP1() { return p1; }
Point getP2() { return p2; }
void setP1(Point p1) {
this.p1 = p1;
}
void setP2(Point p2) {
this.p2 = p2;
}
}
class Point {
private int x = 0, y = 0;
int getX() { return x; }
int getY() { return y; }
void setX(int x) {
this.x = x;
}
void setY(int y) {
this.y = y;
}
}
Line
MoveTracking
aspect MoveTracking {
private boolean flag = false;
public boolean testAndClear() {
boolean result = flag;
flag = false;
return result;
}
pointcut move():
call(void Line.setP1(Point)) ||
call(void Line.setP2(Point));
after(): move() {
flag = true;
}
}
Line
MoveTracking
Point
MoveTracking
aspect MoveTracking {
private boolean flag = false;
public boolean testAndClear() {
boolean result = flag;
flag = false;
return result;
}
pointcut move():
call(void Line.setP1(Point)) ||
call(void Line.setP2(Point)) ||
call(void Point.setX(int)) ||
call(void Point.setY(int));
after(): move() {
flag = true;
}
}
Ejemplos de pointcut:
call(void Figure.set*(..))
call(public * Figure.* (..))
Line
MoveTracking
Point
MoveTracking
Versión más ortogonal. Todos los cambios están concentrados en un solo aspecto.
aspect MoveTracking {
private Set movees = new HashSet();
public Set getmovees() {
Set result = movees;
movees = new HashSet();
return result;
}
pointcut move(FigureElement figElt):
target(figElt) &&
(call(void Line.setP1(Point)) ||
call(void Line.setP2(Point)) ||
call(void Point.setX(int)) ||
call(void Point.setY(int)));
after(FigureElement fe): move(fe) {
movees.add(fe);
}
}
There is a luxury in self-reproach. When we blame ourselves we feel no one else has a right to blame us.
Oscar Wilde, The Picture of Dorian Gray
Ejemplos de situaciones que "no van a ocurrir nunca":
- Con dos dígitos para el año basta
- Esta aplicación nunca va a usarse en el extranjero
- Este contador nunca va a ser negativo
Añadir aserciones al código para chequear esas situaciones:
void writeString(String s) {
assert(s != null);
...
}
...
for (int i = 0; i < num_entries-1; i++) {
assert(sorted[i] <= sorted[i+i]);
}
Las aserciones sirven para expresar invariantes
Invariante = condición que se puede considerar cierta durante la ejecución de un programa o de parte del mismo. Es un predicado lógico que se debe mantener siempre cierto durante una cierta fase de la ejecución.
Por ejemplo, una invariante de bucle es una condición que es cierta al principio y al final de cada ejecución de un bucle
Forma 1:
assert Expression1 ;
Forma 2:
assert Expression1 : Expression2 ;
Expression1
esboolean
Expression2
devuelve un valor que es pasado al constructor deAssertionError
, que usa una representación en forma de string del valor como detalle del mensaje
En versiones antiguas del JDK, notificar al compilador que las acepte:
javac -source 1.4 *.java
Las aserciones en Java imponen un alto coste en rendimiento y puede ser conveniente desabilitarlas en tiempo de ejecución:
java [ -enableassertions | -ea ] [:<package name>"..." | :<class name> ]
java [ -disableassertions | -da ] [:<package name>"..." | :<class name> ]
try {
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
String input;
System.out.print("Please Type Something here: ");
input = in.readLine();
assert((input.equalsIgnoreCase("Y") ||
(input.equalsIgnoreCase("N")); /* bad idea! */
...
} catch (Exception ex) {
System.out.print("We've had an Exception: " + ex.getMessage());
}
while (Iterator i.hasNext() {
assert(i.next() != null); /* side effect */
Object obj = i.next();
// ...
}
while (Iterator i.hasNext() {
Object obj = i.next();
assert(obj != null);
// ...
}
Sustituir los comentarios que indicaban invariantes:
if (i % 3 == 0) {
...
} else if (i % 3 == 1) {
...
} else { // We know (i % 3 == 2)
...
}
Mejor con aserciones:
if (i % 3 == 0) {
...
} else if (i % 3 == 1) {
...
} else {
assert i % 3 == 2 : i;
...
}
Para selectivas:
switch(suit) {
case Suit.CLUBS:
...
break;
case Suit.DIAMONDS:
...
break;
case Suit.HEARTS:
...
break;
case Suit.SPADES:
...
}
-
Añadir:
default: assert false : suit;
-
o también:
default: throw new AssertionError(suit);
Puntos inalcanzables:
void foo() {
for (...) {
if (...)
return;
}
assert false; // Execution should never reach this point!!!
}
Son un tipo de invariantes internas que se aplican a todas las instancias de una clase, en todos los momentos, excepto cuando una instancia está en transición de un estado consistente a otro.
Por ejemplo, en un árbol binario equilibrado, una invariante de clase puede indicar que está ordenado y equilibrado:
- Añadir código en Java:
// Returns true if this tree is properly balanced private boolean isBalanced() { ... }
- Todo constructor y método público debe llamar a
assert isBalanced();
antes delreturn
.
Es recomendable incluir comprobaciones de invariantes de clase al principio de los métodos de clases cuyo estado es modificable por otras clases (v.g. setters).
A veces hace falta guardar datos antes de hacer un cómputo, para poder luego comprobar una condición cuando el cómputo se haya completado. Ejemplo de cómo hacerlo con una inner class que guarda el estado de variables:
void foo(int[] array) {
// Manipulate array
...
// At this point, array will contain exactly the ints that it did
// prior to manipulation, in the same order.
}
void foo(final int[] array) {
class DataCopy {
private int[] arrayCopy;
DataCopy() { arrayCopy = (int[])(array.clone()); }
boolean isConsistent() { return Arrays.equals(array, arrayCopy); }
}
DataCopy copy = null;
// Always succeeds; has side effect of saving a copy of array
assert (copy = new DataCopy()) != null;
... // Manipulate array
assert copy.isConsistent();
}
- Un contrato entre dos partes define derechos y responsabilidades por ambas partes
- Define las repercusiones por incumplimiento del contrato
- Desarrollado para lenguaje Eiffel por Bertrand Meyer
- Documentar y aceptar los derechos y responsabilidades de cada módulo de software para asegurar la correción de un programa
- Un programa correcto es aquél que hace nada más y nada menos que lo que dice hacer
- Qué debe ser cierto antes de llamar a una rutina/método (sus requisitos)
- Una rutina jamás debe ser llamada si se violan sus precondiciones
- Es responsabilidad del que la llama hacer que se cumplan
- Qué garantiza la rutina: estado del mundo cuando la rutina/método termina
- Implica que la rutina debe finalizar: no puede haber bucles infinitos
- Condición que se cumple para todas las instancias de la clase, desde la perspectiva del llamador
- Durante el procesamiento interno, la invariante puede no cumplirse, pero sí cuando la rutina termina y se devuelve el control al llamador
- Una clase no puede dar permiso de escritura sin restricciones sobre las propiedades (data members) que participan en la definición de la invariante
sqrt: DOUBLE is
-- Square root routine
require
sqrt_arg_must_be_positive: Current >= 0;
--- ...
--- calculate square root here
--- ...
ensure
((Result*Result) - Current).abs <= epsilon*Current.abs;
-- Result should be within error tolerance
end;
Si el usuario introduce un número negativo en la consola, es responsabilidad del código que llama a sqrt
que dicho valor no se pase nunca a sqrt
. Opciones:
- Terminar
- Emitir una advertencia y leer otro número
- Pasar el número a complejo (ponerlo en positivo y añadir una i)
Si se llega a pasar un número negativo, Eiffel imprime el error sqrt_arg_must_be_positive
en tiempo de ejecición y una traza de la pila (En otros lenguajes, como Java, se devolvería un Nan
).
class ACCOUNT
feature
balance: INTEGER
owner: PERSON
minimum_balance: INTEGER is 1000
open (who: PERSON) is
-- Assign the account to owner who.
do
owner := who
end
deposit (sum: INTEGER) is
-- Deposit sum into the account.
do
add (sum)
end
withdraw (sum: INTEGER) is
-- Withdraw sum from the account.
do
add (-sum)
end
may_withdraw (sum: INTEGER): BOOLEAN is
-- Is there enough money to withdraw sum?
do
Result := (balance >= sum + minimum_balance)
end
feature {NONE}
add (sum: INTEGER) is
-- Add sum to the balance.
do
balance := balance + sum
end
end -- class ACCOUNT
feature
son las operaciones de la clasefeature { NONE }
son privadosmake
para definir el constructor
class ACCOUNT
create
make
feature
-- ... Attributes as before:
balance, minimum_balance, owner, open ...
deposit (sum: INTEGER) is
-- Deposit sum into the account.
require
sum >= 0
do
add (sum)
ensure
balance = old balance + sum
end
withdraw (sum: INTEGER) is
-- Withdraw sum from the account.
require
sum >= 0
sum <= balance - minimum_balance
do
add (-sum)
ensure
balance = old balance - sum
end
may_withdraw ... -- As before
feature {NONE}
add ... -- As before
make (initial: INTEGER) is
-- Initialize account with initial balance.
require
initial >= minimum_balance
do
balance := initial
end
invariant
balance >= minimum_balance
end -- class ACCOUNT
Forma corta del contrato:
class interface ACCOUNT
create
make
feature
balance: INTEGER
...
deposit (sum: INTEGER) is
-- Deposit sum into the account.
require
sum >= 0
ensure
balance = old balance + sum
withdraw (sum: INTEGER) is
-- Withdraw sum from the account.
require
sum >= 0
sum <= balance - minimum_balance
ensure
balance = old balance - sum
may_withdraw (...): BOOLEAN is ...
end -- class ACCOUNT
Java no permite especificar contratos (los assert no son lo mismo). Así que hay que utilizar extensiones como iContract
Ejemplo: Inserción en una lista ordenada
/**
* @invariant forall Node n in elements() |
* n.prev() != null
* implies
* n.value().compareTo(n.prev().value()) > 0
*/
public class OrderedList {
/**
* @pre contains(aNode) == false
* @post contains(aNode) == true
*/
public void insertNode(final Node aNode) {
// ...
}
// ...
}
Una postcondición puede necesitar expresarse con parámetros pasados a un método para verificar un comportamiento correcto.
Si el método puede cambiar el valor del parámetro pasado (parámetro mutable), el contrato puede incumplirse.
- Eiffel no permite que se pueda cambiar el valor de un parámetro (es inmutable)
- En C++ usar
const
- Opciones en Java:
- Usar
final
para marcar un parámetro constante. Sin embargo, las subclases podrían redefinir los parámetros y volver a hacerlos mutables. Ademásfinal
se aplica a la referencia, no al objeto en sí. - Usar
variable@pre
de iContract
- Usar
- Muchos lenguajes funcionales (Lisp, Haskell, Erlang, Clojure, etc.) definen inmutabilidad por defecto
- Por rendimiento (v.g.
String
en Java): si es inmutable, para copiar un objeto basta con copiar la referencia (interning) - Por thread-safety para código concurrente
- Se recomienda escribir código "perezoso" para los contratos: ser estricto en lo que se acepta al empezar y prometer lo menos posible al terminar.
- Si un contrato indica que se acepta cualquier cosa y promete la luna a cambio, habrá que escribir un montón de código!
El diseño y la programación basada en contratos son una forma de gestionar los errores mediante early crash.
Hay diversas técnicas de gestión de errores (que veremos más adelante), pero en general el principio básico es: cuando el código descubre que sucede algo que supuestamente es imposible o "no debería suceder", el programa ya no es viable: eutanasia.
- En Java se lanza una
RuntimeException
cuando sucede algo extraño en tiempo de ejecución. - Se puede/debe hacer lo mismo con cualquier lenguaje
Aún no hay contratos en C++20
- Ver el video de J. D. García sobre Contracts programming after C++17: Desde el minuto 4'10''
- No hay soporte para propagar aserciones por una jerarquía de herencia: si se redefine un método con contrato, las aserciones que implementan el contrato no serán llamadas correctamente (excepto si se duplican en el código)
- No hay soporte para valores antiguos: si se implementara un contrato mediante aserciones, habría que añadir código a la precondición para guardar la información que quiera usarse en la postcondición. (v.g.
variable@pre
en iContract versusold expression
en Eiffel) - El sistema de runtime y las bibliotecas no están diseñadas para dar soporte a contratos, así que estos no se chequean. Y es precisamente en la frontera entre el cliente y la biblioteca donde hay más problemas.
A routine redeclaration [in a derivative] may only replace the original precondition by one equal or weaker, and the original post-condition by one equal or stronger
–– B. Meyer
Métodos de clase declaran precondiciones y postcondiciones al redefinir una operación en una subclase derivada
- las precondiciones sólo pueden sustituirse por otras más débiles/laxas
- las postcondiciones sólo pueden sustituirse por otras más fuertes/estrictas
Un ejemplo habitual de tratamiento de errores con códigos de error:
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
logger.log("page deleted");
} else {
logger.log("configKey not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}
Con esta técnica creamos imanes de dependencias:
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
Los programadores intentan evitar añadir nuevos motivos de error, porque eso significa tener que volver a compilar y desplegar todo el código.
Usar excepciones en lugar de códigos de error:
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}
¿No queda más claro?
Ventaja: las nuevas excepciones son derivadas de una clase base Exception
, lo que facilita la definición de nuevos motivos de error.
¿Dónde se produce el error?
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
¿No queda más fácil de comprender, modificar y depurar?
-
Checked: instancias de clases derivadas de
java.lang.Throwable
(menosRuntimeException
). Deben declararse en el método mediantethrows
y obligan al llamador a tratar la excepción. -
Unchecked: instancias de clases derivadas de
java.lang.RuntimeException
. No se declaran en el método y no obligan al llamador a tratar la excepción.
Elevar una excepción e
implica:
- Deshacer (roll back) la llamada a un método
- hasta que se encuentre un bloque catch para el tipo de
e
- y, si no se encuentra, la excepción es capturada por la JVM, que detiene el programa
try {
/* guarded region that can send
IOException or Exception */
}
catch (IOException e) {
/* decide what to do when an IOException
or a sub-class of IOException occurs */
}
catch (Exception e) {
// Treats any other exceptions
}
finally {
// in all cases execute this
}
Incluir el contexto de la ejecución:
- Incluir información suficiente con cada excepción para determinar el motivo y la ubicación de un error
- No basta con el stack trace
- Escribir mensajes informativos: operación fallida y tipo de fallo
Usar solamente excepciones unchecked
- C#, C++, Python o Ruby no ofrecen excepciones checked.
- Los beneficios de las checked en Java son mínimos
- Se paga el precio de violar el principio OCP (Open-Closed Principle): si lanzamos una excepción checked desde un método y el
catch
está tres niveles por encima, hay que declarar la excepción en la signatura de todos los métodos que van entre medias. Esto significa que un cambio en un nivel bajo del software puede forzar cambios en niveles altos
Muchas APIs de Java lanzan excepciones checked cuando deberían ser unchecked
Ejemplo: Al ejecutar una consulta mediante executeQuery
en el API de JDBC se lanza una excepción java.sql.SQLException
(de tipo checked) si la SQL es errónea.
Transformar las excepciones checked en unchecked:
try {
// Codigo que genera la excepcion checked
} catch (Exception ex) {
throw new RuntimeException("Unchecked exception", ex)
}
Criticar la siguiente implementación:
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
...
}
Excesiva duplicación de código: llamada a reportPortError()
Excepción encapsulada:
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportPortError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() throws PortDeviceFailure {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
- La encapsulación de excepciones es recomendable cuando se usa un API de terceros, para minimizar las dependencias con respecto al API elegido.
- También facilitan la implementación de mocks del componente que proporciona el API para construir pruebas.
Recomendación de uso: Usar excepciones para problemas excepcionales (eventos inesperados)
Ejemplo: ¿Usar excepciones cuando se intenta abrir un fichero para leer y el fichero no existe?
Depende de si el fichero debe estar ahí
-
Usando excepciones:
public void open_passwd() throws FileNotFoundException { // This may throw FileNotFoundException... ipstream = new FileInputStream("/etc/passwd"); // ... }
-
Sin usar excepciones:
public boolean open_user_file(String name) throws FileNotFoundException { File f = new File(name); if (!f.exists()) return false; ipstream = new FileInputStream(f); return true; }
Obtener un null cuando no se espera puede ser un quebradero de cabeza para el tratamiento de errores
Este código puede parecer inofensivo, pero es maligno:
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
¿Qué pasa si persistentStore
es null?
- Peligro de
NullPointerException
- ¿Se nos ha olvidado añadir un
if null
? - El problema no es que se haya olvidado uno, sino que hay demasiados
- En su lugar, elevar una excepción o devolver un objeto especial
Evitar:
List<Employee> employees = getEmployees();
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
Mejor así:
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if( /* there are no employees */ )
return Collections.emptyList();
}
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
¿Qué sucede si llamamos a xProjection()
así...?
calculator.xProjection(null, new Point(12, 13))
Devolver null es malo, pero ¡pasar un valor null es peor!
¿Es mejor así...?
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw InvalidArgumentException(
"Invalid argument for MetricsCalculator.xProjection");
}
return (p2.x - p1.x) * 1.5;
}
}
¿Qué curso de acción tomar ante un InvalidArgumentException
? ¿Hay alguno?
(solo para JDK $\geq$ 5.0)
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null";
assert p2 != null : "p2 should not be null";
return (p2.x - p1.x) * 1.5;
}
}
Es una buena forma de documentar, pero no resuelve el problema
- En la mayoría de lenguajes no hay forma satisfactoria de tratar con nulls pasados como argumento accidentalmente.
- Para eso están los options u optionals, disponibles actualmente en muchos languajes:
- Scala
Option
- Java 8
java.util.Optional
- C++17
std::optional
- Scala
- TypeScript recomienda usar
undefined
(algo que no se ha inicializado) en lugar denull
(algo que no está disponible)
En Scala, Option[T]
es un contenedor de un valor opcional de tipo T.
- Si el valor de tipo T está presente,
Option[T]
es una intancia deSome[T]
que contiene el valor presente de tipo T. - Si el valor está ausente,
Option[T]
es el objetoNone
.
object Demo {
def main(args: Array[String]) {
val a:Option[Int] = Some(5)
val b:Option[Int] = None
println("a.isEmpty: " + a.isEmpty ) //false
println("b.isEmpty: " + b.isEmpty ) //true
}
}
object Demo {
def main(args: Array[String]) {
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
println("show(capitals.get( \"Japan\")) : " + show(capitals.get( "Japan")) )
println("show(capitals.get( \"India\")) : " + show(capitals.get( "India")) )
}
def show(x: Option[String]) = x match {
case Some(s) => s
case None => "?"
}
}
Programa de prueba:
public class MobileTesterWithoutOptional {
public static void main(String[] args) {
ScreenResolution resolution = new ScreenResolution(750,1334);
DisplayFeatures dfeatures = new DisplayFeatures("4.7", resolution);
Mobile mobile = new Mobile(2015001, "Apple", "iPhone 6s", dfeatures);
MobileService mService = new MobileService();
int mobileWidth = mService.getMobileScreenWidth(mobile);
System.out.println("Apple iPhone 6s Screen Width = " + mobileWidth);
ScreenResolution resolution2 = new ScreenResolution(0,0);
DisplayFeatures dfeatures2 = new DisplayFeatures("0", resolution2);
Mobile mobile2 = new Mobile(2015001, "Apple", "iPhone 6s", dfeatures2);
int mobileWidth2 = mService.getMobileScreenWidth(mobile2);
System.out.println("Apple iPhone 16s Screen Width = " + mobileWidth2);
}
}
Dependencias: MobileService
DisplayFeatures
, ScreenResolution
Cantidad de código boilerplate para comprobar los nulos en la clase principal:
public class MobileService {
public int getMobileScreenWidth(Mobile mobile){
if(mobile != null){
DisplayFeatures dfeatures = mobile.getDisplayFeatures();
if(dfeatures != null){
ScreenResolution resolution = dfeatures.getResolution();
if(resolution != null){
return resolution.getWidth();
}
}
}
return 0;
}
}
Clases de utilidad:
public class ScreenResolution {
private int width;
private int height;
public ScreenResolution(int width, int height){
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
public class DisplayFeatures {
private String size; // In inches
private ScreenResolution resolution;
public DisplayFeatures(String size, ScreenResolution resolution){
this.size = size;
this.resolution = resolution;
}
public String getSize() {
return size;
}
public ScreenResolution getResolution() {
return resolution;
}
}
public class Mobile {
private long id;
private String brand;
private String name;
private DisplayFeatures displayFeatures;
// Likewise we can see Memory Features, Camera Features etc.
public Mobile(long id, String brand, String name, DisplayFeatures displayFeatures){
this.id = id;
this.brand = brand;
this.name = name;
this.displayFeatures = displayFeatures;
}
public long getId() {
return id;
}
public String getBrand() {
return brand;
}
public String getName() {
return name;
}
public DisplayFeatures getDisplayFeatures() {
return displayFeatures;
}
}
Uso de métodos de Optional
en el programa de prueba:
public class MobileTesterWithOptional {
public static void main(String[] args) {
ScreenResolution resolution = new ScreenResolution(750,1334);
DisplayFeatures dfeatures = new DisplayFeatures("4.7", Optional.of(resolution));
Mobile mobile = new Mobile(2015001, "Apple", "iPhone 6s", Optional.of(dfeatures));
MobileService mService = new MobileService();
int width = mService.getMobileScreenWidth(Optional.of(mobile));
System.out.println("Apple iPhone 6s Screen Width = " + width);
Mobile mobile2 = new Mobile(2015001, "Apple", "iPhone 6s", Optional.empty());
int width2 = mService.getMobileScreenWidth(Optional.of(mobile2));
System.out.println("Apple iPhone 16s Screen Width = " + width2);
}
}
Menos código boilerplate en la clase principal:
public class MobileService {
public Integer getMobileScreenWidth(Optional<Mobile> mobile){
return mobile.flatMap(Mobile::getDisplayFeatures)
.flatMap(DisplayFeatures::getResolution)
.map(ScreenResolution::getWidth)
.orElse(0);
}
}
Clases de utilidad:
import java.util.Optional;
public class DisplayFeatures {
private String size; // In inches
private Optional<ScreenResolution> resolution;
public DisplayFeatures(String size, Optional<ScreenResolution> resolution){
this.size = size;
this.resolution = resolution;
}
public String getSize() {
return size;
}
public Optional<ScreenResolution> getResolution() {
return resolution;
}
}
public class Mobile {
private long id;
private String brand;
private String name;
private Optional<DisplayFeatures> displayFeatures;
// Like wise we can see MemoryFeatures, CameraFeatures etc.
// For simplicity, using only one Features
public Mobile(long id, String brand, String name, Optional<DisplayFeatures> displayFeatures){
this.id = id;
this.brand = brand;
this.name = name;
this.displayFeatures = displayFeatures;
}
public long getId() {
return id;
}
public String getBrand() {
return brand;
}
public String getName() {
return name;
}
public Optional<DisplayFeatures> getDisplayFeatures() {
return displayFeatures;
}
}
Tensión proveedor-cliente
- Los proveedores de packages y frameworks quieren amplia aplicabilidad
- Los clientes quieren una interfaz centrada en sus necesidades particulares
Ejemplo: La interfaz java.util.Map
clear() void – Map
containsKey(Object key) boolean – Map
containsValue(Object value) boolean – Map
entrySet() Set – Map
equals(Object o) boolean – Map
get(Object key) Object – Map
getClass() Class<? extends Object> – Object
hashCode() int – Map
isEmpty() boolean – Map
keySet() Set – Map
notify() void – Object
notifyAll() void – Object
put(Object key, Object value) Object – Map
putAll(Map t) void – Map
remove(Object key) Object – Map
size() int – Map
toString() String – Object
values() Collection – Map
wait() void – Object
wait(long timeout) void – Object
wait(long timeout, int nanos) void – Object
Construimos un Map
y lo pasamos.
- Diseño A: Ninguno de los receptores deberá poder borrar algo del map. ¡Pero hay un
clear()
! - Diseño B: solo algunos tipos de objetos deben poderse guardar. ¡Los tipos no están restringidos!
¿La interfaz Map
es siempre satisfactoria? ¿seguro que no va a cambiar?
-
JDK < 5.0:
Map sensors = new HashMap(); ... Sensor s = (Sensor)sensors.get(sensorId);
-
JDK
$\geq$ 5.0:Map<Sensor> sensors = new HashMap<Sensor>(); ... Sensor s = sensors.get(sensorId);
Conclusión: Map ofrece más de lo que necesitamos
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
//...
}
- La interfaz
Map
queda oculta - Filtramos los métodos que no nos sirven
- Más fácil de hacer evolucionar sin impacto en el resto de la aplicación
- El casting queda confinado en la clase Sensors, que es más seguro
Interfaces de frontera: No todo uso de Map
o interfaz de
frontera debe quedar encapsulado. Sólo es un consejo para no ’pasarla’
con métodos que no vamos a necesitar.