Reutilización de código, mantenimiento de aplicaciones (V)
Introducción
En las entradas anteriores, vimos como desarrollar una aplicación a partir de un problema teóricamente trivial.
Los requisitos cambian y las necesidades empresariales nos llevan a ser ágiles y adoptar cambios de manera rápida y flexible, facilitar las pruebas unitarias y en definitiva, codificar código lo más limpio posible y con posibilidades de reutilizarlo.
Hemos llegado a un punto bastante aceptable pero no es suficiente. Al menos no para unos programadores exigentes como nosotros.
Y de esta manera, hemos llegado a mencionar un conjunto de principios básicos de programación orientada a objetos… bueno, en realidad cinco principios, que reciben el nombre de SOLID.
Así que en este punto, lo mejor es hacer una pequeña pausa y hablar de esos cinco principios que deberíamos tratar de cumplir siempre que sea posible con el fin de no perdernos más adelante.
¿Es obligatorio cumplirlos?. No, pero sí es recomendable cumplirlos todos. Sino podemos cumplir todos, cuantos más cumplamos, mejor.
Empecemos entonces…
SOLID
SOLID es un acrónimo de 5 principios básicos de programación orientada a objetos que surgieron alrededor del año 2000.
También lo escucharás citar como los principios SOLID.
Cada letra de la palabra SOLID corresponde a un principio:
- SRP o Single Responsability Principle.
- OCP u Open/Close Principle.
- LSP o Liskov Substitution Principle.
- ISP o Interface Segregation Principle.
- DIP o Dependency Inversion Principle.
Lo mejor para entender cada uno de estos principios, es verlos de forma breve y concisa.
SRP o Single Responsability Principle
Es quizás el más fácil de entender.
El concepto detrás de este principio es el de tener en mente que cada clase tenga una única responsabilidad.
Esa responsabilidad debería estar contenida dentro de la clase.
Si tenemos la necesidad de encapsular una responsabilidad en varias clases, quizás nuestra clase estuviera haciendo más cosas de las que debiera y por lo tanto, deberíamos pensar en refactorizar nuestro código para mejorarlo y llegar a cumplir este principio.
Para este principio, sólo se exige atención a los requisitos, refinamiento si procede y sentido común a la hora de hacer las cosas.
Un ejemplo sencillo podría ser el siguiente:
Partimos de una clase Coche con diferentes comandos y funcionalidades. Básicamente y para no complicarlo, tenemos la posibilidad de arrancar el motor y apagarlo, y de acelerar y frenar.
Dos variables nos indican si el coche está arrancado así como su velocidad.
Desde el punto de responsabilidad única y a simple vista, la clase Coche hace bastantes cosas.
Podríamos mejorar por lo tanto todo esto para lograr algo parecido a (en aproximación):
No pongo el código porque creo que se entiende bien, y lo principal realmente aquí es el concepto de este principio.
Como se puede apreciar, además de separar responsabilidades, podemos hacer pruebas unitarias de cada una de estas partes por separado y de todas ellas conjuntamente.
No es quizás el mejor ejemplo, pero si sirve para aclarar el concepto, con eso me conformo.
OCP u Open/Close Principle
El concepto detrás de este principio dice que deberíamos codificar nuestras clases, funciones, métodos, etc., de forma abierta a su extensión y cerrada en su modificación.
Dicho así puede que suene etéreo, así que digámoslo de otra manera.
Nuestras clases, funciones, métodos, etc., deberían permitir que su comportamiento variara o cambiara sin que tengamos que modificar nuestro código fuente.
Un ejemplo sencillo podría ser el siguiente:
El código de este ejemplo demostrativo sería similar al siguiente:
1: using System;
2:
3:
4: public class Rectangulo
5: {
6: public double Alto { get; set; }
7: public double Ancho { get; set; }
8: } // Rectangulo
9:
10: public class Circulo
11: {
12: public double Radio { get; set; }
13: } // Circulo
14:
15: public class CalculoArea
16: {
17:
18: public double GetArea(Circulo circulo)
19: {
20: return Math.PI * Math.Pow(circulo.Radio, 2);
21: } // GetArea
22:
23: public double GetArea(Rectangulo rectangulo)
24: {
25: return rectangulo.Alto * rectangulo.Ancho;
26: } // GetArea
27:
28: } // CalculoArea
Y si queremos consumir estas clases en nuestra aplicación, emplearíamos un código similar al siguiente:
1: CalculoArea calculoArea = new CalculoArea();
2: Circulo circulo = new Circulo();
3: circulo.Radio = 13;
4: Rectangulo rectangulo = new Rectangulo();
5: rectangulo.Alto = 3;
6: rectangulo.Ancho = 7;
7: MessageBox.Show(calculoArea.GetArea(circulo).ToString());
8: MessageBox.Show(calculoArea.GetArea(rectangulo).ToString());
Como podemos apreciar aquí, el ejemplo que hemos desarrollado parece estar bien. De hecho, funciona y hace lo que esperamos que haga. Así que desde el punto de vista de requisitos lo podríamos dar por bueno.
Sin embargo, si prestamos atención a lo que hemos hecho, veremos que hay algo que nos chirría… o debería chirriarnos… ¿os acordáis del primer principio SOLID?. ¿Ese que trata sobre la responsabilidad simple?. Cuando un principio no se cumple, decimos que violamos ese principio, algo que estamos haciendo aquí.
Tenemos un objeto de cálculo del área que calcula todas las áreas habidas Y POR HABER, es decir, si decidimos agregar una nueva figura geométrica para calcular su área, nos veremos obligado a modificar la clase CalculoArea.
El caso es que podríamos mejorar lo presente (quizás) dando una vuelta de tuerca.
Imaginemos entonces el siguiente diagrama de objetos refactorizando lo anterior:
Aunque a priori hemos mejorado el mantenimiento de nuestra aplicación, sabemos ya a estas alturas que no estamos delante de un marco ideal de cómo hacer las cosas. Así que si incrementamos las figuras geométricas, estaremos nuevamente delante de la necesidad de agregar otra variable dentro de la clase CalculoArea para la nueva figura geométrica además de tener que agregar en la función GetArea su correspondiente cálculo.
En resumidas cuentas, que estaremos más o menos igual que antes.
Parece evidente analizando nuestro código, que todas las figuras geométricas tienen en común algo. La necesidad o el requisito de calcular su área, nada mejor como su propia figura geométrica para saber y conocer perfectamente cuál es el cálculo de su área.
Pero aún mucho mejor si abstraemos esa funcionalidad de manera tal que si agregamos una nueva figura, cumpla con ese requisito.
El siguiente diagrama recoge esta filosofía:
Y el código de esta implementación quedaría de la siguiente manera:
1: using System;
2:
3:
4: public interface IFiguraGeometrica
5: {
6: double GetArea();
7: } // IFiguraGeometrica
8:
9:
10: public class Cuadrado : IFiguraGeometrica
11: {
12:
13: public Cuadrado(double lado)
14: {
15: this.Lado = lado;
16: } // Circulo Constructor
17:
18: private double Lado { get; set; }
19:
20: public double GetArea()
21: {
22: return Math.Pow(this.Lado, 2);
23: } // GetArea
24:
25: } // Cuadrado
26:
27: public class Circulo : IFiguraGeometrica
28: {
29:
30: public Circulo(double radio)
31: {
32: this.Radio = radio;
33: } // Circulo Constructor
34:
35: private double Radio { get; set; }
36:
37: public double GetArea()
38: {
39: return Math.PI * Math.Pow(this.Radio, 2);
40: } // GetArea
41:
42: } // Circulo
43:
44: public class Rectangulo : IFiguraGeometrica
45: {
46:
47: public Rectangulo(double ancho, double alto)
48: {
49: this.Ancho = ancho;
50: this.Alto = alto;
51: } // Rectangulo Constructor
52:
53: private double Ancho { get; set; }
54: private double Alto { get; set; }
55:
56: public double GetArea()
57: {
58: return this.Alto * this.Ancho;
59: } // GetArea
60:
61: } // Rectangulo
Y una aproximación de utilizar esto quedaría de la siguiente manera aproximada:
1: IFiguraGeometrica figuraGeometrica = new Circulo(13);
2: MessageBox.Show(figuraGeometrica.GetArea().ToString());
3: figuraGeometrica = new Rectangulo(7, 3);
4: MessageBox.Show(figuraGeometrica.GetArea().ToString());
Evidentemente, todo esto como podemos ver resulta mucho más simple.
Si modificamos una implementación de Cuadrado, Circulo o Rectangulo, no estaremos “rompiendo” nada de lo que ya existe.
Mejoraremos el mantenimiento, e incorporaremos pautas para reutilizar el código.
LSP o Liskov Substitution Principle
Este principio viene a decir que los objetos de una aplicación deberían ser reemplazables con instancias de sus subtipos sin que por ello tengamos que alterar el funcionamiento y la lógica de la aplicación.
Dicho de otro modo, que las funciones que utilizan referencias o punteros a clases base, deberían ser capaces de utilizar los objetos de las clases derivadas sin conocerlas.
Veámoslo con un ejemplo de aproximación partiendo del siguiente diagrama:
Y el código fuente de lo que estamos viendo quedaría de la siguiente manera:
1: public class Cuadrado : Rectangulo
2: {
3:
4: public override double Ancho
5: {
6: get
7: {
8: return base.Ancho;
9: }
10: set
11: {
12: base.Ancho = value;
13: base.Alto = value;
14: }
15: } // Ancho
16:
17: public override double Alto
18: {
19: get
20: {
21: return base.Alto;
22: }
23: set
24: {
25: base.Ancho = value;
26: base.Alto = value;
27: }
28: } // Alto
29:
30: } // Cuadrado
31:
32: public class Rectangulo
33: {
34: public virtual double Ancho { get; set; }
35: public virtual double Alto { get; set; }
36:
37: public double GetArea()
38: {
39: return this.Ancho * this.Alto;
40: } // GetArea
41:
42: } // Rectangulo
El consumo de este ejemplo quedaría de la siguiente forma:
1: Rectangulo rectangulo = new Cuadrado();
2: rectangulo.Alto = 3;
3: rectangulo.Ancho = 7;
4: MessageBox.Show(rectangulo.GetArea().ToString());
El resultado esperado de este cálculo sería 21, sin embargo, obtendremos 49 que sería el producto de 7 x 7, algo que en el caso del cálculo del rectángulo no es lo que esperábamos.
¿Cómo resolver el problema?.
Una posibilidad sería la siguiente:
En código, este planteamiento quedaría de la siguiente forma:
1: public class Dimensiones
2: {
3:
4: public virtual double Ancho { get; set; }
5: public virtual double Alto { get; set; }
6:
7: public double GetArea()
8: {
9: return this.Ancho * this.Alto;
10: } // GetArea
11:
12: } // Dimensiones
13:
14: public class Cuadrado : Dimensiones
15: {
16:
17: public override double Ancho
18: {
19: get
20: {
21:
22: return base.Ancho;
23: }
24: set
25: {
26: base.Alto = value;
27: base.Ancho = value;
28: }
29: } // Ancho
30:
31: public override double Alto
32: {
33: get
34: {
35: return base.Alto;
36: }
37: set
38: {
39: base.Alto = value;
40: base.Ancho = value;
41: }
42: } // Alto
43:
44: } // Cuadrado
45:
46: public class Rectangulo : Dimensiones
47: {
48: } // Rectangulo
Consumir nuestras clases ahora quedaría de la siguiente manera:
1: Dimensiones figura = new Rectangulo();
2: figura.Alto = 3;
3: figura.Ancho = 7;
4: MessageBox.Show(figura.GetArea().ToString());
De esta manera y con este ejemplo, aproximamos una solución a los problemas planteados.
ISP o Interface Segregation Principle
Este principio nos indica que los clientes no deberían ser forzados a implementar interfaces que no van a utilizar.
Deberíamos tener en cuenta que si tenemos una interfaz bastante grande, deberíamos pensar en segregarla en varias más pequeñas y específicas.
Un ejemplo práctico de esto que estamos comentando en la siguiente interfaz:
Esta interfaz tiene tres métodos muy sencillos a modo de ejemplo:
1: public interface IDesplazarse
2: {
3: void Conducir();
4: void Correr();
5: void Pasar();
6: } // IDesplazarse
Sin embargo, aquí nos encontramos con una característica a tener en cuenta.
Un ser humano que va a desplazarse no tiene porqué saber conducir, o incluso a lo mejor, no tiene edad suficiente para conducir.
Imaginemos un niño de 15 años. Podrá pasear y podrá correr, pero no podrá conducir.
Así que nuestra interfaz es correcta para otro grupo de personas que cumplan todos los requisitos de la interfaz o sobre las que queramos ejecutar el método de Conducir, pero si tuviéramos una clase Persona que implementara IDesplazarse y que no pudiera conducir, se produciría una excepción ya que no hemos implementado dicha funcionalidad.
Es decir, deberíamos segregar la interfaz en más de una interfaz de acuerdo a nuestras necesidades. Y en este caso, nuestra solución quedaría de la siguiente manera:
Y el código de esta nueva implementación quedaría de la siguiente manera:
1: public interface IDesplazarse
2: {
3: void Correr();
4: void Pasar();
5: } // IDesplazarse
6:
7: public interface IDesplazarseConCarnetConducir : IDesplazarse
8: {
9: void Conducir();
10: } // IDesplazarseConCarnetConducir
Como podemos apreciar, tendremos personas que utilicen la interfaz IDesplazarse y otras que además de desplazarse conduzcan, por lo que implementarán la interfaz IDesplazarseConCarnetConducir que implementará a su vez IDesplazarse.
DIP o Dependency Inversion Principle
En este principio se enuncia que deberíamos depender de las abstracciones y no de las implementaciones. Las abstracciones no deberían depender de los detalles.
Por otro lado, Dependency Injection o Inyección de Dependencias es un método que nos permite cumplir con este principio.
Pero ciñéndonos a los ejemplos, vamos a aproximar una comprensión sobre DIP.
El diagrama que recogería esta filosofía sería el siguiente:
El código del diagrama anterior es el que se indica a continuación:
1: public interface IValidator
2: {
3: bool Validate();
4: } // IValidator
5:
6: public class ValidationManager
7: {
8: private IValidator validator;
9:
10: public ValidationManager(IValidator validator)
11: {
12: this.validator = validator;
13: } // ValidationManager
14:
15: public void Execute()
16: {
17: if (!this.validator.Validate())
18: {
19: // Do Something
20: }
21: } // Execute
22:
23: } // ValidationManager
Como podemos ver aquí, dependemos de las abstracciones en lugar de las implementaciones, y todas las referencias están basadas en interfaces.
Independientemente del ejemplo anterior, también podríamos utilizar el patrón factoría (Factory Pattern) con el fin de resolver a través de ese patrón, todas las dependencias.