DEV Community

Vinícius Boscardin
Vinícius Boscardin

Posted on

14 6

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!

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay