Cómo Aplicar el Patrón de Diseño Adapter en LARAVEL

Spread the love

El patrón de diseño adaptador (Adapter) es uno de los patrones de diseño más sencillos. y es muy probable que lo hayas usando sin darte cuenta, cuando implementas una interfaz.

Los adaptadores son un enlace que permite comunicar a dos clases que tienen interfaces incompatibles, de esta forma las dos clases pueden trabajar entre ellas.

Ejemplos de adaptadores en el mundo real son muy diversos, por ejemplo las tomas de corriente no son iguales y requieren que tengas un adaptador para conectar aparatos eléctricos a la toma de corriente,

Conectar dispositivos a tu computadora por lo general requiere de un adaptador para aprovechar por la conexión usb de tu equipo.

Estos son algunos ejemplo de adaptadores, pero no estas aquí para leer sobre algo que ya conoces, así que veamos como puedes usar esta idea en tu código.

Problema

Vamos a suponer que necesitas agregar un servicio extra para hacer verificaciones adicionales a los correo de los usuario que se registran en tu aplicación.

La razón para esto es que necesitas verificar que el usuario de correo tiene un buzón real y esto no lo proporciona la validación actual en Laravel.

Dentro de los requisitos se solicita el uso de dos servicios específicos: Maillbox Layer y Abstract.

Pero encuentras un problema a la hora de querer implementar el requerimiento y es que aparte de que las APIs no cuenta con clientes propios, las llamadas y las respuestas a cada Api son diferentes 😒.

//Mailbox request
https://apilayer.net/api/check
    ? access_key = YOUR_ACCESS_KEY
    & email = support@apilayer.com

//Mailbox Response
{
  "email": "support@apilayer.com",
  "did_you_mean": "",
  "user": "support",
  "domain": "apilayer.net",
  "format_valid": true,
  "mx_found": true,
  "smtp_check": true,
  "catch_all": false,
  "role": true,
  "disposable": false,
  "free": false,
  "score": 0.8
}   
//Abstract Resquest
https://emailvalidation.abstractapi.com/v1/
    ? api_key = YOUR_UNIQUE_API_KEY
    & email = johnsmith@gmail.com

//Abstract Response
{
    "email": "johnsmith@gmail.com",
    "autocorrect": "",
    "is_valid_format": true,
    "is_free_email": true,
    "is_disposable_email": false,
    "is_role_email": false,
    "is_catchall_email": false,
    "is_mx_found": true,
    "is_smtp_valid": true,
    "quality_score": 0.99,
}

Para resolver el problema necesitas buscar una forma de forzar a que las dos apis funcionen de forma similar aunque tengas diferencias entre ellas.

Diagrama conceptual del Adaptador

pero..

¿Cómo podemos hacer esto?

Por supuesto, con el patrón adaptador que vamos a ver en un momento más.

Repositorio

Si quieres ver todos los detalles de la implementación Seba Carrasco Poblete a realizado un gran trabajo creando un repositorio que recrea este ejemplo basándose en esta explicación 👍🏼

¿Qué es el patrón Adaptador o Adapter?

Convierte la interfaz de una clase en otra interfaz que los clientes esperan.

Gof book


El patrón Adaptador permite que las clases con interfaces incompatibles trabajen juntas ya que de otra forma no podrían hacerlo.

💡 ¿Cuándo debe de aplicarse?

Puedes utilizar el patrón adaptador cuado:

  • Quieras usar una clase existente, pero cuya interfaz no es compatible con el resto de tu código.
  • Quieres crear una clase reutilizable que coopere con clases no relacionadas o no previstas, es decir, con clases que no tienen necesariamente interfaces compatibles o que pueden cambiar.

Estructura del Patrón Apatador (Adapter)

Participantes en el Modelo

  • Target define la interfaz específica del dominio que utiliza el Cliente.
  • Client colabora con los objetos que se ajustan a la interfaz Target.
  • Adaptee define una interfaz existente que necesita ser adaptada.
  • Adapter adapta la interfaz de Adaptee a la interfaz Target.

