class Circle
{
public:
explicit Circle (double rad ) : radius { rad }, //... remaining data members
{}
double getRadius() const noexcept;
//... getCenter(), getRotation(), ...
void translate (Vector3D const& );
void rotate ( Quaternion const& );
void draw ( Screen& s, /*...*/ );
void draw ( Printer& p, /*...*/ );
void serialize ( ByteStream& bs, /*...*/ );
//...
private:
double radius;
///... remaining data members
}
class Circle
{
public:
explicit Circle (double rad ) :
radius { rad },
//... remaining data members
{}
double getRadius() const noexcept;
//... getCenter(), getRotation(), ...
void translate (Vector3D const& );
void rotate ( Quaternion const& );
private:
double radius;
///... remaining data members
};
class CircleRenderer
{
public:
virtual ~CircleRenderer() = default;
virtual void draw ( Circle const&, Screen& s,
/*...*/ ) = 0;
virtual void draw ( Circle const&, Printer& p,
/*...*/ ) = 0;
};
class CircleSerializer
{
public:
void serialize ( Circle const&, ByteStream& bs,
/*...*/ ) const;
};
class Circle
{
public:
explicit Circle (double rad ) : radius { rad }, /* ...*/ {}
double getRadius() const noexcept;
//... getCenter(), getRotation(), ...
void translate (Vector3D const& );
void rotate ( Quaternion const& );
private:
double radius;
///... remaining data members
};
// Same namespace as Circle
void draw ( Circle const&, Screen& s, /*...*/ );
void draw ( Circle const&, Printer& p, /*...*/ );
void serialize ( Circle const&, ByteStream& bs, /*...*/ );
//...
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 su comportamiento añadiendo código, no cambiando código existente.
¿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 de ShapeAplicando 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();
}
DrawAllShapes, solo tenemos que añadir una nueva clase derivada de ShapeArreglar para que cumpla OCP
enum ShapeType
{
circle,
square,
rectangle
};
class Shape
{
public:
explicit Shape ( ShapeType t )
: type { t }
{}
virtual ~Shape() = default;
ShapeType getType() const noexcept;
private:
ShapeType type;
};
class Circle: public Shape
{
public:
explicit Circle ( double rad )
: Shape{ circle }
, radius { rad }
, //... remaining data members
{}
virtual ~Circle() = default;
double getRadius() const noexcept;
//... getCenter(), getRotation(), ...
private:
double radius;
///... remaining data members
};
void translate ( Circle&, Vector3D const& );
void rotate ( Circle&, Quaternion const& ) ;
void draw ( Circle const& );
class Square: public Shape
{
public:
explicit Square ( double s )
: Shape{ square }
, side { s }
, // ... remaining data members
{}
virtual ~Square() = default;
double getSide() const noexcept;
//... getCenter(), getRotation(), ...
private:
double side;
// ... remaining data members
};
void translate ( Square&, Vector3D const& );
void rotate ( Square&, Quaternion const& ) ;
void draw ( Square const& );
void draw ( std::vector<std::unique_ptr<<Shape>>>) const & shapes )
{
for ( auto const& s : shapes )
{
switch ( s-> getType() )
{
case circle:
draw ( *static_cast<Circle const*>( s.get() ) );
break;
case square:
draw ( *static_cast<Square const*>( s.get() ) );
break;
case rectangle:
draw ( *static_cast<Rectangle const*>( s.get() ) );
break;
}
}
}
int main()
{
using Shapes = std::vector<std::unique_ptr<Shape>>;
// Creating some shapes
Shapes shapes;
shapes.push_back (std::make:unique<Circle>( 2.0 ));
shapes.push_back (std::make:unique<Square>( 1.5 ));
shapes.push_back (std::make:unique<Circle>( 4.2 ));
// Drawing all shapes
draw ( shapes );
}
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.
Versión en C++ que cumple el OCP y SRP
class Circle : public Shape
{
public:
explicit Circle (double rad )
: radius { rad }
, //... remaining data members
{}
virtual ~Circle() = default;
double getRadius() const noexcept;
//... getCenter(), getRotation(), ...
void translate ( Vector3D const& ) override;
void rotate ( Quaternion const& ) override;
void draw () const override;
private:
double radius;
///... remaining data members
}
class Square : public Shape
{
public:
explicit Square (double s )
: side { s }
, //... remaining data members
{}
virtual ~Square() = default;
double getSide() const noexcept;
//... getCenter(), getRotation(), ...
void translate ( Vector3D const& ) override;
void rotate ( Quaternion const& ) override;
void draw () const override;
private:
double side;
///... remaining data members
}
class Shape
{
public:
Shape() = default;
virtual ~Shape() = default;
virtual void translate ( Vector3D const& ) = 0;
virtual void rotate ( Quaternion const& ) = 0;
virtual void draw() const = 0; // check!
};
void draw ( std::vector<std::unique_ptr<<Shape>>>) const & shapes )
{
for ( auto const& s : shapes )
{
s->draw();
}
}
Los clientes no deben depender de métodos que no usan.
Bob C. Martin
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 con Timer para registrar un temporizadorTimerClientTimerClient puede registrarse a sí mismo en un Timer y recibir de éste un mensaje mediante timeout().public class Timer {
public void register(int timeout, TimerClient client) {
/*code*/
}
}
public interface TimerClient {
void timeout();
}
timeout() de TimedDoor y no debería.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?
TimerClient, pero también a Door y a los clientes de Door (y no debería)Door depende de TimerClient y no todas las variedades de puerta son de seguridad (con temporizador)timeout()Delegación a través del patrón adapter
Adaptador de clases (herencia):

