DISEÑO DE SISTEMAS SOFTWARE

Bloques

  1. Principios de diseño OO
  2. Patrones de diseño
  3. Arquitectura de software

Introducción

Problemáticas

Principios

Técnicas

Paradigmas

Pregunta

¿De qué fecha data cada paradigma? ¿Cuál diríais que es el más antiguo?

Respuesta

¿De qué fecha data cada paradigma?

Casos prácticos

  1. Identificadores
  2. Framework de pruebas unitarias
  3. Caballeros de la mesa redonda
  4. Guitarras Paco
  5. Figuras geométricas

CASO PRÁCTICO 1: identificadores

Identificadores

Versión inicial: Identificadores v0.1

class Empleado { private int dni; Empleado (String dni) throws NumberFormatException { this.dni = new Integer(dni).intValue(); } int getDni() { return dni; } public String toString() { return new Integer(dni).toString(); } public int compareTo(Empleado otro) { return this.dni - otro.getDni(); } public boolean equals(Empleado otro) { return dni==otro.getDni(); } }

Críticas:

Implementación alternativa: Identificadores v0.2

class Empleado { private String nif; Empleado (String nif) { this.nif = nif } String getNif() { return id; } public String toString() { return nif; } public int compareTo(Empleado otro) { return nif.compareTo(otro.getNif()); } public boolean equals(Empleado otro) { return nif.equals(otro.getId()); } }

Críticas:

Hacemos refactoring: patrón handler

Patrón Handler

Diseño de un handler

Implementación del patró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()); } }

Ejemplo: Identificadores en Java – java.lang.Comparable

Implementar un identificador utilizando java.lang.Comparable del JDK.

Comparable es una interfaz implementada por String, File, Date, etc. y todas las llamadas clases de envoltura del JDK (i.e. Integer, Long, etc.)

Métodos de la interfaz:

public int compareTo(Object o) throws ClassCastException

Invariantes:

sgn(x.compareTo(y)) = -sgn(y.compareTo(x))

