Aplicando el Patron Estrategia en nuestra Tienda Online

Comments

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

Instalando Laravel

Antes de comenzar, debemos crear nuestro espacio de trabajo para desarrollar nuestro proyecto. Decidí utilizar Laravel como framework de base para implementar la tienda online, ya que como vamos a utilizar varios componentes de PHP, en lugar de escribir nuestro esquema desde cero, utilizamos los que ya están implementados en Laravel. Solamente aquí, vamos a dar prioridad en hacer uso de los patrones a medida que vayamos avanzando.

Si no sabes como instalar Laravel, en el articulo Curso de laravel: Introducción escribí una breve guia con todo lo necesario.

Vista general

Nuestro objetivo, es elaborar una tienda online aplicable a articulos de diversos rubros, ya sea articulos de indumentaria, vehiculos, alimentos, etc.

Por ejemplo, en un supermercado online, existen varias categorías, y cada categoría dispone artículos con diferentes detalles. Por ejemplo, la categoría Ropa, se visualizará de manera diferente a la categoría Perfumería o Electrodomésticos. Cada artículo tiene sus propias especificaciones, por lo tanto no podemos crear un template para todos los artículos de manera general, ya que éstos comparten unos pocos datos en común, como el nombre, el código del artículo, una descripción, el precio, pero difieren en otros campos.

A pesar de todo, todos los artículos deben tener un metodo display(), que se encargará de visualizar todo el detalle en la página.

Plantilla base

Vamos a utilizar esta plantilla a la hora que diseñar nuestra tineda. La misma está bajo licencia Creative Commons Attribution 3.0 License, la cual es libre de utilizar, aunque la unica limitación es que no nos permite eliminar los creditos de copyright del footer. Pero para nuestro ejemplo, nos sirve.

Adaptando la plantilla a Laravel

El proceso de adaptación de esta plantilla es bastante simple, solamente basta con copiar los assets a la carpeta public y las vistas html en el directorio resources/views cambiando la extensión .html por .blade.php ya estariamos en condiciones de utilizarlas para nuestros ejemplos.

Creando los articulos

En primer lugar, debemos crear los articulos de la tienda. Vamos a prescindir de una base de datos y en este primer ejemplo, confeccionaremos los productos en formato json.

Creamos un archivo llamado articles.json y lo guardamos en el directorio public de nuestro proyecto.

[
    {
        "id": 1,
        "name": "Computadora portatil",
        "category": "electronica",
        "price": 15000,
        "specifications": {
            "ram": "16 Gb",
            "hd": "1 Tb",
            "os": "Windows 10"
        }
    },
    {
        "id": 2,
        "name": "Zapatillas Nike",
        "category": "indumentaria",
        "price": 2500,
        "details": {
            "colour": "red",
            "talle": 42,
            "material": "cuero",
            "style": "running"
        }
    },
    {
        "id": 3,
        "name": "Mermelada de Frutilla",
        "category": "alimentos",
        "price": 112,
        "details": {
            "ingredients": [
                {"name": "frutilla"},
                {"name": "sugar"}
            ],
            "peso": 500
        }

    }
]

Definamos nuestra primer ruta para apuntar a la página principal. En el archivo web.php dentro de routes, eliminá la ruta que trae Laravel por defecto y escriba:

Route::get('/', '[email protected]')->name('home');

Creamos nuestro primer controlador, escribimos en la terminal, dentro del directorio de nuestro proyecto, php artisan make:controller PagesController

En nuestra vista index.blade.php, extraemos un producto de la sección New Products a un archivo llamado por ejemplo, itemHome.blade.php para trabajar de una manera más cómoda y en su lugar, escribimos lo siguiente.

@foreach ($articles as $article)
        @include('itemHome')
    @endforeach

En PagesController, creamos el método index y retornamos la vista index.

...
    public function index()
    {
        return view('index');
    }
...

Si visitamos nuestro sitio en el navegador, Laravel se quejará ya que no tenemos acceso a la variable $articles. Entonces, vamos a modificar nuestro codigo.

class PagesController extends Controller
{
    public $articles;

    public function __construct(ArticleRepository $articles)
    {
        $this->articles = $articles;
    }
    public function index()
    {
        $articles = $this->articles->all();
        return view('index', compact('articles'));
    }
}

Hacemos uso de los repositorios para extraer la responsabilidad del controlador al repositorio, que se encargará de traer todos los articulos mediante el método all(). Por lo que el controlador no tiene ni idea y tampoco deberia porque enterarse de que fuente de datos obtiene los articulos.

Vemos el codigo del repositorio.

namespace App\Http\Repositories;

use App\Http\Models\Article;

class ArticleRepository
{
    public $articles;

    public function __construct()
    {
        $json = file_get_contents('articles.json');
        $articles = json_decode($json);

        foreach($articles as $item){
            $this->articles[$item->id] = new Article($item);
        }

    }

    public function all()
    {
        return $this->articles;
    }
}

Leemos el contenido del archivo articles.json y luego guardamos cada articulo en un objeto de tipo Article.

