大話設(shè)計模式:今天你設(shè)計了嗎掺涛?

背景

在開發(fā)過程中你是否有遇到過這樣的苦惱?產(chǎn)品發(fā)來一個需求铃绒,沒做過鸽照,但是看完需求感覺應(yīng)該處理起來很簡單,然后找到對應(yīng)的業(yè)務(wù)代碼颠悬,發(fā)現(xiàn)代碼像打亂的毛線一樣理不清楚矮燎,各種邏輯嵌套,各種特殊判斷處理赔癌,想要拓展維護(hù)個內(nèi)容卻無從下手诞外,一邊看著代碼,一邊用手撥動著本就為數(shù)不多的秀發(fā)灾票,然后口吐芬芳 峡谊。


image

有沒發(fā)現(xiàn)一個問題,為什么業(yè)務(wù)不復(fù)雜刊苍,但是隨著產(chǎn)品迭代既们,經(jīng)過不斷拓展和維護(hù),慢慢的代碼就越做越亂正什,你可以說產(chǎn)品想法天馬星空啥纸,人員流動大,多人參與慢慢的就被做亂了婴氮,這可能是個不錯的借口斯棒,但是其中本質(zhì)的問題還是前期思考的太少,沒有進(jìn)行合理的抽象設(shè)計主经,沒有去前瞻性的去預(yù)埋一些未來可拓展性的內(nèi)容荣暮,所以最終導(dǎo)致了后來的局面。

經(jīng)常聽到有經(jīng)驗的開發(fā)者說開發(fā)前多思考罩驻,不要一拿到需求就習(xí)慣性的一頓操作穗酥,反手就定義一個function根據(jù)需求邏輯一條龍寫到底。

所以面對相對復(fù)雜的需求我們需要進(jìn)行抽象思考,盡可能做到設(shè)計出來的東西是解決一類問題迷扇,而不是單單解決當(dāng)前問題百揭,然后在代碼實現(xiàn)上也要面向抽象開發(fā),這樣才能做到真正的高質(zhì)量代碼蜓席,可維護(hù)性和可拓展性高器一,才能沉淀出可復(fù)用,健壯性強(qiáng)的系統(tǒng)厨内。

那么我們要如何去抽象呢祈秕?面對需求的抽象思維這個需要平時多鍛煉,拿到需求多想多思考雏胃,不要急于求成请毛,主要圍繞著這幾大要素:可維護(hù)性、可拓展性瞭亮、可復(fù)用性方仿,安全性去設(shè)計解決方案,至于代碼上的抽象就可以使用下面的方式统翩。

不賣關(guān)子了仙蚜,是時候請出今天的主角:《設(shè)計模式》,簡單的說設(shè)計模式就是開發(fā)者們的經(jīng)驗沉淀厂汗,通過學(xué)習(xí)設(shè)計模式并在業(yè)務(wù)開發(fā)過程中加以使用委粉,可以讓代碼的實現(xiàn)更容易拓展和維護(hù),提高整體代碼質(zhì)量娶桦,也可以作為開發(fā)之間溝通的專業(yè)術(shù)語贾节,提到某個模式,可以馬上get到代碼設(shè)計衷畦,減少溝通的成本栗涂。

這里就不一一介紹23種設(shè)計模式和設(shè)計模式的6個原則,可以google回顧下
推薦:學(xué)習(xí)設(shè)計模式地址

下面就將結(jié)合當(dāng)前項目的bad case祈争,手把手的使用設(shè)計模式進(jìn)行重構(gòu)斤程,其中會用到多種設(shè)計模式的使用,并且體現(xiàn)了設(shè)計模式的中的幾個原則铛嘱,做好準(zhǔn)備暖释,發(fā)車了袭厂。

舉例

需求背景概要:

