设计模式专题-解析器设计模式

问题
顾客们在你的机器App或者网页上进行报价。最终的报价包含了2个部分,零件成本和劳动成本。我们希望能够对劳动力部分或者零件成本部分都可以进行适当的调整。这些调整可能来自优惠券,保修索赔或讲价。我们需要一种简单的语言来表达这些定制的调整。例如,我们应该能够定义一个表达式,可以为部分添加20%的折扣。或者是另一个减少10美元劳动力的,或者两者都有。

解决方案
我们会定义一种简单语言来做调整。我们将会有一套规则来涵盖加法,减法,乘法和除法。我们还将定义两个变量,他们将映射到零件和劳动力价格。一旦定义好了语言,我们的用户可以通过遵守语法的字符串来表达调整。例如,从我们将要使用的部分中减去10美元:

var expression = "l + p - 10.00"
//adjustment: total price is labor + partsPrice - $10

在开始之前,让我们先了解一下我们需要构建什么。为了能够解析字符串并将其解释为一个数学函数,我们需要定义一组规则和对象。通过观察我们的问题,我们发现了一些问题:

  • 数字:比如$10或者15%。
  • 变量:零件和劳动成本的占位符或者替代符。
  • 操作: +, -, * , / 这些操作符会定义好2个变量之间的操作运算。

虽然这些对象都有他们自己的含义和解析,但是他们都有一个共同的特性,他们都需要用我们定义的语言来解析。因此,我们定义一个协议,所有表达式都需要实现我们这个协议。

protocol Expression {
    
    func interpret(variables:[String: Expression]) -> Double
}

这个协议有一个函数方法叫作interpret。他有一个参数字典,和一个Double返回值。之前我们提到,没一个表达式都需要一个函数去解析它,而且,我们希望,从表达式得到的最终返回值是一个Double

有了基础的表达式协议之后,我们可以看看var expression = "l + p - 10.00"这个代码,他有3部分组成(我们上面提到的)。
那么,我们先来定义第一部分,值(数字)

class Number: Expression {
    
    var value: Double
    
    init(value: Double) {
        
        self.value = value
    }
    
    func interpret(variables:[String: Expression]) -> Double {
        
        return value
    }
}

Number类是一个值类型,有一个属性value,代表着这个对象的值,有初始化方法,还实现了表达式协议interpret方法,解析出变量的值。

上面我们也提到,变量,变量是一个很重要的东西,我们这个场景下的变量就是l 和 p。那么,我们来定义一个这样的类:

/// 变量类
class Variable: Expression {
    private var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func interpret(variables: [String : Expression]) -> Double {
        
        if let expression = variables[name] {
            return expression.interpret(variables: variables)
        }else {
            return 0.00
        }
    }
}

这个变量类有一个属性name,还实现了表达式协议interpret方法,解析出变量的值。

有了变量和值类型之后,我们需要为他们提供一些加减乘除的基础API

class Add: Expression {
    
    var leftOperand: Expression
    var rightOperand: Expression
    
    init(leftOperand: Expression, rightOperand: Expression) {
        
        self.leftOperand = leftOperand
        self.rightOperand = rightOperand
    }
    
    func interpret(variables:[String: Expression]) -> Double {
        return leftOperand.interpret(variables: variables) + rightOperand.interpret(variables: variables)
    }
}

class Subtract: Expression {
    
    var leftOperand: Expression
    var rightOperand: Expression
    
    init(leftOperand: Expression, rightOperand: Expression) {
        
        self.leftOperand = leftOperand
        self.rightOperand = rightOperand
    }

    func interpret(variables: [String : Expression]) -> Double {
        
        return leftOperand.interpret(variables: variables) - rightOperand.interpret(variables: variables)
    }
}

class Multiply: Expression {
    
    var leftOperand: Expression
    var rightOperand: Expression
    
    init(leftOperand: Expression, rightOperand: Expression) {
        
        self.leftOperand = leftOperand
        self.rightOperand = rightOperand
    }
    