(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:

(x.compareTo(y)=0) $\Leftrightarrow$ (x.equals(y))

Reutilización y flexibilidad

CASO PRÁCTICO 2: pruebas unitarias

Framework de pruebas unitarias

¿Cómo funciona?

¿Cómo probar Saludo.java?

Incluir un método main que pruebe la funcionalidad de la clase:

class Saludo { /** * Imprime "Hola Mundo!" */ void saludar() { System.out.println("Hola Mundo!"); } /** * Imprime un mensaje */ void saludar(String mensaje) { System.out.println(mensaje); } /** * Tests */ public static void main( String[] args ) { Saludo saludo1 = new Saludo(); saludo1.saludar(); Saludo saludo2 = new Saludo("Hola caracola!"); saludo2.saludar(); } }

Pegas

Ejemplo: software cliente del framework

Caso de prueba con jUnit 4

import org.junit.*; import static org.junit.Assert.*; public class SaludoTest { public static void main(String args[]) { junit.textui.TestRunner.run(SaludoTest.class); } @Test public void saludar() { Saludo hola = new Saludo(); assert( hola!=null ); assertEquals("Hola Mundo!", hola.saludar() ); } }

Ejecución de los tests:

import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.notification.Failure; public class MyTestRunner { public static void main(String[] args) { Result result = JUnitCore.runClasses(SaludoTest.class); for (Failure failure : result.getFailures()) { System.out.println(failure.toString()); } } }

¿De qué están hechas las anotaciones como @Test? Veamos una versión anterior de jUnit, que expone más claramente las tripas del framework

Caso de prueba con jUnit 3

import junit.framework.TestCase; import junit.framework.Assert; public class SaludoTest extends TestCase { public SaludoTest(String nombre) { super(nombre); } public void testSaludar() { Saludo hola = new Saludo(); assert( hola!=null ); assertEquals("Hola Mundo!", hola.saludar() ); } }

Diseño del framework jUnit

Estructura de clases:

Clases del framework jUnit

Ejecución de casos de prueba:

Clases del framework jUnit

Ejemplo: aplicación de comercio electrónico

Diseño de una aplicación de comercio electrónico:

Diseño de pruebas unitarias de ShoppingCart para:

ShoppingCart

public class ShoppingCart { private ArrayList items; public ShoppingCart() { ... } public double getBalance() { ... } public void addItem(Product p) { ... } public void removeItem(Product p) throws ProductNotFoundException { ... } public int getItemCount() { ... } public void empty() { ... } public boolean isEmpty() { ... } }

ShoppingCartTestCase con jUnit 3

import junit.framework.TestCase; import junit.framework.TestSuite; import junit.framework.Assert; public class ShoppingCartTest extends TestCase { private ShoppingCart bookCart; private Product defaultBook; //... protected void setUp() { bookCart = new ShoppingCart(); defaultBook = new Product("Extreme Programming", 23.95); bookCart.addItem(defaultBook); } protected void tearDown() { bookCart = null; } public void testEmpty() { bookCart.empty(); assertTrue(bookCart.isEmpty()); } public void testProductAdd() { Product book = new Product("Refactoring", 53.95); bookCart.addItem(book); double expectedBalance = defaultBook.getPrice() + book.getPrice(); assertEquals(expectedBalance, bookCart.getBalance(), 0.0); assertEquals(2, bookCart.getItemCount()); } public void testProductRemove() throws ProductNotFoundException { bookCart.removeItem(defaultBook); assertEquals(0, bookCart.getItemCount()); assertEquals(0.0, bookCart.getBalance(), 0.0); } public void testProductNotFound() { try { Product book = new Product("Ender's Game", 4.95); bookCart.removeItem(book); fail("Should raise a ProductNotFoundException"); } catch(ProductNotFoundException success) { ... } } public static Test suite() { // Use reflection to add all testXXX() methods TestSuite suite = new TestSuite(ShoppingCartTest.class); // Alternatively, but prone to error when adding more // test case methods... // TestSuite suite = new TestSuite(); // suite.addTest(new ShoppingCartTest("testProductAdd")); // suite.addTest(new ShoppingCartTest("testEmpty")); // suite.addTest(new ShoppingCartTest("testProductRemove")); // suite.addTest(new ShoppingCartTestCase("testProductNotFound")); return suite; } }

Ahora agrupamos varios casos de prueba en una misma suite:

import junit.framework.Test; import junit.framework.TestSuite; import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.notification.Failure; public class EcommerceTestSuite extends TestSuite { //... public static Test suite() { TestSuite suite = new TestSuite(); suite.addTest(ShoppingCartTest.suite()); return suite; } } public class MyTestRunner { public static void main(String[] args) { Result result = JUnitCore.runClasses(EcommerceTestSuite.class); for (Failure failure : result.getFailures()) { System.out.println(failure.toString()); } } }

ShoppingCartTestCase con jUnit 4

import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import org.junit.After; import org.junit.Before; import org.junit.Test; public class ShoppingCartTest { private ShoppingCart bookCart; private Product defaultBook; //... @Before protected void setUp() { bookCart = new ShoppingCart(); defaultBook = new Product("Extreme Programming", 23.95); bookCart.addItem(defaultBook); } @After protected void tearDown() { bookCart = null; } @Test public void testEmpty() { bookCart.empty(); assertTrue(bookCart.isEmpty()); } @Test public void testProductAdd() { Product book = new Product("Refactoring", 53.95); bookCart.addItem(book); double expectedBalance = defaultBook.getPrice() + book.getPrice(); assertEquals(expectedBalance, bookCart.getBalance(), 0.0); assertEquals(2, bookCart.getItemCount()); } @Test public void testProductRemove() { bookCart.removeItem(defaultBook); assertEquals(0, bookCart.getItemCount()); assertEquals(0.0, bookCart.getBalance(), 0.0); } @Test(expected = ProductNotFoundException.class) public void testProductNotFound() { Product book = new Product("Ender's Game", 4.95); bookCart.removeItem(book); fail("Should raise a ProductNotFoundException"); } }

EcommerceTestSuite con jUnit 3

public class EcommerceTestSuite extends TestSuite { //... public static Test suite() { TestSuite suite = new TestSuite(); suite.addTest(ShoppingCartTest.suite()); suite.addTest(CreditCardTest.suite()); // etc. return suite; } }

EcommerceTestSuite con jUnit 4

@RunWith(Suite.class) @SuiteClasses({ ShoppingCartTest.class, CreditCardTest.class }) public class EcommerceTestSuite { //... }

Ejercicio: CreditCardTest

Diseñar y codificar una suite de casos de prueba unitaria para CreditCard usando jUnit 4.

Arquitectura del framework

Clases del framework jUnit

En la arquitectura del framework se observan diversos patrones: Composite, Command, Adapter, Factory, Decorator, etc.

Bibliotecas y frameworks

Biblioteca

Flujo de control en una biblioteca

Flujo de control en un framework

Flujo de control en una framework

Frameworks

Definición de framework

Colección de clases e interfaces que cooperan para formar un diseño reutilizable de un tipo específico de software

–– E. Gamma et al.

Principios de diseño

Herramientas de diseño

Framework vs. biblioteca

Principios y técnicas de un framework

CASO PRÁCTICO 3: caballeros de la mesa redonda

Ejemplo: Caballeros de la mesa redonda

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; } }

Diseño de pruebas con jUnit 3

¿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()); } }

Ocultar la implementación detrás de una interfaz:

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(); } }
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; } }

