El Patrón de Diseño Strategy en LARAVEL

Spread the love

Existen situaciones donde queremos encapsular un algoritmo, para poder usarlo de tal forma que podamos resolver el mismo problema en diferentes situaciones según se requiera.

Y este es precisamente el caso cuando usamos adaptadores. Es muy común que terminemos con una serie de clases que hacen lo mismo pero varia su implementación.

Y el Patrón Strategy (Estrategia) nos sirve para tener diferentes algoritmos (adaptadores) que podemos usar de acuerdo a la situación.

Pero es mejor si vemos como podemos aplicar a los adaptadores este patrón

Problema

Cuando trate el tema de los adaptadores terminamos con una solución que nos permitía conectarnos a dos Apis de verificación de correos electrónicos.

Pero tiene el detalle que no se puede cambiar el Adaptador después de que se crea la instancia.

/**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            VerifiableAdapter::class,
            config('adapter.driver',MailboxAdapter::class) //👈🏼
        );
    }

Y desde el controlador tampoco puedo hacer ningún cambio.

    public function store(Request $request, EmailChecker $emailChecker)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ]);

        if (!$emailChecker->verify($request->email)) { //👈🏼
            abort(422, 'The email address is not valid.');
        }

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        event(new Registered($user));

        Auth::login($user);

        return redirect(RouteServiceProvider::HOME);
    }

La única opción que existe es cambiar el adaptador desde el confg cuando se registra en el Contenedor de dependencias de Laravel.

Pero…

¿Qué pasa si llego al limite de peticiones de un Api?

Tendría que interrumpir la petición con una excepción y no tendría forma de cambiar el adaptador.

Pero afortunadamente esto tiene solución gracias al patrón Strategy o Estrategia. Que me permite manejar los adaptadores de forma intercambiable, de esa forma puedo cambiar el adaptador desde el controlador en tiempo de ejecución.

¿Qué es el Patrón Strategy o Estrategia?

El Patrón Strategy (Estrategia) es un patrón de diseño de comportamiento que te permite definir una familia de algoritmos cómo clases separadas y hacer sus objetos intercambiables.

GoF

💡 ¿Cuando debe de aplicarse?

Utiliza el patrón Strategy cuando:

Muchas clases relacionadas difieren sólo en su comportamiento. Las estrategias proporcionan una manera de configurar una clase con uno de muchos comportamientos.

Necesita diferentes variantes de un algoritmo. El ejemplo más común es el de los algoritmos de ordenamiento.

Un algoritmo utiliza datos que los clientes no deberían conocer. Utiliza el patrón Strategy para evitar la exposición de estructuras de datos complejas y específicas del algoritmo. Esto permite que el cliente trabaje con clases que presentan una interfaz menos compleja.

Una clase define muchos comportamientos y estos aparecen como condiciones dentro de la clase. En lugar de usar muchos condicionales, mueve las ramas condicionales relacionadas en su propia clase Strategy.

📕 Estructura del Patrón Strategy

Patrón Strategy

Participantes en el modelo

  • Strategy: declara una interfaz común a todos los algoritmos soportados. El contexto utiliza esta interfaz para llamar al algoritmo definido por cada ConcreteStrategy.
  • ConcreteStrategy: implementa el algoritmo utilizando la interfaz Strategy.
  • Context: se configura con un objeto ConcreteStrategy, mantiene una referencia a un objeto Strategy y puede definir una interfaz que permita a Strategy acceder a sus datos.

🎯Cómo funciona

Un contexto reenvía las peticiones de sus clientes (el que usa al contexto) a su estrategia y los clientes suelen crear y pasar un objeto ConcreteStrategy al contexto; después los clientes interactúan exclusivamente con el contexto. Suele haber una familia de clases ConcreteStrategy para que el cliente pueda elegir.

La estrategia y el contexto interactúan para aplicar el algoritmo elegido. Un contexto
puede pasar todos los datos requeridos por el algoritmo a la estrategia (mediante sus métodos) cuando se llama al algoritmo.

Alternativamente, el contexto puede pasarse a sí mismo como argumento a las operaciones de la estrategia. Esto permite a la estrategia llamar al contexto según que sea necesario.

En términos de código el cliente por lo general tiene interacción con el contexto y la estrategia. Pero el cliente ejecuta un algoritmo a través el contexto es decir que separamos la implementación de su uso y del cliente al tener como un intermediario al contexto.

$context = new Context(new ConcreteAlgorithm1());
$context->doAlgorithm();

$context->setAlgorithm(new ConcreteAlgorithmN());
$context->doAlgorithm();

Implementar la solución

Ya que sabes como funciona el patrón strategy es hora de analizar e implementar una solución usando este patrón.

Determina los colaboradores

Por lo general el contexto es una clase existente, pero en nuestro caso no existe un contexto así que tenemos que agregar uno, y además vamos a aprovechar los adaptadores para que sean parte de nuestra solución, Como los adaptadores estan separados en sus propias clases harán el papel de los “algoritmos” concretos que requiere nuestra estrategia.

Así que nuestra solución queda de la siguiente forma.

Solución empleando el patrón strategy
Solución usando adaptadores como estrategias

EmailChecker:

<?php


namespace App\Services;


use App\Contracts\VerifiableAdapter;

/**
 * Class EmailChecker
 * 
 * @package App\Services
 */
