使用groovy實現一個簡單的DSL

寫在前面

  • 前段時間因爲工作原因接觸了Groovy,Groovy對定義DSL有很好的支持,在這裏給大家分享一些我學到的知識,希望對大家有幫助,例子有點長…

目標:積分獎勵系統

  • 這一章是背景

原有的消費系統:BroadbandPlus

  • BroadbandPlus
  • 假設我們有一個服務:BroadbandPlus
  • 在BroadbandPlus上用戶有三種不同的訂閱級別:BASIC/PLUS/PREMIUM(額外付費),根據訂閱級別不同,每個月給用戶發放不同的積分(access points).當然啦.不同的訂閱級別每個月支付的費用是不同的
Subsciption level cost per month Access points
Basic $9.99 120
Plus $19.99 250
Premium $39.99 550
  • 這些積分的作用就是可以在BroadbandPlus上消費,我們的BroadbandPlus提供了三種產品:game/movie/music.當用戶的積分耗盡後怎麼辦呢?==>充值就會變強.下面就來看看我們的價目表吧
Media Points Type of access Out of plan price
Movies
New Release 40 Daily $3.99
Other 30 Daily $2.99
Games
New Release 30 3 days access $2.99
Other 20 3 days access $1.99
Songs 10 Download $0.99
怎麼樣?
作爲一個額外付費用戶($39.99)
你每個月的消費計劃可能是這樣的
看4場新電影:160積分
玩30天遊戲:300積分
下載9首歌:90積分

而作爲一個屌絲用戶($9.99)
有一個月你突發奇想,也想體驗一把富人的生活.那麼..
看4場新電影:120積分 + $3.99
玩30天遊戲:$2.99*10 = $29.9
下載9首歌:$0.99*9 ≈ $8.9
算下來就是:$9.99+$3.99+$29.9+$8.99 ≈ $53
  • Design
  • 迴歸正題,來設計一下我們的BroadBanPlus服務吧
  • 首先,如果用戶需要使用某產品之前,我們第一步要做如下的校驗,如果校驗通過了用戶就可以直接訪問資源,如果沒有通過,我們就要提升用戶要出錢購買.此時用戶有兩個選擇:1)直接付錢2)升級他們的賬戶
  • 所以我們的API長這樣
class BroadbandPlus {
    boolean canConsume(subscriber,media){
        1.用戶是否已經購買了該產品?
        2.用戶已經購買了該產品,但是否已經過期?
        3.如果用戶沒有購買產品,或者產品過期
          檢測用戶是否有足夠的分數,如果有則扣除響應的分數並授權
        return 1&&2&&3
    }
    
    void consume(subscriber,media){
        subscriber正在用media
    }
    
    void purchase(subscriber,media){
        付錢吧,少年!
    }
    
    void upgrade(subscriber,fromPlan,toPlan){
        分數不夠了,您要升級到VVIP?
        大爺.您請!!
    }
    //什麼?你問爲什麼沒有降級?對不起.當前版本不支持!以後也不會支持!
}

積分獎勵系統

開始

  • 前面我們的BroadbandPlus商城已經做好了,是不是迫不及待來消費了呢?
  • 但這個世界還是窮人多呀.看來我們需要做一個積分獎勵系統來刺激消費!計劃通!
  • 但不幸的是獎勵規則是時刻在變的,而且獎勵規則往往是由市場部人員決定的,所以我們決定寫一個簡單的DSL,市場部的人員只需要根據我們的規則來書寫獎勵規則,就可以了
  • 設計
  • 1.獎勵在系統中會被多種不同的事件觸發。這些事件是:消費(當一個用戶消費一個產品:觀看電影,玩遊戲等)、升級(用戶升級他們的訂閱計劃)、以及購買(只要用戶給錢)
  • 2.獎勵應該是基於一個或多個條件的,例如用戶的花費歷史或者被消費的媒體類型
  • 3.獎勵應該是多種不同好處被授權的結果(獎勵的東西),例如免費訪問多媒體、積分獎勵和額外的訪問授權等
  • 爲了簡單演示,我們這裏僅假設消費(onConsume)會觸發機會規則
  • 根據上面的設計,我們的DSL是這樣的
