1. Code
  2. Coding Fundamentals
  3. Design Patterns

Refactorización del código heredado: Parte 8: Inversión de dependencias para una arquitectura limpia

Código antiguo. Código feo. Código complicado. Código espagueti. Tonterías de galimatías. En dos palabras, Código Heredado. Esta es una serie que te ayudará a trabajar y afrontarla.
Scroll to top
This post is part of a series called Refactoring Legacy Code.
Refactoring Legacy Code: Part 7 - Identifying the Presentation Layer

Spanish (Español) translation by steven (you can also view the original English article)

Código antiguo. Código feo. Código complicado. Código espagueti. Tonterías de galimatías. En dos palabras, Código Heredado. Esta es una serie que te ayudará a trabajar y afrontarla.

Ahora es el momento de hablar sobre arquitectura y cómo organizamos nuestras capas de código recién descubiertas. Es hora de tomar nuestra aplicación e intentar mapearla con el diseño arquitectónico teórico.

Arquitectura limpia

Esto es algo que hemos visto a lo largo de nuestros artículos y tutoriales. Arquitectura limpia.

En un nivel alto, se parece al esquema anterior y estoy seguro de que ya estás familiarizado con él. Es, una solución arquitectónica propuesta por Robert C. Martin.

En el centro de nuestra arquitectura está nuestra lógica de negocio. Estas son las clases que representan los procesos comerciales que nuestra aplicación intenta resolver. Estas son las entidades e interacciones que representan el dominio de nuestro problema.

Luego, hay varios otros tipos de módulos o clases en torno a nuestra lógica de negocio. Estos pueden verse como simples módulos auxiliares de ayuda. Tienen varios propósitos y la mayoría de ellos son indispensables. Proporcionan la conexión entre el usuario y nuestra aplicación a través de un mecanismo de entrega. En nuestro caso, esta es una interfaz de línea de comandos. Hay otro conjunto de clases auxiliares que están conectando nuestra lógica de negocio con nuestra capa de persistencia y con todos los datos de esa capa, pero no tenemos esa capa en nuestra aplicación. Luego están las clases de ayuda como fábricas y constructores que están construyendo y proporcionando nuevos objetos a nuestra lógica de negocio. Finalmente, están las clases que representan el punto de entrada a nuestro sistema. En nuestro caso, GameRunner puede considerarse una clase de este tipo, o todas nuestras pruebas también son puntos de entrada a su manera.

Lo que es más importante notar en el diagrama es la dirección de la dependencia. Todas las clases auxiliares dependen de la lógica de negocio. La lógica de negocio no depende de nada más. Si todos los objetos en nuestra lógica de negocio pudieran aparecer mágicamente, con todos los datos en ellos, y pudiéramos ver lo que sucede dentro de nuestra computadora directamente, deberían poder funcionar. Nuestra lógica de negocio debe poder funcionar sin una interfaz de usuario o sin una capa de persistencia. Nuestra lógica de negocio debe existir aislada, en una burbuja de un universo lógico.

El principio de inversión de dependencia

A. Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deberían depender de abstracciones. B. Las abstracciones no deben depender de los detalles. Los detalles deben depender de abstracciones.

Este es el último principio SÓLIDO y probablemente el que tiene el mayor efecto en tu código. Es bastante simple de entender y bastante simple de implementar.

En términos simples, dice que las cosas concretas siempre deben depender de las abstractas. Tu base de datos es muy concreta, por lo que deberías depender de algo más abstracto. Tu interfaz de usuario es muy concreta, por lo que deberías depender de algo más abstracto. Tus fábricas vuelven a ser muy concretas. Pero, ¿qué pasa con la lógica de tu negocio? Dentro de tu lógica de negocio, debes continuar aplicando estas ideas, de modo que las clases que están más cerca de los límites dependan de clases que son más abstractas, más en el corazón de tu lógica de negocio.

Una lógica de negocio pura, representa de forma abstracta los procesos y comportamientos de un dominio o modelo de negocio definido. Dicha lógica de negocio no contiene detalles (cosas concretas) como valores, dinero, nombres de cuentas, contraseñas, el tamaño de un botón o la cantidad de campos en un formulario. La lógica de negocio no debería preocuparse por cosas concretas. Solo debería preocuparse por tus procesos comerciales.

El truco técnico

Entonces, el Principio de Inversión de Dependencias (DIP) dice que debemos invertir nuestras dependencias siempre que haya código que dependa de algo concreto. En este momento, nuestra estructura de dependencia se ve así.

