技术 Swift计算器的设计逻辑
技术 Swift计算器的设计逻辑
Stanford Paul Hegarty,大约在2017年的iOS开发课程中讲过用swift写计算器app的例子,后来,特别是在iOS开发从MVC框架,转到MVVM框架上来的时候,他已经不再使用这个例子。
但我个人认为,计算器的设计逻辑,对于理解swift语言,是一个很好的入门例子,它比较简单(虽然从完整app的角度,其实一点都不简单),能比较快地写出demo。
下面的内容里面,如果有不准确的地方,是由于我个人的理解。这个文章会持续更新和修改。
大的框架定义:
struct Cal {
// 定义一个内部的参与计算的变量,这个变量从外部无法访问,只能从内部设定
private var accmulator: Double?
// 计算引擎函数
func perform(_ symbol: String) {
}
// 赋值函数,它负责把外部值赋给内部变量,因为内部变量可变,所以它是mutating
mutating func setOperand(_ operand: Double) {
accumulator = operand
}
// 结果取值接口,它对外暴露,并且通过 get 设定为当前结果只读模式
var result: Double {
get {
return accumulator!
}
}
}
上面是一个经典的数据处理引擎的设计方式。
这种设计方式的好处是:
- 易于测试。内部运算变量的值肯定是通过 setOperand这个接口被设定的,而不会是由其它调用函数的设定。对于追踪来源很明确。
- 它的结果是只读的,也就是说,这个结果肯定是在函数的内部产生的,不会被外部干扰和修改。
- 进和出,保持一致。
针对上述代码的外部调用方式示例:
private var brain = Cal()
//传入变量,计算器上显示的数值
brain.setOperand(displayValue)
//执行计算
brain.performOperation(symbol)
//如果有计算结果
if let result = braind.result {
displayValue = result
}
计算引擎核心部分的设计:
switch case
struct Cal {
private var accmulator: Double?
mutating func perform(_ symbol: String) {
switch symbol {
case "pi":
accumulator = Double.pi
default:
break
}
}
一个最mini的演示版本已经可以运行。
上述演示版本,存在的明显问题:
如果要加更多的操作,只能增加更多的switch case;
可以处理直接得出结果的 Double.pi, 但如果类似 5*4=20,参数该如何传入?
dictionary
先解决第一个部分,重复添加switch case的问题:
首先的想法就是,设计一个表格,把对应的操作存储在里面,使用的时候在表格中查询对应的操作。
在swift中,使用Dictionary来承担上述表格的角色。
private var operations: Dictionary<String, Double> =
[
"pi" : Double.pi,
"e" : M_E
]
mutating func perform(_ symbol: String) {
if let constant = operations[symbol] {
accumulator = constant
}
}
这样就完成了字典的设计。 因为字典可以是任何类型,所以在 <> 指明了对应的类型 String 和 Double。 通过把常量加入到字典里的方式,比上面的 switch case 要简洁一些。
enum
接下来,我们将见识到:
- 旧的问题被解决之后,新的问题随之涌现;
- 为解决新出现的问题,引入新的工具;
- 后出现的问题被解决,新的问题随之涌现。
通过上述循环反复的过程,swift的 type “类型”将陆续登场。
上面的字典定义,解决了不停地塞 switch case 的问题,但也有它自身的问题,就是字典定义里面,接受的参数限定为:String 和 Double,那么象 开根号 这样的操作,没有办法放到字典里面。因为后者是带一个参数的函数。
于是接下来,引入一个新的类型,enum。通过 enum 来对应一类明确的操作类型。
private enum Operation {
case constant //定义一类关于常数操作
case unaryOperation //定义象开根号这种一个变量的操作
}
private var operations: Dictionary<String, Operation> =
[
"pi": Operation.constant,
"cos": Operation.unaryOperation,
"sqrt": Operation.unaryOperation
]
现在,计算引擎演变成上面的样子。它能够包容定义的不同类型的操作。
但是呢,上面的方法也仍然存在一个问题,就是它把之前定义的具体操作函数,比如 Double.pi 丢掉了,变成了一个通用的,但无法执行具体计算的类型。
接下来将出场计算引擎设计里面,几乎是最核心的一个部分:
associated value
associated value 的作用类似于,把数据 attach 到 enum 的类型里面。这样 enum 带着数据参数及其类型,就可以映射到 dictionary 里进行具体的操作计算了。
在实际使用中,通过 “let” 获取enum类型里的associated value,代码如下:
private enum Operation {
case constant(Double)
case unaryOperation( (Double)->Double )
//括号里的部分:(Double)->Double 其实是一个函数,如 sqrt(36)
}
private var operations: Dictionary<String, Operation> =
[
"pi" : Operation.constant(Double.pi),
"sqrt" : Operation.unaryOperation(sqrt)
]
mutating func performOperation(_ symbol: String) {
if let operation = operations[symbol] {
switch operation {
case .constant(let value): //情况一
accumulator = value
case .unaryOperation(let function): //情况二
if accumulator != nil {
accumulator = function(accumulator!)
// 把函数作为一个type来调用
}
}
}
}
}
重新理一遍逻辑:
- -> 定义一个字典,来查询对应的操作;
- -> 原生字典,对应的操作类型有限,引入 enum 来定义类型;
- -> enum如果只定义类型,无法进行具体操作,于是引入assocate value;
- -> 函数也是一种类型,在assocate value里,通过函数解决计算问题。
再进一步,
解决剩下的问题,即类似于 5 * 3 = 15 这样的通用问题。
private enum Operation {
case constant(Double)
case unaryOperation( (Double)->Double ) //括号里就变成了一个函数
case binaryOperation( (Double, Double) -> Double)
case equals //这里我们没有定义任何associate value,后面会直接绑定一个函数
}
private var operations: Dictionary<String, Double> =
[
"pi" : Operation.constant(Double.pi),
"sqrt" : Operation.unaryOperation(sqrt),
"x": Operation.binaryOperation(multiply),
"=": Operation.equals
]
private func multiply(op1: Double, op2: Double) -> Double {
return op1 * op2
}
接下来的操作比较复杂一些,
我们要想一个结构,它能够包进去两个变量, 3 和 5,还要包进去操作符号: + - x /
因为 struct 能够包括变量和函数,所以就用 struct 来定义这个结构
private struct PendingOperation {
let function: (Double, Double) -> Double //定义操作
let firstOperand: Double
func perform(with secondOperand: Double) -> Double { // with 只是一个易懂的表达名称,不是关键字
return function(firstOperand, secondOperand)
}
}
之所以要把 secondOperand 作为一个外部参数传进去,而不是直接在 PendingOperation里面计算出来,是因为,计算器 的 “ = “ 才负责出结果的这一步操作。
而 + - x / 这些符号,负责 pendingOperation 的操作。
把上面的结构定义好以后,就可以开始串起全流程:
private var pendingOperation = PendingOperation?
// 问号是因为用户没按 + - 就不存在这个变量
mutating func performOperation(_ symbol: String) {
if let operation = operations[symbol] {
switch operation {
case .constant(let value): //情况一
accumulator = value
case .unaryOperation(let function): //情况二
if accumulator != nil {
accumulator = function(accumulator!)
// 函数可以通过这样来调用,参见上面的第9
}
case .binaryOperation(let function):
if accumulator != nil {
pendingOperation = PendingOperation(function: function, firstOperand: accumulator! )
// 是对上面的结构的初始化
accumulator = nil
// 把它置空,以等待放入第2个数据
}
case .equals:
performPendingOperation()
}
}
private muatating func performPendingOperation() {
if pendingOperation != nil && accumulator != nil {
// 把前面保存的临时数据操作结构拿出来,计算出结果
accumulator = pendingOperation!.perform(with: accumuator!)
pendingOperation = nil
}
}
至此,功能全部设计完备。但是程序还留下一个小瑕疵。
看上面的:
private func multiply(op1: Double, op2: Double) -> Double {
return op1 * op2
}
private var operations: Dictionary<String, Double> =
[
"pi" : Operation.constant(Double.pi),
"sqrt" : Operation.unaryOperation(sqrt),
"x": Operation.binaryOperation(multiply)
]
如果我们要往字典里面加入更多的自定义操作,比如: / - + 就要在外部定义更多的自定义函数。
这个时候,我们引入闭包。
从而简化成:
private var operations: Dictionary<String, Double> =
[
"pi" : Operation.constant(Double.pi),
"sqrt" : Operation.unaryOperation(sqrt),
"x": Operation.binaryOperation($0 * $1),
"+": Operation.binaryOperation($0 + $1)
]
待优化 post status: to be refined
tag: 编程 python swift