注意:此時你的視角是使用者視角
onConsume = {//用戶消費觸發
	rewward("Reward Description"){//獎勵
		condition{
			//想獲取獎勵要達到的條件,當然是各種花$了
		}
		grant{
			//各種好處.無非也就是多下載個電影啥的
		}
	}
}
  • 獎勵觸發
  • 我們使用一個binding的變量來標識是否達到獎勵的條件,並且通過condition閉包去改變它
注意:此時你的視角是開發者視角
binding.condition = { closure->
	closure.delegate = delegate
	//考慮多種條件
	binding.result = (closure() && binding.result)
}

binding.grant = {closure->
	closure.delegate = delegate

	if(binding.result)
		closure()
}
  • 更多的條件
  • 看吧,我們已經實現了我們了積分獎勵系統,但你要知道,我們把這個系統寫成DSL的目的是給市場人員用的
  • 此時市場人員說:現在我們希望用戶打到某一個條件就可以獲得獎勵,或者打到某幾個條件,或者達到某幾個條件中的一個.Fuck.他說他不會用&&,||該怎麼辦?
  • 聰明的我們爲了迎合市場人員,自創了allOf和anyOf閉包(嘴上笑嘻嘻,心裏嗎買皮),市場人員可以這麼使用DSL了
注意:此時你的視角是使用者視角
reward ( "anyOf and allOf blocks" ) {
	allOf {
		//所有條件都滿足時
		condition { }
		 ... more conditions
	}
	condition {
		//單獨的條件
	}
	anyOf {
		//滿足某一個條件時
		condition {}
	 	... more conditions
	}
	grant {
		//我們的獎勵: )
	}
}
  • 而爲了實現它我們不得不將我們的binding做出如下改變,使用一個binding.useAnd來標識我們使用的是and邏輯還是or邏輯
注意:此時你的視角是開發者視角
binding.reward = {
	spec,closure->{
		closure.delegate = delegate
		//在reward的最開始,我們假設結果爲true
		binding.result = true
		//默認的操作符爲and
		binding.and = true
		closure();
	}
}
binding.condition = {closure->
	closure.delegate = delegate
	if(binding.useAnd)
		binding.result = (closure() && binding.result)
	else 
	    binding.result = (clousre() || binding.result)
}
binding.allOf = {closure->
	closure.delegate = delegate
	//在此之前我們先將之前的result和useAnd保存起來
	//這主要是考慮到嵌套的情況,要先用臨時變量保存之前的結果
	def storeResult = binding.result
	def storeAnd = binding.and
	//將binding.result和binding.and設置爲true
	binding.result = ture
	binding.and = true

	closure()

	if(storeAnd){
		binding.result = (storeResult && binding.result)
	}else{
		binding.result = (storeResult || binding.result)
	}

	binding.and = storeAnd
}
binding.anyOf = { closure ->
	closure.delegate = delegate
	def storeResult = binding.result
	def storeAnd = binding.and
	//將binding.result和binding.and設置爲false
	binding.result = false
	binding.and = false
	closure()
	if (storeAnd) {
		binding.result = (storeResult && binding.result)
	} else {
		binding.result = (storeResult || binding.result)
	}
	binding.and = storeAnd
}	
  • OK,現在讓我們來試試吧!!
如果一個市場人員這樣寫:那麼最終結果就是符合條件
reward ( "nested anyOf and allOf conditions" ) {
    anyOf {  
        allOf { 
            condition { true } 
            condition { false }
        }

        condition { false }
        anyOf {
            condition { false }
            condition { true } 
        }
    }
}

看完這一段你一定是崩潰的:
fuck!什麼玩意?我寫個false/true?
不要着急.往下看.

