背景

在开发过程中你是否有遇到过这样的苦恼?产品发来一个需求,没做过,但是看完需求感觉应该处理起来很简单,然后找到对应的业务代码,发现代码像打乱的毛线一样理不清楚,各种逻辑嵌套,各种特殊判断处理,想要拓展维护个内容却无从下手,一边看着代码,一边用手拨动着本就为数不多的秀发,然后口吐芬芳 。
今天你设计了吗? - 图1

有没发现一个问题,为什么业务不复杂,但是随着产品迭代,经过不断拓展和维护,慢慢的代码就越做越乱,你可以说产品想法天马星空,人员流动大,多人参与慢慢的就被做乱了,这可能是个不错的借口,但是其中本质的问题还是前期思考的太少,没有进行合理的抽象设计,没有去前瞻性的去预埋一些未来可拓展性的内容,所以最终导致了后来的局面。

经常听到有经验的开发者说开发前多思考,不要一拿到需求就习惯性的一顿操作,反手就定义一个function根据需求逻辑一条龙写到底。

所以面对相对复杂的需求我们需要进行抽象思考,尽可能做到设计出来的东西是解决一类问题,而不是单单解决当前问题,然后在代码实现上也要面向抽象开发,这样才能做到真正的高质量代码,可维护性和可拓展性高,才能沉淀出可复用,健壮性强的系统。

那么我们要如何去抽象呢?面对需求的抽象思维这个需要平时多锻炼,拿到需求多想多思考,不要急于求成,主要围绕着这几大要素:可维护性、可拓展性、可复用性,安全性去设计解决方案,至于代码上的抽象就可以使用下面的方式。

不卖关子了,是时候请出今天的主角:《设计模式》,简单的说设计模式就是开发者们的经验沉淀,通过学习设计模式并在业务开发过程中加以使用,可以让代码的实现更容易拓展和维护,提高整体代码质量,也可以作为开发之间沟通的专业术语,提到某个模式,可以马上get到代码设计,减少沟通的成本。

这里就不一一介绍23种设计模式和设计模式的6个原则,可以google回顾下

推荐:学习设计模式地址

下面就将结合当前项目的bad case,手把手的使用设计模式进行重构,其中会用到多种设计模式的使用,并且体现了设计模式的中的几个原则,做好准备,发车了。

举例

需求背景概要:

APP首页功能,用模块化的方式去管理配置,后台可以配置模块标识和模块排序,展示条件等,首页API接口获取当前用户的模块列表,并构造模块数据展示。

API Response Data

