建造者模式

编程中什么是 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))

所以我们会发现,设计模式会让我们代码变得很优雅,少写冗余代码,我们只用一个函数,而且并不复杂的逻辑,以及可扩展性非常好,就完成了精准查询和模糊查询,而不是类似函数的重复堆砌