APP首頁功能墨吓,用模塊化的方式去管理配置,后臺可以配置模塊標(biāo)識和模塊排序纹磺,展示條件等帖烘,首頁API接口獲取當(dāng)前用戶的模塊列表,并構(gòu)造模塊數(shù)據(jù)展示橄杨。

API Response Data

偽響應(yīng)數(shù)據(jù)秘症,忽略掉不重要或者重復(fù)的數(shù)據(jù)

{
    "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": [
                // -- 模塊展示數(shù)據(jù) --
            ]
        },
        "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()

    // ... ...

    // 構(gòu)造每個模塊的數(shù)據(jù)
    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瞬痘、一些特殊處理等,讓代碼變得又臭又長板熊,越來越難去拓展和維護(hù)框全,并且每次維護(hù)或者拓展都可能在GetHomeData() 方法里在不斷往里面添油加醋,不小心就會對整個接口產(chǎn)生影響干签。

那么我們要如何去重構(gòu)呢津辩,這就要抽象起來,這個業(yè)務(wù)本身就已經(jīng)有模塊相關(guān)抽象設(shè)計容劳,這里就不進(jìn)行調(diào)整喘沿,主要是針對代碼上的抽象,結(jié)合設(shè)計模式進(jìn)行改造鸭蛙。

以下就是重構(gòu)的過程摹恨。

剛開始的時候,看到這種case 判斷娶视,然后做模塊數(shù)據(jù)的聚合晒哄,我第一反應(yīng)是,能否可以使用工廠模式肪获,定義一個 interface寝凌,每個模塊定義一個struct 實現(xiàn)接口ExportData() 方法,通過工廠方法去根據(jù)模塊標(biāo)識創(chuàng)建對象孝赫,然后調(diào)用導(dǎo)出數(shù)據(jù)方法進(jìn)行數(shù)據(jù)上的聚合 较木。

但是在評估的過程中,發(fā)現(xiàn)有些模塊數(shù)據(jù)里又聚合了多個不同業(yè)務(wù)知識內(nèi)容的數(shù)據(jù)青柄,單純的工廠模式又不太合適伐债,最后決定使用組合模式,結(jié)構(gòu)型設(shè)計模式致开,可以將對象進(jìn)行組合峰锁,實現(xiàn)一個類似層級對象關(guān)系,如:

# 首頁模塊
home
    - my_baby
    - weight
    - early_edu
    - today_knowledge
        - tips
        - life_video
    - weather
    - ... ...

這里我重新定義了下名詞双戳,后臺配置的是模塊虹蒋,在代碼實現(xiàn)上我把每個模塊里展示的數(shù)據(jù)定義成 組件,組件又可以分成 單一組件 和 復(fù)合組件,復(fù)合組件就是使用了多個單一組件組成魄衅。

UML結(jié)構(gòu)圖:

image

Refactor After Code:

定義組件接口 IElement :

// IElement 組件接口
type IElement interface {
    // Add 添加組件峭竣,單一組件,可以不用實現(xiàn)具體方法內(nèi)容
    Add(compUni string, compo IElement)
    // ExportData 輸出組件數(shù)據(jù)
    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)
}

單一組件的實現(xiàn)

// 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
}

// ... ...

復(fù)合組件:

// 今日知識晃虫,組合多個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
}

因為有些知識數(shù)據(jù)的內(nèi)容已經(jīng)有相關(guān)實現(xiàn)皆撩,并且可以構(gòu)造對象進(jìn)行調(diào)用,我們需要做的是根據(jù)組件需求適配成組件需要的數(shù)據(jù)結(jié)構(gòu)進(jìn)行輸出哲银,這里又引入了適配器模式毅访,可以使用適配器模式,將其適配成當(dāng)前組件需要的數(shù)據(jù)結(jié)構(gòu)輸出盘榨。

// 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
}

對象的創(chuàng)建需要進(jìn)行統(tǒng)一管理喻粹,便于后續(xù)的拓展和替換,這里引入工廠模式草巡,封裝組件的對象創(chuàng)建守呜,通過工廠方法去創(chuàng)建組件對象。