    func interpret(variables: [String : Expression]) -> Double {
        
        return leftOperand.interpret(variables: variables) * rightOperand.interpret(variables: variables)
    }
}

class Divide: Expression {
    
    var leftOperand: Expression
    var rightOperand: Expression
    
    init(leftOperand: Expression, rightOperand: Expression) {
        
        self.leftOperand = leftOperand
        self.rightOperand = rightOperand
    }
    
    func interpret(variables: [String : Expression]) -> Double {
        
        return leftOperand.interpret(variables: variables) / rightOperand.interpret(variables: variables)
    }
}

他们的写法都是一样的意思,有左右两边的表达式,可以是Variable 也可能是Number类型,然后可以通过interpret方法解析出值来。

接下来就是整个运算的核心了,他实现了如何报价具体,可以看我们写的注释,我就不详细说明了:

/// 评估价格类
class Evaluator: Expression {
    
    /// 默认的语法树
    var syntaxTree: Expression = Number(value: 0.00)
    
    /// 表达式
    var expression: String
    
    
    /// 自定义的一个堆栈容器
    struct Stack<T> {
        
        var items = [T]()
        
        /// 增加一个元素,入栈
        ///
        /// - Parameter item: 元素
        mutating func push(item: T) {
            items.append(item)
        }
        
        /// 移除最后的元素,出栈
        ///
        /// - Returns: 元素
        mutating func pop() -> T {
            return items.removeLast()
        }
    }
    
    
    /// 初始化
    ///
    /// - Parameter expression: 表达式
    init(expression: String) {
        
        self.expression = expression
    }
    
    /// 建立语法树
    private func buildSyntaxTree() {
        var expressionStack = Stack<Expression>()
        
        /// 把所有元素按空格切割
        var items = expression.components(separatedBy: " ")
        
        var index = 0
        
        while index < items.count {
            
            switch items[index] {
            case "*":
                let nextExpression = getNextExpression(items: items, index: index)
                expressionStack.push(item: Multiply(leftOperand: expressionStack.pop(), rightOperand: nextExpression))
                index += 2
            case "/":
                let nextExpression = getNextExpression(items: items, index: index)
                expressionStack.push(item: Divide(leftOperand: expressionStack.pop(), rightOperand: nextExpression))
                index += 2
            case "+":
                let nextExpression = getNextExpression(items: items, index: index)
                expressionStack.push(item: Add(leftOperand: expressionStack.pop(), rightOperand: nextExpression))
                index += 2
                
            case "-":
                let nextExpression = getNextExpression(items: items, index: index)
                expressionStack.push(item: Subtract(leftOperand: expressionStack.pop(), rightOperand: nextExpression))
                index += 2
                
            default:
                
                /// 如果是值就初始化一个Number变量
                if let doubleValue = items[index].doubleValue {
                    expressionStack.push(item: Number(value: doubleValue))
                    index += 1
                } else {
                    //如果是l,p这样的变量,就初始化一个Variable变量
                    expressionStack.push(item: Variable(name: items[index]))
                    index += 1
                }
            }
            
        }
        syntaxTree = expressionStack.pop()
    }
    
    /// 获取下一个表达式
    ///
    /// - Parameters:
    ///   - items: 元素
    ///   - index: 位置
    /// - Returns: 表达式
    private func getNextExpression(items: [String], index: Int) -> Expression {

        /// 如果
        let next = items[index + 1]
        var nextExpression: Expression
        
        /// 能解析成值
        if let doubleValue = next.doubleValue {
            nextExpression = Number(value: doubleValue)
        } else {
            //不能解析成值,则认为是一个变量,l,p,零件或者劳动力
            nextExpression = Variable(name: next)
        }
        return nextExpression
    }
    
    
    /// 解析
    ///
    /// - Parameter variables: 变量
    /// - Returns: 返回值
    func interpret(variables: [String : Expression]) -> Double {
        
        //如果有2种运算
        if (expression.contains("/") || expression.contains("*")) &&
            (expression.contains("+") || expression.contains("-")) {
            
            let expressions = parseoutAdditionsAndSubtractions(input: expression)
            var newExpression = ""
            var index = 0
            for expression in expressions {
                if expression == "+" || expression == "-" {
                    newExpression += expression
                } else {
                    let eval = Evaluator(expression: expression)
                    let result = eval.interpret(variables: variables)
                    newExpression += String(result)
                }
                
                if index != expressions.count - 1 {
                    newExpression += " "
                }
                index += 1
            }
            let evaluator = Evaluator(expression: newExpression)
            return evaluator.interpret(variables: variables)
        } else {
            //只有一种运算
            
            //建立语法树
            buildSyntaxTree()
            return syntaxTree.interpret(variables: variables)//返回解析值
        }
    }
    
