En relación a los principios de alta cohesión y bajo acoplamiento, criticar la implementación siguiente en Java de un TAD Lista:
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...
}
List<T> diferencia entre el qué y el cómoCohesion refers to the degree to which the elements inside a module belong together
--- E. Yourdon & L. Constantine.
Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design. Prentice Hall, 2nd edition, 1986.
v0.1 • críticas
List<T> aglutina más de una responsabilidad: almacenar y recorrer. La implementación no parece cohesionada.traverse() proporciona una interfaz a los métodos que implementen el recorrido de la lista ¿para hacer qué?traverse()? Si implementamos varias versiones de la lista, introducimos más dependencias (acoplamiento)Hay que crear nuevos tipos de recorrido. 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
}
traverse() con cada elemento (imprimir, sumar, etc.), ¿cuántos métodos hay que cambiar? Hay muchas dependenciasDelegar 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};
}
traverse() con cada elemento individual (imprimir, sumar, etc.)?¿Cómo se resuelve esto en las bibliotecas típicas que conocéis
(v.g. C++ STL, Java Collections, etc.)?
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();
}
List almacena, Iterator recorre. List está más cohesionadaList más cohesionada, se ha tenido que introducir una dependencia (acoplamiento)| Problema | desde v0.1 ... | ... hasta v0.4 |
|---|---|---|
| Cohesión | Baja: List<T> aglutina almacenamiento y recorrido |
Alta: List<T> solo almacena; Iterator<T> recorre |
| Variabilidad | No tratada: difícil cambiar la forma de recorrer | Fácil crear nuevos Iterator sin tocar List |
| Flexibilidad | Poca: cambios en recorrido afectan a List |
Mayor: Cambios aislados en Iterator |
| Acoplamiento | Alto: List depende de cómo se recorre |
Bajo: separación clara de responsabilidades |
Los principios aplicados han sido:
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
--- E. Yourdon & L. Constantine.
Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design. Prentice Hall, 2nd edition, 1986.
Reducir el acoplamiento usando módulos o componentes con distintas responsabilidades, agrupados en bibliotecas
public, private, protected, etc.Reutilizar la interfaz
Redefinir vs. reutilizar el comportamiento
Herencia pura vs. extensión
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
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:
class Complejo(real: Double, imaginaria: Double) {
def re = real
def im = imaginaria
override def toString() =
"" + re + (if (im < 0) "" else "+") + im + "i"
}
overrideoverride es necesario para evitar sobreescrituras accidentales.trait en Scala).Un trait es una forma de separar las dos principales responsabilidades de una clase: definir el estado de sus instancias y definir su comportamiento.
Las clases y los objetos en Scala pueden extender un trait
Los traitde Scala son similares a las interface de Java.
Los trait no pueden instanciarse
Los métodos definidos en una clase tienen precedencia sobre los de un trait
Los trait no tienen estado propio, sino el del objeto o la instancia de la clase a la que se aplica
trait Iterator[A] {
def hasNext: Boolean
def next(): A
}
class IntIterator(to: Int) extends Iterator[Int] {
private var current = 0
override def hasNext: Boolean = current < to
override def next(): Int = {
if (hasNext) {
val t = current
current += 1
t
} else 0
}
}
val iterator = new IntIterator(10)
println(iterator.next()) // prints 0
println(iterator.next()) // prints 1
¿Y en Java no hay traits?
@Override en JavaEste 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 Main {
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) );
}
}
¿Qué sucede si no ponemos @Override a los métodos redefinidos?
Si no se añade @Override, podemos confundirnos y hacer un overload accidental de un método cuando realmente queríamos redefinirlo.
DescribeCar muestra una descripción básica de un coche y llama a ShowDetails para información adicional.ShowDetailsnew y override distintos en cada clase ConvertibleCar y Minivanclass Car
{
public void DescribeCar()
{
System.Console.WriteLine("Four wheels and an engine.");
ShowDetails();
}
public virtual void ShowDetails()
{
System.Console.WriteLine("Standard transportation.");
}
}
class ConvertibleCar : Car
{
public new void ShowDetails()
{
System.Console.WriteLine("A roof that opens up.");
}
}
class Minivan : Car
{
public override void ShowDetails()
{
System.Console.WriteLine("Carries seven people.");
}
}
public static void TestCars1()
{
System.Console.WriteLine("\nTestCars1\n----------");
var cars = new List<Car> {
new Car(),
new ConvertibleCar(),
new Minivan() };
foreach (var car in cars)
{
car.DescribeCar();
System.Console.WriteLine("----------");
}
}
TestCars produce la salida siguiente:
TestCars1
----------
Four wheels and an engine.
Standard transportation.
----------
Four wheels and an engine.
Standard transportation.
----------
Four wheels and an engine.
Carries seven people.
----------
¿Los resultados son los esperados?
ConvertibleCar, pero DescribeCar no accede a la versión de ShowDetails definida en ConvertibleCar (debido a new).Minivan, que redefine con override el método ShowDetails declarado en la clase base.public static void TestCars2()
{
System.Console.WriteLine("\nTestCars2\n----------");
ConvertibleCar car2 = new ConvertibleCar();
Minivan car3 = new Minivan();
car2.ShowDetails();
car3.ShowDetails();
}
public static void TestCars3()
{
System.Console.WriteLine("\nTestCars3\n----------");
Car car2 = new ConvertibleCar();
Car car3 = new Minivan();
car2.ShowDetails();
car3.ShowDetails();
}
Estos métodos producirían las salidas siguientes:
TestCars2
----------
A roof that opens up.
Carries seven people.
TestCars3
----------
Standard transportation.
Carries seven people.
TextCars2, el tipo de los objetos creados coincide con el tipo declarado.TextCars3, el tipo de los objetos creados es una subclase de la clase del tipo declarado.¿Sobre qué mecanismo funciona el polimorfismo?
Tipado estático vs. dinámico
Contrato nominal vs. estructural
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
}
}
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
}
}
template <typename T>
concept SabeLuchar = requires(T t) { t.luchar(); };
template <typename T>
concept SabeNadar = requires(T t) { t.nadar(); };
template <typename T>
concept SabeVolar = requires(T t) { t.volar(); };
// Clase base normal
struct PersonajeDeAccion {
void luchar() { std::cout << "Pum! (Luchando)\n"; }
};
struct Heroe : public PersonajeDeAccion {
void nadar() { std::cout << "Splash! (Nadando)\n"; }
void volar() { std::cout << "Whoosh! (Volando)\n"; }
};
interface (Java), definimos qué requiere el tipoHeroe hereda código de PersonajeDeAccion, pero NO necesita declarar implements SabeNadar, SabeVolarHeroe cumple los concepts automáticamente por tener los métodos// Usamos 'auto&' para pasar por
// referencia (evitar copias).
void t(SabeLuchar auto& x) {
x.luchar();
}
void u(SabeNadar auto& x) {
x.nadar();
}
void v(SabeVolar auto& x) {
x.volar();
}
void w(PersonajeDeAccion& x) {
x.luchar();
}
t, u, v usan concepts (w usa herencia clásica con PersonajeDeAccion o sus hijos explícitos (como en Java).int main() {
Heroe i;
t(i); // Heroe hereda luchar(), así que cumple SabeLuchar
u(i); // Heroe tiene nadar(), cumple SabeNadar
v(i); // Heroe tiene volar(), cumple SabeVolar
w(i); // Heroe es hijo de PersonajeDeAccion
return 0;
}
concept se resuelve en tiempo de compilación, sin necesidad de castingHerencia de interfaz vs. herencia de comportamiento o implementación:
implements = herencia de interfaz y extends = herencia de interfaz + implementaciónHerencia como tipo vs herencia como estructura:
¿El polimorfismo está ligado siempre a la herencia?
Tipos genéricos
Diferencias entre lenguajes:
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); }
}
Account deben cumplir que si AccountWithFee < VerboseAccount < Account, un objeto de tipo AccountWithFee no funciona bien cuando se contempla como un objeto Account. 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
}
abstract class Writer {
def print(str: String): Unit
}
class ConsoleWriter extends Writer {
override def print(str: String) = println(str)
}
class UppercaseWriter extends ConsoleWriter {
override def print(str: String) =
super.print(str.toUpperCase())
}
object Test {
def main(args: Array[String]) {
val writer = new UppercaseWriter
writer.print("abc")
}
}
Ahora hay que añadir un nuevo comportamiento:
class WithSpacesWriter extends ConsoleWriter {
override def print(str: String) =
super.print(str.split("").mkString(" "))
}
object Test {
def main(args: Array[String]) {
val writer1 = new UppercaseWriter
writer1.print("abc")
val writer2 = new WithSpacesWriter
writer2.print("abc")
}
}
¿Y si queremos combinar ambas formas de imprimir?
class UppercaseWithSpacesWriter extends UppercaseWriter {
override def print(str: String) =
super.print(str.split("").mkString(" "))
}
class WithSpacesUppercaseWriter extends WithSpacesWriter {
override def print(str: String) =
super.print(str.toUpperCase())
}
object Test {
def main(args: Array[String]) {
val writer1 = new UppercaseWriter
writer1.print("abc")
val writer2 = new WithSpacesWriter
writer2.print("abc")
val writer3 = new WithSpacesUppercaseWriter
writer3.print("abc")
val writer4 = new UppercaseWithSpacesWriter
writer4.print("abc")
}
}
¿Y si aparece una nueva forma de imprimir?
class ChecksumWriter extends ConsoleWriter {
override def print(str: String) = {
super.print(MessageDigest.getInstance("MD5").
digest(str.getBytes("UTF-8")).
map("%02x".format(_)).
mkString("[","","] ") +
str )
}
}
¡Mal uso de la herencia!
abstract class Writer {
def print(str: String): Unit
}
class ConsoleWriter extends Writer {
override def print(str: String) = println(str)
}
trait Uppercase extends Writer {
abstract override def print(str: String) =
super.print(str.toUpperCase())
}
trait WithSpaces extends Writer {
abstract override def print(str: String) =
super.print(str.split("").mkString(" "))
}
object Test {
def main(args: Array[String]) {
val writer1 = new ConsoleWriter with Uppercase
writer1.print("abc")
val writer2 = new ConsoleWriter with Uppercase with WithSpaces
writer2.print("abc")
}
}
Genera la salida:
ABC
A B C
trait normales son como interfaces, se enlazan en tiempo de ejecución (no tienen acceso a super).abstract override para dar acceso a superabstract no es necesario si se redefine un método no abstracto, que ya tiene una implementaciónGeométricamente, un cuadrado es un rectángulo, así que usamos herencia pura (es-un):
public class Rectangle {
private Point topLeft;
private double width;
private double height;
public double Width {
get { return width; }
set { width = value; }
}
public double Height {
get { return height; }
set { height = value; }
}
}
public class Square: Rectangle {
...
}
Square no es-un objeto Rectangle
Square no tiene propiedades heighty width.Square heredará los métodos accesores de Rectangle.public class Square: Rectangle {
public new double Width
{
set {
base.Width = value;
base.Height = value;
}
}
public new double Height
{
set {
base.Height = value;
base.Width = value;
}
}
}
El comportamiento de un objeto Square no es consistente con el de un objeto Rectangle:
Square s = new Square();
s.SetWidth(1); // fija ambos
s.SetHeight(2); // fija ambos
void f(Rectangle r)
{
r.SetWidth(32); // llama a Rectangle.SetWidth
}
¿Qué sucede si pasamos un Square a la función f?
¡No cambia Height!
Podría argumentarse que el error era que los métodos Widthy Height no se declararon virtual en Rectangle.
Hacemos que los métodos Widthy Height sean virtual en C#:
public class Rectangle
{
private Point topLeft;
private double width;
private double height;
public virtual double Width
{
get { return width; }
set { width = value; }
}
public virtual double Height
{
get { return height; }
set { height = value; }
}
}
public class Square: Rectangle {
public override double Width
{
set {
base.Width = value;
base.Height = value;
}
}
public override double Height
{
set {
base.Height = value;
base.Width = value;
}
}
}
new y override en un método en C# es que new oculta la implementación de la clase base y override la extiende.Sin embargo, cuando la creación de una clase derivada provoca cambios en la clase base, es síntoma de un mal diseño.
El principio LSP pone en evidencia que la relación es-un tiene que ver con el comportamiento público extrínseco, del que los clientes dependen.
Ahora parece que funcionan Square y Rectangle, que matemáticamente quedan bien definidos.
Pero consideremos esto:
void g(Rectangle r)
{
r.Width = 5; // cree que es un Rectangle
r.Height = 4; // cree que es un Rectangle
if(r.Area() != 20)
throw new Exception("Bad area!");
}
¿Qué pasa si llamamos a g(new Square(3))?
El autor de g asumió que cambiar el ancho de un rectángulo deja intacto el alto. Si pasamos un cuadrado esto no es así.
Violación de LSP: Si pasamos una instancia de una clase derivada (Square), se altera el comportamiento definido por la clase base (Rectangle) de forma que g deja de funcionar.
¿Quién tiene la culpa?
¿Diseño o implementación?
¿Quién tiene la culpa?
g por asumir que "en un rectángulo su ancho y alto son independientes" (invariante)?Square por violar el invariante?Rectangle y no de Square!Para evaluar si un diseño es apropiado, no se debe tener en cuenta la solución por sí sola, sino en términos de los supuestos razonables que hagan los usuarios del diseño.
<details> <summary>Bibliotecas</summary> Iteradores </details>
¿En qué se parece el modificador `new` de C# a `final` en Java? Ambos controlan el comportamiento de la herencia, pero tienen finalidades opuestas: - `final` en Java: Impide que una clase derivada sobrescriba (override) el método. El método no puede ser redefinido. - `new` en C#: Oculta un método de la clase base con una nueva implementación. Permite que un método con la misma firma exista en la clase derivada sin ser un verdadero override polimórfico. Ambos permiten que una clase derivada tenga un método con el mismo nombre que el de la clase base, sin seguir el comportamiento de override estándar. Sin embargo: - final prohíbe cualquier redefinición - new permite redefinición pero la marca como intencional y rompe el polimorfismo
Sobre el tipado (no sobre la herencia). Herencia = (sub)tipado + comportamiento
`void u(SabeNadar auto& x)` significa: genera una versión de esta función para el tipo de x, pero solo compila si x cumple los requisitos estructurales de `SabeNadar`"