Refactorizando legacy code en PHP Parte 7 – Capa de presentación

La capa de presentación

Ya llevamos 7 entregas de como refactorizar una aplicación PHP legacy. Ahora llega el momento de empezar a separar responsabilidades, de ir creando distintas clases y sobre todo de ir mejorando nuestra aplicación legacy en PHP.
Hemos visto a lo largo de los posts que la capa de presentación juega un papel fundamental en nuestro juego de Trivial. Vamos a tratar de identificar todos los sitios donde se haga “presentación” y trataremos de empujar esa “reponsabilidad de representar la salida de la aplicación” a un solo lugar. Para ello nos basaremos en los principios SOLID.

Fundamentos SOLID

Cuando comenzamos una refactorización y empezamos a cambiar código debemos guiarnos por unos principios. Estos principios nos ayudarán a ir tomando decisiones en cada paso de la refactorización. Si hablamos de programación orientada a objetos, los principios que debemos tener en mente son entre otros SOLID. Según la Wikipedia SOLID es un acrónimo mnemotécnico introducido por Robert C. Martin “Uncle Bob” que representa cinco principios básicos de la programación orientada a objetos y el diseño. Cuando se aplican estos principios en conjunto es más probable que un desarrollador cree un sistema que sea fácil de mantener y ampliar en el tiempo.

El primero de estos primero de estos 5 principio es SRP (Single Responsability Principle) la noción de que un objeto solo debería tener una única responsabilidad.

Single Responsability Principle

El principio de única responsabilidad dice que una clase debe tener una única razón para cambiar. Pero ¿qué significa eso de una única razón para cambiar? Para responder a esta pregunta tenemos que pensar desde el punto de vista del usuario que utiliza la aplicación.

Un ejemplo de este principio puede verse en el diseño de la interfaz de usuario, si el equipo de UX requiere realizar modificaciones para crear un interfaz web con HTML tendríamos que cambiar mucho código y tendríamos acompañadas las salidas por linea de comandos y web. Por lo que una solución sería empezar a montar una arquitectura con componentes separados.

Clean Architecture

Clean Architecture es un concepto promocionado por “Uncle Bob” que consiste en que nuestra lógica de negocio debe estar separada de otros módulos de nuestra aplicación.

Teniendo en mente un esquema como este vamos a ir modificando la clase Game para ir desacoplando poco a poco lo que vayamos observando.

Observando nuestras funciones

Sin darnos cuenta en el anterior tutorial ya empezamos a separar la lógica casi de manera inconsciente. Si recuerdas, empezamos a extraer distintos métodos algunos de los cuales tienen empiezan llamándose “display”.

Así que ya tenemos una serie de métodos con una sola responsabilidad, por lo que podemos crear una nueva clase y mover estos métodos a la nueva clase.

Refactorizando – Extraer métodos a una clase

Una buena guía para refactorizar es el libro Refactoring de Martin Fowler en ella se explican un mecanismo para extraer a una nueva clase una responsabilidas, para así poder desacoplar el código:

  • Decidir como dividir las responsabilidades de la clase
    — Crear una nueva clase
    — Si las responsabilidades de la clase antigua no concuerdan con su nombre, renombrar la antigua clase.
  • Enlazar la antigua clase con la nueva.
  • Mover cada uno de las variables que necesitemos mover.
  • Testear cada uno de los movimientos anteriores.
  • Mover métodos que necesitemos mover de la antigua clase a la nueva. Empezar por los métodos “más pequeños/fáciles” y seguir por los más “grandes/complicados”
  • Testear cada método movido.
  • Revisar y reducir las interfaces de cada clase.
  • Decidir que es necesario exponer en la nueva clase.

En los artículos anteriores hemos ido utilizando PHPStorm para extraer variables o métodos, pero no es posible extraer clases con este IDE. Así que tendremos que realizar estos pasos de manera manual.

Aplicando la extracción

Decidiendo como dividir las responsabilidades

Esto ya lo hemos comentado antes, hemos decidido empezar a extraer la capa de presentación. Por lo que intentaremos extraer a la nueva clase todas las funciones que “impriman por pantalla”.

Creando la nueva clase

Nuestra nueva clase se llamará Display, por el momento solo será una clase vacía que iremos llenando. Además el nombre de la clase es lo suficientemente representativo como para conocer la responsabilidad de la clase.

Autoloading

En realidad para utilizar la clase podríamos utilizar require_once __DIR__ . 'Display.php' o algo similar para cargar la clase Display, pero como estamos utilizando composer, namespaces y demás creo que lo mejor será cargar la clase utilizando el autoload de composer.

