3 Técnicas de refactorización de código que siempre puedes usar

Spread the love

El otro día estaba viendo un vídeo de Laravel Bussines que trataba sobre refactorización de métodos y me sorprendió ver que en lugar de hacer uso de una técnica para refactorizar se fue inmediatamente a resolver el problema creando métodos encadenados.

Así que decidí escribir este articulo para resolver el mismo problema, pero ahora usando tres métodos para refactorizar que ademas me llevaron a resolver el problema de otra forma, creo que estas técnicas de refactorización se pueden utilizar casi siempre para organizar y limpiar nuestro código y dejar algo mas legible y fácil de usar.

El vídeo

Si no quieres chutarte todo el rollo que ya he escrito puedes ver la vesión en vídeo. Uso el mismo problema y ademas puedes ir viendo la toma de decisiones y como se fue trabajando el código.

El problema

El ejemplo es muy sencillo, es una clase que se encarga de calcular el precio total de una orden y recibe 4 parametros, el detalle con esto es que la clase se hace dificil de leer a simple vista.

La definición de la clase es la siguiente:

namespace App\Services;


class PricingService
{

    public function calculateTotalPrice($price, $discount, $tax, $shippingFeed)
    {
        $orderPrice = $price - $discount + $shippingFeed;
        $totalPrice = $orderPrice * (1 + $tax / 100);
        return $totalPrice;
    }
}
$service = new PricingService();
$totalPrice = $service->calculateTotalPrice($order->price, $order->discount, 15, 50);

Es posible reducir el numero de parámetros de forma muy sencilla aplicando dos principio que no nos toma mucho tiempo llevar a cabo.

Y como siempre antes de comenzar no te concentres en el código, si no en la idea y de esa forma se te va a quedar lo que vamos a hacer y podrás aplicar estas técnicas en cualquier clase de tu proyecto.

El primer método que vamos a ver se llama Conservar todo el objeto.

Conservar todo el objeto (Preserve Whole Object)

Lo que nos va a solucionar esta técnica es evitar que hagamos llamadas a un objeto para obtener valores y luego pasarlo como parámetros.

Así que la solución es simplemente pasar el objeto entero como parámetro y hacer las llamadas necesarias dentro del método.

En nuestro ejemplo eso se puede apreciar en las llamadas a order:

$service = new PricingService();
$totalPrice = $service->calculateTotalPrice($order->price, $order->discount, 15, 50);

Para solucionar solamente quitamos las llamadas a $order->price y $order->discount y modificamos el método de la clase PrincingService para que solo reciba como parámetro el objeto order.

$service = new PricingService();
$totalPrice = $service->calculateTotalPrice($order, 15, 50);

Dentro del método ajustamos las llamadas.

class PricingService
{

    public function calculateTotalPrice(Order $order, $tax, $shippingFeed)
    {
        $orderPrice = $order->price - $order->discount + $shippingFeed;
        $totalPrice = $orderPrice * (1 + $tax / 100);
        return $totalPrice;
    }
}

Hasta aquí ya hemos reducido un poco el esfuerzo visual, pero nos quedan 2 valores primitivos que están relacionados con el impuesto y los gastos de envío.

Si bien se puede dejar así, siempre es una buena idea ver si estos valores no representan algo mas general (Otro objeto).

Introducir un objeto como parámetro (Introduce Parameter Object)

Este técnica nos ayuda a refactorizar parámetros que pueden estar repetidos o relacionados de alguna forma, cuando eso sucede puedes crear una nueva clase con ellos y pasar la clase resultante como un parámetro.

En nuestro servicio de ejemplo, el impuesto y el gasto de envío es una forma de cargos adicionales a la compra de la orden, así que yo podría decir que estos dos elementos representan “Cargos” y de esa forma puedo crear una clase Charge que se encargue de proporcionarme esos dos valores.

veamos como hacer esto

Creamos la clase Charge

Esta clase va a ser muy sencilla para este ejemplo:

class Charge
{
    private $tax;
    private $shippingFeed;

    public function __construct($tax, $shippingFeed)
    {
        $this->tax = $tax;
        $this->shippingFeed = $shippingFeed;
    }

    /**
     * @return mixed
     */
    public function getTax()
    {
        return (1 + $this->tax / 100);
    }

    /**
     * @return mixed
     */
    public function getShippingFeed()
    {
        return $this->shippingFeed;
    }
}

Agregar Charge como parámetro

Cambiamos la definición del método para que ahora solo acepte la clase Charge y ajustamos el código para que llame a Charge de forma correcta.

