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 dev…


This content originally appeared on DEV Community and was authored by Vinícius Boscardin

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
      • 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 }

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:

Image description

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


This content originally appeared on DEV Community and was authored by Vinícius Boscardin


Print Share Comment Cite Upload Translate Updates
APA

Vinícius Boscardin | Sciencx (2022-04-13T22:57:21+00:00) Vue.js, Clean Architecture e Package by feature Pattern. Retrieved from https://www.scien.cx/2022/04/13/vue-js-clean-architecture-e-package-by-feature-pattern/

MLA
" » Vue.js, Clean Architecture e Package by feature Pattern." Vinícius Boscardin | Sciencx - Wednesday April 13, 2022, https://www.scien.cx/2022/04/13/vue-js-clean-architecture-e-package-by-feature-pattern/
HARVARD
Vinícius Boscardin | Sciencx Wednesday April 13, 2022 » Vue.js, Clean Architecture e Package by feature Pattern., viewed ,<https://www.scien.cx/2022/04/13/vue-js-clean-architecture-e-package-by-feature-pattern/>
VANCOUVER
Vinícius Boscardin | Sciencx - » Vue.js, Clean Architecture e Package by feature Pattern. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/04/13/vue-js-clean-architecture-e-package-by-feature-pattern/
CHICAGO
" » Vue.js, Clean Architecture e Package by feature Pattern." Vinícius Boscardin | Sciencx - Accessed . https://www.scien.cx/2022/04/13/vue-js-clean-architecture-e-package-by-feature-pattern/
IEEE
" » Vue.js, Clean Architecture e Package by feature Pattern." Vinícius Boscardin | Sciencx [Online]. Available: https://www.scien.cx/2022/04/13/vue-js-clean-architecture-e-package-by-feature-pattern/. [Accessed: ]
rf:citation
» Vue.js, Clean Architecture e Package by feature Pattern | Vinícius Boscardin | Sciencx | https://www.scien.cx/2022/04/13/vue-js-clean-architecture-e-package-by-feature-pattern/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.