Aplicando el Patron Observador en nuestra Tienda Online

Comments

En este articulo, aplicaremos el uso del patron observador en nuestro ejemplo real "Tienda Online".

Vista general

Ya disponemos de una tienda basica. Los diferentes productos se visualizan correctamente. Ahora es momento de darle un poco de interactividad al sitio.

En este ejemplo básico pero funcional, haremos uso del patron observador para desarrollar la función checkout(), es decir, la funcionalidad que se encargará de finalizar la compra del cliente.

Una vez que el cliente haya añadido sus productos al carrito de compra, cuando haga click en Finalizar compra, o Checkout, nosotros queremos que el sistema haga varias cosas, tales como enviar un email al cliente, enviar un email al administrador y disminuir el stock de cada producto.

Creando los observadores

Nuestros observadores, serán claramente como hemos mencionado en el apartado anterior, tres objetos.

EmailedCustomer se encargará de enviar un email al cliente.

EmailedAdministrator se encargará de enviar un email al administrador de la tienda.

DecreaseProduct se encargará de reducir el stock de los productos que el cliente ha comprado.

Quien disparará los eventos, en este ejemplo será el carrito de compras, entonces debemos crear una clase ShoppingCart.

Observación

De aquí en adelante utilizaremos una base de datos más robusta que simplemente archivos json. Vamos a trabajar con MySQL e introduciremos Eloquent como ORM para abstraernos del motor de base de datos. Aún así seguiremos usando repositorios.

Observación

En este artículo, asumimos que usted ya conoce como trabajar con MySql en Laravel, sin embargo puede consultar la documentación oficial de MySQL y Laravel para mayor información. Por otra parte, pronto actualizaremos el Curso de Laravel.

Creando las migraciones

Es hora de migrar nuestros datos que habiamos hecho en json, a MySql. Para ello, vamos a crear las migraciones para la tabla productos y para la tabla categorias. La migracion de la tabla users en Laravel ya se encuentra implementada. En la terminal de Windows o en la consola de Linux, dentro de la carpeta del proyecto, escribimos php artisan make:migration create_products_table y php artisan make:migration create_categories_table. Laravel nos crea las migraciones en la carpeta database/migrations.

Aqui veremos el codigo de las dos migraciones que acabamos de crear.

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->bigInteger('category_id');
        $table->double('price');
        $table->string('image');
        $table->json('other_details');
        $table->integer('stock');
        $table->timestamps();
    });
}
public function up()
{
    Schema::create('categories', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->timestamps();
    });
}

Vemos que hemos especificado una columna other_details en la tabla products de tipo json, ahi es donde debemos insertar los datos correspondientes del apartado details y specifications de las entidades que creamos en el articulo anterior.

Modificando las vistas

En nuestras vistas index.blade.php y product.blade.php vamos a extraer todo el header de la pagina, ya que ambos son identicos y lo moveremos a otro archivo. Yo lo llame header.blade.php y lo coloque en una carpeta a la que le llame partials. Ahi es donde vamos a ubicar todos los archivos parciales del sitio a medida que lo requiramos.

En el archivo que acabamos de crear, header.blade.php nos encontramos con una seccion llamada -- Cart --, extraemos el codigo y lo movemos a otro archivo, por ejemplo, cart.blade.php dentro de partials.

Vamos a cambiar el link titulado como Checkout por un boton. La vista nos deberia quedar de este modo.