GameRunner, usando las funciones en RunnerFunctions.php está creando una clase Game y luego la usa. Por otro lado, nuestra clase Game, que representa nuestra lógica de negocio, crea y usa un objeto Display.

Entonces, el corredor depende de nuestra lógica de negocio. Eso es correcto. Por otro lado, nuestro juego (Game) depende de la pantalla (Display), lo cual no es bueno. Nuestra lógica de negocio nunca debería depender de nuestra presentación.

El truco técnico más simple que podemos hacer es hacer uso de las construcciones abstractas en nuestro lenguaje de programación. Una clase tradicional es más concreta que una clase abstracta, que es más concreta que una interfaz.

Una clase abstracta es un tipo especial que no se puede inicializar. Contiene solo definiciones e implementaciones parciales. Una clase base abstracta generalmente tiene varias clases secundarias. Estas clases secundarias heredan la funcionalidad parcial común del padre abstracto, están agregando su propio comportamiento extendido y deben implementar todos los métodos definidos en el padre abstracto pero no implementados en él.

Una interfaz es un tipo especial que permite solo la definición de métodos y variables. Es la construcción más abstracta de la programación orientada a objetos. Cualquier implementación siempre debe implementar todos los métodos de su interfaz principal. Una clase concreta puede implementar varias interfaces.

A excepción de los lenguajes orientados a objetos de la familia C, otros como Java o PHP no permiten la herencia múltiple. Entonces, una clase concreta puede extender una sola clase abstracta pero puede implementar varias interfaces, incluso al mismo tiempo si es necesario. O dicho desde otra perspectiva, una sola clase abstracta puede tener muchas implementaciones, mientras que muchas interfaces pueden tener muchas implementaciones.

Para obtener una explicación más completa del DIP, lee el tutorial dedicado a este principio SÓLIDO.

Inversión de la dependencia mediante una interfaz

PHP es totalmente compatible con interfaces. Partiendo de la clase Display como nuestro modelo, podríamos definir una interfaz con los métodos públicos que todas las clases responsables de mostrar datos necesitarán implementar.

Mirando la lista de métodos de Display, hay 12 métodos públicos, incluido el constructor. Esta es una interfaz bastante grande, debes mantener este número lo más bajo posible, exponiendo las interfaces a medida que los clientes las necesiten. El principio de segregación de interfaz tiene algunas buenas ideas al respecto. Tal vez intentemos solucionar este problema en un tutorial futuro.

Lo que queremos lograr ahora es una arquitectura como la que se muestra a continuación.

De esta manera, en lugar de que Game dependa de Display más concreta, ambos dependen de la interfaz muy abstracta. Game usa la interfaz, mientras que Display la implementa.

Nombrando las interfaces

Phil Karlton dijo: "Sólo hay dos cosas difíciles en Ciencias de la Computación: invalidación de caché y nombrar cosas".

Si bien no nos preocupan los cachés, debemos nombrar nuestras clases, variables y métodos. Nombrar interfaces puede ser todo un desafío.

En los viejos tiempos de la notación húngara, lo habríamos hecho de esta manera.

Para este diagrama, usamos los nombres reales de clases/archivos y las mayúsculas reales. La interfaz se llama "IDisplay" con una "I" mayúscula delante de "Display". En realidad, existían lenguajes de programación que requerían tal denominación para las interfaces. Estoy seguro de que hay algunos lectores que todavía los usan y sonríen en este momento.

El problema con este esquema de nombres es la preocupación fuera de lugar. Las interfaces pertenecen a sus clientes. Nuestra interfaz pertenece a Game. Por lo tanto, Game no debe saber que utiliza una interfaz o un objeto real. Game no debe preocuparse por la implementación que realmente obtiene. Desde el punto de vista de Game, solo usa "Display", eso es todo.

Esto resuelve el problema de nombres de Game y Display. Usar el sufijo "Impl" para la implementación es algo mejor. Ayuda a eliminar la preocupación de Game.

También es mucho más eficaz para nosotros. Piensa en Game como se ve ahora. Utiliza un objeto Display y sabe cómo utilizarlo. Si llamamos a nuestra interfaz "Display", reduciremos la cantidad de cambios necesarios en el Juego.

Pero aún así, este nombre es ligeramente mejor que el anterior. Solo permite una implementación para Display y el nombre de la implementación no nos dirá de qué tipo de pantalla estamos hablando.

Ahora eso es considerablemente mejor. Nuestra implementación se denominó "CLIDisplay", ya que se envía a la CLI. Si queremos una salida HTML o una interfaz de usuario de escritorio de Windows, podemos agregar fácilmente todo eso a nuestra arquitectura.

