模板方法模式在開發(fā)中的應(yīng)用

模板方法模式在開發(fā)中的應(yīng)用

先說一下業(yè)務(wù)背景吧变姨,公司這邊需要做一個數(shù)據(jù)聚合的項目聂喇,要從各個數(shù)據(jù)源清洗出來歷史數(shù)據(jù)跷敬,并進行整合統(tǒng)一存儲。數(shù)據(jù)源大概有7亡蓉、8 個,時間粒度包括歷史全量數(shù)據(jù)遍尺、每天的新增數(shù)據(jù)质蕉、從某天開始至今的數(shù)據(jù)。

面對這個需求传睹,首先的想法是耳幢,定義一個接口,抽象各個數(shù)據(jù)源的處理過程蒋歌;通過一個類單獨進行參數(shù)解析帅掘、數(shù)據(jù)源接口實例管理、任務(wù)分發(fā)堂油。定義好方案之后修档,于是我們就開始愉快地進行開發(fā)了。

第一版接口方案

首先我們定義一個數(shù)據(jù)源數(shù)據(jù)的接口府框,接口定義如下

type Executor interface {
    Repair(ctx context.Context, start time.Time) error
}

start 標示數(shù)據(jù)開始處理的時間吱窝,從 start 開始處理目前為止的所有數(shù)據(jù),start 為 0 迫靖,表示處理全部數(shù)據(jù)

還有一個類院峡,進行參數(shù)解析、數(shù)據(jù)源接口實例管理系宜、任務(wù)分發(fā)照激。類的實現(xiàn)代碼如下

type StudentStory struct {   
    //管理接口實例
    executors            map[string]studentstory.Executor
    //要執(zhí)行的任務(wù)
    story                string
    //任務(wù)開始時間
    start                string
    //執(zhí)行當天數(shù)據(jù)的回退時間
    backoff              time.Duration
    //解析參數(shù)的鎖
    paramLock            sync.Mutex
}

func (t *StudentStory) init() 
    //參數(shù)定義
    t.flag.StringVar(&t.story, "story", "", "同步事件類型")
    t.flag.StringVar(
        &t.start,
        "start",
        "daily",
        "同步開始時間(Y-m-d|daily|full),Y-m-d: 從 Y-m-d 開始同步; daily:從前幾天開始同步數(shù)據(jù)盹牧;full:同步全量數(shù)據(jù)",
    )
    t.flag.DurationVar(&t.backoff, "backoff", -24*time.Hour, "")

    //注冊接口實例
    t.register()
}

//將接口實例注冊到結(jié)構(gòu)體
func (t *StudentStory) register() {
    t.executors["credit"] = studentstory.NewCreditExecutor(...)
    t.executors["comment"] = studentstory.NewCommentExecutor(...)
    ...
}