<!-- Cart -->
<div class="dropdown">
    <a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="true">
        <i class="fa fa-shopping-cart"></i>
        <span>Your Cart</span>
        <div class="qty">3</div>
    </a>
    <div class="cart-dropdown">
        <div class="cart-list">
            <div class="product-widget">
                <div class="product-img">
                    <img src="./img/product01.png" alt="">
                </div>
                <div class="product-body">
                    <h3 class="product-name"><a href="#">product name goes here</a></h3>
                    <h4 class="product-price"><span class="qty">1x</span>$980.00</h4>
                </div>
                <button class="delete"><i class="fa fa-close"></i></button>
            </div>

            <div class="product-widget">
                <div class="product-img">
                    <img src="./img/product02.png" alt="">
                </div>
                <div class="product-body">
                    <h3 class="product-name"><a href="#">product name goes here</a></h3>
                    <h4 class="product-price"><span class="qty">3x</span>$980.00</h4>
                </div>
                <button class="delete"><i class="fa fa-close"></i></button>
            </div>
        </div>
        <div class="cart-summary">
            <small>3 Item(s) selected</small>
            <h5>SUBTOTAL: $2940.00</h5>
        </div>

            <form action="{{route('checkout')}}" method="post">
                @csrf
            <div class="cart-btns">
                <a href="#">View Cart</a>
                <button type="submit">Checkout <i class="fa fa-arrow-circle-right"></i></button>
            </div>
            </form>

    </div>
</div>
<!-- /Cart -->

Rutas y controladores...

Definamos la ruta checkout en el archivo web.php dentro de routes.

...

Route::post('/checkout', '[email protected]')->name('checkout');

...

Nuestra objectivo es, cuando el cliente haga click en el boton checkout o finalizar compra del carrito, se ejecute las acciones que vamos a implementar en el metodo checkout del controlador CartController.

Vamos a crear dicho controlador con el comando, php artisan make:controller CartController

De momento, vamos a añadir los productos manualmente directamente en CartController, mas adelante actualizaremos el sitio para que, el carrito de compras se actualize automaticamente cada vez que el cliente agregue un producto al mismo.

En CartController, implementemos el metodo checkout.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Repositories\ShoppingCart;
use App\Http\Repositories\ArticleRepository;

class CartController extends Controller
{
    public function checkout(ArticleRepository $articles)
    {
        $mycart = new ShoppingCart();

        $prod1 = $articles->get(1);
        $mycart->add($prod1->id, 5);

        $prod2 = $articles->get(2);
        $mycart->add($prod2->id, 3);

        $mycart->checkout();
    }
}

¿Se acuerdan de nuestro repositorio? Bien, aqui lo utilizamos nuevamente para que vayamos obteniendo los productos que queremos agregar, en este ejemplo, solo agregamos el producto con id 1 y el producto con id 2. Ademas creamos un repositorio llamado ShoppingCart que es finalmente quien se encargara de añadir los productos al carrito y procesar el envio. Para ello, el metodo add recibira dos parametros, uno con el id del producto y el otro con la cantidad que el cliente haya decidido comprar. Una vez agregado los diferentes productos, la clase ShoppingCart invocara al metodo checkout.

Actualizando el repositorio

Como hemos migrado la base de datos que teniamos en json a mySql, debemos actualizar ArticleRepository, nuestro consutructor ha quedado de esta manera

public function __construct()
{
    /*
    Solo reemplazamos las lineas

        $json = file_get_contents('articles.json');
        $articles = json_decode($json);
    */
    $articles = \App\Product::all();       

    foreach($articles as $item){

        if($item->category->name == "electronica")
            $this->articles[$item->id] = new ElectronicArticle($item);
        elseif($item->category->name == "indumentaria")
            $this->articles[$item->id] = new IndumentaryArticle($item);
        elseif($item->category->name == "alimentos")
            $this->articles[$item->id] = new FoodArticle($item);
    }

}

Definiendo nuestro subscriptor

Hechemos un vistazo a la clase ShoppingCart

<?php
namespace App\Http\Repositories;

use App\Subjects\Subject;
use App\Interfaces\Observer;
use App\Observers\DecreaseStock;
use App\Observers\EmailedCustomer;
use App\Observers\EmailedAdministrator;
use Ramsey\Uuid\Exception\UnsupportedOperationException;

class ShoppingCart implements Subject
{
    public $cart = array();

