Entre transistores y bytes

MarkU Blog

Principios Solid (01/xx)


Este es el primer post de una serie de artículos donde hablare de los principios de diseño SOLID. Estos principios fueron introducidos por Robert C. Martin ("Uncle Bob") por el año 2000 e introduce cinco principios para el diseño en la programación orientada a Objetos.

Las siglas SOLID provienen de la primera letra de cada uno de los principios, siendo estos:

  • S (Single responsibility principle) Principio de Responsabilidad Única
  • O (Open/closed principle) Principio de abierto/cerrado
  • L (Liskov substitution principle) Principio de sustitución de Liskov
  • I (Interface segregation principle) Principio de segregación de la interfaz
  • D (Dependency inversion principle) Principio de inversión de la dependencia

Estos principios son algunos de los muchos que existen en el diseño de software, por lo que esta bueno seguir aprendiendo sobre otros principios de diseño.

A continuación, desarrollemos el primero.

Principio de Responsabilidad Única

Este principio indica que cada módulo o clase debe tener responsabilidad sobre una sola parte de la funcionalidad del software. Es decir, encargarse de realizar sólo las tareas estrechamente ligadas con una determinada responsabilidad.

Robert C. Martin, expresa que "Una clase debe tener solo un motivo para cambiar", de esta forma se puede detectar si estamos violando este principio o no. Cuando deseamos agregar una funcionalidad o mejorar nuestro código y para lograrlo debemos modificar varias clases, lo mas probable es que estemos violando este principio.

Vamos a ver un ejemplo en C++:

Supongamos que tenemos un programa hecho con la librería SFML y decidimos hace run objeto Pelota. El único objetivo del objeto pelota es rebotar por la pantalla, por lo que podríamos tener algo así:

/**
 * https://github.com/UCC-ArquitecturaSoftwareI/principios-solid/blob/master/SRP/problem/ball_sfml.cpp
 */
/**
    Example of ball implementation for sfml without following SRP principle.

    @author Marcucci, Ricardo Martin
    @version 0.1 2020-03-07
*/
#include "ball_sfml.h"

Ball::Ball(float x, float y, float velX, float velY) : x(x), y(y), vel_x(velX), vel_y(velY) {
    radius = 5;
    circle.setRadius(radius);
    circle.setOutlineThickness(0);
    circle.setOutlineColor(sf::Color::Black);
    circle.setFillColor(sf::Color::Red);
    circle.setOrigin(radius, radius);
    circle.setPosition(x, y);
}

void Ball::move(int wWith, int wHeight, int beginX, int beginY, float t) {

    // update positions
    x += vel_x * t;
    y += vel_y * t;

    // check inside box
    if (x - radius < beginX) { // left side
        vel_x *= -1;
        x = beginX + radius;
    } else if (x + radius > beginX + wWith) { // right side
        vel_x *= -1;
        x = (beginX + wWith) - radius;
    } else if (y - radius < beginY) { // left side
        vel_y *= -1;
        y = beginY + radius;
    } else if (y + radius > beginY + wHeight) { // right side
        vel_y *= -1;
        y = (beginY + wHeight) - radius;
    }
    circle.setPosition(x, y);
}

void Ball::draw(sf::RenderWindow *w) {
    w->draw(circle);
}

Básicamente, verifica el objeto Ball (Pelota) se dedica a mantener la lógica de una pelota. Se puede crear, se puede mover y se puede dibujar. Para probar si se cumple el principio de responsabilidad simple, debemos ver si realmente se podría modificar esta clase si se quisiera modificar una responsabilidad.

Supongamos que tenemos otra clase llamada Jugador, esta clase también tendría su constructor, su método mover y su método dibujar.

De manera siguiente, se dan dos situaciones, la primera es que debemos modificar el comportamiento de la pelota. Ahora la pelota debe desacelerar, por lo que nos ponemos y modificamos la función mover, para que la velocidad se reduzca en cada llamado. Bien, esto es correcto, ya que esta modificación reduzca en la responsabilidad de la Pelota.

Ahora, la otra situación, es nuestro jefe que nos informa que se cambiará la lógica que está debajo del programa y que ya no se usará SFML y ahora se utilizará RayLib, otra biblioteca grafica, para realizar el programa.

Esto desencadenará que tengamos que modificar tanto la clase Jugador como nuestra clase Pelota, quedando esta ultima clase algo así...