func (t *StudentStory) Run(ctx context.Context, params []string) {
    //參數(shù)解析俩垃,使用鎖進行并發(fā)控制
    t.paramLock.Lock()
    err := external.FlagSetSmartParse(params, t.flag)
    if err != nil {
        xlog.Ctx(ctx).Errorw("studentstory:  param parse error", "param", params, "err", err)
    }
    
    //為了避免并發(fā)問題,使用局部變量
    story := t.story
    start := t.start
    backoff := t.backoff
    t.reset()
    t.paramLock.Unlock()

    //根據(jù) story 參數(shù)獲取 execotur
    xlog.Ctx(ctx).Infow("studentstory: run command", "story", story, "start", start, "backoff", backoff)
    executor, ok := t.executors[story]
    if !ok {
        xlog.Ctx(ctx).Errorw("studentstory:  executor not exists", "story", story)
        return
    }
     
    //根據(jù)參數(shù)解析出來時間
    var begin time.Time
    if start == "daily" {
      //同步當天之前一段時間的數(shù)據(jù)
      yesterday := time.Now().Add(backoff)
      beginStr := yesterday.Format("2006-01-02") + " 00:00:00"
      begin, err = time.Parse("2006-01-02 15:04:05", beginStr)
      if err != nil {
        xlog.Ctx(ctx).Errorw("studentstory:  time parse err", "beginStr", beginStr, "err", err)
        return
      }
    } else if start == "full" {
        //同步全部數(shù)據(jù)
        begin = time.Unix(0, 0)
    } else {
        //根據(jù) start 參數(shù)指定的時間同步數(shù)據(jù)
        beginStr := start + " 00:00:00"
        begin, err = time.Parse("2006-01-02 15:04:05", beginStr)
        if err != nil {
            xlog.Ctx(ctx).Errorw("studentstory:  time parse err", "beginStr", beginStr, "err", err)
            return
        }
    }

    xlog.Ctx(ctx).Infow("studentstory: run executor", "story", story, "begin", begin.Format("2006-01-02 15:04:05"), "conf", cronConf)
    err = executor.Repair(ctx, begin)
    if err != nil {
        //統(tǒng)一進行錯誤處理
    }

接口方案問題

當以上方案定義好之后汰寓,接下來我們就開始愉快地寫業(yè)務(wù)代碼口柳。但是在不斷的接入業(yè)務(wù)源的過程中,因為接入各個數(shù)據(jù)源都是存儲在數(shù)據(jù)庫里面的有滑,機智的我逐漸發(fā)現(xiàn)了如下問題

  1. Repair 接口實現(xiàn)缺少規(guī)范跃闹。因為目前方案沒有對 Repair 接口如何實現(xiàn)做限制,各個業(yè)務(wù)方在實現(xiàn)的時候就可以隨心所欲毛好,信馬由韁望艺,缺少規(guī)范
  2. Repair 接口實現(xiàn)存在大量重復(fù)代碼。因為數(shù)據(jù)源大部分都是從數(shù)據(jù)庫里面接入數(shù)據(jù)肌访,實現(xiàn)的流程大部分是相似荣茫,將數(shù)據(jù)進行統(tǒng)一保存的邏輯也是相同的,但是目前方案并沒有對此流程進行抽象场靴,所以各個業(yè)務(wù)方都要重讀寫這塊相似代碼
  3. Repair 接口實現(xiàn)代碼質(zhì)量無法保證啡莉。
  4. Repair 接口難以修改港准,擴展。雖然理想方案是接口定義完成之后不進行修改咧欣,但實際開發(fā)往往計劃趕不上變化浅缸。而目前實現(xiàn)方案由于是各個數(shù)據(jù)源的類直接實現(xiàn) Repair 接口,接口一旦修改就會影響到每個數(shù)據(jù)源魄咕。修改成本比較高衩椒。

模板方法實現(xiàn)方案

針對上面方案存在的問題,模板方法模式正好可以解決這個問題哮兰。(模板方法模式

定義一個操作中的算法的骨架毛萌,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟喝滞。

通過模板方法的描述我們知道實現(xiàn)模板方法模式需要父類調(diào)用子類實現(xiàn)的模板方法阁将,但是 go 語言的繼承是通過匿名屬性組合來實現(xiàn)的,并且父類無法調(diào)用子類的方法右遭。

這怎么辦呢做盅?我們知道首先設(shè)計模式主要是一種思想,并沒有完全嚴格的格式窘哈,并且大部分的設(shè)計模式都有繼承和組合兩種實現(xiàn)方法吹榴。是不是想到解決方案了,既然 go 不支持完整的繼承滚婉,我們可以用組合的方式來實現(xiàn)模板方法模式啊图筹。

首先我們先定義模板方法的接口

type ExecutorTemplate interface {
    //描述要執(zhí)行的任務(wù)
    GetTitle() string
    //獲取某個時間點之前的最大 id
    GetMaxIDBeforeCreateTime(context.Context, time.Time) (int64, error)
    //獲取整個數(shù)據(jù)的最大 id
    GetMaxID(context.Context) (int64, error)
    //獲取整個數(shù)據(jù)的最小 id
    GetMinID(context.Context) (int64, error)
    //獲取某個 id 后面的一批數(shù)據(jù)
    GetItemsAfterID(context.Context, int64) ([]interface{}, error)
    //將從數(shù)據(jù)庫里面查出來的數(shù)據(jù),裝換成可以統(tǒng)一保存的數(shù)據(jù)
    ConvertItemsToEvents(context.Context, []interface{}) ([]entity.StudentStoryEvent, int64, error)
}

定義好模板方法之后让腹,接來下我們定義一個類远剩,來利用模板方法實現(xiàn)算法流程

type Executor struct {
    template        IdExecutorTemplate
    studentStorySrv *service.StudentstorySrv
}

//實現(xiàn) Repair 接口,利用模板方法實現(xiàn)算法流程
func (e *Executor) Repair(ctx context.Context, start time.Time) error {
    var startID int64
    var err error
    if start.IsZero() {
        //同步全量數(shù)據(jù)哨鸭,查出來數(shù)據(jù)的最小 id
        startID, err = e.template.GetMinID(ctx)
        startID -= 1
    } else {
        //按某個時間點同步數(shù)據(jù),查出來時間點之前的最大 id
        startID, err = e.template.GetMaxIDBeforeCreateTime(ctx, start)
    }
    if err != nil {
        return err
    }

    if startID < 0 {
        startID = 0
    }

    endID, err := e.template.GetMaxID(ctx)
    if err != nil {
        return err
    }
   
   //bar 是一個進度條組件娇妓,用來顯示進度條像鸡,endID-startID 用來估算要處理數(shù)據(jù)的總的條數(shù)
    bar := processbar.NewProcessBar(e.template.GetTitle(), endID-startID)
    for {
        items, err := e.template.GetItemsAfterID(ctx, startID)
        if err != nil {
            return err
        }
        if len(items) == 0 {
            return nil
        }

        events, maxID, err := e.template.ConvertItemsToEvents(ctx, items)
        if err != nil {
            return err
        }

        err = e.studentStorySrv.StudentstoryRepo.Saves(ctx, events)
        if err != nil {
            return err
        }
        startID = maxID

        bar.Advance(int64(len(items)))
    }
}

func newExecutor(
    studentStorySrv *service.StudentstorySrv,
    template IdExecutorTemplate,
) *Executor {
    return &Executor{
        studentStorySrv: studentStorySrv,
        template:        template,
    }
}

我們通過模板方法模式將數(shù)據(jù)同步的流程固定下來,很好地解決了方案一的問題

  1. Repair 接口由 Executor 來實現(xiàn)哈恰,代碼有了規(guī)范只估,質(zhì)量也有了保障
  2. 各個數(shù)據(jù)源的處理流程由 Executor 來實現(xiàn),不用寫大量的重復(fù)代碼
  3. Repair 接口有 Executor 來實現(xiàn)着绷,修改蛔钙、擴展直接修改 Executor 就可以

模板方法模式的擴展

到目前為止,似乎一切都是完美的荠医,于是我們就又開始愉快地寫代碼了吁脱。但是天有不測風云 桑涎,果然寫代碼的過程不能是順順利利的。目前的查詢流程是按照數(shù)據(jù)的創(chuàng)建時間查出來id兼贡,然后按照 id 來取數(shù)據(jù)攻冷。

突然有一天又要接入兩個新的數(shù)據(jù)源,一個是數(shù)據(jù)有更新遍希,更新也要獲取到等曼;一個是通過接口取數(shù)據(jù),接口只支持按照時間取數(shù)據(jù)凿蒜,這就讓我頭疼了禁谦。最開始的我的想法是擴展 Executor 的 Repair 的執(zhí)行流程,讓它支持新的查詢方案废封,但總覺得怪怪的州泊。因為代碼會變得越來越復(fù)雜,也會越來越難以維護虱饿,并且也違背了職責單一原則拥诡。

突然聰明的我又靈機一動,在寫一個模板來實現(xiàn)這個處理流程不就行了氮发。果然只要想對了方向渴肉,一切都會豁然開朗。于是我將上線的模板接口和 Executor 改名為 IdExecutorTemplate 和 IdExecotor爽冕,并對新的需求仇祭,建新的接口 TimeExecutorTemplate 和執(zhí)行器 TimeExecotor來實現(xiàn)

TimeExecutorTemplate 接口定義如下

type TimeExecutorTemplate interface {
    //描述要執(zhí)行的任務(wù)
    GetTitle() string
    //獲取時間步長
    GetTimeStep() int64
    //從某個時間范圍內(nèi)獲取一批數(shù)據(jù)
    GetItemsBetweenTime(context.Context, time.Time, time.Time, int64) ([]interface{}, error)
    //獲取某個時間范圍的數(shù)據(jù)總數(shù)
    GetTotalBetweenTime(context.Context, time.Time, time.Time) (int64, error)
    //獲取某個時間之后的數(shù)據(jù)總數(shù)
    GetTotalAfterTime(context.Context, time.Time) (int64, error)
    //獲取所有數(shù)據(jù)的最小時間
    GetMinTime(context.Context) (time.Time, error)
    //將從數(shù)據(jù)庫里面查出來的數(shù)據(jù),裝換成可以統(tǒng)一保存的數(shù)據(jù)
    ConvertItemsToEvents(context.Context, []interface{}) ([]entity.StudentStoryEvent, error)
}

TimeExecotor 類實現(xiàn)如下

var globalBar *processbar.ProcessBar
var stepBar *processbar.ProcessBar

type TimeExecutor struct {
    template        TimeExecutorTemplate
    studentStorySrv *service.StudentstorySrv
}

func (e *TimeExecutor) Repair(ctx context.Context, startTime time.Time, conf config.StudentStoryDataCron) error {
    var err error
    endTime := time.Now()
    if startTime.IsZero() {
        //獲取全量數(shù)據(jù)颈畸,取出來最小時間
        startTime, err = e.template.GetMinTime(ctx)
        if err != nil {
            return err
        }
    }
    
    //獲取全部要處理的數(shù)據(jù)總數(shù)
    total, err := e.template.GetTotalAfterTime(ctx, startTime)
    if err == nil {
        //如果獲取成功乌奇,定義全局進度條
        globalBar = processbar.NewProcessBar(e.template.GetTitle(), total)
    }
  
    //因為按照時間取數(shù)據(jù),一般都會用到分頁進行查詢眯娱,為了盡量分頁的 offset 過大礁苗,將時間進行分段查詢
    timeStep := time.Duration(e.template.GetTimeStep()) * time.Second
    for stepStartTime := startTime; stepStartTime.Before(endTime); stepStartTime = stepStartTime.Add(timeStep) {
        stepEndTime := stepStartTime.Add(timeStep)
        if globalBar == nil {
            //全局進度條初始化失敗,初始化 step 進度條
            stepTotal, err := e.template.GetTotalBetweenTime(ctx, stepStartTime, stepEndTime)
            if err == nil {
                title := fmt.Sprintf("%s [%s - %s]", e.template.GetTitle(), stepStartTime.Format("2006.01.02 15:04:05"), stepEndTime.Format("2006.01.02 15:04:05"))
                stepBar = processbar.NewProcessBar(title, stepTotal)
            }
        }
        var offset int64
        for {
            //按照 step 時間查詢數(shù)據(jù)
            items, err := e.template.GetItemsBetweenTime(ctx, stepStartTime, stepEndTime, offset)
            if err != nil {
                return err
            }
            if len(items) == 0 {
                break
            }

            events, err := e.template.ConvertItemsToEvents(ctx, items)
            if err != nil {
                return err
            }

            err = e.studentStorySrv.StudentstoryRepo.Saves(ctx, events)
            if err != nil {
                return err
            }

            offset += int64(len(items))
            if globalBar != nil {
                globalBar.Advance(int64(len(items)))
            }
            if stepBar != nil {
                stepBar.Advance(int64(len(items)))
            }
        }
    }
    return nil
}

func newTimeExecutor(
    studentStorySrv *service.StudentstorySrv,
    template TimeExecutorTemplate,
) *TimeExecutor {
    return &TimeExecutor{
        studentStorySrv: studentStorySrv,
        template:        template,
    }
}

哈哈徙缴,現(xiàn)在我們就又可以愉快地寫代碼了

類 UML 圖

接下來我們我們來看一下這些類的 UML 圖

template.png

總結(jié)

  1. 抽象的過程是從具體到抽象试伙,在從抽象到具體。沒有具體的 case 于样,沒有具體的需求疏叨,抽象是沒有意義的
  2. 設(shè)計模式最重要的是思想,掌握思想穿剖,可以使用各種方法實現(xiàn)蚤蔓。比如這個需求最終實現(xiàn)的效果,如果看 UML 圖的話糊余,更像是橋接模式秀又。并且整體分析的話单寂,也有符合橋接模式的思想。但從本質(zhì)上來說涮坐,還是模板方法模式
  3. 抽象不是銀彈凄贩,抽象不能解決所有問題,并且抽象是有害的袱讹。抽象在規(guī)范的同時疲扎,也屏蔽了細節(jié),并且失去了靈活性捷雕。比如上面的例子椒丧,Repair 接口的抽象使得接口的修改變得困難,Tempate 接口的抽象救巷,使得接入的數(shù)據(jù)源只能按照接口提供的方法實現(xiàn)功能壶熏。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浦译,隨后出現(xiàn)的幾起案子棒假,更是在濱河造成了極大的恐慌,老刑警劉巖精盅,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帽哑,死亡現(xiàn)場離奇詭異,居然都是意外死亡叹俏,警方通過查閱死者的電腦和手機妻枕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粘驰,“玉大人屡谐,你說我怎么就攤上這事◎蚴” “怎么了愕掏?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長顶伞。 經(jīng)常有香客問我饵撑,道長,這世上最難降的妖魔是什么枝哄? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任肄梨,我火速辦了婚禮阻荒,結(jié)果婚禮上挠锥,老公的妹妹穿的比我還像新娘。我一直安慰自己侨赡,他們只是感情好蓖租,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布粱侣。 她就那樣靜靜地躺著,像睡著了一般蓖宦。 火紅的嫁衣襯著肌膚如雪齐婴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天稠茂,我揣著相機與錄音柠偶,去河邊找鬼。 笑死睬关,一個胖子當著我的面吹牛诱担,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播电爹,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蔫仙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了丐箩?” 一聲冷哼從身側(cè)響起摇邦,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎屎勘,沒想到半個月后施籍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡挑秉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年法梯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片犀概。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡立哑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出姻灶,到底是詐尸還是另有隱情铛绰,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布产喉,位于F島的核電站捂掰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏曾沈。R本人自食惡果不足惜这嚣,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望塞俱。 院中可真熱鬧姐帚,春花似錦、人聲如沸障涯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至九秀,卻和暖如春遗嗽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鼓蜒。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工痹换, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人都弹。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓晴音,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缔杉。 傳聞我的和親對象是個殘疾皇子锤躁,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

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