    public $observers = array();

    public function add($id, $quantity)
    {
        $this->cart[] = [ 'id' => $id, 'quantity' => $quantity];
    }

    public function checkout()
    {
        $this->registerObserver(new EmailedCustomer($this->cart));
        $this->registerObserver(new EmailedAdministrator($this->cart));
        $this->registerObserver(new DecreaseStock($this->cart));

        $this->notifyObserver();
    }

    //Metodo implementado por Subject
    public function registerObserver(Observer $o)
    {
        $this->observers[] = $o;
    }

    //Metodo implementado por Subject
    public function unregisterObserver(Observer $o)
    {
        throw new UnsupportedOperationException("Operation not implemented");
    }

    //Metodo implementado por Subject
    public function notifyObserver()
    {
        foreach ($this->observers as $o) {
            $o->update();
        }
    }
}

Excelente, la clase ShoppingCart implementa la interfaz Subject, por lo que nos obliga a implementar los metodos registerObserverunregisterObserver y notifyObserver. Como observamos, el metodo add($id, $quantity) añade un producto con la cantidad deseada a la propiedad $cart. No requiere mayor explicacion.

Que pasa con el metodo checkout? Aqui esta la magia, en realidad, aqui estamos haciendo uso del patron observador. Estamos registrando los diferentes observadores que deseamos cuando el cliente finalize su compra. En este ejemplo, solo hay tres observadores subscriptos a nuestro carrito. El primero se encargara de enviar un mail al cliente, informandole los detalles de su compra, el segundo se encargara de enviarle un mail al administrador de la tienda y por ultimo, el tercer observador es el encargado de disminuir el stock de los productos que el usuario compro.

En este ejemplo, no vamos a entrar en mayores detalle, y asumimos que siempre hay stock disponible y el cliente siempre es capaz de adquirir dichos productos, pero en la vida real esto no siempre es asi, por lo que debemos realizar diferentes validaciones, que lo trabajaremos en futuros articulos. Solamente nos ocupamos de aplicar los patrones en diferentes escenarios, sin centrarnos demasiado en la logica de negocio.

Observando a los observadores

Este es el codigo de los observadores

namespace App\Observers;

use App\Interfaces\Observer;

class EmailedCustomer implements Observer
{
    public function update()
    {
        echo "<p>Estimado cliente: En instantes se le enviara un mail a su casilla  de correo con el contenido de su carrito</p>";
    }
}
namespace App\Observers;

use App\Interfaces\Observer;

class EmailedAdministrator implements Observer
{
    public function update()
    {
        echo "<p>Se le ha enviado un mail al administrador de la tienda.</p>";
    }
}
namespace App\Observers;

use App\Interfaces\Observer;
use App\Product;

class DecreaseStock implements Observer
{
    public $cart;

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

    public function update()
    {
        echo "<p>A continuacion, se producira la disminucion de su stock</p>";

        $this->decrease();
    }

    public function decrease()
    {
        foreach ($this->cart as $item) {
            $product = Product::find($item['id']);
            $product->stock = $product->stock - $item['quantity'];
            $product->save();
        }
    }
}

  En este tutorial, no hemos desarrollado la logica para enviar mails pero usted deberia ser capaz de escribir el codigo necesario para tal fin. En el observador DecreaseStock, lo que hace simplemente es disminuir el stock de cada uno de los productos que se encuentran en el carrito actual.

Cabe destacar que cada observador implementa la interfaz Observer, y esto nos obliga a implementar el metodo update

Si visitamos nuestro sitio, cuando hacemos click en el boton checkout del carrito, nos deberia llevar a una pagina mostrandonos los mensajes

Estimado cliente: En instantes se le enviara un mail a su casilla  de correo con el contenido de su carrito

Se le ha enviado un mail al administrador de la tienda.

A continuacion, se producira la disminucion de su stock

El código final está diponible en nuestro GitHub