Ejercicio: Discutir el tipo de retorno Object de embarkonQuest:

Inyección de dependencias

Inversión de control

The question is: what aspect of control are they inverting? [...] Early user interfaces were controlled by the application program. You would have a sequence of commands like "Enter name", "enter address"; your program would drive the prompts and pick up a response to each one. With graphical (or even screen based) UIs the UI framework would contain this main loop and your program instead provided event handlers for the various fields on the screen. The main control of the program was inverted, moved away from you to the framework.

Martin Fowler, IoC containers and the DI pattern [1]

[1] http://martinfowler.com/articles/injection.html

IoC–Inversion of Control / DI–Dependency Injection

Factorías

Una factoría proporciona un mecanismo de inyección de dependencias, visto desde el lado opuesto (los clientes adquieren las dependencias, no se les inyecta)

Ejemplo: Spring FactoryBean

Discusión sobre la reutilización

We most likely would have been better off not attempting to create a reusable function in the first place

-- Roger Sessions, The Misuse of Reuse [2]

[2] http://simplearchitectures.blogspot.com.es/2012/07/misuse-of-reuse.html

Factorizar una función

Reutilización de una función

Reutilización de una función

Ventajas (supuestas) de reutilizar:

Ahorro:

Si $\exists s$ sistemas $\wedge ~ coste(Function~1) = c$ € $\Rightarrow$ ahorro = $c \times (s-1)$ €

Amenazas (reales):

Reutilización de una función

Conclusión

No crear funciones reutilizables en primer lugar

Aplicar el principio YAGNI: You Ain't Gonna Need It

CASO PRÁCTICO 4: guitarras

Guitarras Paco

El cliente (Paco) quiere:

Problemas de la aplicación heredada:

Una aplicación heredada

Guitar e Inventory

Guitar

Inventory

Implementación: Guitarra

public class Guitar { private String serialNumber, builder, model, type, backWood, topWood; private double price; public Guitar(String serialNumber, double price, String builder, String model, String type, String backWood, String topWood) { this.serialNumber = serialNumber; this.price = price; this.builder = builder; this.model = model; this.type = type; this.backWood = backWood; this.topWood = topWood; } public String getSerialNumber() {return serialNumber;} public double getPrice() {return price;} public void setPrice(float newPrice) { this.price = newPrice; } public String getBuilder() {return builder;} public String getModel() {return model;} public String getType() {return type;} public String getBackWood() {return backWood;} public String getTopWood() {return topWood;} }

Implementación: Inventario

public class Inventory { private List guitars; public Inventory() { guitars = new LinkedList(); } public void addGuitar(String serialNumber, double price, String builder, String model, String type, String backWood, String topWood) { Guitar guitar = new Guitar(serialNumber, price, builder, model, type, backWood, topWood); guitars.add(guitar); } public Guitar getGuitar(String serialNumber) { for (Iterator i = guitars.iterator(); i.hasNext(); ) { Guitar guitar = (Guitar)i.next(); if (guitar.getSerialNumber().equals(serialNumber)) { return guitar; } } return null; } public Guitar search(Guitar searchGuitar) { for (Iterator i = guitars.iterator(); i.hasNext(); ) { Guitar guitar = (Guitar)i.next(); String builder = searchGuitar.getBuilder().toLowerCase(); if ((builder != null) && (!builder.equals("")) && (!builder.equals(guitar.getBuilder().toLowerCase()))) continue; String model = searchGuitar.getModel().toLowerCase(); if ((model != null) && (!model.equals("")) && (!model.equals(guitar.getModel().toLowerCase()))) continue; String type = searchGuitar.getType().toLowerCase(); if ((type != null) && (!searchGuitar.equals("")) && (!type.equals(guitar.getType().toLowerCase()))) continue; String backWood = searchGuitar.getBackWood().toLowerCase(); if ((backWood != null) && (!backWood.equals("")) && (!backWood.equals(guitar.getBackWood().toLowerCase()))) continue; String topWood = searchGuitar.getTopWood().toLowerCase(); if ((topWood != null) && (!topWood.equals("")) && (!topWood.equals(guitar.getTopWood().toLowerCase()))) continue; return guitar; } return null; } }

