项目开发流程思路 | 青训营笔记

105 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天

此次课程主要呈现了一个项目的开发思路与基本流程。

需求描述

构建社区话题页面,实现功能为:展示话题(标题,文字描述)和回帖列表,实现一个本地的web服务。话题和回帖数据用文件存储。

需求设计

ER图(Entity Relationship Diagram) 设计了概念模型。基于ER图可以有效描述现实世界的概念模型,包括实体的属性和实体的联系。话题页抽取出实体topic,postList,可以显示Topic和Post的属性以及两者的一对多关系。

结构设计

常用分层模型包括repository数据层,service逻辑层和controller视图层。

  • 数据层:主要关联数据模型,封装外部数据的增删查改,拉取数据,面向逻辑层并屏蔽数据差异
  • 逻辑层:负责处理核心业务逻辑,计算打包业务实体entity,处理核心业务逻辑输出。
  • 视图层:包装Json格式化的请求结果,以API方式处理和外部的交互逻辑

代码开发

组件工具上主要基于高性能web框架gin。

data

创建data文件夹,定义topic和post的数据模型结构,以Json格式保存在文件内:

data/topic:
{
	"id":1,
	"title":"青训营来啦!",
	"content":"新同学冲冲!",
	"create_time":1650437625
}

{
	"id":2,
	"title":"青训营来啦!",
	"content":"老同学冲冲!",
	"create_time":1650437627
}

data/post:

Post:
{
	"id":1,
	"parent_id":1,
	"content":"好期待呀!",
	"create_time":1650437616
}

{
	"id":2,
	"parent_id":1,
	"content":"666!",
	"create_time":1650437617
}

{
	"id":3,
	"parent_id":2,
	"content":"真不错!",
	"create_time":1650437618
}

{
	"id":4,
	"parent_id":2,
	"content":"下次继续!",
	"create_time":1650437619
}

repository

repository部分,以topic为例,其主要功能包括初始化和查询。查询功能主要基于索引,按照topic的id查找对应结构体返回。为实现O(1)的时间复杂度,采用Map的数据结构,对Topic数据建立内存Map作为索引,这是基于data初始化索引的工作,Post部分同理,初始化工作如下:

创建索引,其中Topic和Post的数据结构在各自的文件中定义,列举如下:

//topic.go
type Topic struct {
	Id         int64  `json:"id"`
	Title      string `json:"title"`
	Content    string `json:"content"`
	CreateTime int64  `json:"create_time"`
}

//init_db.go
var(
	topicIndexMap map[int64]*Topic
	postIndexMap map[int64][]*Post
    rwMutex sync.RWMutex//
)

初始化话题数据索引的函数

//init_db.go
func initTopicIndexMap(filePath string) error{
	open, err := os.Open(filePath + "topic")
	if err != nil{
		return err
	}
	
	scanner := bufio.NewScanner(open)
	topicTmpMap := make(map[int64]*Topic)
	for scannner.Scan() {
		text:=scanner.Text()
		var topic Topic
		if err:= json.Unmarshal([]byte(text), &topic); err != nil{
		return err
	}
        topicTmpMap[topic.Id] = &topic
    }   
    topicIndexMap = topicTmpMap
    return nil
}

初始化回帖列表内存索引的函数同理,通过查询回帖对应的话题id(parent_id),返回一组回帖数组,每个数组的结构为指向post的指针。

//init_db.go
func initPostIndexMap(filePath string) error{
	open, err := os.Open(filePath + "post")
	if err != nil{
		return err
	}
	
	scanner := bufio.NewScanner(open)
	postTmpMap := make(map[int64][]*Post)
	for scannner.Scan() {
		text:=scanner.Text()
		var post Post
		if err:= json.Unmarshal([]byte(text), &post); err != nil{
		return err
	}
        posts,ok := postTmpMap[post.PartentId]//对键值为切片的操作方法
        if !ok{
            postTmpMap[post.ParentId] = []*Post{&post}
            continue
        }
        posts = append(posts, &post)
        postTmpMap[post.ParentId] = posts
    }   
    postIndexMap = postTmpMap
    return nil
}