class EmailChecker
{
    private VerifiableAdapter $adapter;

    /**
     * EmailChecker constructor.
     * @param VerifiableAdapter $adapter
     */
    public function __construct(VerifiableAdapter $adapter)
    {
        $this->adapter = $adapter;
    }

    /**
     * @param VerifiableAdapter $adapter
     */
    public function setAdapter(VerifiableAdapter $adapter)
    {
        $this->adapter = $adapter;
    }

    /**
     * @param string|null $email
     * @return bool
     */
    public function verify(string $email = null): bool
    {
        return !empty($email) && $this->adapter->verify($email);
    }
}

Declarar la interfaz de la estrategia

Usaremos la misma interfaz VerifiableAdapter, así que no requerimos crear una nueva.

<?php


namespace App\Contracts;


interface VerifiableAdapter
{
    /**
     * @param string $email
     * @return bool
     */
    public function verify(string $email) : bool;
}

Crear la implementación de cada estrategia

Igual que antes ya tenemos dos implementaciones así que de momento no requerimos crear nuevas estrategias.

te dejo de ejemplo una implementación, si quieres mas detalles aquí puedes ver los dos adaptadores.

<?php


namespace App\Services\Adapters;


use App\Contracts\VerifiableAdapter;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

class AbstractAdapter implements VerifiableAdapter
{
    private PendingRequest $client;

    private string $baseUrl = 'https://emailvalidation.abstractapi.com/';

    private string $path = '/v1';

    public function __construct()
    {
        $this->client = Http::baseUrl($this->baseUrl);
    }

    public function verify($email): bool
    {
        return $this->client->get($this->path, [
            'api_key' => 'YOUR_ACCESS_KEY',
            'email' => $email,
        ])['is_smtp_valid'];
    }
}

Como usar la implementación

Muy bien ya solo nos queda ajustar el código del controlador y registrar nuestro contexto en el contenedor de Laravel.

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            VerifiableAdapter::class,
            config('adapter.driver',MailboxAdapter::class)
        );

        $this->app->bind(EmailChecker::class, function($app){
            return new EmailChecker($app->make(VerifiableAdapter::class));
        });
    }

Para finalizar ajustamos el controlador:

public function store(Request $request, EmailChecker $emailChecker)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ]);

        if (!$emailChecker->verify($request->email)) { //👈🏼 ahora usa la estrategia
            abort(422, 'The email address is not valid.');
        }

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        event(new Registered($user));

        Auth::login($user);

        return redirect(RouteServiceProvider::HOME);
    }

Con este cambio ahora es posible decidir que estrategia usar después de que se resuelve EmailChecker y además igual que antes se pueden agregar nuevas estrategias y en nuestro caso esto significa agregar nuevos adaptadores para otros servicios.

Conclusión

Este patrón por lo general se confunde con el patrón state, ya que a nivel de estructura de clases son muy parecidos pero cumple diferentes propósitos.

Algunas ventajas de usar el patrón Strategy:

  • Es fácil cambiar entre diferentes algoritmos (estrategias) en tiempo de ejecución porque estás usando polimorfismo en las interfaces.
  • Código limpio porque evitas el código infestado de condicionales (no complejo).
  • Código más limpio porque separas las responsabilidad en clases (una clase para cada estrategia).

Si tienes alguna duda o sugerencia no olvides dejar tu comentario.