Muéstrame el código

Como tenemos dos tipos de pruebas, las lentas pruebas maestras de oro y las pruebas unitarias rápidas, queremos confiar en las pruebas unitarias tanto como podamos y en las pruebas maestras de oro tan poco como podamos. Así que marquemos nuestras pruebas maestras de oro como omitidas e intentemos confiar en nuestras pruebas unitarias. Están pasando en este momento y queremos hacer un cambio que los mantendrá pasando. Pero, ¿cómo podemos hacer tal cosa sin hacer todos los cambios propuestos anteriormente?

¿Existe alguna forma de prueba que nos permita dar un paso más pequeño?

Burlarse salva el día

Existe una forma de hacerlo. En las pruebas, hay un concepto llamado "burlarse".

Wikipedia define Mocking como tal: "En la programación orientada a objetos, los objetos simulados son objetos simulados que imitan el comportamiento de objetos reales de forma controlada".

Un objeto así nos sería de gran ayuda. De hecho, ni siquiera necesitamos algo tan complejo como simular todo el comportamiento. Todo lo que necesitamos es un objeto falso y estúpido que podamos enviar a Game en lugar de la lógica de visualización real.

Creación de la interfaz

Creemos una interfaz llamada Display con todos los métodos públicos de la clase concreta actual.

Como puedes observar, el antiguo Display.php fue renombrado a DisplayOld.php. Este es solo un paso temporal, que nos permite sacarlo del camino y concentrarnos en la interfaz.

1
interface Display {
2
3
} 

Eso es todo lo que hay que hacer para crear una interfaz. Puedes ver que está definido como "interfaz" y no como una "clase". Agreguemos los métodos.

1
interface Display {
2
	function statusAfterRoll($rolledNumber, $currentPlayer);
3
	function playerSentToPenaltyBox($currentPlayer);
4
	function playerStaysInPenaltyBox($currentPlayer);
5
	function statusAfterNonPenalizedPlayerMove($currentPlayer, $currentPlace, $currentCategory);
6
	function statusAfterPlayerGettingOutOfPenaltyBox($currentPlayer, $currentPlace, $currentCategory);
7
	function playerAdded($playerName, $numberOfPlayers);
8
	function  askQuestion($currentCategory);
9
	function correctAnswer();
10
	function correctAnswerWithTypo();
11
	function incorrectAnswer();
12
	function playerCoins($currentPlayer, $playerCoins);
13
} 

Si. Una interfaz es solo un montón de declaraciones de funciones. Imagínalo como un archivo de encabezado C. Sin implementaciones, solo declaraciones. No puede contener una implementación en absoluto. Si intentas implementar cualquiera de los métodos, se producirá un error.

Pero estas definiciones tan abstractas nos permiten algo maravilloso. Nuestra clase Game ahora depende de ellos, en lugar de una implementación concreta. Sin embargo, si intentamos ejecutar nuestras pruebas, fallarán.

1
Fatal error: Cannot instantiate interface Display

Esto se debe a que Game intenta crear una nueva pantalla por sí solo en la línea 25, en el constructor.

Sabemos que no podemos hacer eso. No se pueden crear instancias de una interfaz o una clase abstracta. Necesitamos un objeto real.

Inyección de dependencia

Necesitamos un objeto ficticio para usar en nuestras pruebas. Una clase simple, que implementa todos los métodos de la interfaz Display, pero no hace nada. Escribámoslo directamente dentro de nuestra prueba unitaria. Si tu lenguaje de programación no permite varias clases en el mismo archivo, no dudes en crear un nuevo archivo para tu clase ficticia.

1
class DummyDisplay implements Display {
2
3
	function statusAfterRoll($rolledNumber, $currentPlayer) {
4
		// TODO: Implement statusAfterRoll() method.

5
	}
6
7
	function playerSentToPenaltyBox($currentPlayer) {
8
		// TODO: Implement playerSentToPenaltyBox() method.

9
	}
10
11
	function playerStaysInPenaltyBox($currentPlayer) {
12
		// TODO: Implement playerStaysInPenaltyBox() method.

13
	}
14
15
	function statusAfterNonPenalizedPlayerMove($currentPlayer, $currentPlace, $currentCategory) {
16
		// TODO: Implement statusAfterNonPenalizedPlayerMove() method.

17
	}
18
19
	function statusAfterPlayerGettingOutOfPenaltyBox($currentPlayer, $currentPlace, $currentCategory) {
20
		// TODO: Implement statusAfterPlayerGettingOutOfPenaltyBox() method.

21
	}
22
23
	function playerAdded($playerName, $numberOfPlayers) {
24
		// TODO: Implement playerAdded() method.

25
	}
26
27
	function  askQuestion($currentCategory) {
28
		// TODO: Implement askQuestion() method.

29
	}
30
31
	function correctAnswer() {
32
		// TODO: Implement correctAnswer() method.

33
	}
34
35
	function correctAnswerWithTypo() {
36
		// TODO: Implement correctAnswerWithTypo() method.

37
	}
38
39
	function incorrectAnswer() {
40
		// TODO: Implement incorrectAnswer() method.

41
	}
42
43
	function playerCoins($currentPlayer, $playerCoins) {
44
		// TODO: Implement playerCoins() method.

45
	}
46
}

