- Principios de diseño OO
- Patrones de diseño
- Arquitectura de software
- Variabilidad
- Acoplamiento
- Complejidad
- Robustez
- Reutilización
- Flexibilidad
- Ocultación: OCP, ISP, LSP
- Cohesión: SRP
- Ortogonalidad: ISP
- Delegación
- Refactoring
- Bibliotecas y frameworks
- Contratos
- Inyección de dependencias
- Patrones
- Estructurado (E. W. Dijsktra)
- Objetos (O. J. Dahl & K. Nygaard)
- Funcional (J. McCarthy)
- Aspectos (G. Kiczales)
- Eventos
¿De qué fecha data cada paradigma? ¿Cuál diríais que es el más antiguo?
¿De qué fecha data cada paradigma?
- Estructurado (E. W. Dijsktra, 1968)
- Objetos (O. J. Dahl & K. Nygaard, 1966)
- Funcional (J. McCarthy, 1958)
- Aspectos (G. Kiczales, 1997)
- Eventos
- Identificadores
- Framework de pruebas unitarias
- Caballeros de la mesa redonda
- Guitarras Paco
- Figuras geométricas
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();
}
}
- Flexibilidad: El Real Decreto 338/1990 regula el uso de NIFs en lugar de DNIs. ¡Hay que cambiar toda la implementación!
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());
}
}
- Reutilización: Posiblemente haya más situaciones (algunas futuras) donde hagan falta identificadores que incluso pueden cambiar. Por ejemplo: números de la seguridad social, tarjetas de identidad, números de cuenta corriente, IBAN, etc.
-
Manejo de identificadores de forma independiente de la implementación del objeto identificado.
-
Cambio fácil de implementación de los identificadores (
int
,String
, etc.) hacia cualquier tipo básico o clase primitiva, sencilla o compuesta.
-
Identifiable: Clase cliente que necesita identificar a sus objetos a través de algún atributo identificador
-
Handler: Interfaz para declarar los identificadores de los objetos de la clase
Identifiable
-
ConcreteHandler: Implementación concreta de la interfaz
Handler
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());
}
}
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)
x.compareTo(z)>0
x.compareTo(y)=0
sgn(x.compareTo(z))=sgn(y.compareTo(z))
z
Consistencia con equals
:
(x.compareTo(y)=0)
(x.equals(y))
- Flexibilidad: Adaptarse a cambios de requisitos y construir software fácil de cambiar
- Reutilización: Construir software fácil de reutilizar sin tener que cambiar los módulos ya escritos
- JUnit es un framework en Java que sirve para diseñar, construir y ejecutar pruebas unitarias
- Una prueba unitaria comprueba la corrección de un módulo de software en cuanto a funcionalidades que ofrece.
- En el caso de Java, las pruebas unitarias comprueban la corrección de cada uno de los métodos de cada clase.
¿Cómo funciona?
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();
}
}
-
Cuanto más grande sea la interfaz de la clase, mayor será el main
-
El tamaño del código de la clase crece por las pruebas
-
Poco fiable, porque main forma parte de la misma clase y tiene acceso a los elementos privados
-
Difícil de automatizar las pruebas, incluso pasando argumentos a
main
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
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 de una aplicación de comercio electrónico:
ShoppingCart
- carrito de la compraCreditCard
- tarjeta de créditoProduct
- artículos- Etc.
Diseño de pruebas unitarias de ShoppingCart
para:
- Probar carrito de la compra (añadir/eliminar artículos)
- Probar validación de tarjetas de crédito
- Probar manejo de varias monedas
- Etc.
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() { ... }
}
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());
}
}
}
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");
}
}
public class EcommerceTestSuite extends TestSuite {
//...
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(ShoppingCartTest.suite());
suite.addTest(CreditCardTest.suite());
// etc.
return suite;
}
}
@RunWith(Suite.class)
@SuiteClasses({ ShoppingCartTest.class,
CreditCardTest.class })
public class EcommerceTestSuite {
//...
}
Diseñar y codificar una suite de casos de prueba unitaria para CreditCard
usando jUnit 4.
En la arquitectura del framework se observan diversos patrones: Composite, Command, Adapter, Factory, Decorator, etc.
Colección de clases e interfaces que cooperan para formar un diseño reutilizable de un tipo específico de software
- El framework proporciona unas guías arquitectónicas (diseño empaquetado) para dividir el diseño en clases abstractas y definir sus responsabilidades y colaboraciones.
- El framework se debe personalizar definiendo subclases y combinando instancias, o bien configurando valores que definen el comportamiento por defecto
- Datos encapsulados
- Interfaces y clases abstractas
- Métodos polimórficos
- Delegación
- Patrones: elementos reutilizables de diseño
- Frameworks: colecciones de patrones abstractos a aplicar
- API orientado a objetos
- Flujo de control invertido
- Programador cliente (código específico) vs. programador de bibliotecas (código reutilizable)
-
Abstracción
- Clases y componentes abstractos
- Interfaces abiertas
- Uso de patrones de diseño
- Componentes de un dominio específico
-
Máxima cohesión, mínimo acoplamiento
- Minimizar dependencias: Una clase presenta una dependencia con otra clase si la primera usa una instancia de la segunda.
- Cuando no se pueden eliminar las dependencias, mantener las abstractas e inyectar las concretas.
-
Inyección de dependencias: una clase o módulo no debería configurar sus dependencias estáticamente, sino ser configurada desde fuera
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)
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();
}
}
- 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 sólo sabe del desafío a través de su interfaz
Quest
. -
Puede asignársele cualquier implementación de
Quest
(HolyGrailQuest
,RescueDamselQuest
, etc.)
Ejercicio: Discutir el tipo de retorno Object
de embarkonQuest
:
- Puede provocar
ClassCastException
- Solución: rediseñar la interfaz
Quest
- Es la base de la inyección de dependencias
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
- Una aplicación está compuesta por dos o más clases que colaboran.
- Los objetos deben recibir las dependencias en su creación, por parte de una entidad externa o contenedor que los coordina.
- Inversión de la responsabilidad de cómo un objeto obtiene referencias a los objetos con los que colabora
- Ventaja = bajo acoplamiento: un objeto sólo sabe de sus dependencias por su interfaz, no por su implementación, ni por cómo fueron instanciados.
- Entonces la dependencia puede cambiarse por una implementación distinta (incluso en tiempo de ejecución)
- Hollywood Principle: Don't call us, we'll call you".
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
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
Ahorro:
Si
- Realmente el ahorro depende de la complejidad. Y muchas veces, la complejidad de la función está exponencialmente relacionada con el número de sistemas.
- Con un único punto de fallo, Si la función 1 falla, todos los sistemas pueden fallar a la vez.
- La seguridad es inversamente proporcional a la complejidad del sistema.
- Se incrementan los costes de llevar los sistemas a la nube.
No crear funciones reutilizables en primer lugar
Aplicar el principio YAGNI: You Ain't Gonna Need It
El cliente (Paco) quiere:
-
Mantener un inventario de guitarras
-
Encontrar guitarras para sus clientes
Problemas de la aplicación heredada:
-
Caso de uso: un cliente busca una guitarra flamenca ‘Valeriano Bernal’, pero no encuentra ninguna
-
¿Problemas?
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;}
}
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;
}
}
-
Se compara el fabricante sin tener en cuenta mayúsculas/minúsculas
-
Se comparan todos los campos sin tener en cuenta mayúsculas/minúsculas
-
No hay definidas constantes para cada fabricante
¿Estas soluciones abordan el verdadero problema?
Preguntar a Paco...
Preguntemos a Paco, que no tiene por qué saber nada de objetos ni bases de datos:
-
¿Sólo vendes guitarras?
-
¿Cómo actualizas el inventario?
-
¿Cómo funciona la búsqueda de guitarras?
-
¿Necesitarás informes de inventario y de ventas?
Paco dice que:
-
Los clientes no siempre conocen las características exactas de la guitarra que quieren
-
Los clientes suelen buscar guitarras dentro de un rango de precios
-
Suele haber más de una guitarra que casa con las necesidades del cliente
-
Sí, necesito informes y demás, pero ¡la prioridad nº 1 es encontrar las guitarras!
Hacer refactoring de la aplicación heredada de Guitarras Paco
Los principios SOLID nos dicen:
- Cómo organizar en módulos (en OO, clases) las estructuras de datos y las funciones
- Cómo deben quedar interconectadas las clases
El objetivo de SOLID es crear estructuras software de nivel intermedio que sean:
- tolerantes a los cambios
- fáciles de comprender
- la base de componentes útiles para muchos sistemas software
SRP: Single responsibility Principle
A class should have only one reason to change –– Uncle Bob Martin
- Una clase que modela múltiples aspectos genera acoplamiento entre los distintos aspectos
- Un cambio en algún aspecto obligará a cambios accidentales en los clientes que no dependen de dicho aspecto
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
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() {...}
}
- ¿Cuántas responsabilidades tienen las clases que implementan la interfaz
Shape
? - ¿Cuáles son estas responsabilidades?
- ¿Qué parte no cumple SRP en el ejemplo?
- Dos responsabilidades: geometría computacional + dibujo en pantalla
- Todas las figuras tienen métodos
draw
ytoString
(dibujar en pantalla) además del métodoarea
que calcula el área (geometría computacional)$\rightarrow$ Violación del SRP
Patrones de diseño: Visitor
- Buscar información de los patrones ActiveRecord y Data Access Object (DAO).
- Discutir si cumplen o violan el SRP.
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.
- Si un cambio en un sitio origina una cascada de cambios en otros puntos del sistema, el resultado es un sistema frágil y rígido
- Es difícil averiguar todos los puntos que requieren cambios
- Código cerrado para modificaciones, pero abierto para extensión mediante delegación en vertical (subclases) u horizontal (composición)
¿Qué parte no cumple OCP en el ejemplo?
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;
}
}
}
DrawAllShapes
no está cerrado para modificaciones cuando aparecen nuevos tipos deShape
- Abstracción (ocultación de la implementación): clase abstracta y métodos polimórficos.
- Patrones de diseño: template method y/o strategy
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();
}
- Si queremos ampliar el comportamiento de
DrawAllShapes
, solo tenemos que añadir una nueva clase derivada deShape
- Si se aplica bien OCP, los cambios de un cierto tipo obligan a añadir nuevo código, no a modificar el existente
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
OCP es un principio más arquitectónico que de diseño de clases y módulos. Lo veremos más adelante...
ISP: Interface Segregation Principle
Los clientes no deben depender de métodos que no usan.
Bob C. Martin
- Las interfaces son para los clientes, no para hacer jerarquías
- Evitar interfaces gruesas con muchos métodos (descohesionadas)
- Los cambios en los métodos ignorados pueden provocar cambios en un cliente que no los usa
- La interfaz de una clase puede dividirse en bloques de métodos relacionados. Unos clientes usan un bloque y otros clientes usan otro bloque. Si un cliente necesita conocer una interfaz no cohesionada, debe hacerlo combinando una o más clases (sus interfaces)
- ISP es a las interfaces lo que SRP es a clases y métodos
- Violar el ISP es muy común en lenguajes de tipos estáticos (C++, Java, C#). Los lenguajes dinámicos (Ruby, Scala) ayudan algo más a no violar el ISP (v.g. con los mixins)
- ¿Cómo se comunican Strategy y Context (acoplado/desacoplado)?
- Context Puede definir una interfaz que le permita a Strategy acceder a sus datos
Una implementación de puertas de seguridad con temporizador (TimedDoor
) que hace sonar una alarma cuando la puerta está abierta durante un cierto tiempo.
-
TimedDoor
se comunica conTimer
para registrar un temporizador -
Cuando salta el temporizador, avisa a un
TimerClient
-
Con la siguiente solución, un
TimerClient
puede registrarse a sí mismo en unTimer
y recibir un mensajeTimeout()
.
public class Timer {
public void Register(int timeout, TimerClient client) {
/*code*/
}
}
public interface TimerClient {
void TimeOut();
}
- Si se cierra la puerta antes de que venza el timeout
$t_0$ y se vuelve a abrir, se registra uno nuevo$t_1$ antes de que el antiguo haya expirado. - Cuando el primer temporizador
$t_0$ expira, se produce la llamada aTimeout()
deTimedDoor
y no debería. - Así que cambiamos la implementación:
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
?
- El cambio afecta a los usuarios de
TimerClient
, pero también aDoor
y a los clientes deDoor
(y no debería) - El problema es que
Door
depende deTimerClient
y no todas las variedades de puerta son de seguridad (con temporizador) - Si hacen falta más variedades de puerta, todas ellas deberán implementar implementaciones degeneradas de
Timeout
- Las interfaces empiezan a engrosarse. Esto puede acabar violando también el LSP
Delegación a través del patrón adapter (de objetos o de clases)
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() {...}
}
- Las funcionalidades para pintar (
draw
) y para imprimir (toString
) pueden descohesionar las clases y atentar contra OCP y SRP. - Saquémoslas fuera utilizando aspectos...
La Orientación a aspectos (AOD/AOP) es un paradigma cuyo objetivo es incrementar la modularidad (ortogonalidad) de los componentes 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
- 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.
Ejemplo: Shapes versión 2 con aspectos
// 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){...} }
LSP: Liskov Substitution Principle
Un subtipo debe poder ser sustituible por sus tipos base
Barbara Liskov,
Si una función
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 */}
}
-
DrawShape
viola claramente el OCP -
Además
Square
yCircle
no son sustuibles porShape
: no redefinen ninguna función deShape
, sino que añadenDraw()
(violación del LSP) -
Esta violación de LSP es la que provoca la violación de OCP en
DrawShape
-
Violación más sutil de LSP...
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 {
...
}
-
Un cuadrado podría ser un rectángulo, pero definitivamente un objeto
Square
no es un objetoRectangle
-
Un
Square
no tiene propiedadesheight
ywidth
. Pero supongamos que no nos importa el desperdicio de memoria. -
Square
heredará los métodos accesores deRectangle
. -
Así que hacemos lo siguiente...
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 objetoRectangle
:Square s = new Square(); s.SetWidth(1); // fija ambos s.SetHeight(2); // fija ambos void f(Rectangle r) { r.SetWidth(32); // calls Rectangle.SetWidth }
-
¿Qué sucede si pasamos un
Square
a la funciónf
?¡No cambia
Height
! -
Podría argumentarse que el error era que los métodos
Width
yHeight
no se declararonvirtual
enRectangle
.
- La diferencia entre
new
yoverride
en un método en C# es quenew
oculta la implementación de la clase base yoverride
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 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.
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?
- ¿El autor de
g
por asumir que "en un rectángulo su ancho y alto son independientes" (invariante)? - ¿El autor de
Square
por violar el invariante? - ¿De qué clase se ha violado el invariante? ¡De
Rectangle
y no deSquare
!
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.
- Robert C. Martin & Micah Martin: Agile Principles, Patterns and Practices in C#, Prentice Hall, 2006
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
- 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
-
Postcondición del setter de
Rectangle.Width
(En C++ seríaRectangle::SetWidth(double w)
):assert((Width == w) && (Height == old.Height));
-
Postcondición del setter de
Square.Witdh
En C++ seríaSquare::SetWidth(double w)
):assert(Width==w);
-
La postcondición de
Square::SetWidth(double w)
viola el contrato de la clase base porque es más débil que la deRectangle
DIP: Dependency Inversion Principle
-
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
-
Las abstracciones no deben depender de los detalles, sino los detalles de las abstracciones
Depend on abstractions
Robert C. Martin
###Ejemplo: estructura en capas
Diseño inicial:
- Las dependencias son transitivas
- Policy depende de todo lo que depende Mechanism.
Diseño invertido:
- Cada nivel declara una interfaz para lo que necesita de otros niveles inferiores
- Los niveles inferiores dependen de interfaces definidas en los superiores
- El cliente puede definir la abstracción que necesita (ISP)
- Cada nivel es intercambiable por un sustituto
- Ninguna variable debería guardar una referencia a una clase concreta
- Ninguna clase debería ser derivada de una clase concreta
- Ningún método debería redefinir un método ya implementado de ninguna de sus clases base
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.
- Martin Fowler – Patterns in Enterprise Software: Catálogos de patrones a distintos niveles
- Martin Fowler – Patterns of Enterprise Application Architecture (EAA)
- Hohpe y Woolf – Enterprise Integration Patterns (EIP)
- Buschmann y otros – Pattern-Oriented Software Architecture (POSA) Volume 1: A system of patterns
- Peter Norvig – Design Patterns in Dynamic Programming: Implementaciones más simples para los patrones de diseño del GoF en lenguajes dinámicos
- David Arno – Are design patterns compatible with modern software techniques?
- Implementaciones de los patrones de diseño del GoF en diversos lenguajes de programación:
- Kamran Ahmed – Design Patterns for Humans!: Explicación de los patrones de diseño del GoF implementados en PHP
- Márk Török – Design Patterns in TypeScript
- Bogdab Vliv - Design Patterns in Ruby
- Lewis y Fowler – Microservicios