Vue.js, Clean Architecture e Package by feature Pattern

booscaaa

Vinícius Boscardin

Posted on April 13, 2022

Vue.js, Clean Architecture e Package by feature Pattern

Lá vamos nós de novo falar de clean architecture... Mas agora não vamos mais falar de golang, e sim de Vue.js. Vamos implementar o fronted da nossa api da série de Clean Architecture com Golang em Vue.js.

Vamos lá! Nossa implementação do frontend deve ter os mesmos requisitos da nossa api:

  • Uma listagem de produtos
  • Um formulário para adicionar produtos na lista

Package by feature Pattern

Nesta estrutura de projeto, os pacotes contêm todas as classes necessárias para um recurso. A independência do pacote é assegurada colocando classes intimamente relacionadas no mesmo pacote. Aqui um post com um ótimo exemplo de como funciona.

Implementação

Primeira coisa que precisamos fazer é criar nosso projeto vue, ainda com o Vue 2 com typescript.

vue create clean-vue
cd clean-vue
vue add vuetify
npm i axios
npm i @types/axios --save-dev
npm run serve
Enter fullscreen mode Exit fullscreen mode

Projetinho rodando liso, bora codar!

Vamos apagar a pasta src/components e estruturar o projeto da seguinte forma:

  • src
    • di
    • module
      • pagination
        • domain
          • model
      • product
        • const
        • components
        • repository
        • domain
          • model
          • usecase
        • controller
        • view

Agora tudo está dando erro, nada mais funciona! Calma lá, vamos estruturar nosso código que tudo se resolve. :D

Model

Primeira coisa é nós definirmos o model com o que volta da API no arquivo src/module/product/domain/model/product.ts.

import { AxiosResponse } from "axios"

interface ProductI {
    id?: number
    name?: string
    description?: string
    price?: number
}

class Product {
    id: number
    name: string
    description: string
    price: number

    constructor({ id = 0, name = "", description = "", price = 0.00 }: ProductI) {
        this.id = id
        this.name = name
        this.description = description
        this.price = price
    }
}

class ProductPagination {
    items: ProductI[]
    total: number

    constructor(response?: AxiosResponse) {
        this.items = response?.data?.items?.map((product: any) => new Product(product)) ?? []
        this.total = response?.data?.total ?? 0
    }
}

export { Product, ProductPagination }
Enter fullscreen mode Exit fullscreen mode

E também o model de paginação default de toda a aplicação no arquivo src/module/pagination/domain/model/pagination.ts.

interface PaginationI {
    page: number
    itemsPerPage: number
    sort: string
    descending: string
    search: string
}

class Pagination {
    page: number
    itemsPerPage: number
    sort: string
    descending: string
    search: string

    constructor({ page, itemsPerPage, sort, descending, search }: PaginationI) {
        this.page = page
        this.itemsPerPage = itemsPerPage
        this.descending = descending
        this.search = search
        this.sort = sort
    }
}

export { Pagination } 
Enter fullscreen mode Exit fullscreen mode

Repository

Com nossos models prontos, podemos já preparar nosso repository para manipular os endpoint dos nossos produtos.
Criaremos o arquivo src/module/product/repository/fetchProductsRepository.ts.

import { Pagination } from '@/module/pagination/domain/model/pagination'
import { ProductPagination } from '../domain/model/product'
import { AxiosInstance } from 'axios'

interface FetchProductsRepository {
    (pagination: Pagination): Promise<ProductPagination>
}

const fetchProductsRepository = (axios: AxiosInstance): FetchProductsRepository => async (pagination: Pagination) => {
    const response = await axios.get("/product", {
        params: pagination
    })

    const productPagination = new ProductPagination(response)
    return productPagination
}

export { fetchProductsRepository, FetchProductsRepository } 
Enter fullscreen mode Exit fullscreen mode

E também criaremos o arquivo src/module/product/repository/createProductRepository.ts.

