拯救你的程式碼

caibirdme發表於2018-02-06

稍微有點標題黨,不過很短,可以看

背景

最近一直在參加開發公司新運營系統的引擎部分,寫了很多很多程式碼,也逐漸產生了一些自己對此類業務的解決方案的簡單想法。運營系統乾的事,其實就是支援運營人員配置活動。所謂活動其實可以簡單定義為:一系列條件都滿足就執行某個 (些) 動作。對應到程式設計師的黑話就是:一坨 if else 之後執行某 (幾) 個函式

if else

說到寫 if else,大多數程式設計師可能都會會心一笑,畢竟不論寫啥系統,程式碼裡大部分語句還是 if else。業務系統就是對 PM 的需求堆 if else,基礎服務就是對 OS 資源和網路問題堆 if else。越來越多的需求造成了程式碼的膨脹,如何來管理這坨 if else,就衍生出了設計模式:通過各種手段來把 if else 劃分到更小的粒度。當然,還沒有任何一套方法論可以解決 if else 複雜性的問題,因為 if else 反映的其實是真實業務的需求,它其實代表了業務的複雜性。就像魯迅或者馬雲或者巴菲特說的:“不論用什麼方法,if else 不會憑空消失,只會從一個地方轉移到另一個地方”。但是,在面對特定的場景,特定的業務,程式碼的閱讀性、可維護性以及擴充套件性是可以得到提升的。

規則引擎

規則引擎不是一個新東西,而且已經存在了很長時間了,市面上也有各種規則引擎。比如大家常用的 iptables,就是一種規則引擎,crontab 也是一種規則引擎。我們平時寫的業務邏輯,其實也是規則引擎,只是用一種不那麼明顯的方式來體現。比如:“如果重置的密碼沒有包含大小寫和標點符號,就不讓提交”,“如果使用者是抽獎送的 VIP 雖然不播廣告但給他彈窗”……這些業務邏輯,無論怎麼優化,要處理的程式碼分支是一定存在的。規則引擎通過自定義的一套語法(DSL),提供了程式語言所不具備的語法糖,來最大限度減少開發量。同時大部分規則都需要支援使用者隨意配置,因此 DSL 大多是解釋執行。使用者在後臺配置一條規則比如,遊戲中使用者線上 1000 分鐘後扣 20 的點卡:

When
{
   ?customer: Customer(totalTime >=1000);
}
Then
{
   execute {?customer.setAmount(getAmount()-20.00);
} 

市面上比較常用的幾款規則引擎都屬於非常重量級的,使用者必須去學習它的 DSL 才能使用,當然基本上都是 JAVA 的包。這有一篇文章是幾款規則引擎的比較。沒有深度用過,我也沒法對此進行評價,但由於其概念實在太多且使用場景受限,我覺得它始終是個臨時方案,畢竟 “大道至簡”。而且 DSL 會越寫越複雜,真正用到語法糖的地方還是少數,到最後變成另一種蹩腳的而且領域受限的程式語言了。雖然如此,但由於其規則可動態增加,老規則便於複用,對於提供通用解決方案的一些軟體公司來說,規則引擎還是很有吸引力的。畢竟程式碼不需要動,加一些規則就能再次銷售。

但是對於進行業務開發的 RD 來說,其實需求很簡單,如果一個東西:

  • 正確
  • 簡單
  • 能減少繁瑣的工作

那麼這便是一個有吸引力的解決方案,如果它能再快點兒,那就是一個絕妙的主意了。對於減少 if else,我覺得 Linq 是一個非常好的方案。

Linq

Linq是 C# 中的一種常見技術,用過的都覺得爽。它就是一種語法糖,編譯器會編譯成真正的程式碼而不是在執行期解釋執行,因此效率也很高。Linq 程式碼大概就是這樣:

// Specify the data source.
int[] scores = new int[] { 97, 92, 81, 60 };

// Define the query expression.
IEnumerable<int> scoreQuery =
    from score in scores
    where score > 80
    select score;

可以看到,在程式碼中一些邏輯如果能這樣表達,那程式碼將會變得非常簡單。linq 基本上就是把 sql 語句搬到了 C# 中,唯一的區別就是它把 select 放在了最後而 sql 是在前面。這其實也是因為 IDE 智慧提示的需求,而不是查詢結構本身要這麼設計。把 select 放在最後,IDE 就知道你前面用過哪些變數,就可以分析出你可以 select 什麼從而進行提示。但是 Linq 是 C# 中的技術,和編譯器是強相關的,想移植到別的語言,還是很不容易的。當然,社群也是有一些嘗試的,比如 go 的移植版linq-go。然而,你懂的,由於 go 本身不支援泛型,編譯器也不支援 linq 語法,因此使用起來當然還是比較蹩腳,而且肯定是 interface{}和型別推斷滿天飛了……

sql

其實我們常用的 sql 中的 where 部分是完備的,Linq 也是把 sql 搬到了 C# 中。換句話說,sql 的 where 部分其實可以組合任意的布林邏輯。所有 RD 都會使 sql,而且是經常使 sql,既然想從編譯器的方向搞事情太麻煩(尤其是 go 是一門以簡單著稱的語言,官方對於加關鍵字是深惡痛絕的,更別說語法糖了),那麼把 mysql 查詢功能幹掉,解釋執行 where 比較的那部分功能做成一個函式提供出來呢,比如查詢一個人是不是頂級程式設計師可以這麼查:

sql := `sex='male' 
        and (
            dislike in ('girl', 'woman', 'female') 
            or 
            hobby in ('dress up', 'makeup')
        )`

但是由於沒有表資料,那麼真正用於比較的資料就需要業務方自己來提供了,以上功能可以寫為:

func isTopProgrammer(userInfo User) bool {
    sql := `sex='male' 
        and (
            dislike in ('girl', 'woman', 'female') 
            or 
            hobby in ('dress up', 'makeup')
        )`
    ok,_ := yql.Match(sql, map[string]interface{}{
        "sex": userInfo.Sex,
        "dislike": userInfo.Dislike,
        "hobby": userInfo.Hobbies,
    })
    return ok
}

yql 其實就是我最近搞的一個 lib求 star,求 pr,求指導),幫你執行 sql 的比較。

如果這麼搞,其實業務邏輯中經常變動的部分可以抽象到配置檔案裡,比如:

// 先定義好不同case的處理函式
type handleFunc func(map[string]interface{}) error
var handlers = map[int]handleFunc {
    1: sendEmail,
    2: sendCoupon,
    3: sendSms,
    4: sendAlert2Boss,
    5: runAway,
}
// 從當前請求中解析資料
data := resolveDataFromPostParams(request.Body)
// 從配置檔案載入規則
rules := loadRuleFromConf()

//列舉每條規則進行匹配,如果匹配成功則執行對應handler
for _,rule := range rules {
    success,err := yql.Match(rule.SQL, data)
    if nil != err || !success {
        continue
    }
    handler := handlers[rule.ID]
    handler(data)
    break
}

當然這種方式也有利有弊,比如說,把規則分離到配置檔案中,在 debug 時得幾個檔案之間跳來跳去地看程式碼,比較蛋疼(少寫點 bug 就行了,哈哈)。優點之一比如說可以利用推送平臺實時更新配置檔案,從而達到程式碼熱更新的目的。

最後

其實 yql 還有很多應用場景(我感覺),當然也有一些不足,比如功能還比較單一,解釋執行,快不快其實也沒和誰對比過……不過希望大家能夠嘗試嘗試,如果能夠提升工作效率,還是不錯的。求關注,求 star

更多原創文章乾貨分享,請關注公眾號
  • 拯救你的程式碼
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章