Tan pronto como digas que tu clase implementa una interfaz, el IDE te permitirá completar automáticamente los métodos que faltan. Esto hace que la creación de estos objetos sea muy rápida, en solo unos segundos.

Ahora usémoslo en Game inicializándolo en su constructor.

1
function  __construct() {
2
3
	$this->players = array();
4
	$this->places = array(0);
5
	$this->purses = array(0);
6
	$this->inPenaltyBox = array(0);
7
8
	$this->display = new DummyDisplay();
9
}

Esto hace que la prueba pase, pero presenta un gran problema. Game debe conocer su prueba. Realmente no queremos esto. Una prueba es solo otro punto de entrada. El DummyDisplay es solo otra interfaz de usuario. Nuestra lógica de negocio, la clase Game, no debería depender de la interfaz de usuario. Así que hagamos que dependa solo de la interfaz.

1
function  __construct(Display $display) {
2
3
	$this->players = array();
4
	$this->places = array(0);
5
	$this->purses = array(0);
6
	$this->inPenaltyBox = array(0);
7
8
	$this->display = $display;
9
}

Pero para probar Game, necesitamos enviar la pantalla ficticia de nuestras pruebas.

1
function setUp() {
2
	$this->game = new Game(new DummyDisplay());
3
}

Eso es todo. Necesitábamos modificar una sola línea en nuestras pruebas unitarias. En la configuración, enviaremos, como parámetro, una nueva instancia de DummyDisplay. Esa es una inyección de dependencia. El uso de interfaces y la inyección de dependencias ayudan especialmente si estás trabajando en equipo. En Syneto observamos que especificar un tipo de interfaz para una clase e inyectarlo nos ayudará a comunicar mucho mejor las intenciones del código del cliente. Cualquiera que mire al cliente sabrá qué tipo de objeto se utiliza en los parámetros. Y una ventaja interesante es que tu IDE completará automáticamente los métodos para esos parámetros porque puede determinar sus tipos.

Una implementación real para las pruebas maestras de oro

La prueba maestra de oro, ejecuta nuestro código como en el mundo real. Para que pase, necesitamos transformar nuestra antigua clase de visualización en una implementación real de la interfaz y enviarla a nuestra lógica de negocio. Aquí tienes una forma de hacerlo.

1
class CLIDisplay implements Display {
2
	// ... //

3
}

Cambia el nombre a CLIDisplay y haz que implemente Display.

1
function run() {
2
	$display = new CLIDisplay();
3
	$aGame = new Game($display);
4
	$aGame->add("Chet");
5
	$aGame->add("Pat");
6
	$aGame->add("Sue");
7
8
	do {
9
		$dice = rand(0, 5) + 1;
10
		$aGame->roll($dice);
11
	} while (!didSomebodyWin($aGame, isCurrentAnswerCorrect()));
12
}

En RunnerFunctions.php, en la función run(), crea una nueva pantalla para CLI y pásalo a Game cuando se cree.

Descomenta y ejecuta tus pruebas maestras de oro. Pasarán.

Reflexiones finales

Esta solución conduce efectivamente a una arquitectura como la del siguiente diagrama.

Así que ahora nuestro corredor de juegos, que es el punto de entrada a nuestra aplicación, crea un CLIDisplay concreto y, por lo tanto, depende de él. CLIDisplay depende solo de la interfaz que se encuentra en el límite entre la presentación y la lógica de negocio. Nuestro corredor también depende directamente de la lógica de negocio. Así es como se ve nuestra aplicación cuando se proyecta en la arquitectura limpia con la que comenzamos este artículo.

Gracias por leer, y no se pierda el próximo tutorial en el que hablaremos sobre la burla y la interacción de la clase con más detalles.