建造者模式
编程中什么是 Context(上下文)
正文
这篇文章引起我对上下文的思考,首先我们先聊聊,上下文是什么,这里我引用其中的一句话
在做决策的步骤中,一个关键的步骤就是根据足够多的信息做决策。所有必要的信息统称为【上下文】。
比如说,你现在要查询一个用户,那么ID就可以是上下文,我们根据ID来去数据库中查询数据,这个就是我们做出决策的上下文,而如果这个上下文一旦变得复杂,繁多,维护这个上下文就变得困难,甚至低效。而很多场景,我们的上下文不是像上面的简单,它可以变得很多,而且他的数量也可以不固定,比如说,查询一个用户一定只能用ID吗,phone行吗,难道我们又去编写getuserByPhone(phone string)吗,又比如说,个数很多怎么办,难道要将这个函数参数列表撑爆吗?这样的写法既不美观,也很低效。
文章里讲了一些解决办法,这里我主要讲这个:要利用对象来构造一套上下文,支撑相关的所有业务逻辑。
我们就一边改造一边学习
开始我们只有一个函数GetUser
1type Dao struct{
2 db *gorm.DB
3}
4
5func (d *Dao) GetUser(ctx context.Context,id uint64) (User,error)
现在我们加需求,除了精确查询,这个还要支持模糊查询,比如说根据name,gender等,模糊查询还要支持联合查询,比如name = "123" and gender = "man"
那我们来利用对象来构造上下文,来改造一下
1type SearchCtx struct{
2 ByPrimaryKey bool //是否根据主键查询
3 ID uint64
4 Name string
5 Gender string
6}
7
8func (d *Dao) GetUser(ctx context.Context, searchCtx SearchCtx) ([]User,error) {
9 panic("implement me")
10}
我们现在想一下SearchCtx怎么构造?
既然我们在学习建造者模式,我们来用建造者模式来构建SearchCtx ,所以我们添加下列代码
1type SearchCtxBuilder struct {
2 SearchCtx
3}
4
5func NewSearchCtxBuilder() *SearchCtxBuilder {
6 return &SearchCtxBuilder{}
7}
8
9func (b *SearchCtxBuilder) WithByPrimaryKey(id uint64) *SearchCtxBuilder {
10 b.ByPrimaryKey = true
11 b.ID = id
12 return b
13}
14
15func (b *SearchCtxBuilder) WithName(name string) *SearchCtxBuilder {
16 b.Name = name
17 return b
18}
19
20func (b *SearchCtxBuilder) WithGender(gender string) *SearchCtxBuilder {
21 b.Gender = gender
22 return b
23}
24func (b *SearchCtxBuilder) Build() SearchCtx {
25 return SearchCtx{
26 ByPrimaryKey: b.ByPrimaryKey,
27 ID: b.ID,
28 Name: b.Name,
29 Gender: b.Gender,
30 }
31}
因此,我们的GetUser 可以这样调用
1users,err := dao.GetUser(context.Background(),NewSearchCtxBuilder().
2 WithByPrimaryKey(1).Build())
3
4//或者这样
5users,err := dao.GetUser(context.Background(), NewSearchCtxBuilder().
6 WithName("123").WithGender("man").Build())
看起来很有条理,在构建的时候,就不必关心一些可选字段了,而且这样链式调用,也使得代码流畅,符合自然思维
接下来我们再把实现完成下
1func (d *Dao) GetUser(ctx context.Context, searchCtx SearchCtx) ([]User, error) {
2 db := d.db.WithContext(ctx)
3 var users []User
4 if searchCtx.ByPrimaryKey {
5 var user User
6 err := db.Model(&User{}).Where("id = ?",searchCtx.ID).First(&user).Error
7 if err!=nil {
8 return nil,err
9 }
10 users = append(users, user)
11 }else {
12 //两个条件可以叠加
13 if searchCtx.Name != "" {
14 db = db.Where("name = ?", searchCtx.Name)
15 }
16 if searchCtx.Gender != "" {
17 db = db.Where("gender = ?", searchCtx.Gender)
18 }
19 err := db.Model(&User{}).Find(&users).Error
20 if err!=nil {
21 return nil,err
22 }
23 }
24 return users, nil
25}
补充 options 模式
接下来给大家介绍另一种建造者模式的实现思路,这是我个人更加推崇的一种编程风格:Options 模式,当然上面也很不错
SearchCtx 不变
添加新的类,可供大家参考
1type SearchCtxOpts struct {
2 SearchCtx
3}
4
5type SearchCtxOpt func(*SearchCtxOpts)
6
7func WithByPrimaryKey(id uint64) SearchCtxOpt {
8 return func(opts *SearchCtxOpts) {
9 opts.ByPrimaryKey = true
10 opts.ID = id
11 }
12}
13func WithName(name string) SearchCtxOpt {
14 return func(opts *SearchCtxOpts) {
15 opts.Name = name
16 }
17}
18func WithGender(gender string) SearchCtxOpt {
19 return func(opts *SearchCtxOpts) {
20 opts.Gender = gender
21 }
22}
23func NewSearchCtx(opts ...SearchCtxOpt) SearchCtx {
24 opt := &SearchCtxOpts{}
25 for _, o := range opts {
26 o(opt)
27 }
28 return SearchCtx{
29 ByPrimaryKey: opt.ByPrimaryKey,
30 ID: opt.ID,
31 Name: opt.Name,
32 Gender: opt.Gender,
33 }
34}
同样的,给出如何构造
1users,err := dao.GetUser(context.Background(),
2 NewSearchCtx(WithByPrimaryKey(1)))
3users,err := dao.GetUser(context.Background(),
4 NewSearchCtx(WithName("123"), WithGender("man")))
小问题:如果模糊查询的字段增多了怎么办,难道还一个一个if吗?
原始的:
1func (d *Dao) GetUser(ctx context.Context, searchCtx SearchCtx) ([]User, error) {
2 db := d.db.WithContext(ctx)
3 var users []User
4 if searchCtx.ByPrimaryKey {
5 var user User
6 err := db.Model(&User{}).Where("id = ?",searchCtx.ID).First(&user).Error
7 if err!=nil {
8 return nil,err
9 }
10 users = append(users, user)
11 }else {
12 //两个条件可以叠加
13 if searchCtx.Name != "" {
14 db = db.Where("name = ?", searchCtx.Name)
15 }
16 if searchCtx.Gender != "" {
17 db = db.Where("gender = ?", searchCtx.Gender)
18 }
19 err := db.Model(&User{}).Find(&users).Error
20 if err!=nil {
21 return nil,err
22 }
23 }
24 return users, nil
25}
改造:
1type QueryOption func(db *gorm.DB)
2
3type SearchCtx struct {
4 ByPrimaryKey bool //是否根据主键查询
5 ID uint64
6 QueryOptions []QueryOption
7}
8
9func QueryName(name string) QueryOption {
10 return func(db *gorm.DB) {
11 db.Where("name = ?", name)
12 }
13}
14func QueryGender(gender string) QueryOption {
15 return func(db *gorm.DB) {
16 db.Where("gender = ?", gender)
17 }
18}
19
20
21
22
23type SearchCtxOpts struct {
24 SearchCtx
25}
26
27type SearchCtxOpt func(*SearchCtxOpts)
28
29func WithByPrimaryKey(id uint64) SearchCtxOpt {
30 return func(opts *SearchCtxOpts) {
31 opts.ByPrimaryKey = true
32 opts.ID = id
33 }
34}
35func WithQueryOptions(qopts ...QueryOption) SearchCtxOpt {
36 return func(opts *SearchCtxOpts) {
37 opts.QueryOptions = qopts
38 }
39}
40func NewSearchCtx(opts ...SearchCtxOpt) SearchCtx {
41 opt := &SearchCtxOpts{}
42 for _, o := range opts {
43 o(opt)
44 }
45 return SearchCtx{
46 ByPrimaryKey: opt.ByPrimaryKey,
47 ID: opt.ID,
48 QueryOptions: opt.QueryOptions,
49 }
50}
51
52
53
54func (d *Dao) GetUser(ctx context.Context, searchCtx SearchCtx) ([]User, error) {
55 db := d.db.WithContext(ctx)
56 var users []User
57 if searchCtx.ByPrimaryKey {
58 var user User
59 err := db.Model(&User{}).Where("id = ?", searchCtx.ID).First(&user).Error
60 if err != nil {
61 return nil, err
62 }
63 users = append(users, user)
64 } else {
65 for _, opt := range searchCtx.QueryOptions {
66 opt(db)
67 }
68 err := db.Model(&User{}).Find(&users).Error
69 if err != nil {
70 return nil, err
71 }
72 }
73 return users, nil
74}
75
76
77user,err := dao.GetUser(context.Background(),NewSearchCtx(
78 WithQueryOptions(QueryName("123",
79 QueryGender("man"))),
80))
所以我们会发现,设计模式会让我们代码变得很优雅,少写冗余代码,我们只用一个函数,而且并不复杂的逻辑,以及可扩展性非常好,就完成了精准查询和模糊查询,而不是类似函数的重复堆砌