/**
 * https://github.com/UCC-ArquitecturaSoftwareI/principios-solid/blob/master/SRP/problem/ball_raylib.cpp
 */
/**
    Example of ball implementation for raylib without following SRP principle.

    @author Marcucci, Ricardo Martin
    @version 0.1 2020-03-07
 */
#include "ball_raylib.h"

Ball::Ball(float x, float y, float velX, float velY) : x(x), y(y), vel_x(velX), vel_y(velY) {
    radius = 5;
}

void Ball::move(int wWith, int wHeight, int beginX, int beginY, float t) {

    // update positions
    x += vel_x * t;
    y += vel_y * t;

    // check inside box
    if (x - radius < beginX) { // left side
        vel_x *= -1;
        x = beginX + radius;
    } else if (x + radius > beginX + wWith) { // right side
        vel_x *= -1;
        x = (beginX + wWith) - radius;

    } else if (y - radius < beginY) { // left side
        vel_y *= -1;
        y = beginY + radius;
    } else if (y + radius > beginY + wHeight) { // right side
        vel_y *= -1;
        y = (beginY + wHeight) - radius;
    }

}

void Ball::draw() {
    Vector2 ballPosition = {x, y};
    DrawCircleV(ballPosition, radius, RED);
}

Como se puede observar, tendremos que modificar tanto el constructor como la función que dibuja el objeto, y, probablemente, también debamos hacer lo mismo en la clase jugador. Esto nos está diciendo, que la clase pelota no tiene una sola responsabilidad como debería. Analizando podemos darnos cuenta que la pelota tiene la responsabilidad de mantener el estado y funcionamiento de la pelota, pero también, la responsabilidad de saber como se dibuja una pelota. Esta clase, no debería realizar esta tarea, sino que debería haber una dedicada a saber como dibujar en pantalla los distintos objetos.

Una forma de abordar esto, es crear una clase RenderManager, que sea la que sabe dibujar pelotas, por lo que solucionaríamos el problema delegándose esta responsabilidad a dicha clase. Echo esto, logramos que al tener que cambiar como dibujar, solo modificamos la clase RenderManager, y si modificamos el comportamiento de la pelota, modificamos solo la pelota.

Aplicando esto, nos podrían quedar los archivos de la siguiente manera:

/**
 * https://github.com/UCC-ArquitecturaSoftwareI/principios-solid/blob/master/SRP/solution/ball.cpp
 */
/**
    Example of ball implementation following SRP principle.

    @author Marcucci, Ricardo Martin
    @version 0.1 2020-03-07
 */
#include "ball.h"

Ball::Ball(float x, float y, float velX, float velY) : x(x), y(y), vel_x(velX), vel_y(velY) {
    radius = 5;
}

void Ball::move(int wWith, int wHeight, int beginX, int beginY, float t) {

    // update positions
    x += vel_x * t;
    y += vel_y * t;

    // check inside box
    if (x - radius < beginX) { // left side
        vel_x *= -1;
        x = beginX + radius;
    } else if (x + radius > beginX + wWith) { // right side
        vel_x *= -1;
        x = (beginX + wWith) - radius;
    } else if (y - radius < beginY) { // left side
        vel_y *= -1;
        y = beginY + radius;
    } else if (y + radius > beginY + wHeight) { // right side
        vel_y *= -1;
        y = (beginY + wHeight) - radius;
    }

}


float Ball::getX() const {
    return x;
}

float Ball::getY() const {
    return y;
}

float Ball::getRadius() {
    return radius;
}

Quedándonos solo una clase dedicada al funcionamiento de la pelota y delegando lo de dibujar a la clase RenderManager

/**
 * https://github.com/UCC-ArquitecturaSoftwareI/principios-solid/blob/master/SRP/solution/render_manager_sfml.cpp
 */
/**
    Example renderer class added to follow SRP principle.

    @author Marcucci, Ricardo Martin
    @version 0.1 2020-03-07
*/
#include "render_manager_sfml.h"

void render_manager::draw_ball(Ball &b, sf::RenderWindow *w) {
    sf::CircleShape ball;
    ball.setRadius(b.getRadius());
    ball.setFillColor(sf::Color::Red);
    ball.setOrigin(b.getRadius(), b.getRadius());
    ball.setPosition(b.getX(),b.getY());
    w->draw(ball);

}

El proyecto completo se puede ver en https://github.com/UCC-ArquitecturaSoftwareI/principios-solid