使用groovy实现一个简单的DSL

写在前面

  • 前段时间因为工作原因接触了Groovy,Groovy对定义DSL有很好的支持,在这里给大家分享一些我学到的知识,希望对大家有帮助,例子有点长…

目标:积分奖励系统

  • 这一章是背景

原有的消费系统:BroadbandPlus

  • BroadbandPlus
  • 假设我们有一个服务:BroadbandPlus
  • 在BroadbandPlus上用户有三种不同的订阅级别:BASIC/PLUS/PREMIUM(额外付费),根据订阅级别不同,每个月给用户发放不同的积分(access points).当然啦.不同的订阅级别每个月支付的费用是不同的
Subsciption levelcost per monthAccess points
Basic$9.99120
Plus$19.99250
Premium$39.99550
  • 这些积分的作用就是可以在BroadbandPlus上消费,我们的BroadbandPlus提供了三种产品:game/movie/music.当用户的积分耗尽后怎么办呢?==>充值就会变强.下面就来看看我们的价目表吧
MediaPointsType of accessOut of plan price
Movies
New Release40Daily$3.99
Other30Daily$2.99
Games
New Release303 days access$2.99
Other203 days access$1.99
Songs10Download$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 供大家参考学习,转载请注明出处
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值