    private func parseoutAdditionsAndSubtractions(input: String) -> [String] {
        var result = [String]()
        
        let items = input.components(separatedBy: " ")
        
        var sentence = ""
        var index = 0
        for item in items {
            if item == "+" || item == "-" {
                result.append(sentence.trim())
                result.append(item)
                sentence = ""
            } else {
                sentence += item
                if index != items.count - 1 {
                    sentence += " "
                }
            }
            index += 1
        }
        result.append(sentence)
        return result
    }
}

然后我们再提供一个用户操作的封装类:

/// 报价类
class Quote {
    
    /// 零件价格
    var partsPrice: Double
    
    /// 劳动力价格
    var laborPrice: Double
    
    /// 调整价格
    var adjustments: String?
    
    
    /// 初始化报价
    ///
    /// - Parameters:
    ///   - partsPrice: 零件价格
    ///   - laborPrice: 劳动力价格
    ///   - adjustments: 调整价格
    init(partsPrice: Double, laborPrice: Double, adjustments: String?) {
        self.partsPrice = partsPrice
        self.laborPrice = laborPrice
        self.adjustments = adjustments
    }
    
    /// 初始化报价
    ///
    /// - Parameters:
    ///   - partsPrice: 零件报价
    ///   - laborPrice: 劳动力报价
    convenience init(partsPrice: Double, laborPrice: Double) {
        self.init(partsPrice: partsPrice, laborPrice: laborPrice, adjustments: nil)
    }
    
    
    /// 总价格
    var totalPrice: Double {
        
        /// 需要调整价格
        if let adjustments = adjustments {
            var variables = [String: Expression] ()
            variables["l"] = Number(value: laborPrice)
            variables["p"] = Number(value: partsPrice)
            
            /// 评估价格
            let evaluator = Evaluator(expression: adjustments)
            return evaluator.interpret(variables: variables)
        } else {
            return partsPrice + laborPrice
        }
    }
}

最后,我们写一个测试方法:

func interpreterTest() {
    let quote = Quote(partsPrice: 145.00, laborPrice: 45.00)
    
    //adjustment: total price is only labor plus $20
    quote.adjustments = "l + 20.00"
    print(quote.totalPrice)

    //adjustment: total price is partsPrice - $10
    quote.adjustments = "p - 10.00"
    print(quote.totalPrice)

    //adjustment: total price is labor and part - $10
    quote.adjustments = "l + p - 10.00"
    print(quote.totalPrice)
    
    //adjustment: total price is labor + 10% off parts
    quote.adjustments = "l + p - p * 0.1"
    print(quote.totalPrice)
    
    //adjustment: total price is 20% off labor + 10% off parts
    quote.adjustments = "l - l * 0.2 + p - p * 0.1"
    print(quote.totalPrice)
    
    //adjustment: total price is %20 off total price
    quote.adjustments = "l - l * 0.2 + p - p * 0.2"
    print(quote.totalPrice)
    
    //adjustment total price is parts * labor :|
    quote.adjustments = "p * l"
    print(quote.totalPrice)
}

这样,我们就可以自定义我们的表达式来做一些运算了。有兴趣可以查看具体的英文链接
打印的结果:

image.png

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容