伪响应数据,忽略掉不重要或者重复的数据

  1. {
  2. "code": 0,
  3. "data": {
  4. "tools": {
  5. // -- 模块信息 --
  6. "id": 744,
  7. "icon": "",
  8. "name": "",
  9. "sub_title": "",
  10. "module": "lm_tools",
  11. "sort": 1,
  12. "is_lock": true,
  13. "is_show": true,
  14. "more_text": "",
  15. "more_uri": "xxx:///tools/more",
  16. "list": [
  17. // -- 模块展示数据 --
  18. ]
  19. },
  20. "my_baby": {
  21. // ... ...
  22. },
  23. "knowledge_parenting": {
  24. // ... ...
  25. },
  26. "early_due": {
  27. // ... ...
  28. },
  29. // ... ...
  30. "message": ""
  31. }

Before Code

伪代码,忽略掉一些不重要的code

  1. func (hm *HomeModule) GetHomeData() map[string]interface{} {
  2. result := make(map[string]interface{})
  3. // ... ...
  4. // 获取模块列表
  5. module := lm.GetHomeSortData()
  6. // ... ...
  7. // 构造每个模块的数据
  8. for _, module := range moduleList {
  9. // ... ...
  10. switch module.Module {
  11. case "my_baby":
  12. // ... ...
  13. result["my_baby"] = data
  14. case "lm_tools":
  15. // ... ...
  16. result["lm_tools"] = data
  17. case "weight":
  18. // ... ...
  19. result["weight"] = data
  20. case "diagnose":
  21. result["diagnose"] = data
  22. case "weather":
  23. // ... ...
  24. result["weather"] = data
  25. case "early_edu":
  26. // ... ...
  27. result["early_edu"] = data
  28. case "today_knowledge":
  29. // ... ...
  30. data["tips"]=list
  31. // ... ...
  32. data["life_video"]=lifeVideo
  33. // ... ...
  34. result["today_knowledge"] = data
  35. default:
  36. result[module.Module] = module
  37. }
  38. // ... ...
  39. return result
  40. }

看完这个代码,是否有一种要坏起来的味道,随着模块不断增加,case会越来越多,而且每个case里面又有一些针对版本、针对AB、一些特殊处理等,让代码变得又臭又长,越来越难去拓展和维护,并且每次维护或者拓展都可能在GetHomeData() 方法里在不断往里面添油加醋,不小心就会对整个接口产生影响。

那么我们要如何去重构呢,这就要抽象起来,这个业务本身就已经有模块相关抽象设计,这里就不进行调整,主要是针对代码上的抽象,结合设计模式进行改造。

以下就是重构的过程。

刚开始的时候,看到这种case 判断,然后做模块数据的聚合,我第一反应是,能否可以使用工厂模式,定义一个 interface,每个模块定义一个struct 实现接口ExportData() 方法,通过工厂方法去根据模块标识创建对象,然后调用导出数据方法进行数据上的聚合 。

但是在评估的过程中,发现有些模块数据里又聚合了多个不同业务知识内容的数据,单纯的工厂模式又不太合适,最后决定使用组合模式,结构型设计模式,可以将对象进行组合,实现一个类似层级对象关系,如:

  1. # 首页模块
  2. home
  3. - my_baby
  4. - weight
  5. - early_edu
  6. - today_knowledge
  7. - tips
  8. - life_video
  9. - weather
  10. - ... ...

这里我重新定义了下名词,后台配置的是模块,在代码实现上我把每个模块里展示的数据定义成 组件,组件又可以分成 单一组件 和 复合组件,复合组件就是使用了多个单一组件组成。

UML结构图:

今天你设计了吗? - 图2

Refactor After Code:

定义组件接口 IElement :

  1. // IElement 组件接口
  2. type IElement interface {
  3. // Add 添加组件,单一组件,可以不用实现具体方法内容
  4. Add(compUni string, compo IElement)
  5. // ExportData 输出组件数据
  6. ExportData(parameter map[string]interface{}) (interface{}, error)
  7. }

定义组件类型枚举

  1. // EElement 组件类型
  2. type EElement string
  3. const (
  4. EElementTips EElement = "tips" // 贴士
  5. EElementLifeVideo EElement = "life_video" // 生命一千天
  6. EElementEarlyEdu EElement = "early_edu" // 早教
  7. EElementBaby EElement = "baby" // 宝宝
  8. ECompositeTodayKnowledge EElement = "today_knowledge" // 今日知识
  9. // ....
  10. )
  11. func (ec EElement) ToStr() string {
  12. return string(ec)
  13. }

单一组件的实现

  1. // ElemTips 贴士组件
  2. type ElemTips struct {
  3. }
  4. func NewCompoTips() *ElementTips {
  5. return &ElementTips{}
  6. }
  7. func (c *ElementTips) Add(compoUni string, comp IElement) {
  8. }
  9. func (c ElementTips) ExportData(parameter map[string]interface{}) (interface{}, error) {
  10. tips := []map[string]interface{}{
  11. {
  12. "id": 1,
  13. "title": "贴士1",
  14. },
  15. {
  16. "id": 2,
  17. "title": "贴士2",
  18. },
  19. {
  20. "id": 3,
  21. "title": "贴士3",
  22. },
  23. {
  24. "id": 4,
  25. "title": "贴士4",
  26. },
  27. }
  28. return tips, nil
  29. }
  30. // ElemLifeVideo 生命一千天组件
  31. type ElemLifeVideo struct {
  32. }
  33. func NewCompoLifeVideo() *ElementLifeVideo {
  34. return &ElementLifeVideo{}
  35. }
  36. func (c ElementLifeVideo) Add(compoUni string, comp IElement) {
  37. }
  38. func (c ElementLifeVideo) ExportData(parameter map[string]interface{}) (interface{}, error) {
  39. lifeVideos := []map[string]interface{}{
  40. {
  41. "id": 1,
  42. "title": "生命一千天1",
  43. },
  44. {
  45. "id": 2,
  46. "title": "生命一千天2",
  47. },
  48. {
  49. "id": 3,
  50. "title": "生命一千天3",
  51. },
  52. {
  53. "id": 4,
  54. "title": "生命一千天4",
  55. },
  56. }
  57. return lifeVideos, nil
  58. }
  59. // ... ...

复合组件:

  1. // 今日知识,组合多个dan'yi组件
  2. type ElemTodayKnowledge struct {
  3. Composite map[string]IElement
  4. }
  5. func NewCompoTodayKnowledge() *ElemTodayKnowledge {
  6. factory := NewElementFactory()
  7. c := new(ElemTodayKnowledge)
  8. c.Add(EElementTips.ToStr(), factory.CreateElement(EElementTips.ToStr()))
  9. c.Add(EElementEarlyEdu.ToStr(), factory.CreateElement(EElementEarlyEdu.ToStr()))
  10. return c
  11. }
  12. func (c *ElemTodayKnowledge) Add(compoUni string, comp IElement) {
  13. if c.Composite == nil {
  14. c.Composite = map[string]IElement{}
  15. }
  16. c.Composite[compoUni] = comp
  17. }
  18. func (c ElemTodayKnowledge) ExportData(parameter map[string]interface{}) (interface{}, error) {
  19. data := map[string]interface{}{}
  20. for uni, compo := range c.Composite {
  21. data[uni], _ = compo.ExportData(parameter)
  22. }
  23. return data, nil
  24. }

因为有些知识数据的内容已经有相关实现,并且可以构造对象进行调用,我们需要做的是根据组件需求适配成组件需要的数据结构进行输出,这里又引入了适配器模式,可以使用适配器模式,将其适配成当前组件需要的数据结构输出。

  1. // ElemEarlyDduAdapter 早教组件 - 适配
  2. type ElemEarlyDduAdapter struct {
  3. edu earlyEdu.ThemeManager
  4. }
  5. func NewElementLifeVideoAdapter(edu earlyEdu.ThemeManager) *ElemEarlyDduAdapter {
  6. return &ElemEarlyDduAdapter{edu: edu}
  7. }
  8. func (c ElemEarlyDduAdapter) Add(compoUni string, comp IElement) {
  9. }
  10. func (c ElemEarlyDduAdapter) ExportData(parameter map[string]interface{}) (interface{}, error) {
  11. age, ok := parameter["age"].(uint32)
  12. if !ok {
  13. return nil, errors.New("缺少age")
  14. }
  15. birthday, ok := parameter["birthday"].(string)
  16. if !ok {
  17. return nil, errors.New("缺少birthday")
  18. }
  19. list := c.edu.GetList(age, birthday)
  20. return list, nil
  21. }

对象的创建需要进行统一管理,便于后续的拓展和替换,这里引入工厂模式,封装组件的对象创建,通过工厂方法去创建组件对象。

  1. // ElemFactory 组件工厂
  2. type ElemFactory struct {
  3. }
  4. func NewElementFactory() *ElemFactory {
  5. return &ElemFactory{}
  6. }
  7. // CreateElement 内容组件对象工厂
  8. func (e ElemFactory) CreateElement(compType string) IElement {
  9. switch compType {
  10. case EElementBaby.ToStr():
  11. return NewCompoBaby()
  12. case EElementEarlyEdu.ToStr():
  13. return NewElementLifeVideoAdapter(earlyEdu.ThemeManager{})
  14. case EElementLifeVideo.ToStr():
  15. return NewCompoLifeVideo()
  16. case EElementTips.ToStr():
  17. return NewCompoTips()
  18. case ECompositeTodayKnowledge.ToStr():
  19. return NewCompoTodayKnowledge()
  20. default:
  21. return nil
  22. }
  23. }

辣妈首页模块数据聚合:

  1. type HomeModule struct {
  2. GCtx *gin.Context
  3. }
  4. func NewHomeModule(ctx *gin.Context) *HomeModule {
  5. // 构建模块对象
  6. lh := &HomeModule{
  7. GCtx: ctx,
  8. }
  9. return lh
  10. }
  11. func (lh HomeModule) GetHomeModules() interface{} {
  12. // 请request context 上文获取请求参数
  13. parameter := map[string]interface{}{
  14. "baby_id": 22000025,
  15. "birthday": "2021-12-11",
  16. "age": uint32(10),
  17. // ... ...
  18. }
  19. // 从db获取模块列表
  20. compos := []string{
  21. "early_edu",
  22. "baby",
  23. "tips",
  24. "today_knowledge",
  25. }
  26. // 组装组件
  27. elements := map[string]element.IElement{}
  28. elementFactory := element.NewElementFactory()
  29. for _, compoUni := range compos {
  30. comp := elementFactory.CreateElement(compoUni)
  31. if comp == nil {
  32. continue
  33. }
  34. elements[compoUni] = comp
  35. }
  36. // 聚合数据
  37. data := map[string]interface{}{}
  38. for uni, compo := range elements {
  39. data[uni], _ = compo.ExportData(parameter)
  40. }
  41. return data
  42. }

改造相关内容,over ~

经过改造,后续再拓展或者维护首页模块数据的时候,基本不需要动到获取数据的方法:GetHomeModules() ,拓展的时候只需要去拓展一个组件枚举类型,然后定义组件 struct 实现 组件接口 IElement 方法,在组件工厂 ElemFactory 中拓展对象创建,维护组件的时候也只需要对ExportData() 修改。

这次的重构方案中体现了设计模式的几个原则,我们抽象了组件接口,针对接口编程,不针对实现编程,满足接口隔离原则,并且对修改关闭,对拓展开放,满足了开闭原则。

总结:

最后,为了减少重复的代码开发,避免做添油加醋的事情,为了项目的可维护性,可拓展性,也避免成为后人口吐芬芳的对象,我们需要设计起来,实现可以应对变化,有弹性的系统。