提示

分享到:

提示

提示

阅读这篇文章,需要有以下准备:

  1. 在微服务下挣扎过
  2. 需要了解DDD和COLA架构思想
  3. 本篇文章围绕应用架构进行讨论

前言

    公司在开始探索微服务架构时,使用的是三层架构(controller/service/model)。随着时间的推移,发现三层应用架构在微服务架构下越来越不适用,主要体现在下面2点:

  1. 业务逻辑离散在service层,不能很好的复用和表达能力差
  2. 业务代码和技术实现进行了强耦合,导致调试和测试困难

针对以上问题,我们开始探索新的应用架构,整理形成我们自己研发的业务框架:Esim (Make everything simple)。

回顾

此处输入图片的描述

    我对应用架构的思考,来自一道比较经典的面试题:什么是MVC?估计刚毕业的同学,都避免不了这道面试题。当然时间总是飞逝的,从毕业到现在,经历了PC时代,移动时代,到现在的微服务时代的技术变迁。技术的层出不穷,让我应接不暇。在回顾这个变迁的过程中发现一些比较有趣的事情,所以拿出来分享:

  • 架构一直在演进

    之所以用“演进”这个词,是因为新的架构思想需要一步一步形成,换句话说需要时间。我们以三层架构开始探索微服务,用了2年多,因为越来越痛苦才开始探索新的应用架构,但也花了1年多的时间,才有一个成形的框架。

  • 都是围绕模型,行为,数据进行变化

    自从把数据,模型,行为3兄弟从大杂烩解放出来后,他们就一直缠着你,这种纠缠很有可能伴随你的整个职业生涯。在PC时代,我们把3兄弟放到model里,所以当时有胖M,廋C的说法,有了经验后,在移动时代,我们把行为抽出来放到service,model留下数据和模型,再到现在的微服务时代,我们把行为和模型放到domain,数据放到了infrastructure。整个演进过程都围绕着这3兄弟。

  • 边界明显

    不同时代的的架构边界很清晰,PC时代说的是职责分离,移动时代说的是前后端分离,微服务时代说的是业务逻辑和技术分离。这些边界的出现和当时的环境脱不了关系。

事务脚本领域模型

使用过程来组织业务逻辑,每个过程处理来自表现层的请求。

事务脚本胜在简单,也正是简单,身边的很多同事也在使用相同的方式来组织代码,我自己也沉浸在里面很长时间,没有思考是否有更好的方式(需要吸取这个教训)。

在领域中合并了数据和行为的对象模型

领域模型强调的是组织业务逻辑前,先关注对象的行为,而事务脚本关注数据。

  • 例子

以我们最近重构的红包业务逻辑举个例看看他们之间的区别:只能在指定的洗车业务和A商家才能使用该红包。

事务脚本实现

 1couponService.go
 2//是否满足红包使用条件
 3func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
 4    couponInfo := cs.CouponDao.FindById(couponId)
 5    ......
 6    couponConfInfo := cs.CouponConfigDao.FindById(couponInfo.ConfigId)
 7    ......
 8    //空代表所有业务都可以
 9    if couponConfInfo.allowBussiness == "" {
10        return true
11    }
12    
13    if bussinessType == "" {
14        return false
15    }
16    
17    var allowBussiness bool
18    allowBussinesses := strings.Split(couponConfInfo.allowBussiness, ",")
19    for _, val := range allowBussinesses {
20        if bussinessType == val {
21            allowBussiness = true
22        }
23    }
24    
25    if inBusiness == false {
26        return false
27    }
28    
29    //空所有商家都允许使用
30    if couponConfInfo.allowSellers == "" {
31        return true
32    }
33    
34    if sellerId == "" {
35        return false
36    }
37    
38    var allowSeller bool
39    allowSellers := strings.Split(couponConfInfo.allowSellers, ",")
40    for _, seller := range allowSellers {
41        if sellerId == seller {
42            allowSeller = true
43        }
44    }
45    
46    if allowSeller == true {
47        return true
48    } else {
49        return false
50    }
51}

    上面的代码就是比较典型的”一杆到底“,这样形式的代码在我们的系统很常见。 经常导致业务逻辑的代码不能很好的复用,业务逻辑分散在多个不同的方法或service文件里,很少有人能把他们慢慢找出来, 封装成共用方法。即使找到了又不敢轻易的把它们提取出来,因为它有可能和其他业务逻辑已经绑在了一起。

    当你抱着提升代码质量的情怀把它们提取出来,又因为没有很好的方法验证是否会影响了原有的业务逻辑。 导致出了很多次和原来预期对不上的问题(当时个个都坚信不会出问题),也让很多同学对自己产生了怀疑。 所以为了避免这些问题发生,我们通常对这些能复用的代码睁一只眼闭一眼,包括我自己。

