Mejora tu código usando tell don’t ask en Laravel

Spread the love

Tell don’t ask es un estilo de programación en el cual los objetos solo toman decisiones sobre sus datos internos o sobre los que reciben como parámetros y no deben de hacerlo sobre los datos internos de otros objetos.

Es decir que en lugar de pedirle a los objetos datos, debemos decirles que hacer y esperar el resultado de esa acción, con esto evitamos usar la información interna de los objetos para tomar decisiones fuera de ellos y luego pedirles que hacer o afectar su comportamiento interno.

Por lo cual en lugar de tener algo así.

$age = $sebastian->getAge();

if ($age >= 18) {
    $store->sellsBeerTo($sebastian)
}

nosotros podemos escribir algo como esto.

if ($sebastian->isAdult()) {
    $store->sellsBeerTo($sebastian)
}

¿Sí notas la diferencia?, seguro estarás de acuerdo que se entiende mas el segundo ejemplo ya que es sencillo de leer.

Podemos dar un paso mas y dejar que la tienda decida si puede venderle una cerveza a Sebastian moviendo la condición dentro del método sellsBeerTo.

$store->sellsBeerTo($sebastian);

Ahora tenemos una tienda (Objeto) mas autónoma que puede decidir por si misma, solo tienes que proporcionarle un cliente (Sebastian) y la tienda hará el resto.

Es muy posible que estés pensando, sí muy bien pero ese es un ejemplo muy sencillo, ¿cómo lo voy a emplear en un controlador lleno de condiciones, ciclos y consultas?

Afortunadamente uno de los síntomas que debes de observar es cuando comienzas a tener código espagueti orientado a objetos, es ese tipo de código que puede estar en un controlador y tiene muchas condiciones y ciclos operando los resultados de un objeto sin mencionar que el código se vuelve poco legible a simple vista. Así que, es muy probable que una buena parte de ese código realmente deba de estar en otro lugar, como por ejemplo dentro de un Modelo.

¿Cómo puedo evitar violar tell don’t ask?

Código estructurado

Para evitar caer en la trampa del código espagueti orientado a objetos no debes de perder de vista que tus clases deben de tener responsabilidades claras, es decir que cualquier lógica que afecte el estado de un objeto debe de estar dentro de él.

Qué te parece si dejamos la teoría y vemos un ejemplo de la vida real, de esos que quieres ver y que se acerca más a lo que necesitas, posiblemente en algún oscuro rincón de alguno de tus proyectos y que no quieres mencionar que existe.

No hagas lo que puede hacer el objeto por si mismo

Vamos a suponer que tenemos un sistema para inscripción a talleres que se realiza mediante un código que se te proporciona cuando te inscribes, para darte de alta en un taller requieres tener un código válido y además no haberte inscrito en otro taller previamente. El código relacionado con esta parte tiene el siguiente aspecto.

public function postWorkshop(Request $request)
    {
        /** @var Participant $participant **/
        $participant = Participant::whereCode($request->code)->first();

        if (!$participant) {
            \Alert::message('El código no existe o su código no a sido autorizado', 'warning');
            return redirect()->route('workshop.search');
        }

        if (!$participant->isRegisteredAtWorkshop()) {
            \Alert::message('Ya esta inscrito a un Taller', 'warning');
            return redirect()->route('workshop.search');
        }

        $workshops = Workshop::all();

        return view('subscribe.workshop-form', compact('participant', 'workshops'));
    }

¿Puedes ver donde aplicar tell don’t ask? Si no es así no te preocupes, que te parece si organizamos un poco y quitamos el código repetido que se encuentra en las condiciones.

Así, podemos crear un método donde moveremos ese código duplicado como te muestro.

public function postWorkshop(Request $request)
    {
        /** @var Participant $participant **/
        $participant = Participant::whereCode($request->code)->first();

        if (!$participant) {
            return $this->notifyException('El código no existe o no a sido autorizado');
        }

        if (!$participant->isRegisteredAtWorkshop()) {
            return $this->notifyException('Ya esta inscrito a un Taller');
        }

        $workshops = Workshop::all();

        return view('subscribe.workshop-form', compact('participant', 'workshops'));
    }

