Código heredado de refactorización Parte 8 - Inversión de dependencias para una arquitectura limpia

Código antiguo. Código feo Código complicado. Código de espagueti. Tonterías gibernéticas. En dos palabras, Código Legado. Esta es una serie que te ayudará a trabajar y lidiar con ella..

Ahora es el momento de hablar sobre la arquitectura y cómo organizamos nuestras capas de código recién encontradas. Es hora de tomar nuestra aplicación y tratar de asignarla al diseño arquitectónico teórico..

Arquitectura limpia

Esto es algo que hemos visto en nuestros artículos y tutoriales. Arquitectura limpia.

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

En el centro de nuestra arquitectura se encuentra nuestra lógica empresarial. Estas son las clases que representan los procesos de negocio 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 empresarial. Estos pueden ser vistos como simples módulos auxiliares auxiliares. 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 conectan nuestra lógica de negocios a nuestra capa de persistencia y a todos los datos en esa capa, pero no tenemos tal capa en nuestra aplicación. Luego están ayudando a clases como fábricas y constructores que están construyendo y proporcionando nuevos objetos a nuestra lógica empresarial. Finalmente están las clases que representan el punto de entrada a nuestro sistema. En nuestro caso, GameRunner pueden ser considerados como una clase, o todas nuestras pruebas también son puntos de entrada a su manera.

Lo más importante de 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 empresarial no depende de nada más. Si todos los objetos en nuestra lógica de negocios 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 empresarial debe poder funcionar sin una interfaz de usuario o sin una capa de persistencia. Nuestra lógica de negocios 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 deben depender de las abstracciones..
B. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones..

Este es el último principio de SOLID y, probablemente, el que tenga mayor efecto en su 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 cosas abstractas. Su base de datos es muy concreta, por lo que debe depender de algo más abstracto. Su interfaz de usuario es muy concreta, por lo que debería depender de algo más abstracto. Tus fábricas vuelven a ser muy concretas. Pero ¿qué pasa con su lógica de negocios. Dentro de su lógica empresarial, debe continuar aplicando estas ideas, de modo que las clases más cercanas a los límites dependan de clases más abstractas, más en el corazón de su lógica empresarial..

Una lógica empresarial pura, representa de manera abstracta, los procesos y comportamientos de un dominio o modelo de negocio definido. Dicha lógica de negocios no contiene datos específicos (cosas concretas) como valores, dinero, nombres de cuentas, contraseñas, el tamaño de un botón o el número de campos en un formulario. La lógica de negocios no debería preocuparse por cosas concretas. Solo debe preocuparse por sus procesos de negocio..

El truco técnico

Entonces, el principio de inversión de dependencia (DIP) dice que debemos invertir nuestras dependencias siempre que haya un 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 un Juego clase y luego lo usa. Por otro lado, nuestra Juego clase, que representa nuestra lógica de negocios, crea y utiliza una Monitor objeto.

Entonces, el corredor depende de nuestra lógica de negocio. Eso es correcto. Por otro lado, nuestra Juego depende de Monitor, que no es bueno Nuestra lógica de negocios nunca debe 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..

Un Clase abstracta Es un tipo especial que no se puede inicializar. Contiene solo definiciones e implementaciones parciales. Una clase base abstracta usualmente tiene varias clases de niños. 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..

Un Interfaz Es un tipo especial que permite solo la definición de métodos y variables. Es el constructo más abstracto en programación orientada a objetos. Cualquier implementación debe implementar siempre 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, los otros como Java o PHP no permiten la herencia múltiple. Por lo tanto, una clase concreta puede extender una sola clase abstracta, pero puede implementar varias interfaces, incluso al mismo tiempo si es necesario. O, desde otra perspectiva, una clase abstracta única puede tener muchas implementaciones, mientras que muchas interfaces pueden tener muchas implementaciones.

Para una explicación más completa del DIP, lea el tutorial dedicado a este principio SOLID.

Invertir la dependencia utilizando una interfaz

PHP es totalmente compatible con las interfaces. A partir de la Monitor 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 a MonitorEn la lista de métodos, hay 12 métodos públicos, incluido el constructor. Esta es una interfaz bastante grande, debe 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 sobre esto. Tal vez intentaremos resolver este problema en un futuro tutorial..

Lo que queremos lograr ahora es una arquitectura como la de abajo..

De esta manera, en lugar de Juego Dependiendo de lo mas concreto. Monitor, Ambos dependen de la interfaz muy abstracta.. Juego usa la interfaz, mientras Monitor lo implementa.

Interfaces de nombres

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

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

En los viejos tiempos de la notación húngara, lo hubiéramos hecho de esta manera.

Para este diagrama, utilizamos los nombres de clase / archivo reales y el uso de mayúsculas real. La interfaz se llama "IDisplay" con una "I" mayúscula delante de "Pantalla". En realidad había lenguajes de programación que requerían tal denominación para 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 Juego. Así Juego No debe saber que utiliza una interfaz o un objeto real.. Juego No debe preocuparse por la implementación que realmente obtiene. Desde JuegoDesde el punto de vista, solo usa una "Pantalla", eso es todo..

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

También es mucho más efectivo para nosotros. Pensar en Juego como se ve ahora mismo. Utiliza un Monitor Objeto y sabe como usarlo. Si llamamos a nuestra interfaz "Pantalla", reduciremos el número de cambios necesarios en Juego.

Pero aún así, esta denominación es ligeramente mejor que la anterior. Permite una sola implementación para Monitor y el nombre de la implementación no nos dirá de qué tipo de pantalla estamos hablando.