查询功能部分,其中sync.once主要适用于高并发场景下只执行一次的场景,once即为单例模式。以topic为例,设计了topic的json格式,并保证只初始化一次topicDao,然后基于索引实现查询函数QueryTopicById。

//topic.go
type Topic struct{
    Id int64 `json:"id"`
    Title string `json:"title"`
    Content string `json:"Content"`
    CreateTime int64 `json:"create_time"`
}
type TopicDao struct{
}
var (
	topicDao *TopicDao
    topicOnce sync.Once
)
func NewTopicDaoInstance()* TopicDao{
    topicOnce.Do(
        func(){
            topicDao = &TopicDao{}
        }
    )
    return topicDao
}
func (*TopicDao) QueryTopicById(id int64) * Topic{
    return topicIndexMap[id]
}

service

service层负责组建实体话题页信息,内容主要为Topic和PostList部分:

type PageInfo struct{
	Topic *repository.Topic
	PostList []*repository.Post
}

service中执行函数的过程包括参数校验,准备数据和组装实体,其中参数校验checkParam部分是保证服务安全性的,准备数据prepareInfo,通过调用repository层的因为话题和回帖信息的获取都依赖topic id,而且可以并行执行来提高执行效率,通过流程合并可以降低接口耗时,充分利用CPU资源。而组装实体packPageInfo将信息准备为合适的格式并发送给controller层。

func QueryPageInfo(topicId int64) (*PageInfo, error) {
	return NewQueryPageInfoFlow(topicId).Do()
}

func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
	return &QueryPageInfoFlow{
		topicId: topId,
	}
}

func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
    if err:=f.checkParam(); err != nil{//id校验,保证服务
        return nil, err
    }
    if err:= f.prepareInfo(); err != nil{//准备数据
        return  nil, err
    }
    if err:= f.packPageInfo(); err != nil{
        return nil, err
    }
    return f.pageInfo, nil
}
//参数校验
func (f *QueryPageInfoFlow) checkParam() error {
	if f.topicId <= 0 {
		return errors.New("topic id must be larger than 0")
	}
	return nil
}
//准备信息
func (f *QueryPageInfoFlow) prepareInfo() error{
var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		topic := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
		f.topic = topic
	}()
	//获取post列表
	go func() {
		defer wg.Done()
		posts := repository.NewPostDaoInstance().QueryPostsByParentId(f.topicId)
		f.posts = posts
	}()
	wg.Wait()
	return nil
}
//打包信息
func (f *QueryPageInfoFlow) packPageInfo() error {
	f.pageInfo = &PageInfo{
		Topic:    f.topic,
		PostList: f.posts,
	}
	return nil
}

controller

在QueryPageInfo部分,基于topic的id信息,调用service层的 QueryPageInfo函数,并返回PageInfo,再封装成PageData格式的响应信息。其包含响应代码Code,显示信息Msg以及数据接口Data。

//query_page_info.go

type PageData struct{
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}
func QueryPageInfo(topicIdStr string) *PageData {
	topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}
	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: pageInfo,
	}

}

主函数

主函数的实现主要包括基于gin框架的初始化数据索引,初始化引擎配置,构建路由以及启动服务。

package main

import (
	"github.com/Moonlight-Zhao/go-project-example/cotroller"
	"github.com/Moonlight-Zhao/go-project-example/repository"
	"gopkg.in/gin-gonic/gin.v1"
	"os"
)

func main() {
	if err := Init("./data/"); err != nil {
		os.Exit(-1)
	}
	r := gin.Default()
	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")
		data := cotroller.QueryPageInfo(topicId)
		c.JSON(200, data)
	})
	err := r.Run()
	if err != nil {
		return
	}
}

func Init(filePath string) error {
	if err := repository.Init(filePath); err != nil {
		return err
	}
	return nil
}

以上即为创建可查询话题下回帖信息的接口的实现过程。

参考资料

字节青训营课程:juejin.cn/course/byte…