import { Product } from '../domain/model/product'
import { AxiosInstance } from 'axios'

interface CreateProductRepository {
    (product: Product): Promise<Product>
}

const createProductRepository = (axios: AxiosInstance): CreateProductRepository => async (product: Product) => {
    const response = await axios.post("/product", product)
    return new Product(response?.data)
}

export { createProductRepository, CreateProductRepository } 
Enter fullscreen mode Exit fullscreen mode

Usecase

Com nossos repositories criados, podemos implementar nosso usecase de produtos.
Criaremos o arquivo src/module/product/domain/usecase/fetchProductsUseCase.ts.

import { FetchProductsRepository } from "../../repository/fetchProductsRepository"
import { Pagination } from "@/module/pagination/domain/model/pagination"
import { ProductPagination } from "../model/product"
import { DataOptions } from "vuetify"

interface FetchProductsUseCase {
    (options: DataOptions, search: string): Promise<ProductPagination>
}

const fetchProductsUseCase = (repository: FetchProductsRepository): FetchProductsUseCase => async (options: DataOptions, search: string) => {
    const pagination = new Pagination({
        descending: options.sortDesc.join(","),
        sort: options.sortBy.join(","),
        page: options.page,
        itemsPerPage: options.itemsPerPage,
        search: search,
    })

    const productPagination = await repository(pagination)
    return productPagination
}

export { fetchProductsUseCase, FetchProductsUseCase } 
Enter fullscreen mode Exit fullscreen mode

E também criaremos o arquivo src/module/product/domain/usecase/createProductUseCase.ts.

import { CreateProductRepository } from "../../repository/createProductRepository"
import { Product } from "../model/product"

interface CreateProductsUseCase {
    (product: Product): Promise<Product>
}

const createProductUseCase = (repository: CreateProductRepository): CreateProductsUseCase => async (product: Product) => {
    const productCreated = await repository(product)
    return productCreated
}

export { createProductUseCase, CreateProductsUseCase } 
Enter fullscreen mode Exit fullscreen mode

Controller

Com nossos usecases criados, podemos implementar nosso Controller no arquivo module/product/controller/productController.ts.

import { CreateProductsUseCase } from "../domain/usecase/createProductUseCase";
import { FetchProductsUseCase } from "../domain/usecase/fetchProductUseCase";
import { Product, ProductPagination } from "../domain/model/product";
import { headers } from "../const/header";

class ProductController {
    options: any
    public product = new Product({})
    public productPagination = new ProductPagination()
    public headers = headers
    public formDialog = false

    constructor(
        private context: any,
        private fetchProductsUseCase: FetchProductsUseCase,
        private createProductUseCase: CreateProductsUseCase
    ) { }

    async paginate() {
        this.productPagination = await this.fetchProductsUseCase(this.options, "")
    }

    async save() {
        if (this.context.$refs.productForm.$refs.form.validate()) {
            await this.createProductUseCase(this.product)
            this.cancel()
            this.paginate()
        }
    }

    cancel() {
        this.product = new Product({})
        this.context.$refs.productForm.$refs.form.resetValidation()
        this.formDialog = false
    }
}

export { ProductController }
Enter fullscreen mode Exit fullscreen mode

Tudo pronto! Brincadeira... Estamos quase lá, vamos configurar nossa injeção de dependências. Para configurar a injeção de dependência do nosso product vamos criar um arquivo em module/di/di.ts.

import { fetchProductsRepository } from "../product/repository/fetchProductsRepository";
import { createProductRepository } from "../product/repository/createProductRepository";
import { createProductUseCase } from "../product/domain/usecase/createProductUseCase";
import { fetchProductsUseCase } from "../product/domain/usecase/fetchProductUseCase";
import { ProductController } from "../product/controller/productController";
import axios from "axios";

const axiosInstance = axios.create({
    baseURL: "https://clean-go.herokuapp.com",
    headers: {
        "Content-Type": "application/json"
    }
})

