大話設計模式:今天你設計了嗎?

SFLYQ 發表於 2022-01-24
設計模式

背景

在開發過程中你是否有遇到過這樣的苦惱?產品發來一個需求,沒做過,但是看完需求感覺應該處理起來很簡單,然後找到對應的業務程式碼,發現程式碼像打亂的毛線一樣理不清楚,各種邏輯巢狀,各種特殊判斷處理,想要擴充維護個內容卻無從下手,一邊看著程式碼,一邊用手撥動著本就為數不多的秀髮,然後口吐芬芳 。
大話設計模式:今天你設計了嗎?

有沒發現一個問題,為什麼業務不復雜,但是隨著產品迭代,經過不斷擴充和維護,慢慢的程式碼就越做越亂,你可以說產品想法天馬星空,人員流動大,多人蔘與慢慢的就被做亂了,這可能是個不錯的藉口,但是其中本質的問題還是前期思考的太少,沒有進行合理的抽象設計,沒有去前瞻性的去預埋一些未來可擴充性的內容,所以最終導致了後來的局面。

經常聽到有經驗的開發者說開發前多思考,不要一拿到需求就習慣性的一頓操作,反手就定義一個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結構圖:

大話設計模式:今天你設計了嗎?

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() 修改。

這次的重構方案中體現了設計模式的幾個原則,我們抽象了元件介面,針對介面程式設計,不針對實現程式設計,滿足介面隔離原則,並且對修改關閉,對擴充開放,滿足了開閉原則。

總結:

最後,為了減少重複的程式碼開發,避免做添油加醋的事情,為了專案的可維護性,可擴充性,也避免成為後人口吐芬芳的物件,我們需要設計起來,實現可以應對變化,有彈性的系統。