# 4.1 设计模式与架构模式

![](https://466720491-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FnQd4I20b6KRKRXYAtLxX%2Fuploads%2Fphot0ioj7LlEXtqg57RI%2Fimage.png?alt=media\&token=78e431d8-ff42-4f27-92b3-3ffd610c567f)

铺垫了这么久，今天开始我们就要进入正式的开发环节来。 话不多说，从最核心的提示词开始，我们先解决后端的接口。

始终要记得，我们前面已经提到的先要处理“**第一条数据是从哪里来的**”这个问题。

我们现在先设定一个小目标：**编写添加一条提示词的功能**。

动手前，先来两个概念：设计模式和架构模式。

### 4.1.1 设计模式与MVC

> 在 1994 年，四人帮（全拼 Gang of Four）合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software（中文译名：**设计模式 - 可复用的面向对象软件元素**） 的书，该书首次提到了软件开发中设计模式的概念。两个核心点：
>
> * 对接口编程而不是对实现编程。
> * 优先使用对象组合而不是继承。

而设计模式的经典代表MVC (Model-View-Controller)模式，无论在什么语言里都是一个常见的设计模式，用于组织代码以实现**可维护**和**可扩展**的架构。

1. **Model（模型）**:
   * 模型负责管理程序的数据和业务逻辑。
   * 在Go中，模型通常通过结构体和接口来实现，并可能与数据库或其他数据源进行交互。
   * 模型层提供了数据的存取、验证和转换等功能，它是与业务逻辑直接相关的部分。

```go
type User struct {
    ID        string
    Name      string
    Email     string
}

func (u *User) Save() error {
    // 保存用户数据到数据库
    // ...
    return nil
}
```

2. **View（视图）**:
   * 视图负责用户界面的展示，它用于生成用户界面并呈现数据。
   * 在基于Go的Web开发中，视图通常通过模板引擎来实现，如`html/template`或`text/template`。

```go
// views/user.gohtml
<html>
  <body>
    <h1>User: {{.Name}}</h1>
    <p>Email: {{.Email}}</p>
  </body>
</html>
```

3. **Controller（控制器）**:
   * 控制器接收用户的输入，并调用模型和视图来完成用户的请求。
   * 在Go中，控制器通常通过HTTP处理函数或者路由处理函数来实现。控制器负责解析用户的请求，调用模型来处理数据，并通过视图来生成用户界面。

```go
func UserController(w http.ResponseWriter, r *http.Request) {
    user := User{
        ID: "1",
        Name: "John Doe",
        Email: "john.doe@example.com",
    }
    tpl, _ := template.New("user.gohtml").ParseFiles("views/user.gohtml")
    tpl.Execute(w, user)
}
```

以上片是在Go中MVC模式的简单地实现。使用Go的一些流行的Web框架，如Gin或Echo，可以进一步简化和优化MVC架构的实现。

### 4.1.2 架构模式与前后端分离

> 架构模式（Architectural Pattern）是软件架构中在给定环境下，针对常遇到的问题的、通用且可重用的解决方案。类似于前文所提到的**设计模式**但覆盖范围更广。

前后端分离的架构模式属于**分层架构**和**模块化设计**，分层架构属于软件架构设计领域的架构模式。它是一种垂直的系统层次划分方式,通常包括表示层、业务层、数据访问层等。使得软件系统在垂直方向上保持结构清晰、组件职责明确。

而模块化设计属于软件设计领域的代码组织方式。它是一种水平的系统划分方式,将系统划分为功能明确、相对独立的模块。有利于代码的复用和维护。

这两者在软件设计中通常会同时采用,既关注系统的垂直层次划分,也注重水平方向的模块化拆分,从不同粒度引导软件设计。它们相辅相成,共同推动代码的复用、逻辑清晰和维护性。

我们来看一下基于`gin-vue-admin`的后端代码目录结构：

```
├── api // API接口
│   └── v1 // v1版本API
├── config // 配置
├── core // 核心组件
├── docs // 文档
├── global // 全局对象
├── initialize // 初始化
│   └── internal // 内部初始化函数
├── middleware // 中间件
├── model // 模型
│   ├── request // 请求参数结构体
│   └── response // 响应结构体 
├── packfile // 打包文件
├── resource // 资源
│   ├── excel // Excel文件
│   ├── page // 页面文件
│   └── template // 模板
├── router // 路由
├── service // 业务逻辑
├── source // 源数据函数
└── utils // 工具函数
    ├── timer // 定时器封装
    └── upload // 上传封装
```

（详细描述见附录4。）

在前端代码作为视图层(见前文MVC模式)分离后，后端代码又可以分为4层：

* 路由层（Router）: 负责路由的定义和请求的分发。
* 控制器层（API）: 负责处理具体的请求，通常会调用服务层的逻辑。
* 服务层（Service）: 负责具体的业务逻辑处理，通常会调用模型层来进行数据处理。
* 模型层（Model）: 负责数据的存储和检索，与数据库交互。

在这样的架构中，所有和数据库交互的操作全部在模型层中完成，这样在上层只需要调用接口，告诉模型我需要怎样的数据，以及筛选条件，不需要关心数据是存在MySQL里，或者是Redis中，抑或者是已经被迁移到了MongoDB。

我们现在就先来为提示词建立一个数据表和对应的模型：

```go
// 提示词 结构体  Prompts
type Prompts struct {
	global.GVA_MODEL
	UserId       *int   `json:"UserId" form:"UserId" gorm:"column:userid;comment:;default:0"`                   //创建者 默认为0
	Title        string `json:"Title" form:"Title" gorm:"column:title;comment:;size:100;"`                      //标题
	Uri          string `json:"Uri" form:"Uri" gorm:"column:uri;comment:;size:30;"`                             //短地址
	Description  string `json:"Description" form:"Description" gorm:"column:description;comment:;type:text;"`   //提示词说明
	ThumbnailURL string `json:"ThumbnailURL" form:"ThumbnailURL" gorm:"column:thumbnai_url;comment:;size:150;"` //封面地址
	Upvotes      *int   `json:"Upvotes" form:"Upvotes" gorm:"column:upvotes;comment:;"`                         //点赞
	Saves        *int   `json:"Saves" form:"Saves" gorm:"column:saves;comment:;"`                               //收藏数
	Uses         *int   `json:"Uses" form:"Uses" gorm:"column:uses;comment:;"`                                  //使用数
	Views        *int   `json:"Views" form:"Views" gorm:"column:views;comment:;"`                               //查看数
	Nsfw         *bool  `json:"Nsfw" form:"Nsfw" gorm:"column:is_nsfw;comment:;"`                               //敏感数据
	Type         *int   `json:"Type" form:"Type" gorm:"column:type;comment:;size:1;"`                           //提示词类型
	Available    *bool  `json:"Available" form:"Available" gorm:"column:available;comment:;default:true"`       //可用状态 默认为true
	Body         string `json:"Body" form:"Body" gorm:"column:body;comment:;type:text;"`                        //提示词正文

}
```

如果项目很复杂，还可以独立出DAO层，它可以：

* **提高代码复用性**：DAO 层可以对数据库操作进行封装，提高代码复用性。我个人很讨厌业务中的各种复制粘贴，这种项目的后期维护成本非常高，经常会出现莫名其妙的问题。
* **解耦业务逻辑和数据库操作**：DAO 层负责与数据库交互，将数据库操作与业务逻辑分离，可以使业务逻辑更简洁、易于维护。
* **提高代码可测试性**：DAO 层的代码可以单独进行测试，提高代码可测试性。

你如果有过更换数据源，或者要为某些查询逻辑进行修改的经历，那你就知道同一套逻辑在不同地方复制粘贴时要修改时的痛苦。而封装这些接口，只需要管理传入参数和返回参数，中间的整个过程都不需要业务层去考虑，非常适合团队协作，避免出现屎山代码。

当然，因为我们项目的复杂度相对较低，所以可以直接在上层封装Service层。

```go
// CreatePrompts 创建提示词记录
// Author [piexlmax](https://github.com/piexlmax)
func (promptsService *PromptsService) CreatePrompts(prompt *onion.Prompts) (err error) {
	err = global.GVA_DB.Create(prompt).Error

	return err
}
```

这一层是可以封装较为复杂的业务逻辑的，它跟数据库无关，而跟业务有关，关心的是某个具体的业务的实现。而在其上的控制器(Controller)相对而言就更不关系数据村在哪里，而是关心数据从哪里来，

```go
package onion
type PromptsApi struct {
}

func (promptsApi *PromptsApi) CreatePrompts(c *gin.Context) {
	// 准备prompts容器并且绑定数据
	var prompts onion.Prompts
	err = c.ShouldBindJSON(&prompts)
	// 如果绑定出现错误
	if err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	// 对数据进行校验
	verify := utils.Rules{
		"Title": {utils.NotEmpty()},
	}
	if err := utils.Verify(prompts, verify); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	// 调用Service创建提示词
	if err := promptsService.CreatePrompts(&prompts); err != nil {
		fmt.Println("开始创建提示")
		global.GVA_LOG.Error(global.Translate("general.creationFail"), zap.Error(err))
		response.FailWithMessage(global.Translate("general.creationFailErr"), c)
	} else {
		response.OkWithMessage(global.Translate("general.createSuccss"), c)
	}
}
```

这样最简单的实现就完成了。 我们只需要加入路由，让其能被访问就好。

```go
// InitPromptsRouter 初始化 提示词 路由信息
func (s *PromptsRouter) InitPromptsRouter(Router *gin.RouterGroup) {
	promptsRouter := Router.Group("prompts").Use(middleware.OperationRecord())
	promptsRouterWithoutRecord := Router.Group("prompts")
	var promptsApi = v1.ApiGroupApp.OnionApiGroup.PromptsApi
	{
		promptsRouter.POST("createPrompts", promptsApi.CreatePrompts)             // 新建提示词
		promptsRouter.DELETE("deletePrompts", promptsApi.DeletePrompts)           // 删除提示词
		promptsRouter.DELETE("deletePromptsByIds", promptsApi.DeletePromptsByIds) // 批量删除提示词
		promptsRouter.PUT("updatePrompts", promptsApi.UpdatePrompts)              // 更新提示词
	}
	{
		promptsRouterWithoutRecord.GET("findPrompts", promptsApi.FindPrompts)       // 根据ID获取提示词
		promptsRouterWithoutRecord.GET("getPromptsList", promptsApi.GetPromptsList) // 获取提示词列表
	}
}

```

当然，如果想要简化Http请求，只用`GET`和`POST`其实也足够了，删除和修改不使用`DELETE`和`PUT`，而使用POST请求也是没有问题的。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://walkman42.gitbook.io/best-practice-in-7days/mainbody/day4/4.1-design-patterns-and-architectural-patterns.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