輕量速記的方法

  • 在DSL最終上線之前,我們還需要定義一些輕量速記語法,讓我們的DSL和我們的系統結合起來.我們定義下面的幾種方法
  • 1.binding.extend:給用戶賬戶增加時間
binding.extend = { days ->
	//當然.你先不需要知道BroadbandPlus類是什麼.請往下看
	def bbPlus = new BroadbandPlus()
	bbPlus.extend(binding.account, binding.media, days)
}
  • 2.binding.points:給用戶賬戶增加點數
binding.points = { points ->
	binding.account.points += points
}
  • 除此之外我們再定義一些狀態值作爲條件
  • 1.binding.is_new_release = media.newRelease
  • 2.binding.is_video = (media.type == “VIDEO”)
media又是什麼鬼??
media顯然就是我們的產品,包括GAME,VIDEO,SONG,是我們主營業務.請往下看

集成

接下來就是最後一步了.我們通過一個service將我們寫的DSL規則和市場人員寫的獎勵規則集成起來,應用到我們的系統中去

  • 系統類
  • 1.BroadBandPlus:爲了展示我們的這個 DSL 是如何工作的,我們必須得構建一些應用的基本骨架來運行我們的 BroadbandPlus 服務。這裏我們也不必太糾結這些骨架類的細節,它們唯一的目的只是 爲了提供一個鉤子來運行我們的 DSL,並非是一個實際的工作的系統。
class BroadbandPlus {
    //後面會說.這個類是我們DSL的核心類
    def rewards = new RewardService() 
    
    def canConsume = { account, media ->
        def now = new Date()
        if (account.mediaList[media]?.after(now))
            return true 
            account.points > media.points
        }
        
    def consume = { account, media ->
        // 第一次消費才獎勵
        if (account.mediaList[media.title] == null) {
            def now = new Date()
            account.points -= media.points account.mediaList[media] = now + media.daysAccess // 應用 DSL 獎勵規則 rewards.applyRewardsOnConsume(account, media)
        }
    }
    
    def extend = {account, media, days ->
        if (account.mediaList[media] != null) {
            account.mediaList[media] += days
        }
    }
}
  • 2.Account
class Account {
    String subscriber
    String plan
    int points
    double spend
    Map mediaList = [:]
    void addMedia (media, expiry) {
        mediaList[media] = expiry
    }
    void extendMedia(media, length) {
        mediaList[media] += length
    }
    Date getMediaExpiry(media) {
        if(mediaList[media] != null) {
            return mediaList[media]
        }
    }

    @Override
    String toString() {
        String str = "subscriber:"+subscriber+"\n" +
                "plan:"+plan+"\n" +
                "points:"+points+"\n" +
                "spend:"+spend+"\n"

        mediaList.keySet().each {
            str +=  it.title+","+mediaList.get(it)+"\n"
        }
        return str
    }
}
  • 3.Media
class Media {
    String title
    String publisher
    String type //類型是 VIDEO\GAME\SONG 
    boolean newRelease
    int points
    double price
    int daysAccess
}
  • 系統類
  • RewardService
class RewardService {

    static Binding baseBinding = new Binding();

    static {
        loadDSL(baseBinding)
        loadRewardRules(baseBinding)
    }