Cómo funciona

La forma de trabajo de este patrón es muy sencillo, cada llamada al adaptador pasa la solicitud al segundo objeto (Adaptee), pero en un formato y orden que ese segundo objeto espera.

En términos de código basado en el diagrama de clases que acabamos de ver, el Adaptador se usaría como sigue:

$adaptee = new Adaptee;
$adapter = new Adapter($adaptee);
$adapter->request()

Implementar la solución

Ya que analizamos como funciona el Patrón Adapter vamos a implementar una solución para nuestro problema.

Identificar a los actores

Aquí tenemos que identificar quien es el cliente y que es lo que adaptaremos, para nuestro ejemplo en este momento el cliente será un controlador y lo que adaptaremos serán los servicios de Mailbox Layer y Abstract.

Diseñar una interfaz común

Para nuestro ejemplo vamos a obligar a trabajar de la misma a las dos apis mediante un interfaz común que llamaremos VerifiableAapter

namespace App\Contracts;


interface VerifiableAdapter
{
    public function verify(string $email) : bool;
}

Crear un Adaptador (Adapter) que Implemente la Interfaz

Para nuestro ejemplo crearemos dos adaptadores; MailboxAdapter y AbstractAdapter.

MailboxAdapter:

namespace App\Services\Adapters;


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

class MailboxAdapter implements VerifiableAdapter
{
    private $client;

    public function __construct()
    {
        $this->client = Http::baseUrl('http://apilayer.net');
    }

    /**
     * @param string $email
     * @return bool
     */
    public function verify(string $email): bool
    {
        return $this->client->post('/api/check', [
            'access_key' => 'bca1f217938af282c13e67799196a',
            'email' => $email,
        ])['smtp_check'];
    }
}

AbstractAdapter

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;

    public function __construct()
    {
        $this->client = Http::baseUrl('https://emailvalidation.abstractapi.com/');
    }

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

Desacoplamos el Cliente de los Adaptadores Concretos

En este punto tenemos que reducir las llamada a las clases concretas asi que para eso vamos a instruir a nuestro Service Provider para que pueda proporcionar una implementación de la interfaz VerifiableAdapter

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

Para finalizar tenemos que cambiar el código que teníamos antes para el registro de usuarios.

 /**
     * Handle an incoming registration request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function store(Request $request, VerifiableAdapter $emailVerifier)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ]);

        if (!$emailVerifier->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);
    }

Con esto resolvemos el problema y dejamos la puerta abierta para integrar el api de otros servicios si se rquiere.

Es posible dar un paso mas y hacer que los adaptadores trabajen con el patrón Strategy y de esa forma e puede hacer intercambiable en tiempo de ejecución las llamadas a las apis.

Además si se requiere se puede mover la lógica a una validación personalizada sin mucho problema

Conclusión

El patrón Adapter también se conoce como patrón wrapper porque envuelve una interfaz existente dentro de la interfaz que el cliente esper

Este patrón está muy presente en nuestro día a día y además es una herramienta muy potente que nos va a permitir:

  • Desacoplar nuestro código de las implementaciones de los servicios de terceros, forzando su adaptación a nuestro dominio.
  • Potenciar la cambiabilidad (podemos sustituir el servicio cambiando solo la inyección).
  • Simplificar nuestros tests (no necesitamos más que mockear la interfaz, sin necesidad de conocer detalles de implementación del código de terceros)

Una de las desventajas de este patrón es que, si tienes dos clases que implementan muchos métodos, será muy difícil de adaptar, lo que puede dejar tu patrón de adaptación roto. El patrón adaptador debe ser utilizado siempre que el adaptador y el cliente tengan un objetivo común.

Si tienes alguna pregunta o sugerencia para mejorar este post, por favor, deja tus comentarios.