在Go中实现干净的架构

目录

项目要求

创建新套牌

打开套牌

抽牌

设计领域

提供API

定义OpenAPI规范

容器化API

结论


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,其中包括:——套牌IDUUID)——套牌属性,如洗牌(布尔值)和此套牌中剩余的总牌数(整数)——所有剩余的牌(牌对象)。

{
    "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"
        }
    ]
}

设计领域

由于领域是我们应用程序不可或缺的一部分,因此我们将从领域开始设计我们的系统。

让我们将我们的ShapeRank类型编码为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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值