Adaptador de objetos (composición):

class Circle;
class Square;
class DrawStrategy
{
public:
virtual ~DrawStrategy() {}
virtual void draw ( const Circle& circle ) const = 0;
virtual void draw ( const Square& square ) const = 0;
};
class Shape
{
public:
Shape() = default;
virtual ~Shape() = default;
virtual void translate ( Vector3D const& ) = 0;
virtual void rotate ( Quaternion const& ) = 0;
virtual void draw() const = 0;
};
class Circle : public Shape
{
public:
explicit Circle ( double rad, std::unique_ptr<DrawStrategy> ds )
: radius { rad }
, //... remaining data members
, drawing { std::move(ds) }
{}
virtual ~Circle() = default;
double getRadius() const noexcept;
//... getCenter(), getRotation(), ...
void translate ( Vector3D const& ) override;
void rotate ( Quaternion const& ) override;
void draw () const override;
private:
double radius;
///... remaining data members
std::unique_ptr<DrawStrategy> drawing;
};
class Square: public Shape
{
//...
}
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() {...}
}
draw) y para imprimir (toString) pueden descohesionar las clases y atentar contra OCP y SRP.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).

// 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){...} }
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 OCPSquare y Circle no son sustuibles por Shape: no redefinen ninguna función de Shape, sino que añaden Draw() (violación del LSP)DrawShapeA continuación, una violación más sutil del 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 Rectangle(double width, double height) {
this.topLeft = default(Point);
this.width = width;
this.height = height;
}
public double Width {
get { return width; }
set { width = value; }
}
public double Height {
get { return height; }
set { height = value; }
}
}
Pero un día hace falta manejar cuadrados además de rectángulos. Geométricamente, un cuadrado es un rectángulo, así que utilizamos una relación es-un:
public class Square: Rectangle {
...
}
Un cuadrado podría ser matemáticamente un rectángulo, pero definitivamente un objeto Square no es un objeto Rectangle
Un Square no tiene propiedades heighty width. Pero supongamos que no nos importa el desperdicio de memoria.
Square heredará los métodos accesores de Rectangle.
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 objeto Rectangle:
Square s = new Square();
s.Width = 1; // fija ambos
s.Height = 2; // fija ambos
void f(Rectangle r)
{
r.Width = 3; // calls 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.
public class Rectangle
{
private Point topLeft;
private double width;
private double height;
public Rectangle(double width, double height) {
this.topLeft = default(Point);
this.width = width;
this.height = 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;
}
}
}
Diferencia entre new y override: new oculta la implementación de la clase base y override la redefine.
Cuando una clase derivada realiza cambios en la clase base, es síntoma de un mal diseño.
El LSP 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!");
}
Pero si llamamos a...
g(new Square())
... el autor de g asume 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 correctamente.
¿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.
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
Postcondición del setter de Rectangle.Width
(En C++ sería Rectangle::SetWidth(double w)):
assert((Width == w) && (Height == old.Height));
Postcondición del setter de Square.Witdh
(En C++ sería Square::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 de Rectangle
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
Diseño inicial:
Diseño invertido:
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 (inyección de dependencias).
Gracias a la introspección o la carga dinámica de clases, en algunos lenguajes de programación se puede indicar el nombre de la clase a instanciar (por ejemplo, en un fichero de configuración XML o JSON).
Los módulos enmarañados que nunca cambian no son problemáticos
<details> <summary>ActiveRecord y SRP</summary> En general, ActiveRecord tiene la responsabilidad de modelar los datos en la base de datos, proporcionar una interfaz para acceder y manipular esos datos, y también puede incluir la lógica de negocio necesaria para trabajar con los datos. Desde una perspectiva del principio de responsabilidad única (SRP), ActiveRecord no cumple completamente con este principio porque tiene varias responsabilidades. Específicamente, ActiveRecord tiene la responsabilidad de: - Representar y manipular datos en la base de datos - Proporcionar una interfaz para acceder y manipular esos datos - Incluir la lógica de negocio necesaria para trabajar con los datos Sin embargo, a menudo se considera que ActiveRecord sigue una variante del principio de responsabilidad única, llamada "Principio de responsabilidad única de dominio" (Single Responsibility Principle of Domain, en inglés), que establece que una clase debe tener una única responsabilidad dentro del dominio de la aplicación. En este sentido, ActiveRecord tiene la responsabilidad de modelar los datos dentro del dominio de la aplicación. El patrón Data Access Object (DAO) es un patrón de diseño de software que se utiliza comúnmente en el desarrollo de aplicaciones para separar la lógica de negocio de la lógica de acceso a datos. El objetivo principal del patrón DAO es proporcionar una interfaz unificada para acceder a los datos desde una variedad de fuentes de datos, como una base de datos, un archivo o un servicio web, entre otros. La clase DAO encapsula la lógica de acceso a datos y proporciona métodos para realizar operaciones CRUD (Crear, Leer, Actualizar y Eliminar) en la fuente de datos correspondiente. Desde una perspectiva del principio de responsabilidad única (SRP), el patrón DAO cumple con este principio. Esto se debe a que la clase DAO tiene una única responsabilidad, que es la de encapsular la lógica de acceso a datos y proporcionar una interfaz unificada para acceder a los datos. La lógica de negocio se encuentra en otra clase o conjunto de clases, lo que permite separar las responsabilidades y facilita la reutilización del código. </details>
El refactor 1 separa responsabilidades en clases (estrategias/servicios), lo que facilita sustituir implementaciones y testear. Pero no es una implementación típica en C++. El código parece hecho por un programador de Java.
El refactor 2 usa funciones libres en el mismo namespace: reduce acoplamiento sin inflar la interfaz de la clase, pero requiere coordinar puntos de extension fuera de la clase.
En los lenguajes de tipos estáticos, los tipos deben ser declarados y especificados en tiempo de compilación. Esto significa que las interfaces deben ser definidas de antemano, antes de que se implementen las clases que las utilizan. En algunas ocasiones, esto puede llevar a la definición de interfaces grandes y complejas que contienen muchos métodos que no son necesarios para todos los clientes que utilizan la interfaz. En cambio, en los lenguajes de tipos dinámicos, las interfaces pueden ser definidas en tiempo de ejecución. Esto permite que las interfaces sean más pequeñas y específicas para cada cliente, ya que solo contienen los métodos necesarios para cada caso de uso particular.
No sólo es arriesgada la extensión (es-como-un) de los métodos de una clase, también lo es su redefinición (es-un). En el ejemplo, `Square` redefine el comportamiento de `Rectangle` de forma que los clientes de `Rectangle` dejan de funcionar correctamente.

