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

背景

在開發過程中你是否有遇到過這樣的苦惱?產品發來一個需求,沒做過,但是看完需求感覺應該處理起來很簡單,然後找到對應的業務代碼,發現代碼像打亂的毛線一樣理不清楚,各種邏輯嵌套,各種特殊判斷處理,想要拓展維護個內容卻無從下手,一邊看着代碼,一邊用手撥動着本就爲數不多的秀髮,然後口吐芬芳 。

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

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

這次的重構方案中體現了設計模式的幾個原則,我們抽象了組件接口,針對接口編程,不針對實現編程,滿足接口隔離原則,並且對修改關閉,對拓展開放,滿足了開閉原則。

總結:

最後,爲了減少重複的代碼開發,避免做添油加醋的事情,爲了項目的可維護性,可拓展性,也避免成爲後人口吐芬芳的對象,我們需要設計起來,實現可以應對變化,有彈性的系統。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章