Implementando Clean Architecture com Golang
干净的建筑和节奏。 Mas... Como podemos estruturar uma arquitetura limpa com golang?
最初的要求是干净的建筑、特定的和不实施的。作为实现 arquitetura mais famosas são:
- 六边形
- 数据中心
- 尖叫
- 洋葱
我们使用六角形结构或端口和适配器作为示例。这是一个关于实际应用场景的设计,以及目前作为功能存在的必要条件。您可以通过 http com REST 协议解决问题。
要求:
- Criação de produtos(id、名称、说明和描述)
- Listagem de produtos(com paginação no servidor)
Com os requisitos definidos, bora codar isso ai!
冷静,ainda não! Vamos 定义了 vamos usar 的技术、银行、连接的驱动程序以及主要的数据库,以供应用程序使用。
我们的目标是:
- 银行:
- 自由党不行
Crie uma 面食没有本地 desejado com 或 nome clean-go/
Na 面食,没有 seu 编辑器首选,estruture 或 projeto:
- 适配器/
- http/
- 主程序
- postgres/
- 连接器.go
- http/
- 核/
- 领域/
- 产品.go
- dto/
- 产品.go
- 领域/
- 数据库
- 迁移
数据库
安装 CLI迁移并保存项目所需的迁移。
migrate create -ext sql -dir database/migrations -seq create_product_table
编辑并获取database/migrations/000001.create_product_table.up.sql
SQL 数据表产品。
CREATE TABLE product (
id SERIAL PRIMARY KEY NOT NULL,
name VARCHAR(50) NOT NULL,
price FLOAT NOT NULL,
description VARCHAR(500) NOT NULL
);
另请参阅或存档database/migrations/000001.create_product_table.down.sql
。
DROP TABLE IF EXISTS product;
Go 模块
Vamos 的初始模块为 go com o comando:
# go mod init github.com/<seu usuario>/<nome do repo>
# no meu caso:
go mod init github.com/booscaaa/clean-go
DTO(数据传输对象)
core/dto/product.go
根据对服务商的新产品的请求,我们可以编辑或定义数据模型。
package dto
import (
"encoding/json"
"io"
)
// CreateProductRequest is an representation request body to create a new Product
type CreateProductRequest struct {
Name string `json:"name"`
Price float32 `json:"price"`
Description string `json:"description"`
}
// FromJSONCreateProductRequest converts json body request to a CreateProductRequest struct
func FromJSONCreateProductRequest(body io.Reader) (*CreateProductRequest, error) {
createProductRequest := CreateProductRequest{}
if err := json.NewDecoder(body).Decode(&createProductRequest); err != nil {
return nil, err
}
return &createProductRequest, nil
}
后续定义或 DTO 请求不保留分页core/dto/pagination.go
。
package dto
import (
"net/http"
"strconv"
"strings"
)
// PaginationRequestParms is an representation query string params to filter and paginate products
type PaginationRequestParms struct {
Search string `json:"search"`
Descending []string `json:"descending"`
Page int `json:"page"`
ItemsPerPage int `json:"itemsPerPage"`
Sort []string `json:"sort"`
}
// FromValuePaginationRequestParams converts query string params to a PaginationRequestParms struct
func FromValuePaginationRequestParams(request *http.Request) (*PaginationRequestParms, error) {
page, _ := strconv.Atoi(request.FormValue("page"))
itemsPerPage, _ := strconv.Atoi(request.FormValue("itemsPerPage"))
paginationRequestParms := PaginationRequestParms{
Search: request.FormValue("search"),
Descending: strings.Split(request.FormValue("descending"), ","),
Sort: strings.Split(request.FormValue("sort"), ","),
Page: page,
ItemsPerPage: itemsPerPage,
}
return &paginationRequestParms, nil
}
领域
您可以使用 DTO 配置核心应用程序。 Criaremos um aquivo chamado core/domain/pagination.go
。
package domain
// Pagination is representation of Fetch methods returns
type Pagination[T any] struct {
Items T `json:"items"`
Total int32 `json:"total"`
}
没有任何core/domain/product.go
关于银行和银行产品的模型的定义,作为实施方法的接口,首先定义了 3 个基本接口:service, usecase
eo nosso repository
。
服务是作为外部 API 的必要条件提供的,或者是银行的应用程序和存储库的适配器。
package domain
import (
"net/http"
"github.com/boooscaaa/clean-go/core/dto"
)
// Product is entity of table product database column
type Product struct {
ID int32 `json:"id"`
Name string `json:"name"`
Price float32 `json:"price"`
Description string `json:"description"`
}
// ProductService is a contract of http adapter layer
type ProductService interface {
Create(response http.ResponseWriter, request *http.Request)
Fetch(response http.ResponseWriter, request *http.Request)
}
// ProductUseCase is a contract of business rule layer
type ProductUseCase interface {
Create(productRequest *dto.CreateProductRequest) (*Product, error)
Fetch(paginationRequest *dto.PaginationRequestParms) (*Pagination[[]Product], error)
}
// ProductRepository is a contract of database connection adapter layer
type ProductRepository interface {
Create(productRequest *dto.CreateProductRequest) (*Product, error)
Fetch(paginationRequest *dto.PaginationRequestParms) (*Pagination[[]Product], error)
}
存储库
我们定义了 api 的实现。无需配置银行adapter/postgres/connector.go
账户。
package postgres
import (
"context"
"fmt"
"log"
"os"
"github.com/golang-migrate/migrate/v4"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/spf13/viper"
_ "github.com/golang-migrate/migrate/v4/database/pgx" //driver pgx used to run migrations
_ "github.com/golang-migrate/migrate/v4/source/file"
)
// PoolInterface is an wraping to PgxPool to create test mocks
type PoolInterface interface {
Close()
Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
QueryFunc(
ctx context.Context,
sql string,
args []interface{},
scans []interface{},
f func(pgx.QueryFuncRow) error,
) (pgconn.CommandTag, error)
SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults
Begin(ctx context.Context) (pgx.Tx, error)
BeginFunc(ctx context.Context, f func(pgx.Tx) error) error
BeginTxFunc(ctx context.Context, txOptions pgx.TxOptions, f func(pgx.Tx) error) error
}
// GetConnection return connection pool from postgres drive PGX
func GetConnection(context context.Context) *pgxpool.Pool {
databaseURL := viper.GetString("database.url")
conn, err := pgxpool.Connect(context, "postgres"+databaseURL)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
os.Exit(1)
}
return conn
}
// RunMigrations run scripts on path database/migrations
func RunMigrations() {
databaseURL := viper.GetString("database.url")
m, err := migrate.New("file://database/migrations", "pgx"+databaseURL)
if err != nil {
log.Println(err)
}
if err := m.Up(); err != nil {
log.Println(err)
}
}
Com nosso 连接器很快就会实现,vamos 实现了 ProductRepository 接口,该接口属于 nosso 域吗? Criaremos 是一个实现 assim 的工具:
- 适配器
- PostgreSQL
- 产品库
- 新.go
- 创建.go
- fetch.go
- 产品库
- PostgreSQL
adapter/postgres/productrepository/new.go
没有任何问题与 ProductRepository 界面的“contrato”有关。
package productrepository
import (
"github.com/boooscaaa/clean-go/adapter/postgres"
"github.com/boooscaaa/clean-go/core/domain"
)
type repository struct {
db postgres.PoolInterface
}
// New returns contract implementation of ProductRepository
func New(db postgres.PoolInterface) domain.ProductRepository {
return &repository{
db: db,
}
}
没有任何问题adapter/postgres/productrepository/create.go
需要考虑创建合同的逻辑或方法。
package productrepository
import (
"context"
"github.com/boooscaaa/clean-go/core/domain"
"github.com/boooscaaa/clean-go/core/dto"
)
func (repository repository) Create(productRequest *dto.CreateProductRequest) (*domain.Product, error) {
ctx := context.Background()
product := domain.Product{}
err := repository.db.QueryRow(
ctx,
"INSERT INTO product (name, price, description) VALUES ($1, $2, $3) returning *",
productRequest.Name,
productRequest.Price,
productRequest.Description,
).Scan(
&product.ID,
&product.Name,
&product.Price,
&product.Description,
)
if err != nil {
return nil, err
}
return &product, nil
}
没有任何问题adapter/postgres/productrepository/fetch.go
需要考虑逻辑或方法来获取合同。
package productrepository
import (
"context"
"github.com/boooscaaa/clean-go/core/domain"
"github.com/boooscaaa/clean-go/core/dto"
"github.com/booscaaa/go-paginate/paginate"
)
func (repository repository) Fetch(pagination *dto.PaginationRequestParms) (*domain.Pagination[[]domain.Product], error) {
ctx := context.Background()
products := []domain.Product{}
total := int32(0)
query, queryCount, err := paginate.Paginate("SELECT * FROM product").
Page(pagination.Page).
Desc(pagination.Descending).
Sort(pagination.Sort).
RowsPerPage(pagination.ItemsPerPage).
SearchBy(pagination.Search, "name", "description").
Query()
if err != nil {
return nil, err
}
{
rows, err := repository.db.Query(
ctx,
*query,
)
if err != nil {
return nil, err
}
for rows.Next() {
product := domain.Product{}
rows.Scan(
&product.ID,
&product.Name,
&product.Price,
&product.Description,
)
products = append(products, product)
}
}
{
err := repository.db.QueryRow(ctx, *queryCount).Scan(&total)
if err != nil {
return nil, err
}
}
return &domain.Pagination[[]domain.Product]{
Items: products,
Total: total,
}, nil
}
存储库即将到来!:D
用例
Com nosso 存储库最终完成了应用程序的注册。 Criaremos 是一个实现 assim 的工具:
- 核
- 领域
- 用例
- 产品用例
- 新.go
- 创建.go
- fetch.go
- 产品用例
- 用例
- 领域
没有任何关于core/domain/usecase/productusecase/new.go
ProductUseCase 接口的“contrato”问题。
package productusecase
import "github.com/boooscaaa/clean-go/core/domain"
type usecase struct {
repository domain.ProductRepository
}
// New returns contract implementation of ProductUseCase
func New(repository domain.ProductRepository) domain.ProductUseCase {
return &usecase{
repository: repository,
}
}
没有任何问题core/domain/usecase/productusecase/create.go
需要考虑逻辑或方法来创建合同。
package productusecase
import (
"github.com/boooscaaa/clean-go/core/domain"
"github.com/boooscaaa/clean-go/core/dto"
)
func (usecase usecase) Create(productRequest *dto.CreateProductRequest) (*domain.Product, error) {
product, err := usecase.repository.Create(productRequest)
if err != nil {
return nil, err
}
return product, nil
}
没有任何问题core/domain/usecase/productusecase/fetch.go
需要考虑逻辑或方法来获取合同。
package productusecase
import (
"github.com/boooscaaa/clean-go/core/domain"
"github.com/boooscaaa/clean-go/core/dto"
)
func (usecase usecase) Fetch(paginationRequest *dto.PaginationRequestParms) (*domain.Pagination[[]domain.Product], error) {
products, err := usecase.repository.Fetch(paginationRequest)
if err != nil {
return nil, err
}
return products, nil
}
服务
Com nosso 用例最终实现了适配器以作为应用程序的要求接收 HTTP。 Criaremos 是一个实现 assim 的工具:
- 适配器
- http
- 产品服务
- 新.go
- 创建.go
- fetch.go
- 产品服务
- http
adapter/http/productservice/new.go
没有任何问题与 ProductService 界面的“矛盾”有关。
package productservice
import "github.com/boooscaaa/clean-go/core/domain"
type service struct {
usecase domain.ProductUseCase
}
// New returns contract implementation of ProductService
func New(usecase domain.ProductUseCase) domain.ProductService {
return &service{
usecase: usecase,
}
}
没有任何问题adapter/http/productservice/create.go
需要考虑逻辑或方法来创建合同。
package productservice
import (
"encoding/json"
"net/http"
"github.com/boooscaaa/clean-go/core/dto"
)
func (service service) Create(response http.ResponseWriter, request *http.Request) {
productRequest, err := dto.FromJSONCreateProductRequest(request.Body)
if err != nil {
response.WriteHeader(500)
response.Write([]byte(err.Error()))
return
}
product, err := service.usecase.Create(productRequest)
if err != nil {
response.WriteHeader(500)
response.Write([]byte(err.Error()))
return
}
json.NewEncoder(response).Encode(product)
}
没有任何问题adapter/http/productservice/fetch.go
需要考虑逻辑或方法来获取合同。
package productservice
import (
"encoding/json"
"net/http"
"github.com/boooscaaa/clean-go/core/dto"
)
func (service service) Fetch(response http.ResponseWriter, request *http.Request) {
paginationRequest, err := dto.FromValuePaginationRequestParams(request)
if err != nil {
response.WriteHeader(500)
response.Write([]byte(err.Error()))
return
}
products, err := service.usecase.Fetch(paginationRequest)
if err != nil {
response.WriteHeader(500)
response.Write([]byte(err.Error()))
return
}
json.NewEncoder(response).Encode(products)
}
都多快点!布林卡迪拉... 是的,您可以配置依赖项,并使用 json 格式的应用程序配置银行连接adapter/http/main.go
。
配置依赖于该产品的注入di/product.go
。
package di
import (
"github.com/boooscaaa/clean-go/adapter/http/productservice"
"github.com/boooscaaa/clean-go/adapter/postgres"
"github.com/boooscaaa/clean-go/adapter/postgres/productrepository"
"github.com/boooscaaa/clean-go/core/domain"
"github.com/boooscaaa/clean-go/core/usecase/productusecase"
)
// ConfigProductDI return a ProductService abstraction with dependency injection configuration
func ConfigProductDI(conn postgres.PoolInterface) domain.ProductService {
productRepository := productrepository.New(conn)
productUseCase := productusecase.New(productRepository)
productService := productservice.New(productUseCase)
return productService
}
E por fim configurar nosso arquivoadapter/http/main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/boooscaaa/clean-go/adapter/postgres"
"github.com/boooscaaa/clean-go/di"
"github.com/gorilla/mux"
"github.com/spf13/viper"
)
func init() {
viper.SetConfigFile(`config.json`)
err := viper.ReadInConfig()
if err != nil {
panic(err)
}
}
func main() {
ctx := context.Background()
conn := postgres.GetConnection(ctx)
defer conn.Close()
postgres.RunMigrations()
productService := di.ConfigProductDI(conn)
router := mux.NewRouter()
router.Handle("/product", http.HandlerFunc(productService.Create)).Methods("POST")
router.Handle("/product", http.HandlerFunc(productService.Fetch)).Queries(
"page", "{page}",
"itemsPerPage", "{itemsPerPage}",
"descending", "{descending}",
"sort", "{sort}",
"search", "{search}",
).Methods("GET")
port := viper.GetString("server.port")
log.Printf("LISTEN ON PORT: %v", port)
http.ListenAndServe(fmt.Sprintf(":%v", port), router)
}
Agora 可以配置连接到银行和 api 的端口,无需进行项目config.json
:
{
"database": {
"url": "://postgres:postgres@localhost:5432/devtodb"
},
"server": {
"port": "3000"
}
}
E 最终结构:
现在是真天!
Será mesmo que o projeto vai rodar lisinho? É o que veremos。
执行项目和rodar 的API 位置:
go run adapter/http/main.go
Com isso vai aparecer algo assim 无终端:
Testando,1..2..3.. Teste som!
Para criar um produto basta mandar um JSON em uma request POST na URL: localhost: port /product
Para listar os produtos com paginação é so mandar um GET maroto na URL localhost: port /product
苏阿维兹
Vai na fé! Acredito Totalmente em você,独立于技术的结合,可以通过 GO 的 api 进行编写。
如果问题没有解决,请直接联系我们。 Vamos 解析器 isso juntos。
Onde os testes unitários?
Bora lá,próximo post vamos abordar isso 和 também mexer bastante com o 覆盖 Go。 Vai ser muito 合法!阿特标志
鏂囩珷鏉ユ簮锛�https://dev.to/booscaaa/implementando-clean-architecture-com-golang-4n0a