Ahora que es considerablemente mejor. Nuestra implementación se llamó "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, el maestro dorado lento y las pruebas unitarias rápidas, queremos confiar en las pruebas unitarias tanto como podamos, y en el maestro dorado lo menos posible. Entonces, marquemos nuestras pruebas maestras de oro como omitidas e intentemos confiar en nuestras pruebas unitarias. Ellos están pasando en este momento y queremos hacer un cambio que los mantendrá a los que pasan. Pero, ¿cómo podemos hacer tal cosa, sin hacer todos los cambios propuestos anteriormente??

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

La burla salva el día

Hay tal manera. En las pruebas, hay un concepto llamado "burlón".

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".

Tal objeto sería de gran ayuda para nosotros. 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 podemos enviar a Juego en lugar de la lógica de visualización real.

Creando la interfaz

Vamos a crear una interfaz llamada Monitor Con todos los métodos públicos de la actual clase concreta..

Como se puede observar, lo viejo. Display.php fue renombrado a DisplayOld.php. Este es solo un paso temporal, que nos permite sacarlo del camino y concentrarnos en la interfaz..

interfaz de pantalla  

Eso es todo lo que hay para crear una interfaz. Puede ver que se define como "interfaz" y no como una "clase". Añadamos los métodos..

interfaz Display function statusAfterRoll ($ rolledNumber, $ currentPlayer); function playerSentToPenaltyBox ($ currentPlayer); function playerStaysInPenaltyBox ($ currentPlayer); function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); function statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); función playerAdded ($ playerName, $ numberOfPlayers); función askQuestion ($ currentCategory); funcion correctAnswer (); function correctAnswerWithTypo (); function incorrectAnswer (); function playerCoins ($ currentPlayer, $ playerCoins);  

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

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

Error grave: no se puede crear una instancia de la pantalla de la interfaz

Eso es porque Juego intenta crear una nueva pantalla en la línea 25, en el constructor.

Sabemos que no podemos hacer eso. Una interfaz o una clase abstracta no puede ser instanciada. Necesitamos un objeto real..

Inyección de dependencia

Necesitamos un objeto ficticio para ser utilizado en nuestras pruebas. Una clase sencilla, implementando todos los métodos del Monitor Interfaz, pero sin hacer nada. Vamos a escribirlo directamente dentro de nuestra prueba de unidad. Si su lenguaje de programación no permite varias clases en el mismo archivo, no dude en crear un nuevo archivo para su clase ficticia.

clase DummyDisplay implementa Display function statusAfterRoll ($ rolledNumber, $ currentPlayer) // TODO: Implementar el método statusAfterRoll ().  function playerSentToPenaltyBox ($ currentPlayer) // TODO: Implementa el método playerSentToPenaltyBox ().  function playerStaysInPenaltyBox ($ currentPlayer) // TODO: Implementa el método playerStaysInPenaltyBox ().  function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementar el método statusAfterNonPenalizedPlayerMove ().  function statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementar el método statusAfterPlayerGettingOutOfPenaltyBox ().  function playerAdded ($ playerName, $ numberOfPlayers) // TODO: Implementa el método playerAdded ().  función askQuestion ($ currentCategory) // TODO: Implementar el método askQuestion ().  function correctAnswer () // TODO: Implementar el método correctAnswer ().  function correctAnswerWithTypo () // TODO: Implementar el método correctAnswerWithTypo ().  function incorrectAnswer () // TODO: Implementar el método incorrectAnswer ().  function playerCoins ($ currentPlayer, $ playerCoins) // TODO: Implementa el método playerCoins (). 

Tan pronto como usted dice que su clase implementa una interfaz, el IDE le permitirá completar automáticamente los métodos que faltan. Esto hace que la creación de tales objetos sea muy rápida, en tan solo unos segundos..

Ahora vamos a usarlo en Juego Al inicializarlo en su constructor..

función __construct () $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = new DummyDisplay (); 

Esto hace que la prueba pase, pero presenta un gran problema.. Juego Debe saber sobre su prueba. Realmente no queremos esto. Una prueba es solo otro punto de entrada. los DummyDisplay es solo otra interfaz de usuario. Nuestra lógica de negocio, la Juego clase, no debe depender de la interfaz de usuario. Así que vamos a hacer que dependa solo de la interfaz.

function __construct (Display $ display) $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = $ display; 

Pero para probar Juego, Necesitamos enviar la pantalla de prueba de nuestras pruebas..

función setUp () $ this-> game = new Game (new DummyDisplay ()); 

Eso es. Necesitamos modificar una sola línea en nuestras pruebas unitarias. En la configuración, enviaremos, como parámetro, una nueva instancia de DummyDisplay. Eso es una inyección de dependencia. El uso de interfaces y la inyección de dependencia ayuda especialmente si está 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 usa en los parámetros. Y una ventaja adicional es que su IDE autocompletará los métodos para esos parámetros porque puede determinar sus tipos.

Una implementación real para Golden Master

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

clase CLIDisplay implementa Display //… //

Renombrarlo a CLIDisplay y ponerlo en práctica Monitor.

función run () $ display = new CLIDisplay (); $ aGame = juego nuevo ($ display); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); do $ dice = rand (0, 5) + 1; $ aGame-> roll ($ dice);  while (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ())); 

En RunnerFunctions.php, en el correr() función, cree una nueva pantalla para CLI y pásela a Juego cuando se crea.

Descomenta y ejecuta tus pruebas maestras de oro. Ellos pasaran.

Pensamientos finales

Esta solución conduce efectivamente a una arquitectura como en el diagrama a continuación..

Así que ahora nuestro corredor de juegos, que es el punto de entrada a nuestra aplicación, crea un CLIDisplay y así depende de ello. CLIDisplay depende solo de la interfaz que se encuentra en el límite entre la presentación y la lógica empresarial. 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 cuando hablaremos sobre la burla y la interacción en clase con más detalles..