目录
Go中干净架构的整体示例,涵盖Web API的开发、文档和部署
关于干净的架构已经写了很多。它的主要价值是能够维护无副作用的域层,这使我们能够在不利用大量模拟的情况下测试核心业务逻辑。
这是通过编写依赖于域的无依赖性核心域逻辑和外部适配器(无论是数据库存储还是API层)来实现的,反之亦然。
在本文中,我们将了解如何使用示例Go项目实现干净的体系结构。我们将介绍一些其他主题,例如容器化和使用Swagger实现OpenAPI规范。
虽然我将在文章中重点介绍兴趣点,但您可以在我的Github上查看整个项目。
项目要求
我们需要提供REST API的实现来模拟一副牌。
我们需要为您的API提供以下方法来处理卡片和套牌:
- 创建一个新的Deck
- 打开一个Deck
- 绘制一个Card
创建新套牌
它将创建标准的 52 张牌组法国扑克牌,它包括四种花色中每一种的所有 13 个等级:梅花(♣)、方块(♦)、红桃(♥)和黑桃(♠)。您无需担心此任务的小丑卡。
- 要洗牌或不洗牌的套牌——默认情况下,套牌是顺序的:A-黑桃、2-黑桃、3-黑桃......其次是方块、梅花,然后是红桃。
- 套牌为完整或部分——默认情况下,它返回标准的52张牌,否则请求将接受想要的牌,如本例所示?cards=AS,KD,AC,2C,KH
响应需要返回一个JSON,其中包括:
- 套牌ID(UUID)
- 这副牌中剩余的套牌属性,如洗牌(布尔值)和总牌数(整数)
{
"deck_id": "a251071b-662f-44b6-ba11-e24863039c59",
"shuffled": false,
"remaining": 30
}
打开套牌
它将通过其UUID返回给定的套牌。如果套牌未被传递或无效,则应返回错误。此方法将“打开牌组”,这意味着它将按创建顺序列出所有牌。
响应需要返回一个JSON,其中包括:——套牌ID(UUID)——套牌属性,如洗牌(布尔值)和此套牌中剩余的总牌数(整数)——所有剩余的牌(牌对象)。
{
"deck_id": "a251071b-662f-44b6-ba11-e24863039c59",
"shuffled": false,
"remaining": 3,
"cards": [
{
"value": "ACE",
"suit": "SPADES",
"code": "AS"
},
{
"value": "KING",
"suit": "HEARTS",
"code": "KH"
},
{
"value": "8",
"suit": "CLUBS",
"code": "8C"
}
]
}
抽牌
我们会抽出一张给定套牌的牌。如果套牌未通过或无效,则应返回错误。需要提供一个计数参数来定义从牌组中抽出多少张牌。
响应需要返回一个JSON,其中包括:所有绘制的卡片 (卡片对象)
{
"cards": [
{
"value": "QUEEN",
"suit": "HEARTS",
"code": "QH"
},
{
"value": "4",
"suit": "DIAMONDS",
"code": "4D"
}
]
}
设计领域
由于领域是我们应用程序不可或缺的一部分,因此我们将从领域开始设计我们的系统。
让我们将我们的Shape和Rank类型编码为iota。如果你熟悉其他语言,你可能认为它是一个enum,这很整洁,因为我们的任务假设某种内置顺序,所以我们可以利用潜在的数值。
type Shape uint8
const (
Spades Shape = iota
Diamonds
Clubs
Hearts
)
type Rank int8
const (
Ace Rank = iota
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Jack
Queen
King
)
完成此操作后,我们可以将其Card编码为其形状和等级的组合。
type Card struct {
Rank Rank
Shape Shape
}
领域驱动设计的功能之一是使非法状态无法表示,但由于等级和形状的所有组合都是有效的,因此创建卡片非常简单。
func CreateCard(rank Rank, shape Shape) Card {
return Card{
Rank: rank,
Shape: shape,
}
}
现在让我们看一下Deck:
type Deck struct {
DeckId uuid.UUID
Shuffled bool
Cards []Card
}
Deck将展示三种操作:创建一副牌,抽牌和计算剩余的牌。
func CreateDeck(shuffled bool, cards ...Card) Deck {
if len(cards) == 0 {
cards = initCards()
}
if shuffled {
shuffleCards(cards)
}
return Deck{
DeckId: uuid.New(),
Shuffled: shuffled,
Cards: cards,
}
}
func DrawCards(deck *Deck, count uint8) ([]Card, error) {
if count > CountRemainingCards(*deck) {
return nil, errors.New("DrawCards: Insufficient amount of cards in deck")
}
result := deck.Cards[:count]
deck.Cards = deck.Cards[count:]
return result, nil
}
func CountRemainingCards(d Deck) uint8 {
return uint8(len(d.Cards))
}
请注意,在抽卡时,我们会检查是否有足够的卡片数量来执行操作。为了在我们无法继续的情况下发出信号,我们利用了Go 多个返回值功能。
在这一点上,我们可以观察到干净架构的主要好处之一:核心域逻辑没有外部依赖关系,这大大简化了单元测试。虽然它们中的大多数都是微不足道的,为了简洁起见,我们将省略它们,但让我们来看看那些验证套牌是否被洗牌的那些。
func TestCreateDeck_ExactCardsArePassed_Shuffled(t *testing.T) {
jackOfDiamonds := CreateCard(Jack, Diamonds)
aceOfSpades := CreateCard(Ace, Spades)
queenOfHearts := CreateCard(Queen, Hearts)
cards := []Card{jackOfDiamonds, aceOfSpades, queenOfHearts}
deck := CreateDeck(false, cards...)
deckCardsCount := make(map[Card]int)
for _, resCard := range deck.Cards {
value, exists := deckCardsCount[resCard]
if exists {
value++
deckCardsCount[resCard] = value
} else {
deckCardsCount[resCard] = 1
}
}
for _, inputCard := range cards {
value, found := deckCardsCount[inputCard]
assert.True(t, found, "Expected all cards to be present")
assert.Equal(t, 1, value, "Expected cards not to be duplicate")
}
}
显然,我们无法验证洗牌的顺序。相反,我们可以做的是验证洗牌后的牌组是否满足感兴趣的属性,即我们有每张牌,并且我们的牌组中没有重复的牌。这种技术与基于属性的测试非常相似。
作为旁注,值得一提的是,为了消除样板断言代码,我们利用了testify库。
提供API
让我们从定义路由开始。
func main() {
r := gin.Default()
r.POST("/create-deck", api.CreateDeckHandler)
r.GET("/open-deck", api.OpenDeckHandler)
r.PUT("/draw-cards", api.DrawCardsHandler)
r.Run()
}
一些读者可能会对以下事实感到困惑:根据上面列出的要求,创建套牌端点接受参数作为 URL 请求的一部分,并可能考虑让此端点接受GET请求而不是POST。但是,GET请求的一个重要先决条件是它们表现出幂等性,而此端点并非如此。这就是我们坚持使用POST的确切原因。
处理程序遵循相同的模式。我们解析查询参数,基于它们创建一个域实体,对其执行操作,更新存储并返回专用DTO。让我们来看看更多细节。
type CreateDeckArgs struct {
Shuffled bool `form:"shuffled"`
Cards string `form:"cards"`
}
type OpenDeckArgs struct {
DeckId string `form:"deck_id"`
}
type DrawCardsArgs struct {
DeckId string `form:"deck_id"`
Count uint8 `form:"count"`
}
func CreateDeckHandler(c *gin.Context) {
var args CreateDeckArgs
if c.ShouldBind(&args) == nil {
var domainCards []domain.Card
if args.Cards != "" {
for _, card := range strings.Split(args.Cards, ",") {
domainCard, err := parseCardStringCode(card)
if err == nil {
domainCards = append(domainCards, domainCard)
} else {
c.String(400, "Invalid request. Invalid card code "+card)
return
}
}
}
deck := domain.CreateDeck(args.Shuffled, domainCards...)
storage.Add(deck)
dto := createClosedDeckDTO(deck)
c.JSON(200, dto)
return
} else {
c.String(400, "Ivalid request.
Expecting query of type ?shuffled=<bool>&cards=<card1>,<card2>,...<cardn>")
return
}
}
func OpenDeckHandler(c *gin.Context) {
var args OpenDeckArgs
if c.ShouldBind(&args) == nil {
deckId, err := uuid.Parse(args.DeckId)
if err != nil {
c.String(400, "Bad Request. Expecing request in format ?deck_id=<uuid>")
return
}
deck, found := storage.Get(deckId)
if !found {
c.String(400, "Bad Request. Deck with given id not found")
return
}
dto := createOpenDeckDTO(deck)
c.JSON(200, dto)
return
} else {
c.String(400, "Bad Request. Expecing request in format ?deck_id=<uuid>")
return
}
}
func DrawCardsHandler(c *gin.Context) {
var args DrawCardsArgs
if c.ShouldBind(&args) == nil {
deckId, err := uuid.Parse(args.DeckId)
if err != nil {
c.String(400, "Bad Request. Expecing request in format ?deck_id=<uuid>")
return
}
deck, found := storage.Get(deckId)
if !found {
c.String(400, "Bad Request.
Expecting request in format ?deck_id=<uuid>&count=<uint8>")
return
}
cards, err := domain.DrawCards(&deck, args.Count)
if err != nil {
c.String(400, "Bad Request. Failed to draw cards from the deck")
return
}
var dto []CardDTO
for _, card := range cards {
dto = append(dto, createCardDTO(card))
}
storage.Add(deck)
c.JSON(200, dto)
return
} else {
c.String(400, "Bad Request.
Expecting request in format ?deck_id=<uuid>&count=<uint8>")
return
}
}
定义OpenAPI规范
我们应该对待OpenAPI规范的方式不仅仅是作为一个花哨的文档生成器(尽管对于我们的文章来说已经足够了),而且还是一个描述REST API的标准,简化了客户端的使用。
让我们从装饰主方法的声明性注释开始。这些注释稍后将用于自动生成Swagger规范。在这里,您可以查找格式。
// @title Deck Management API
// @version 0.1
// @description This is a sample server server.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /
// @schemes http
func main() {
我们的处理程序也是如此。让我们以其中之一为例。
// CreateDeckHandler godoc
// @Summary Creates new deck.
// @Description Creates deck that can be either shuffled or unshuffled.
// It can accept the list of exact cards which can be shuffled or unshuffled as well.
// In case no cards provided, it returns a deck with 52 cards.
// @Accept */*
// @Produce json
// @Param shuffled query bool true "indicates whether deck is shuffled"
// @Param cards query array false "array of card codes i.e. 8C,AS,7D"
// @Success 200 {object} ClosedDeckDTO
// @Router /create-deck [post]
func CreateDeckHandler(c *gin.Context) {
现在让我们拉动Swagger库。
go get -v github.com/swaggo/swag/cmd/swag
go get -v github.com/swaggo/gin-sagger
go get -v github.com/swaggo/files
现在我们将生成规范:
swag init -g main.go --output docs
此命令将在 docs 文件夹中生成所需的文件。
下一步是使用必要的导入更新我们的 main.go 文件:
_ "toggl-deck-management-api/docs"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
和端点:
url := ginSwagger.URL("/swagger/doc.json")
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
完成所有这些操作后,现在我们可以运行我们的应用程序并查看Swagger生成的文档。
容器化API
最后但并非最不重要的是我们将如何部署我们的应用程序。执行此操作的传统方法是在专用服务器上安装运行时,并在安装的运行时上运行应用程序。
容器化是将运行时与应用程序一起打包的便捷方式,如果我们想利用自动缩放功能,并且我们可能没有安装所有需要的服务器并安装环境,那么这种方式可能会很方便。
Docker是最流行的容器化解决方案,因此我们将利用它。为此,我们将在项目的根目录下创建dockerfile。
我们要做的第一件事是选择应用程序将基于的运行时镜像:
FROM golang:1.18-bullseye
之后,我们将源代码复制到工作目录中并构建它:
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN go build -o server .
最后一步是向外界公开端口并运行应用程序:
EXPOSE 8080
CMD [ "/app/server" ]
现在,假设Docker已安装在我们的机器上,我们可以通过以下方式运行该应用程序:
docker build -t <image-name> .
docker run -it --rm -p 8080:8080 <image-name>
结论
在本文中,我们介绍了在Go中编写干净架构API的整体过程。从经过良好测试的领域开始,为其提供API层,使用OpenAPI标准对其进行记录,并将我们的运行时与应用程序打包在一起,从而简化部署过程。
https://www.codeproject.com/Articles/5347633/Implementing-Clean-Architecture-in-Go