领域模型的实现

 1coupon_service.go
 2//是否满足红包使用条件
 3func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
 4    couponInfo := cs.CouponDao.FindById(couponId)
 5    ......
 6    couponConfInfo := cs.CouponConfigDao.FindById(couponInfo.ConfigId)
 7    ......
 8    if couponConfInfo.CheckAllowBusiness(bussinessType) == false {
 9        return false
10    }
11  
12    if couponConfInfo.CheckAllowSeller(sellerId) == false {
13        return false
14    } 
15    
16    return true
17}
 1entity/coupon_config.go
 2type CouponConfig struct {
 3    id int 
 4    
 5    allowBussiness string
 6    
 7    allowSellers string
 8    
 9    ......
10}
11
12func (cc CouponConfig) CheckAllowBusiness(bussinessType string) bool {
13    //所有业务都可以
14    if cc.allowBussiness == "" {
15        return true
16    }
17    
18    if bussinessType == "" {
19        return false
20    }
21    
22    allowBussinesses := strings.Split(cc.allowBussiness, ",")
23    for _, val := range allowBussinesses {
24        if bussinessType == val {
25            return true
26        }
27    }
28    
29    return false
30}
31
32func (cc CouponConfig) CheckAllowSeller(sellerId string) bool {
33    //所有商家都允许使用
34    if cc.allowSellers == "" {
35        return true
36    }
37    
38    if sellerId == "" {
39        return false
40    }
41    
42    allowSellers := strings.Split(cc.allowSellers, ",")
43    for _, seller := range allowSellers {
44        if sellerId == seller {
45            return true
46        }
47    }
48    
49    return false
50}

    从上面的代码可以看出,我们把原来在coupon_service.go的业务逻辑都放到了实体coupon_config.go里面(行为和模型绑在了一起)。 业务逻辑不再离散,更内聚,能很好的复用,且写单元测试变得简单。

 1entity/coupon_config_test.go
 2func TestEntity_CheckAllowSeller(t *testing.T)  {
 3	testCases := []struct{
 4		caseName string
 5		sellerId string
 6		allowSellers string
 7		expected bool
 8	}{
 9		{"允许—空", "100", "", true},
10		{"允许2", "1", "1,100", true},
11		{"不允许", "1", "2,3,4", false},
12	}
13
14	for _, test := range testCases{
15		t.Run(test.caseName, func(t *testing.T) {
16			cc := CouponConfig{}
17			cc.allowSellers = test.allowSellers
18			result := cc.CheckAllowSeller(test.sellerId)
19			assert.Equal(t, test.expected, result)
20		})
21	}
22}

领域模型让我们写单元测试的时候不再关注所依赖的存储实现,让写单元测试这件事变得轻松、简单。

三层架构 到 四层架构

三层架构和四层架构一个明显的区别是业务和实现技术分离。

    在三层架构,业务和实现技术进行了强耦合,让开发在调试和测试时都要依赖真实的服务,导致浪费了很多时间在部署服务,造数据环节上,这个问题在微服务架构下更突出。四层架构可以很好的解决这个问题。还是以上面的代码为例(直接依赖了mysql):

 1coupon_service.go
 2//是否满足红包使用条件
 3func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
 4    couponInfo := cs.CouponRepo.FindById(couponId)
 5    ......
 6    couponConfInfo := cs.CouponConfigDao.FindById(couponInfo.ConfigId)
 7    ......
 8    
 9    return true
10}