Para ello lo primero será añadir un namespace a la clase Display, algo así:namespace GameLegacy;. En la clase Game es necesario indicar que queremos utilizar la nueva clase Display de esta manera use GameLegacy\Display; y en el constructor instanciamos la nueva clase más $this->display = new Display();.

Por último solo tenemos que ejecutar en el terminal composer dumpautoloadpara cargar la nueva clase. Para asegurarnos que todo está correcto con solo ejecutar los test veremos si algo falla 🙂

Mover los métodos a la nueva clase

Fácil ¿no?, bueno ahora queda la parte delicada, que es mover los métodos que tienen que ver con mostrar por pantalla a la nueva clase. Empezaremos por los métodos que conocemos y hemos tocado (los que empiezan por display…), por ejemplo displayPlayerNewLocationpuede ser un buen candidato.

En una primera lectura podemos ver que este método utiliza echoln
y que no llama a otros métodos de la clase.

function echoln($string) {
    echo $string . "\n";
}

echoln hace exactamente lo que dice, imprime una cadena con un salto de linea al final, así que creo que lo mejor será mover este método a la clase Display y ejecutar los test a ver que pasa.

Fatal error: Call to undefined function echoln() in .... line 61ç

¡Genial! El test falla y nos dice que tenemos un error en el método add, así que como el método echoln ha cambiado de clase, ahora tenemos que llamarlo utilizando display, algo más o menos así

    function add($playerName)
    {
        array_push($this->players, $playerName);
        $this->setDefaultParameterForPlayer($this->howManyPlayers());

        $this->display->echoln($playerName . " was added");
        $this->display->echoln("They are player number " . count($this->players));

        return true;
    }

Seguramente esto mismo pase en otros métodos, así que tenemos hacer lo mismo hasta que lleguemos a nuestro primer método diplay…

    protected function displayCurrentPlayer()
    {
        echoln($this->players[$this->currentPlayer] . " is the current player");
    }

Este podría ser el primer método que vamos a mover, aunque llegados este punto tenemos diversas opciones:

  • hacer un replace de echoln por $this->display->echoln y después mover los métodos display…
  • Ir moviendo los métodos display… y echoln poco a poco.

Personalmente creo que la 1ºopción es la mejor porque después del replace tendremos todos los tests en verde y más adelante podemos mover los métodos display… a la nueva clase, así que manos a la obra.

Justo después de reemplazar todas las llamadas a echon por $this->display->echoln podemos pasar los tests sin problemas. Así que podemos empezar a mover todos los métodos display a la nueva clase display.

Tenemos que ser muy cuidadosos con los cambios ya que todos los cambios no podremos hacerlos con tan solo reemplazar. Por ejemplo nos encontraremos casos como el siguiente:

    function  roll($rolledNumber)
    {
        $this->displayStatusAfterRoll($rolledNumber);

        if ($this->inPenaltyBox[$this->currentPlayer]) {
            $this->playNextMoveForPlayerInPenaltyBox($rolledNumber);
        } else {
            $this->playNextMove($rolledNumber);
        }
    }

Una llamada al método displayStatusAfterRoll que debemos modificar para que se utilice a través de la variable display y además modificaremos el nombre del método para que no sea redundante.

    function roll($rolledNumber)
    {
        $this->display->statusAfterRoll($rolledNumber);

        if ($this->inPenaltyBox[$this->currentPlayer]) {
            $this->playNextMoveForPlayerInPenaltyBox($rolledNumber);
        } else {
            $this->playNextMove($rolledNumber);
        }
    }

Además de modificar el nombre del método, también modificaremos su visibilidad para que sea publico, ya que así conseguimos obtener una API de la clase Display.

Debemos ir poco a poco, ejecutando nuestros tests a menudo, ya que seguramente hallemos algunos errores como este:

 GameTest::testAPlayersNextPositionIsCorrectlyDeterminedWhenNoNewLapIsInvolved
Undefined property: GameLegacy\Display::$currentPlayer

No hay que preocuparnos, este error se debe a la variable $this->currentplayer. Al haber movido a la clase Display los métodos, ya no tenemos la variable local currentPlayer.

  function displayCurrentPlayer()
    {
        $this->echoln($this->players[$this->currentPlayer] . " is the current player");
    }

Una solución es modificar el método para pasar por parámetros esta variable.

    function displayCurrentPlayer($currentPlayer)
    {
        $this->echoln($currentPlayer . " is the current player");
    }