Algunos problemas

¿Estas soluciones abordan el verdadero problema?

Preguntar a Paco...

Preguntar al cliente

Preguntemos a Paco, que no tiene por qué saber nada de objetos ni bases de datos:

Respuestas del cliente

Paco dice que:

Ejercicio

Hacer refactoring de la aplicación heredada de Guitarras Paco

CASO PRÁCTICO 5: figuras geométricas

Principios SOLID

Los principios SOLID nos dicen:

El objetivo de SOLID es crear estructuras software de nivel intermedio que sean:

Uncle Bob Martin principles

Principio de responsabilidad única

SRP: Single responsibility Principle

A class should have only one reason to change –– Uncle Bob Martin

SRP es lo mismo que el principio de cohesión de DeMarco

Consecuencia de la ley de Conway:

Organizations which design systems [...] are constrained to produce designs which are copies of the communication structures of these organizations. –– M. Conway

Ejemplo: Shapes v1 en Java

package shapes; interface Shape { double area(); void draw(); } class Point { double getX() {...} double getY() {...} } abstract class Polygon implements Shape { Point getVertex(index i) {...} void draw() {...} String toString() {...} } class Triangle extends Polygon { double area() {...} } abstract class RectParallelogram extends Polygon { double area() {...} } class Square extends RectParallelogram {...} class Rectangle extends RectParallelogram {...} abstract class ClosedCurve implements Shape {...} class Circle extends ClosedCurve { double getRadius() {...} Point getCenter() {...} double area() {...} void draw() {...} String toString() {...} } class Ellipse extends ClosedCurve { double getApogeeRadius() {...} double getPerigeeRadius() {...} Point getFocus1() {...} Point getFocus2() {...} Point getCenter() {...} double area() {...} void draw() {...} String toString() {...} }

Preguntas

Respuestas

Solución

Patrones de diseño: Visitor

Ejercicio

Principio de Abierto-Cerrado

OCP: Open-Closed Principle

Toda clase, módulo, aspecto o función debe quedar abierto para extensiones pero cerrado para modificaciones

B. Meyer, Object Oriented Software Construction

Para que un sistema software sea fácil de cambiar, debe diseñarse para que permita cambiar el comportamiento del sistema añadiendo código, no cambiando código existente.

Ejemplo: Shapes versión 2 en C++

¿Qué parte no cumple OCP en el ejemplo?

Versión imperativa (sin objetos):

enum ShapeType {circle, square}; struct Shape { ShapeType itsType; }; struct Circle { ShapeType itsType; double itsRadius; Point itsCenter; }; void DrawCircle(struct Circle*); struct Square { ShapeType itsType; double itsSide; Point itsTopLeft; }; void DrawSquare(struct Square*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; i++) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*)s); break; case circle: DrawCircle((struct Circle*)s); break; } } }

Solución

Aplicando el OCP...