public function notifyException($message)
    {
        \Alert::message($message, 'warning');

        return redirect()->route('workshop.search');
    }

¿Qué te parece?, creo que ahora se ve un poco mejor y resalta el problema, ¿no lo crees?.

Si observas las dos condiciones vas a notar que tienen algo en común y es que ambas siguen el patrón de; si pasa esto, realiza esto otro y si te das cuenta esas acciones se hacen sobre los datos que regresa el modelo Participant y no se tú, pero esto tiene pinta de que el controlador está tomando decisiones que pueden estar dentro del modelo Participant.

Que te parece si te muestro como podemos hacer eso.

class Participant extends Model
{

    public function isRegisteredAtWorkshop()
    {
        if ($this->workshops->isEmpty()) {
            return false;
        }

        return true;
    }

    public static function checkCode($code)
    {
        try {
            $participant = self::whereCode($code)->firstOrFail();
        } catch (ModelNotFoundException $e) {
            throw new InvalidCodeException('El código no existe o no esta autorizado');
        }

        if (!$participant->isRegisteredAtWorkshop()) {
            throw new RegisteredAtWorkshopException('Ya esta inscrito a un Taller');
        }

        return $participant;
    }

}

Con esto, ahora solo vamos a obtener un participante si el código es válido y no está inscrito en ningún otro taller, para todo lo demás el modelo Participante lanzará excepciones que el controlador puede manejar fácilmente sin tener que hacerle preguntas al modelo para saber qué hacer.

Quitando las condiciones del controlador y reemplazando por try/catch; nuestro código finalmente debe de quedar como te muestro a continuación.

public function postWorkshop(Request $request)
    {
        try {
            /** @var Participant $participant * */
            $participant = Participant::checkCode($request->code);

            $workshops = Workshop::all();
        
        } catch (InvalidCodeExceptionn $e) {
            return $this->notifyException($e->getMessage());
        } catch (RegisteredAtWorkshopException $e) {
            return $this->notifyException($e->getMessage());
        }

        return view('subscribe.workshop-form', compact('participant', 'workshops'));
    }

Adiós condiciones!, Ahora el código es más claro y a simple vista podemos darnos una idea de lo que se pretende en este controlador, además ya no es necesario especificar los mensajes de error ya que los trasladamos a las excepciones y con eso, podemos aprovecharlas para mandar el mensaje correspondiente a nuestra vista si algo sucede.

Bonus

Como paso adicional, puedes mover las excepciones al Exception Handler y despejar tu controlador.

  //Controller
public function postWorkshop(Request $request)
{
        /** @var Participant $participant * */
        $participant = Participant::checkCode($request->code);

        $workshops = Workshop::all();

        return view('subscribe.workshop-form', compact('participant', 'workshops'));
}

//Exception Handler
public function report(Exception $exception)
{
    if ($exception instanceof InvalidCodeExceptionn) {
        //
    }

    if ($exception instanceof RegisteredAtWorkshopException) {
        //
    }

    return parent::report($exception);
}

¿Qué sigue?

Practicar y experimentar en tu código, comienza mirado tus controladores y decide que partes de código corresponden a tus modelos o  a otras clases que estas usando. No va ser algo que vas a hacer en un día, te va tomar un tiempo aprender a aplicar el principio, pero si lo haces un paso a la vez muy pronto lo dominaras.

Si aún tienes dudas de como distinguir en qué momento aplicar tell don’t ask solo recuerda lo siguiente.

  • No hagas lo que el objeto puede hacer por sí mismo.
  • No le pidas datos al objeto para tomar decisiones por él.

Si te ha servido este articulo por favor compartelo en tus redes sociales.

No olvides poner en práctica lo que acabas de leer y déjame saber como te va aplicando el principio en los comentarios.

Finalmente recuerda hacer un cambio a la vez.