y la llamada al método dentro quedará así:

    function roll($rolledNumber)
    {
        $this->display->statusAfterRoll($rolledNumber, $this->players[$this->currentPlayer]);

        if ($this->inPenaltyBox[$this->currentPlayer]) {
            $this->playNextMoveForPlayerInPenaltyBox($rolledNumber);
        } else {
            $this->playNextMove($rolledNumber);
        }
    }

Por ello debemos ser cuidadosos ya que cambios como estos afectan también a la clase Game, tendremos que modificar muchos métodos y paso de variables. Poco a poco y con la ayuda de los tests haremos un desacoplamiento de funciones de nuestra aplicación legay php.

Llegados aquí tenemos que ser coherentes con el naming de variables y métodos, así que todos los métodos que están en la clase Display creo que no deberían empezar por Display. Utilizando la función rename de PHPStorm podemos fácilmente solventar este pequeño inconveniente, que a la larga nos ayudará a entender mejor el código.

Una vez hecho todos estos cambios, debemos tener en cuenta que la idea detrás de todo el proceso de desacople es hacer que el método echoln no pueda utilizarse desde la clase Game directamente. Parece que ya casi lo hemos conseguido. Todavía hay llamadas al método echoln desde la clase Game, el método askQuestion utiliza el método echoln así que vamos a minimizar el constructor de Game llevandolo a Display así:


class Display
{
    protected $popQuestions = [];
    protected $scienceQuestions = [];
    protected $sportsQuestions = [];
    protected $rockQuestions = [];

    public function __construct()
    {
        $this->initializeQuestions();
    }

    protected function initializeQuestions()
    {
        $categorySize = 50;
        for ($i = 0; $i < $categorySize; $i++) {
            array_push($this->popQuestions, "Pop Question " . $i);
            array_push($this->scienceQuestions, "Science Question " . $i);
            array_push($this->sportsQuestions, "Sports Question " . $i);
            array_push($this->rockQuestions, "Rock Question " . $i);
        }
    }

No debemos olvidarnos tampoco del método askQuestion. Ahora este método va a recibir como parámetro $currentCategory pero no debemos preocuparnos, solo es cuestión de ejecutar los tests e ir modificando las variables que estos nos vayan diciendo.

Esta fase de la refactorización tiene una norma no escrita que debemos seguir:
No modificar los tests hasta no haber acabado de mover todos los métodos y estar seguro de que todo funciona correctamente.

Parece que vamos por el buen camino, las preguntas son solo Strings, así que encajan mejor dentro de Display. AHora que estamos refactorizando, tambien es una buena oportunidad para refactorizar los métodos recién movidos. Por ejemplo, al declarar los arrays iniciales de las preguntas.

Teniendo nuestras pruebas en verde podemos manejar la refactorización muy bien, por ejemplo creando funciones para terminar de desacoplar echoln de esta manera:

Crear una nueva función en la clase display llamada correctAnswer:

 public function correctAnswer()
    {
        $this->echoln("Answer was correct!!!!");
    }

Buscar en la clase Game Answer was correct!!!!y cambiar el echoln por nuestra nueva función. Esto podemos hacerlo con unos cuantos trozos de código más. En mi cuenta de github tienes el resto del código con los cambios. Tened en cuenta que estamos refactorizando así que aunque halla errores tipográficos como Answer was corrent!!!! debemos dejarlos y NO corregirlos.

Revisando y reduciendo las interfaces

Ahora toda la funcionalidad de presentación está expuesta con métodos, así que debemos revisar los métodos y mantener públicos solo los métodos que se utilizan en Game.

En nuestro caso, la manera más sencilla de ver si cambiamos la visibilidad a los métodos es corriendo los tests. Para ello modificamos la visibilidad de un método y ejecutamos los tests, si los tests fallan debemos dejarlo público. Si el test pasa cogemos otro método.

Conclusiones

Hemos aprendido a separar responsabilidades, empezamos a tener una arquitectura propiamente dicha. De la misma forma hemos modificado mucho nuestro código apoyándonos en los tests, haciendo que nuestros pasos sean firmes y seguros.

Este post como el resto de la serie están basados en http://code.tutsplus.com/tutorials/refactoring-legacy-code-part-7-identifying-the-presentation-layer–cms-21593

Todo el código está en mi cuenta de github además lo he etiquetado para ir viendo la evolución de la aplicación.

Anuncios

Comenta la entrada

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s