public interface Shape { void Draw(); } public class Square: Shape { public void Draw() { //draw a square } } public class Circle: Shape { public void Draw() { //draw a circle } } public void DrawAllShapes(IList shapes) { foreach(Shape shape in shapes) shape.Draw(); }

In general, no matter how closed a module is, there will always be some kind of change against which it is not closed. There is no model that is natural to all contexts!

Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close the design, must guess at the kinds of changes that are most likely, and then construct abstractions to protect against those changes.

-- Bob C. Martin

Implicaciones arquitectónicas

OCP es un principio más arquitectónico que de diseño de clases y módulos. Lo veremos más adelante...

Principo de segregación de interfaces

ISP: Interface Segregation Principle

Los clientes no deben depender de métodos que no usan.

Bob C. Martin

Ejemplo: acoplamiento contexto-strategy

Ejemplo: puertas de seguridad

Una implementación de puertas de seguridad con temporizador (TimedDoor) que hace sonar una alarma cuando la puerta está abierta durante un cierto tiempo.

Diseño

Implementación inicial

public class Timer { public void Register(int timeout, TimerClient client) { /*code*/ } } public interface TimerClient { void TimeOut(); }

Implementación mejorada

public class Timer { public void Register(int timeout, int timeOutId, TimerClient client) { /*code*/ } } public interface TimerClient { void TimeOut(int timeOutID); }

¿En qué ha afectado el cambio en la implementación de TimerClient?

Rediseño: puertas de seguridad

Delegación a través del patrón adapter (de objetos o de clases)

Aplicación de OCP y SRP

Ejemplo: Shapes versión 1

package shapes; interface Shape { double area(); void draw(); } class Point { double getX() {...} double getY() {...} } abstract class Polygon implements Shape { Point getVertex(index i) {...} void draw() {...} String toString() {...} } class Triangle extends Polygon { double area() {...} } abstract class RectParallelogram extends Polygon { double area() {...} } class Square extends RectParallelogram {...} class Rectangle extends RectParallelogram {...} abstract class ClosedCurve implements Shape {...} class Circle extends ClosedCurve { double getRadius() {...} Point getCenter() {...} double area() {...} void draw() {...} String toString() {...} } class Ellipse extends ClosedCurve { double getApogeeRadius() {...} double getPerigeeRadius() {...} Point getFocus1() {...} Point getFocus2() {...} Point getCenter() {...} double area() {...} void draw() {...} String toString() {...} }
// Ficheros <X>ToString.aj (uno por aspecto) package shapes.tostring; // para todos los toString() aspect PolygonToString { String Polygon.toString() { StringBuffer buff = new StringBuffer(); buff.append(getClass().getName()); //... añadir nombre y área... //... añadir cada línea desde un vértice al siguiente return buff.toString(); } } aspect CircleToString { String Circle.toString() {...} } aspect EllipseToString { String Ellipse.toString() {...} } // Drawable.java package drawing; interface Drawable { void draw(); } // Ficheros Drawable<X>.aj package shapes.drawing; // para todos los draw()... import drawing.Drawable; abstract aspect DrawableShape { declare parents: Shape implements Drawable; void Shape.draw () //template method { String drawCommand = makeDrawCommand(); // enviar orden al motor gráfico... } String Shape.makeDrawCommand() { return getClass().getName() + "\n" + makeDetails("\t"); } abstract String Shape.makeDetails (String indent); } aspect DrawablePolygon extends DrawableShape { String Polygon.makeDetails (String indent){...} } aspect DrawableCircle extends DrawableShape { String Circle.makeDetails (String indent){...} } aspect DrawableEllipse extends DrawableShape { String Ellipse. makeDetails (String indent){...} }

Principio de sustitución de Liskov

LSP: Liskov Substitution Principle

Un subtipo debe poder ser sustituible por sus tipos base

Barbara Liskov,

Si una función $f$ depende de una clase base $B$ y hay una $D$ derivada de $B$, las instancias de $D$ no deben alterar el comportamiento definido por $B$ de modo que $f$ deje de funcionar

Ejemplo: Shapes versión 3

struct Point {double x, y;} public enum ShapeType {square, circle}; public class Shape { private ShapeType type; public Shape(ShapeType t){type = t;} public static void DrawShape(Shape s) { if(s.type == ShapeType.square) (s as Square).Draw(); else if(s.type == ShapeType.circle) (s as Circle).Draw(); } } public class Circle: Shape { private Point center; private double radius; public Circle(): base(ShapeType.circle) {} public void Draw() {/* draws the circle */} } public class Square: Shape { private Point topLeft; private double side; public Square(): base(ShapeType.square) {} public void Draw() {/* draws the square */} }

Ejemplo: rectángulos versión 1

De momento solo necesitamos rectángulos y escribimos esta versión:

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; } } }

Un día hace falta manejar cuadrados además de rectángulos.

Geométricamente, un cuadrado es un rectángulo, así que hacemos uso de la herencia (relación es-un):

public class Square: Rectangle { ... }

Problema: cuadrados como rectángulos

Ejemplo: rectángulos versión 2

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; } } }

Extensión y ocultación de métodos

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 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.

Ejemplo: rectángulos versión 3

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; } } }

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?

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.

Ejercicios de LSP

Diseño por Contrato

Relación entre LSP y el Design-By-Contract (DBC) de Bertrand Meyer:

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

Ejemplo: rectángulos

Principio de Inversión de Dependencias

DIP: Dependency Inversion Principle

Depend on abstractions

Robert C. Martin

###Ejemplo: estructura en capas

Diseño inicial:

estructura en capas

Diseño invertido:

capas invertidas

Heurística ingenua:

Hay que violar alguna vez estas heurísticas, pues alguien tiene que crear las instancias de las clases concretas. El módulo que lo haga presentará una dependencia de dichas clases concretas.

Gracias a la introspección o la carga dinámica de clases, los lenguajes de programación pueden indicar el nombre de la clase a instanciar (por ejemplo, en un fichero de configuración).

Hay clases concretas que no cambian, como string, así que no hace ningún daño depender de ellas.