新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「董澤潤的技術(shù)筆記」,作者董澤潤。轉(zhuǎn)載本文請(qǐng)聯(lián)系董澤潤的技術(shù)筆記公眾號(hào)。

前幾天某個(gè)服務(wù) ut 失敗,導(dǎo)致別人無法構(gòu)建。查看下源代碼以及 ut case, 發(fā)現(xiàn)槽點(diǎn)蠻多,分享下如何修復(fù),寫單測要注意的一些點(diǎn),由此引出設(shè)計(jì)模式中的概念依賴反轉(zhuǎn)、依賴注入、控制反轉(zhuǎn)
失敗 case
- func toSeconds(in int64) int64 {
- if in > time.Now().Unix() {
- nanosecondSource := time.Unix(0, in)
- if dateIsSane(nanosecondSource) {
- return nanosecondSource.Unix()
- }
- millisecondSource := time.Unix(0, in*int64(time.Millisecond))
- if dateIsSane(millisecondSource) {
- return millisecondSource.Unix()
- }
- // default to now rather than sending something stupid
- return time.Now().Unix()
- }
- return in
- }
- func dateIsSane(in time.Time) bool {
- return (in.Year() >= (time.Now().Year()-1) &&
- in.Year() <= (time.Now().Year()+1))
- }
函數(shù) toSeconds 接收一個(gè)時(shí)間參數(shù),可能是秒、毫秒和其它時(shí)間,經(jīng)過判斷后返回秒值
- ......
- {
- desc: "less than now",
- args: 1459101327,
- expect: 1459101327,
- },
- {
- desc: "great than year",
- args: now.UnixNano()/6000*6000 + 7.55424e+17,
- expect: now.Unix(),
- },
- ......
上面是 test case table, 最后報(bào)錯(cuò) great than year 斷言失敗了。簡單的看下實(shí)現(xiàn)邏輯就能發(fā)現(xiàn),函數(shù)是想修正到秒值,但假如剛好 go gc STW 100ms, 就會(huì)導(dǎo)致 expect 與實(shí)際結(jié)果不符
如何從根本上修復(fù)問題呢?要么修改函數(shù)簽名,外層傳入 time.Now()
- func toSeconds(in int64, now time.Time) int64 {
- ......
- }
要么將 time.Now 函數(shù)定義成當(dāng)前包內(nèi)變量,寫單測時(shí)修改 now 變量
- var now = time.Now
- func toSeconds(in int64) int64 {
- if in > now().Unix() {
- ......
- }
以上兩種方式都比較常見,本質(zhì)在于單測 ut 不應(yīng)該依賴于當(dāng)前系統(tǒng)環(huán)境,比如 mysql, redis, 時(shí)間等等,應(yīng)該僅依賴于輸入?yún)?shù),同時(shí)函數(shù)執(zhí)行多次結(jié)果應(yīng)該一致。去年遇到過 CI 機(jī)器換了,新機(jī)器沒有 redis/mysql, 導(dǎo)致一堆 ut failed, 這就是不合格的寫法
如果依賴環(huán)境的資源,那么就變成了集成測試。如果進(jìn)一步再依賴業(yè)務(wù)的狀態(tài)機(jī),那么就變成了回歸測試,可以說是層層遞進(jìn)的關(guān)系。只有做好代碼的單測,才能進(jìn)一步確保其它測試正常。同時(shí)也不要神話單測,過份追求 100% 覆蓋
依賴注入
剛才我們非常自然的引入了設(shè)計(jì)模式中,非常重要的 依賴注入 Dependenccy injection 概念
- func toSeconds(in int64, now time.Time) int64
簡單的講,toSeconds 函數(shù)調(diào)用系統(tǒng)時(shí)間 time.Now, 我們把依賴以參數(shù)的形式傳給 toSeconds 就是注入依賴,定義就這么簡單
關(guān)注 DI, 設(shè)計(jì)模式中抽像出來四個(gè)角色:
- service 我們所被依賴的對(duì)像
- client 依賴 service 的角色
- interface 定義 client 如何使用 service 的接口
- injector 注入器角色,用于構(gòu)造 service, 并將之傳給 client
我們來看一下面像對(duì)像的例子,Hero 需要有武器,NewHero 是英雄的構(gòu)造方法
- type Hero struct {
- name string
- weapon Weapon
- }
- func NewHero(name string) *Hero {
- return &sHero{
- name: name,
- weapon: NewGun(),
- }
- }
這里面問題很多,比如換個(gè)武器 AK 可不可以呢?當(dāng)然行。但是 NewHero 構(gòu)造時(shí)依賴了 NewGun, 我們需要把武器在外層初始化好,然后傳入
- type Hero struct {
- name string
- weapon Weapon
- }
- func NewHero(name string, wea Weapon) *Hero {
- return &Hero{
- name: name,
- weapon: wea,
- }
- }
- func main(){
- wea:= NewGun();
- myhero = NewHero("killer47", wea)
- }
在這個(gè) case 里面,Hero 就是上面提到的 client 角色,Weapon 就是 service 角色,injector 是誰呢?是 main 函數(shù),其實(shí)也是碼農(nóng)
這個(gè)例子還有問題,原因在于武器不應(yīng)該是具體實(shí)例,而應(yīng)該是接口,即上面提到的 interface 角色
- type Weapon interface {
- Attack(damage int)
- }
也就是說我們的武器要設(shè)計(jì)成接口 Weapon, 方法只有一個(gè) Attack 攻擊并附帶傷害。但是到現(xiàn)在還不是理想的,比如說我沒有武器的時(shí)候,就不能攻擊人了嘛?當(dāng)然能,還有雙手啊,所以有時(shí)我們要用 Option 實(shí)現(xiàn)默認(rèn)依賴
- type Weapon interface {
- Attack(damage int)
- }
- type Hero struct {
- name string
- weapon Weapon
- }
- func NewHero(name string, opts ...Option) *Hero {
- h := &Hero{
- name: name,
- }
- for _, option := range options {
- option(i)
- }
- if h.weapon == nil {
- h.weapon = NewFist()
- }
- return h
- }
- type Option func(*Hero)
- func WithWeapon(w Weapon) Option {
- return func(i *Hero) {
- i.weapon = w
- }
- }
- func main() {
- wea := NewGun()
- myhero = NewHero("killer47", WithWeapon(wea))
- }
上面就是一個(gè)生產(chǎn)環(huán)境中,比較理想的方案,看不明白的可以運(yùn)行代碼試著理解下
第三方框架
剛才提到的例子比較簡單,injector 由碼農(nóng)自己搞就行了。但是很多時(shí)候,依賴的對(duì)像不只一個(gè),可能很多,還有交叉依賴,這時(shí)候就需要第三方框架來支持了
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
Java 黨寫配置文件,用注解來實(shí)現(xiàn)。對(duì)于 go 來講,可以使用 wire, https://github.com/google/wire
- // +build wireinject
- package main
- import (
- "github.com/google/wire"
- "wire-example2/internal/config"
- "wire-example2/internal/db"
- )
- func InitApp() (*App, error) {
- panic(wire.Build(config.Provider, db.Provider, NewApp)) // 調(diào)用wire.Build方法傳入所有的依賴對(duì)象以及構(gòu)建最終對(duì)象的函數(shù)得到目標(biāo)對(duì)象
- }
類似上面一樣,定義 wire.go 文件,然后寫上 +build wireinject 注釋,調(diào)用 wire 后會(huì)自動(dòng)生成 injector 代碼
- //go:generate go run github.com/google/wire/cmd/wire
- //+build !wireinject
- package main
- import (
- "wire-example2/internal/config"
- "wire-example2/internal/db"
- )
- // Injectors from wire.go:
- func InitApp() (*App, error) {
- configConfig, err := config.New()
- if err != nil {
- return nil, err
- }
- sqlDB, err := db.New(configConfig)
- if err != nil {
- return nil, err
- }
- app := NewApp(sqlDB)
- return app, nil
- }
我司有項(xiàng)目在用,感興趣的可以看看官方文檔,對(duì)于構(gòu)建大型項(xiàng)目很有幫助
依賴反轉(zhuǎn) DIP 原則
我們還經(jīng)常聽說一個(gè)概念,就是依賴反轉(zhuǎn) dependency inversion principle, 他有兩個(gè)最重要的原則:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
高層模塊不應(yīng)該依賴低層模塊,需要用接口進(jìn)行抽像。抽像不應(yīng)該依賴于具體實(shí)現(xiàn),具體實(shí)現(xiàn)應(yīng)該依賴于抽像,結(jié)合上面的 Hero&Weapon 案例應(yīng)該很清楚了
那我們學(xué)習(xí) DI、DIP 這些設(shè)計(jì)模式目的是什么呢?使我們程序各個(gè)模塊之間變得松耦合,底層實(shí)現(xiàn)改動(dòng)不影響頂層模塊代碼實(shí)現(xiàn),提高模塊化程度,增加括展性
但是也要有個(gè)度,服務(wù)每個(gè)都做個(gè) interface 抽像一個(gè)模塊是否可行呢?當(dāng)然不,基于這么多年的工程實(shí)踐,我這里面有個(gè)準(zhǔn)則分享給大家:易變的模塊需要做出抽像、跨 rpc 調(diào)用的需要做出抽像
控制反轉(zhuǎn) IOC 思想
本質(zhì)上依賴注入是控制反轉(zhuǎn) IOC 的具體一個(gè)實(shí)現(xiàn)。在傳統(tǒng)編程中,表達(dá)程序目的的代碼調(diào)用庫來處理通用任務(wù),但在控制反轉(zhuǎn)中,是框架調(diào)用了自定義或特定任務(wù)的代碼,Java 黨玩的比較多
推薦大家看一下 coolshell 分享的 undo 例子。比如我們有一個(gè) set 想實(shí)現(xiàn) undo 撤回功能
- type IntSet struct {
- data map[int]bool
- }
- func NewIntSet() IntSet {
- return IntSet{make(map[int]bool)}
- }
- func (set *IntSet) Add(x int) {
- set.data[x] = true
- }
- func (set *IntSet) Delete(x int) {
- delete(set.data, x)
- }
- func (set *IntSet) Contains(x int) bool {
- return set.data[x]
- }
這是一個(gè) IntSet 集合,擁有三個(gè)函數(shù) Add, Delete, Contains, 現(xiàn)在需要添加 undo 功能
- type UndoableIntSet struct { // Poor style
- IntSet // Embedding (delegation)
- functions []func()
- }
- func NewUndoableIntSet() UndoableIntSet {
- return UndoableIntSet{NewIntSet(), nil}
- }
- func (set *UndoableIntSet) Add(x int) { // Override
- if !set.Contains(x) {
- set.data[x] = true
- set.functions = append(set.functions, func() { set.Delete(x) })
- } else {
- set.functions = append(set.functions, nil)
- }
- }
- func (set *UndoableIntSet) Delete(x int) { // Override
- if set.Contains(x) {
- delete(set.data, x)
- set.functions = append(set.functions, func() { set.Add(x) })
- } else {
- set.functions = append(set.functions, nil)
- }
- }
- func (set *UndoableIntSet) Undo() error {
- if len(set.functions) == 0 {
- return errors.New("No functions to undo")
- }
- index := len(set.functions) - 1
- if function := set.functions[index]; function != nil {
- function()
- set.functions[index] = nil // For garbage collection
- }
- set.functions = set.functions[:index]
- return nil
- }
上面是具體的實(shí)現(xiàn),有什么問題嘛?有的,undo 理論上只是控制邏輯,但是這里和業(yè)務(wù)邏輯 IntSet 的具體實(shí)現(xiàn)耦合在一起了
- type Undo []func()
- func (undo *Undo) Add(function func()) {
- *undo = append(*undo, function)
- }
- func (undo *Undo) Undo() error {
- functions := *undo
- if len(functions) == 0 {
- return errors.New("No functions to undo")
- }
- index := len(functions) - 1
- if function := functions[index]; function != nil {
- function()
- functions[index] = nil // For garbage collection
- }
- *undo = functions[:index]
- return nil
- }
上面就是我們 Undo 的實(shí)現(xiàn),跟本不用關(guān)心業(yè)務(wù)具體的邏輯
- type IntSet struct {
- data map[int]bool
- undo Undo
- }
- func NewIntSet() IntSet {
- return IntSet{data: make(map[int]bool)}
- }
- func (set *IntSet) Undo() error {
- return set.undo.Undo()
- }
- func (set *IntSet) Contains(x int) bool {
- return set.data[x]
- }
- func (set *IntSet) Add(x int) {
- if !set.Contains(x) {
- set.data[x] = true
- set.undo.Add(func() { set.Delete(x) })
- } else {
- set.undo.Add(nil)
- }
- }
- func (set *IntSet) Delete(x int) {
- if set.Contains(x) {
- delete(set.data, x)
- set.undo.Add(func() { set.Add(x) })
- } else {
- set.undo.Add(nil)
- }
- }
這個(gè)就是控制反轉(zhuǎn),不再由控制邏輯 Undo 來依賴業(yè)務(wù)邏輯 IntSet, 而是由業(yè)務(wù)邏輯 IntSet 來依賴 Undo. 想看更多的細(xì)節(jié)可以看 coolshell 的博客
再舉兩個(gè)例子,我們有 lbs 服務(wù),定時(shí)更新司機(jī)的坐標(biāo)流,中間需要處理很多業(yè)務(wù)流程,我們埋了很多 hook 點(diǎn),業(yè)務(wù)邏輯只需要對(duì)相應(yīng)的點(diǎn)注冊就可以了,新增加業(yè)務(wù)邏輯無需改動(dòng)主流程的代碼
很多公司在做中臺(tái),比如阿里做的大中臺(tái),原來各個(gè)業(yè)務(wù)線有自己的業(yè)務(wù)處理邏輯,每條業(yè)務(wù)線都有工程師只寫各自業(yè)務(wù)相關(guān)的代碼。中臺(tái)化會(huì)抽像出共有的流程,每個(gè)新的業(yè)務(wù)只需要配置文件自定義需要的哪些模塊即可,這其實(shí)也是一種控制反轉(zhuǎn)的思想
當(dāng)前名稱:一個(gè)UTFailed引出的思考
轉(zhuǎn)載源于:http://www.5511xx.com/article/cohdocg.html


咨詢
建站咨詢