// ElemFactory 組件工廠
type ElemFactory struct {
}

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

// CreateElement 內(nèi)容組件對象工廠
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
    }
}

辣媽首頁模塊數(shù)據(jù)聚合:

type HomeModule struct {
    GCtx *gin.Context
}

func NewHomeModule(ctx *gin.Context) *HomeModule {
    // 構(gòu)建模塊對象
    lh := &HomeModule{
        GCtx: ctx,
    }
    return lh
}

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

    // 請request context 上文獲取請求參數(shù)
    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
    }

    // 聚合數(shù)據(jù)
    data := map[string]interface{}{}
    for uni, compo := range elements {
        data[uni], _ = compo.ExportData(parameter)
    }

    return data
}

改造相關(guān)內(nèi)容山憨,over ~

經(jīng)過改造查乒,后續(xù)再拓展或者維護(hù)首頁模塊數(shù)據(jù)的時候禽车,基本不需要動到獲取數(shù)據(jù)的方法:GetHomeModules() 订歪,拓展的時候只需要去拓展一個組件枚舉類型,然后定義組件 struct 實現(xiàn) 組件接口 IElement 方法宁舰,在組件工廠 ElemFactory 中拓展對象創(chuàng)建棚亩,維護(hù)組件的時候也只需要對ExportData() 修改蓖议。

這次的重構(gòu)方案中體現(xiàn)了設(shè)計模式的幾個原則,我們抽象了組件接口讥蟆,針對接口編程勒虾,不針對實現(xiàn)編程,滿足接口隔離原則瘸彤,并且對修改關(guān)閉修然,對拓展開放,滿足了開閉原則质况。

總結(jié):

最后愕宋,為了減少重復(fù)的代碼開發(fā),避免做添油加醋的事情结榄,為了項目的可維護(hù)性中贝,可拓展性,也避免成為后人口吐芬芳的對象潭陪,我們需要設(shè)計起來雄妥,實現(xiàn)可以應(yīng)對變化,有彈性的系統(tǒng)依溯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末老厌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子黎炉,更是在濱河造成了極大的恐慌枝秤,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慷嗜,死亡現(xiàn)場離奇詭異淀弹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)庆械,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門薇溃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缭乘,你說我怎么就攤上這事沐序。” “怎么了堕绩?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵策幼,是天一觀的道長。 經(jīng)常有香客問我奴紧,道長特姐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任黍氮,我火速辦了婚禮唐含,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沫浆。我一直安慰自己觉壶,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布件缸。 她就那樣靜靜地躺著铜靶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪他炊。 梳的紋絲不亂的頭發(fā)上争剿,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機(jī)與錄音痊末,去河邊找鬼蚕苇。 笑死,一個胖子當(dāng)著我的面吹牛凿叠,可吹牛的內(nèi)容都是我干的涩笤。 我是一名探鬼主播嚼吞,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蹬碧!你這毒婦竟也來了舱禽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤恩沽,失蹤者是張志新(化名)和其女友劉穎誊稚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罗心,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡里伯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了渤闷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疾瓮。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖飒箭,靈堂內(nèi)的尸體忽然破棺而出爷贫,到底是詐尸還是另有隱情,我是刑警寧澤补憾,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布漫萄,位于F島的核電站,受9級特大地震影響盈匾,放射性物質(zhì)發(fā)生泄漏腾务。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一削饵、第九天 我趴在偏房一處隱蔽的房頂上張望岩瘦。 院中可真熱鬧,春花似錦窿撬、人聲如沸启昧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽密末。三九已至,卻和暖如春跛璧,著一層夾襖步出監(jiān)牢的瞬間严里,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工追城, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留刹碾,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓座柱,卻偏偏與公主長得像迷帜,于是被迫代替她去往敵國和親物舒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容