namespace App\Http\Models;

class Article
{
    public $id;

    public $name;

    public $price;

    public $category;

    public $image;

    public $details;

    public function __construct($article)
    {
        $this->id = $article->id;
        $this->name = $article->name;
        $this->price = $article->price;
        $this->category = $article->category;
        $this->image = $article->image;

        if($this->category == "electronica")
            $this->details = $article->specifications;
        else
            $this->details = $article->details;
    }

    public function renderItemHome()
    {
        return view('dataItemHome', ["article" => $this]);
    }
}

Aqui tenemos un problema, debido a que los articulos tienen diferentes especificaciones, dependiendo de la categoria, tenemos que preguntar si el articulo corresponde a la categoria "Electronica", guardar el contenido del campo "specifications" en "details". Si existiera otra categoria con una especificación diferente, deberiamos preguntar lo mismo.

El metodo renderItemHome() obtiene el html de la vista y lo imprime en la página principal con los datos.

Actualizamos la vista itemHome.blade.php

<!-- product -->
<div class="product">

    {!!$article->renderItemHome()!!}

    <div class="add-to-cart">
        <button class="add-to-cart-btn"><i class="fa fa-shopping-cart"></i> add to
            cart</button>
    </div>
</div>
<!-- /product -->

Creamos una vista llamada dataItemHome.blade.php

<div class="product-img">
    <img src="{{$article->image}}" alt="">
</div>
<div class="product-body">
    <p class="product-category">{{$article->category}}</p>
    <h3 class="product-name">
        <a href="/product/{{$article->id}}">{{$article->name}}</a>
    </h3>
    <h4 class="product-price">{{$article->price}}</h4>
</div>

Ahora vamos a desarrollar la página de detalle de cada producto. Primero creamos una nueva ruta en el archivo routes/web.phpRoute::get('/product/{id}', '[email protected]')->name('product.show'); y actualizamos el controlador.

public function show($id)
{
    $article = $this->articles->get($id);
    return view('product', compact('article'));
}

Aqui tenemos que implementar el método get de nuestro repositorio. Vamos a ello.

...
public function get($id)
{
    return $this->articles[$id];
}
...

Bien, simplementemos, retornamos el articulo cuyo id lo obtenemos en el request de la ruta. Ahora como hicimos con el detalle de los item en la pagina principal, lo hacemos con el detalle de la vista product, lo extraemos en un nuevo archivo.

A la hora de mostrar los detalles en la vista, como cada producto difiere en sus especificaciones, tendriamos que preguntar con un if, cual es su categoria para imprimir los campos correspondientes. Podriamos imprimir solamente los datos pero nosotros queremos tambien visualizar el nombre de cada campo, por ejemplo, el dato "16 Gb" se corresponde a "RAM" o "Memoria RAM" y tambien imprimir un icono determinado, pero en nuestro objeto Article no tenemos esos valores,ya que solo procesa datos, de eso de debe encargar la vista, pero como tenemos uan vista general, no podemos discriminar cada articulo por su categoria.

Una solución un poco casera, radica en la sección detalle, hacer un include a un archivo con el mismo nombre de la categoria, por ejemplo @include($article->category) y en la vista correspondiente, pintar cada uno de los campos, pero eso solo funcionaria si solamente la seccion detalles es la que cambia. Si tenemos otras areas de la vista que tambien varien, debemos tambien hacer nuevos includes, y esto no solucionaria el problema anterior, de la clase Article.

Es hora de poner a prueba el patrón estrategia que ya hemos explicado en su momento. Vamos a crear un modelo especifico para cada categoria.

En primer lugar, modificamos la clase Article de la siguiente manera.

namespace App\Http\Models;

abstract class Article
{
    public $id;

    public $name;

    public $price;

    public $category;

    public $image;

    public function __construct($article)
    {
        $this->id = $article->id;
        $this->name = $article->name;
        $this->price = $article->price;
        $this->category = $article->category;
        $this->image = $article->image;
    }

    public function renderItemHome()
    {
        return view('dataItemHome', ["article" => $this]);
    }

    abstract public function render();
}

Eliminamos los if del constructor, hacemos abstracta la clase para que no se pueda instanciar y agregamos un método abstracto render().

namespace App\Http\Models;

class ElectronicArticle extends Article
{
    public $specifications;

    public function __construct($article)
    {
        parent::__construct($article);

        $this->specifications = $article->specifications;
    }

    public function render()
    {
        return view('product.electronic', ['article' => $this]);
    }
}

En la clase ElectronicArticle, que extiende de Article, agregamos en el constructor el atributo que varia, en este caso specifications, ademas implementamos metodo render(). El objetivo del método es retornar la vista especifica de este tipo de producto.

Nos queda modificar el repositorio, actualizamos el foreach para que nos quede de esta manera.

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

Aquí vemos que estamos creando un objeto segun la categoria del articulo, seguimos dependiendo de un if anidado, en otros capitulos vamos a mejorar el codigo utilizando otros patrones.

El código final está diponible en nuestro GitHub