三层实现测试IsSatisfyUse(使用gorm的mock SDK):

 1coupon_service_test.go
 2func TestCouponRepo_IsSatisfyUse(t *testing.T) {
 3  cs := NewCouponService()
 4  ......
 5  couponReply := []map[string]interface{}{{"config_id": "100"}}
 6  couonConfigReply := []map[string]interface{}{{"allow_bussinesses": "1,2", "allow_sellers" : "1,2"}}
 7  Catcher.Attach([]*FakeResponse{
 8		{
 9			Pattern:"SELECT * FROM coupon WHERE", 
10			Response: couponReply, 
11			Once: false, 
12		},
13		{
14			Pattern:"SELECT * FROM coupon_config WHERE", 
15			Response: couonConfigReply, 
16			Once: false, 
17		},
18	})
19    
20  result := cs.IsSatisfyUse(100, "1", "1")
21  assert.Equal(t, true, result)
22}

    上面的代码问题在于:如果业务代码依赖了某个技术实现,就要用对应的mock SDK来写单元测试。 只依赖一个mysql可能不会有太大问题,但技术发展到现在,业务逻辑基本不可能只依赖mysql。 还有可能是:redis,mongodb,http,grpc等,这说明你需要学习各式各样的mock SDk。 我当初就被这些海量的SDK,折腾的异常痛苦。也是这个原因才去寻找更好的办法:分离业务逻辑和技术实现。

四层实现 IsSatisfyUse(使用依赖倒置)

 1coupon_service.go
 2//是否满足红包使用条件
 3func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
 4    couponInfo := cs.CouponRepo.FindById(couponId)
 5    ......
 6    couponConfInfo := cs.CouponConfigRepo.FindById(couponInfo.ConfigId)
 7    ......
 8    return true
 9}
10
11infra/repo/coupon_repo.go
12//定义接口
13type CouponRepo interface {
14	FindById(int64) entity.Coupon
15}
16
17//db实现
18type DBCouponRepo struct {
19	couponDao *dao.CouponDao
20}
21
22func (dcr *DBCouponRepo) FindById(id int64) entity.Coupon {
23  ......
24  coupon, err = dcr.couponDao.Find("*", "id = ? ", id)
25  ......
26  return coupon
27}
28
29//coupon_config 同理

四层实现测试IsSatisfyUse(使用mockery SDK):

 1coupon_service_test.go
 2func TestCouponRepo_IsSatisfyUse(t *testing.T) {
 3  cs := NewCouponService()
 4  ......
 5  couponRepo := &mocks.CouponRepo{}
 6  couponRepo.On("FindById", int64(100)).Return(entity.Coupon{ConfigId : 100})
 7  cs.CouponRepo = couponRepo
 8  
 9  couponConfigRepo := &mocks.CouponConfigRepo{}
10  couponConfigRepo.On("FindById", int64(100)).Return(entity.CouponConfig{AllowBussiness : "1", "AllowSellers" : "1"})
11  cs.CouponConfigRepo = couponConfigRepo
12  
13  result := cs.IsSatisfyUse(100, "1", "1")
14  assert.Equal(t, true, result)
15}

    通过依赖倒置将具体的技术实现和业务分离,你将不再需要学习各式各样的mock SDK。 使用这种方式还有其他好处:

  1. 如果你要从mysql切换成其他存储层,只需要重新实现CouponRepo就可以了。 不需要改动任何业务逻辑,且TestCouponRepo_IsSatisfyUse,还能正常使用。
  2. 使用接口分离技术实现,可以让你在开发过程不用关注依赖的服务是否可用,非常的便利。

结语

领域模型和四层架构可以很好的解决了我们当前存在的问题,但它们也存在其他问题:

  1. 有一定的学习成本

有学习成本的一个原因是:现在大量的开发都是在使用事务脚本和三层架构做业务开发,要想转向领域模型和四层架构, 需要花点时间(他们向工程师提了要求),但是如果转成功了,将会对公司的业务代码在测试性和扩展性上有很大的提升。

  1. 增加了一些繁琐工作

四层比三层多了一些繁琐的文件创建:对每个资源都要提取接口和实现,依赖注入等,这些工作都很繁琐,所以我们才写了一个工具db2entity,把这些工作交由一个工具解决。

探索的过程可能很痛苦,但是探索出成果后会感到成就感,这估计就是探索的乐趣了,码农还需要前行。