    //構造 reward、condition、allOf、anyOf、grant 等核心閉包到 binding 中
    //而這些 binding 構建的變量、上下文信息都可以傳入給 DSL,讓編寫 DSL
    //的人員可以利用!
    static void loadDSL(Binding binding) {

        binding.reward = { spec, closure ->
            closure.delegate = delegate
            binding.result = true
            binding.and = true
            closure()
        }

        binding.condition = { closure ->
            closure.delegate = delegate
            if (binding.and)
                binding.result = (closure() && binding.result)
            else
                binding.result = (closure() || binding.result)
        }

        binding.allOf = { closure ->
            //closure.delegate = delegate
            def storeResult = binding.result
            def storeAnd = binding.and
            binding.result = true // Starting premise is true binding.and = true
            closure()
            if (storeAnd) {
                binding.result = (storeResult && binding.result)
            } else {
                binding.result = (storeResult || binding.result)
            }
            binding.and = storeAnd
        }

        binding.anyOf = { closure ->
            closure.delegate = delegate
            def storeResult = binding.result
            def storeAnd = binding.and
            binding.result = false // Starting premise is false binding.and = false
            closure()
            if (storeAnd) {
                binding.result = (storeResult && binding.result)
            } else {
                binding.result = (storeResult || binding.result)
            }
            binding.and = storeAnd
        }

        binding.grant = { closure ->
            closure.delegate = delegate
            if (binding.result)
                closure()
        }

        binding.extend = { days ->
            def bbPlus = new BroadbandPlus()
            bbPlus.extend(binding.account, binding.media, days)
        }

        binding.points = { points ->
            binding.account.points += points
        }

    }

    //構建一些媒體信息和條件短語

    void prepareMedia(binding, media) {
        binding.media = media
        binding.isNewRelease = media.newRelease
        binding.isVideo = (media.type == "VIDEO")
        binding.isGame = (media.type == "GAME")
        binding.isSong = (media.type == "SONG")
    }

    //初始化加載獎賞腳本,在這個腳本中,可以定義 onConsume 等 DSL
    static void loadRewardRules(Binding binding) {
        Binding selfBinding = new Binding()
        GroovyShell shell = new GroovyShell(selfBinding)
        //市場人員寫的 DSL 腳本就放在這個文件下,裏面定義 onConsume //這些個 rewards 獎勵
        shell.evaluate(new File("./rewards.groovy")) //將外部 DSL 定義的消費、購買獎勵賦值
        binding.onConsume = selfBinding.onConsume
    }

    //真正的執行方法
    void apply(account, media) {
        Binding binding = baseBinding;
        binding.account = account
        prepareMedia(binding,media)
        GroovyShell shell = new GroovyShell(binding)
        shell.evaluate("onConsume.delegate=this;onConsume()")
    }

}
  • reward.groovy:市場人員寫的獎勵規則
package com.tianhaollin.groovy

onConsume = {
    reward ( "觀看迪斯尼的電影, 你可以獲得 25%的積分." ) {
        allOf {
            condition {
                media.publisher == "Disney"
            }
            condition {
                isVideo
            }
        }
        grant {
            points media.points / 4
        }
    }

    reward ( "查看新發布的媒體,可以延長一天" ) {
        condition {
            isNewRelease
        }
        grant {
            extend 1
        }
    }
}
  • test.groovy
account = new Account(subscriber: "Mr.tian",plan:"BASIC", points:120, spend:0.0)
terminator = new Media(title:"Terminator", type:"VIDEO",
        newRelease:true, price:2.99, points:30,
        daysAccess:1, publisher:"Fox")
up = new Media(title:"UP", type:"VIDEO", newRelease:true,
        price:3.99, points:40, daysAccess:1,
        publisher:"Disney")
account.addMedia(terminator,terminator.daysAccess)
account.addMedia(up,up.daysAccess)
def rewardService = new RewardService()
rewardService.apply(account,terminator)
rewardService.apply(account,up)
println account

總結

  • 本文主要是通過Binding對象來實現一種簡單的DSL,在我們寫好DSL之後,每次系統啓動時候只需要從特定的地方加載reward.groovy就可以確定獎勵規則,並且在用戶消費時調用鉤子方法就可以執行特定的action(onConsume)了
  • 我們還可以使用MetaClass的動態方法生成、groovy方法指針、方法鏈、命名參數等高級特性來定義自己更加優雅的DSL,甚至可以讓DSL像寫英語一樣簡單,比如:sendEmail from:“xiaoming”,to:“xiaohong”,context:"i love you"執行一個發送郵件的操作
  • 本文項目地址:https://github.com/tianhaolin1991/groovyDsl 供大家參考學習,轉載請註明出處
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章