Vue.js, Clean Architecture e Package by feature Pattern
Vinícius Boscardin
Posted on April 13, 2022
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
Projetinho rodando liso, bora codar!
Vamos apagar a pasta src/components
e estruturar o projeto da seguinte forma:
- src
- di
- module
- pagination
- domain
- model
- domain
- product
- const
- components
- repository
- domain
- model
- usecase
- controller
- view
- pagination
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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>
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>
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>
E a estrutura final ficou:
Testando, 1..2..3.. Teste som!
Posted on April 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.