class PricingService
{

    public function calculateTotalPrice(Order $order, Charge $charge)
    {
        $orderPrice = $order->price - $order->discount + $charge->getShippingFeed();
        $totalPrice = $orderPrice * $charge->getTax();
        return $totalPrice;
    }
}

Finalmente vemos como va quedando el uso de nuestra clase.

$service = new PricingService();
$totalPrice = $service->calculateTotalPrice($order, $charge);

Mucho mejor no crees ?

Moviendo dependencias al constructor

Bien ya logramos reducir a un numero de parámetros razonables, pero si observas las dos clases que usamos en el método calculateTotalPrice, pueden ser pasados como dependencias en el constructor y de esta forma podemos liberar el método.

class PricingService
{
    /**
     * @var Order
     */
    private $order;
    /**
     * @var Charge
     */
    private $charge;

    public function __construct(Order $order, Charge $charge)
    {
        $this->order = $order;
        $this->charge = $charge;
    }

    public function calculateTotalPrice()
    {
        $orderPrice = $this->order->price - $this->order->discount + $this->charge->getShippingFeed();
        $totalPrice = $orderPrice * $this->charge->getTax();
        return $totalPrice;
    }
}

De esta forma hemos simplificado un poco mas nuestra clase visualmente:

$service = new PricingService($order, $charge);
$totalPrice = $service->calculateTotalPrice();

Pero como cuando abres un dispositivo y le ves todas las tripas; de esta misma forma nuestra clase a comenzado a tener problemas de código espagueti.

class PricingService
{
    //...

    public function calculateTotalPrice()
    {
        $orderPrice = $this->order->price - $this->order->discount + $this->charge->getShippingFeed();
        $totalPrice = $orderPrice * $this->charge->getTax();
        return $totalPrice;
    }
}

Si observas el méotodo claculateTotalPrice a dejado de ser intuitivo y una opción seria agregar unos comentarios para aclara un poco la idea del método.

Pero, existe otra opción que nos evita esa molestia.

Método de extracción (Extract Method)

Este técnica busca que agrupemos fragmentos de código que se puedan agrupar de una forma lógica para el problema.

Por ejemplo a la diferencia del precio de la orden y el descuento.

$this->order->price - $this->order->discount

Se puede extraer hacia un nuevo método que se llame aplicar descuento.

$this->applyDiscount()

Si intentamos hacer esto para todo el código del método calculateTotalPrice, podemos simplificarlo y dejar algo mas legible.

 class PricingService
{
    //...

    public function calculateTotalPrice()
    {
        return $this->applyDiscount()
                    ->applyShippingFeed()
                    ->applyTaxes();
    }

    public function applyDiscount()
    {
        $this->totalPrice -= $this->order->discount;
        
        return $this;
    }

    public function applyShippingFeed()
    {
        $this->totalPrice += $this->charge->getShippingFeed();
        
        return $this;
    }

    public function applyTaxes()
    {
        return $this->totalPrice * $this->charge->getTax();
    }
}

Conclusión

Hemos visto como podemos reorganizar el código de cualquier clase, con pocos principios de refactorización y obtener muy buenos resultados.

Solo quiero aclarar que no debes de abusar de la técnica de pasar un objeto como parametro, es decir no crees clases que solo obtengan los valores, para justificar su uso.

Siempre ve si esta clase que estas descubriendo puede tener alguna lógica adicional, por ejemplo en nuestro ejemplo pasamos a Charge el calculo del impuesto como porcentaje, pero podemos haber ido un poco mas y que esa clase recibiera ademas una clase Factory que permitiera decidir que tipo de envió se puede usar y calcular el costo de envío con base a eso.

Para el método de extracción existe una variante que se llama Replace Temp with Query, que igual te puede interesar ver.

Mover las dependencias al constructor es una buena practica que solamente no recomiendo en los casos en los que tengo que hacer inyección de dependencias y las dependencias no están disponibles para construir el objeto.

Por otro lado tambien aplicamos el principio tell dont ask, si pruebas el ejemplo te vas a dar cuenta que puedes prescindir del cargo por envío y descuento y la clase claculara sin que tengas que hacer nada mas.

De echo es posible que puedas prescindir de cualquiera de los valores y obtener le resultado correcto.

Finalmente si quieres aprender mas sobre el tema, te recomiendo el libro de Martin Fowler, no es un libro económico pero en versión kindle esta accesible, ademas creo que vale la pena que inviertas en tu desarrollo profesional como programador, así que un libro como este, no esta de mas en tu colección.