大话设计模式:今天你设计了吗?

背景

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


image

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

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

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

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

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

这里就不一一介绍23种设计模式和设计模式的6个原则,可以google回顾下
推荐:学习设计模式地址

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

举例

需求背景概要:

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

API Response Data

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

{
    "code": 0,
    "data": {
        "tools": {
            // -- 模块信息 --
            "id": 744,
            "icon": "",
            "name": "",
            "sub_title": "",
            "module": "lm_tools",
            "sort": 1,
            "is_lock": true,
            "is_show": true,
            "more_text": "",
            "more_uri": "xxx:///tools/more",
            "list": [
                // -- 模块展示数据 --
            ]
        },
        "my_baby": {
            // ... ...
        },
        "knowledge_parenting": {
            // ... ...
        },
        "early_due": {
            // ... ...
        },

        // ... ...

        "message": ""
}

Before Code

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

func (hm *HomeModule) GetHomeData() map[string]interface{} {
  result := make(map[string]interface{})
    // ... ...

    // 获取模块列表
    module := lm.GetHomeSortData()

    // ... ...

    // 构造每个模块的数据
    for _, module := range moduleList {
        // ... ...
        switch module.Module {
        case "my_baby":
            // ... ...
            result["my_baby"] = data
        case "lm_tools":
            // ... ...
            result["lm_tools"] = data
        case "weight":
            // ... ...
            result["weight"] = data
        case "diagnose":
                result["diagnose"] = data
        case "weather":
            // ... ...
            result["weather"] = data
        case "early_edu":
            // ... ...
            result["early_edu"] = data
        case "today_knowledge":
            // ... ...
            data["tips"]=list
            // ... ...
            data["life_video"]=lifeVideo
            // ... ...
            result["today_knowledge"] = data
        default:
            result[module.Module] = module
        }
        // ... ...
        return result
    }

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

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

以下就是重构的过程。

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

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

# 首页模块
home
    - my_baby
    - weight
    - early_edu
    - today_knowledge
        - tips
        - life_video
    - weather
    - ... ...

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

UML结构图:

image

Refactor After Code:

定义组件接口 IElement :

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

定义组件类型枚举

// EElement 组件类型
type EElement string

const (
    EElementTips             EElement = "tips"            // 贴士
    EElementLifeVideo        EElement = "life_video"      // 生命一千天
    EElementEarlyEdu         EElement = "early_edu"       // 早教
    EElementBaby              EElement = "baby"             // 宝宝
    ECompositeTodayKnowledge EElement = "today_knowledge" // 今日知识
    // ....
)

func (ec EElement) ToStr() string {
    return string(ec)
}

单一组件的实现

// ElemTips 贴士组件
type ElemTips struct {
}

func NewCompoTips() *ElementTips {
    return &ElementTips{}
}

func (c *ElementTips) Add(compoUni string, comp IElement) {
}

func (c ElementTips) ExportData(parameter map[string]interface{}) (interface{}, error) {
    tips := []map[string]interface{}{
        {
            "id":    1,
            "title": "贴士1",
        },
        {
            "id":    2,
            "title": "贴士2",
        },
        {
            "id":    3,
            "title": "贴士3",
        },
        {
            "id":    4,
            "title": "贴士4",
        },
    }

    return tips, nil
}

// ElemLifeVideo 生命一千天组件
type ElemLifeVideo struct {
}

func NewCompoLifeVideo() *ElementLifeVideo {
    return &ElementLifeVideo{}
}

func (c ElementLifeVideo) Add(compoUni string, comp IElement) {
}

func (c ElementLifeVideo) ExportData(parameter map[string]interface{}) (interface{}, error) {
    lifeVideos := []map[string]interface{}{
        {
            "id":    1,
            "title": "生命一千天1",
        },
        {
            "id":    2,
            "title": "生命一千天2",
        },
        {
            "id":    3,
            "title": "生命一千天3",
        },
        {
            "id":    4,
            "title": "生命一千天4",
        },
    }
    return lifeVideos, nil
}

// ... ...

复合组件:

// 今日知识,组合多个dan'yi组件
type ElemTodayKnowledge struct {
    Composite map[string]IElement
}

func NewCompoTodayKnowledge() *ElemTodayKnowledge {
    factory := NewElementFactory()
    c := new(ElemTodayKnowledge)
    c.Add(EElementTips.ToStr(), factory.CreateElement(EElementTips.ToStr()))
    c.Add(EElementEarlyEdu.ToStr(), factory.CreateElement(EElementEarlyEdu.ToStr()))
    return c
}

func (c *ElemTodayKnowledge) Add(compoUni string, comp IElement) {
    if c.Composite == nil {
        c.Composite = map[string]IElement{}
    }
    c.Composite[compoUni] = comp
}

func (c ElemTodayKnowledge) ExportData(parameter map[string]interface{}) (interface{}, error) {
    data := map[string]interface{}{}
    for uni, compo := range c.Composite {
        data[uni], _ = compo.ExportData(parameter)
    }
    return data, nil
}

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

// ElemEarlyDduAdapter 早教组件 - 适配
type ElemEarlyDduAdapter struct {
    edu earlyEdu.ThemeManager
}

func NewElementLifeVideoAdapter(edu earlyEdu.ThemeManager) *ElemEarlyDduAdapter {
    return &ElemEarlyDduAdapter{edu: edu}
}

func (c ElemEarlyDduAdapter) Add(compoUni string, comp IElement) {
}

func (c ElemEarlyDduAdapter) ExportData(parameter map[string]interface{}) (interface{}, error) {
    age, ok := parameter["age"].(uint32)
    if !ok {
        return nil, errors.New("缺少age")
    }
    birthday, ok := parameter["birthday"].(string)
    if !ok {
        return nil, errors.New("缺少birthday")
    }
    list := c.edu.GetList(age, birthday)
    return list, nil
}

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

// ElemFactory 组件工厂
type ElemFactory struct {
}

func NewElementFactory() *ElemFactory {
    return &ElemFactory{}
}

// CreateElement 内容组件对象工厂
func (e ElemFactory) CreateElement(compType string) IElement {
    switch compType {
    case EElementBaby.ToStr():
        return NewCompoBaby()
    case EElementEarlyEdu.ToStr():
        return NewElementLifeVideoAdapter(earlyEdu.ThemeManager{})
    case EElementLifeVideo.ToStr():
        return NewCompoLifeVideo()
    case EElementTips.ToStr():
        return NewCompoTips()
    case ECompositeTodayKnowledge.ToStr():
        return NewCompoTodayKnowledge()
    default:
        return nil
    }
}

辣妈首页模块数据聚合:

type HomeModule struct {
    GCtx *gin.Context
}

func NewHomeModule(ctx *gin.Context) *HomeModule {
    // 构建模块对象
    lh := &HomeModule{
        GCtx: ctx,
    }
    return lh
}

func (lh HomeModule) GetHomeModules() interface{} {

    // 请request context 上文获取请求参数
    parameter := map[string]interface{}{
        "baby_id":  22000025,
        "birthday": "2021-12-11",
        "age":      uint32(10),
        // ... ...
    }

    // 从db获取模块列表
    compos := []string{
        "early_edu",
        "baby",
        "tips",
        "today_knowledge",
    }

    // 组装组件
    elements := map[string]element.IElement{}
    elementFactory := element.NewElementFactory()
    for _, compoUni := range compos {
        comp := elementFactory.CreateElement(compoUni)
        if comp == nil {
            continue
        }
        elements[compoUni] = comp
    }

    // 聚合数据
    data := map[string]interface{}{}
    for uni, compo := range elements {
        data[uni], _ = compo.ExportData(parameter)
    }

    return data
}

改造相关内容,over ~

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

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

总结:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容