axiosInstance.interceptors.response.use((response) => response, async (err) => {
    const status = err.response ? err.response.status : null

    if (status === 500) {
        // Do something here or on any status code return
    }

    return Promise.reject(err);
});

// Implementation methods from products feature
const fetchProductsRepositoryImpl = fetchProductsRepository(axiosInstance)
const fetchProductsUseCaseImpl = fetchProductsUseCase(fetchProductsRepositoryImpl)
const createProductRepositoryImpl = createProductRepository(axiosInstance)
const createProductUseCaseImpl = createProductUseCase(createProductRepositoryImpl)

const productController = (context: any) => new ProductController(
    context,
    fetchProductsUseCaseImpl,
    createProductUseCaseImpl
)

export { productController }
Enter fullscreen mode Exit fullscreen mode

Agora sim, bora para nossa tela! Fique a vontade para montar ela do jeito que quiser!

Criaremos o arquivo module/product/components/productTable.vue

<template>
  <v-card>
    <v-card-title>
      Products
      <v-spacer></v-spacer>
      <v-btn
        color="primary"
        @click="controller.formDialog = true"
      >
        <v-icon left>mdi-plus</v-icon>new
      </v-btn>
    </v-card-title>
    <v-card-text class="pa-0">
      <v-data-table
        dense
        :items="controller.productPagination.items"
        :headers="controller.headers"
        :options.sync="controller.options"
        @pagination="controller.paginate()"
        :server-items-length="controller.productPagination.total"
      ></v-data-table>
    </v-card-text>
  </v-card>
</template>

<script>
export default {
  props: {
    controller: {
      require: true,
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

E o arquivo module/product/components/productForm.vue

<template>
  <v-dialog
    persistent
    width="400"
    v-model="controller.formDialog"
  >
    <v-card>
      <v-card-title class="pa-0 pb-4">
        <v-toolbar
          flat
          dense
          color="primary"
          class="white--text"
        >
          New product
        </v-toolbar>
      </v-card-title>
      <v-card-text>
        <v-form ref="form">
          <v-text-field
            label="Name"
            dense
            filled
            v-model="controller.product.name"
            :rules="[(v) => !!v || 'Required']"
          ></v-text-field>
          <v-text-field
            label="Price"
            dense
            filled
            v-model.number="controller.product.price"
            :rules="[(v) => !!v || 'Required']"
          ></v-text-field>
          <v-textarea
            label="Description"
            dense
            filled
            v-model="controller.product.description"
            :rules="[(v) => !!v || 'Required']"
          ></v-textarea>
        </v-form>
      </v-card-text>
      <v-card-actions>
        <v-btn
          @click="controller.cancel()"
          color="red"
          text
        >cancel</v-btn>
        <v-spacer></v-spacer>
        <v-btn
          color="primary"
          @click="controller.save()"
        >
          <v-icon left>mdi-content-save</v-icon>save
        </v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script>
export default {
  props: {
    controller: {
      require: true,
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

E por fim criaremos o arquivo module/product/view/product.vue

<template>
  <v-app>
    <v-main>
      <v-row
        class="fill-height"
        justify="center"
        align="center"
      >
        <v-col
          cols="12"
          lg="6"
        >
          <product-table
            ref="productTable"
            :controller="controller"
          />
          <product-form
            ref="productForm"
            :controller="controller"
          />
        </v-col>
      </v-row>
    </v-main>
  </v-app>
</template>

<script>
import { productController } from "../../di/di";
import ProductTable from "../components/productTable";
import ProductForm from "../components/productForm";
export default {
  components: {
    ProductTable,
    ProductForm,
  },
  data: (context) => ({
    controller: productController(context),
  }),
};
</script>
Enter fullscreen mode Exit fullscreen mode

E a estrutura final ficou:

Image description

Testando, 1..2..3.. Teste som!

💖 💪 🙅 🚩
booscaaa
Vinícius Boscardin

Posted on April 13, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related