在上一期中 ,我探索了Java泛型在Scala中模仿模式匹配的使用,它使您可以编写简洁,可读的条件语句。 Scala模式匹配是替代调度机制的一个示例,我将其广泛用作描述语言动态选择行为的方式。 本期文章扩展了讨论,以显示各种功能JVM语言中的调度机制如何比Java对应语言提供更多的简洁性和灵活性。
使用Groovy改善调度
在Java中,条件执行最终使用if
语句完成,除非在非常有限的情况下使用switch
语句。 由于难以理解长系列的if
语句,因此Java开发人员依赖于四人一组(GoF)Factory(或Abstract Factory)模式(请参阅参考资料 )。 如果使用包含更灵活的决策表达的语言,则可以进一步简化很多代码。
Groovy有一个功能强大的switch
语句,它模仿Java的switch
语句的语法(而不是行为),如清单1所示:
清单1. Groovy大大改进的switch
语句
class LetterGrade {
def gradeFromScore(score) {
switch (score) {
case 90..100 : return "A"
case 80..<90 : return "B"
case 70..<80 : return "C"
case 60..<70 : return "D"
case 0..<60 : return "F"
case ~"[ABCDFabcdf]" : return score.toUpperCase()
default: throw new IllegalArgumentException("Invalid score: ${score}")
}
}
}
Groovy switch
语句接受各种动态类型。 在清单1中 , score
参数应该是0到100之间的数字或字母等级。 与Java中一样,您必须使用相同的掉线语义以return
或break
结束每种case
。 但是在Groovy中,与Java不同,我可以指定范围( 90..100
,非包含范围( 80..<90
),正则表达式( ~"[ABCDFabcdf]"
)和默认条件。
Groovy的动态类型使我能够发送不同类型的参数并做出适当的React,如清单2中的单元测试所示:
清单2.测试Groovy字母等级
@Test
public void test_letter_grades() {
def lg = new LetterGrade()
assertEquals("A", lg.gradeFromScore(92))
assertEquals("B", lg.gradeFromScore(85))
assertEquals("D", lg.gradeFromScore(65))
assertEquals("F", lg.gradeFromScore("f"))
}
功能更强大的switch
为您在串行if
s和Factory设计模式之间提供了有用的中间立场。 Groovy的switch
可让您匹配范围和其他复杂类型,其目的与Scala中的模式匹配相似。
Scala模式匹配
Scala模式匹配使您可以指定具有相应行为的匹配案例。 考虑上一期文章的字母级示例,如清单3所示:
清单3. Scala中的字母等级
val VALID_GRADES = Set("A", "B", "C", "D", "F")
def letterGrade(value: Any) : String = value match {
case x:Int if (90 to 100).contains(x) => "A"
case x:Int if (80 to 90).contains(x) => "B"
case x:Int if (70 to 80).contains(x) => "C"
case x:Int if (60 to 70).contains(x) => "D"
case x:Int if (0 to 60).contains(x) => "F"
case x:String if VALID_GRADES(x.toUpperCase) => x.toUpperCase
}
在Scala中,我通过将参数类型声明为Any
允许动态输入。 起作用的运算符是match
,它尝试匹配第一个真实条件并返回结果。 如清单3所示,每种情况都可以包括指定条件的保护条件。
清单4显示了执行一些字母级选择的结果:
清单4.在Scala中测试字母
printf("Amy scores %d and receives %s\n", 91, letterGrade(91))
printf("Bob scores %d and receives %s\n", 72, letterGrade(72))
printf("Sam never showed for class, scored %d, and received %s\n", 44, letterGrade(44))
printf("Roy transfered and already had %s, which translated as %s\n",
"B", letterGrade("B"))
Scala中的模式匹配通常与Scala的case类一起使用, case类旨在表示代数和其他结构化数据类型。
Clojure的“可弯曲”语言
针对Java平台的另一个新一代功能性的语言是Clojure的(参见相关主题 )。 Clojure是JVM上Lisp的一种实现,它的语法与大多数现代语言截然不同。 尽管开发人员很容易适应语法,但是它给主流Java开发人员留下了深刻的印象。 Lisp语言系列的最佳功能之一是同源性 ,这意味着该语言是使用其自己的数据结构实现的,从而可以扩展到其他语言无法提供的程度。
Java及其类似的语言都包含关键字 -语言的语法框架。 开发人员无法使用该语言创建新的关键字(尽管某些类似Java的语言允许通过元编程进行扩展),并且关键字具有开发人员无法使用的语义。 例如,Java if
语句“理解”诸如短路布尔评估之类的东西。 尽管您可以使用Java创建方法和类,但是您无法创建基本的构建基块,因此您需要将问题转换为编程语言的语法。 (实际上,许多开发人员认为他们的工作就是执行这种翻译。)在类似Clojure的Lisp变体中,开发人员可以针对问题修改语言 ,从而模糊了语言设计人员和使用该语言的开发人员之间的区分。 我将在以后的文章中探讨同音的全部含义。 这里要理解的重要特征是Clojure(和其他Lips)背后的哲学。
在Clojure中,开发人员使用该语言创建可读(Lisp)代码。 例如,清单5显示了Clojure中的字母级示例:
清单5. Clojure中的字母等级
(defn letter-grade [score]
(cond
(in score 90 100) "A"
(in score 80 90) "B"
(in score 70 80) "C"
(in score 60 70) "D"
(in score 0 60) "F"
(re-find #"[ABCDFabcdf]" score) (.toUpperCase score)))
(defn in [score low high]
(and (number? score) (<= low score high)))
在清单5中 ,我编写了letter-grade
方法来很好地阅读,然后实现了in
方法以使其工作。 在这段代码中, cond
函数使我能够评估由我的in
方法处理的一系列测试。 与前面的示例一样,我同时处理数字和现有字母级字符串。 最终,返回值应为大写字符,因此,如果输入为小写,我将对返回的字符串调用toUpperCase
方法。 在Clojure中,方法是一等公民,而不是类,使方法调用“由内而外”:Java中对score.toUpperCase()
的调用等效于Clojure的(.toUpperCase score)
。
我在清单6中测试Clojure的字母级实现:
清单6.测试Clojure字母等级
(ns nealford-test
(:use clojure.test)
(:use lettergrades))
(deftest numeric-letter-grades
(dorun (map #(is (= "A" (letter-grade %))) (range 90 100)))
(dorun (map #(is (= "B" (letter-grade %))) (range 80 89)))
(dorun (map #(is (= "C" (letter-grade %))) (range 70 79)))
(dorun (map #(is (= "D" (letter-grade %))) (range 60 69)))
(dorun (map #(is (= "F" (letter-grade %))) (range 0 59))))
(deftest string-letter-grades
(dorun (map #(is (= (.toUpperCase %)
(letter-grade %))) ["A" "B" "C" "D" "F" "a" "b" "c" "d" "f"])))
(run-all-tests)
在这种情况下,测试代码比实现更复杂! 但是,它显示了Clojure代码的简洁程度。
在numeric-letter-grades
测试中,我要检查适当范围内的每个值。 如果您不熟悉Lisp,则对其进行解码的最简单方法是从内向外读取。 首先,代码#(is (= "A" (letter-grade %)))
创建一个带有单个参数的新匿名函数(如果您有一个带有单个参数的匿名函数,则可以将其表示为%
正文),如果返回正确的字母等级,则返回true
。 map
函数将此匿名函数映射到第二个参数中的整个集合中,第二个参数是适当范围内的数字列表。 换句话说, map
在集合中的每个项目map
调用此函数,返回修改后的值的集合(我忽略了)。 dorun
函数允许产生副作用,而该副作用是测试框架所依赖的。 调用清单6中每个range
map
返回所有true
值的列表。 clojure.test
命名空间中的is
方法验证该值是否为副作用。 在dorun
调用映射函数可以使副作用正确发生并返回测试结果。
Clojure多方法
一连串的if
语句很难阅读和调试,但是Java在语言级别上没有任何特别好的替代方法。 通常通过使用GoF中的Factory或Abstract Factory设计模式来解决此问题。 由于基于类的多态性,Factory模式在Java中起作用,这使我可以在父类或接口中定义常规方法签名,然后选择动态执行的实现。
工厂与多态
与Java相比,Groovy的语法更加冗长和易于阅读,因此在接下来的两个代码示例中,我将使用Java而不是Java来实现它-但是多态在两种语言中都相同。 考虑将接口和类的这种组合定义一个Product
工厂,如清单7所示:
清单7.在Groovy中创建产品工厂
interface Product {
public int evaluate(int op1, int op2)
}
class Multiply implements Product {
@Override
int evaluate(int op1, int op2) {
op1 * op2
}
}
class Incrementation implements Product {
@Override
int evaluate(int op1, int op2) {
def sum = 0
op2.times {
sum += op1
}
sum
}
}
class ProductFactory {
static Product getProduct(int maxNumber) {
if (maxNumber > 10000)
return new Multiply()
else
return new Incrementation()
}
}
在清单7中 ,我创建一个接口来定义如何获取两个数字的乘积并实现两种不同版本的算法的语义。 在ProductFactory
,我确定从工厂返回哪种实现的规则。
我将工厂用作通过某些决策标准得出的具体实现的抽象占位符。 例如,考虑清单8中的代码:
清单8.动态选择一个实现
@Test
public void decisionTest() {
def p = ProductFactory.getProduct(10010)
assertTrue p.getClass() == Multiply.class
assertEquals(2*10010, p.evaluate(2, 10010))
p = ProductFactory.getProduct(9000)
assertTrue p.getClass() == Incrementation.class
assertEquals(3*3000, p.evaluate(3, 3000))
}
在清单8中 ,我创建了两个版本的Product
实现,以验证正确的版本是从工厂返回的。
在Java中,继承和多态是紧密耦合的概念:多态触发对象的类。 换句话说,这种耦合被放松了。
Clojure中的点菜多态性
许多开发人员因为不是一种面向对象的语言而拒绝了Clojure,他们认为面向对象的语言是力量的巅峰之作。 这是一个错误:Clojure具有面向对象语言的所有功能,与其他功能无关地实现。 例如,Clojure支持多态性,但不仅限于评估类以确定派遣。 Clojure支持多态多方法 ,从而可以通过开发人员想要的任何特征(或组合)触发分派。
这是一个例子。 在Clojure中,数据通常驻留在struct
,该struct
模仿类的数据部分。 考虑清单9中的Clojure代码:
清单9.在Clojure中定义颜色结构
(defstruct color :red :green :blue)
(defn red [v]
(struct color v 0 0))
(defn green [v]
(struct color 0 v 0))
(defn blue [v]
(struct color 0 0 v))
在清单9中 ,我定义了一个结构,该结构包含三个对应于颜色值的值。 我还创建了三个方法,这些方法返回用单色饱和的结构。
Clojure中的多重方法是一种接受调度函数的方法定义,该函数返回决策标准。 后续定义使您可以充实该方法的不同版本。
清单10显示了一个多方法定义的示例:
清单10.定义多重方法
(defn basic-colors-in [color]
(for [[k v] color :when (not= v 0)] k))
(defmulti color-string basic-colors-in)
(defmethod color-string [:red] [color]
(str "Red: " (:red color)))
(defmethod color-string [:green] [color]
(str "Green: " (:green color)))
(defmethod color-string [:blue] [color]
(str "Blue: " (:blue color)))
(defmethod color-string :default [color]
(str "Red:" (:red color) ", Green: " (:green color) ", Blue: " (:blue color)))
在清单10中 ,我定义了一个名为basic-colors-in
的调度函数,该函数返回所有非零颜色值的向量。 对于方法的各种变化,我指定了如果调度函数返回单一颜色该怎么办; 在这种情况下,它将返回带有颜色的字符串。 最后一种情况包括可选的:default
关键字,用于处理其余的情况。 对于这个,我不能假设我收到了一种颜色,因此返回值列出了所有颜色的值。
清单11中显示了测试这些多重方法的测试:
清单11.在Clojure中测试颜色
(ns colors-test
(:use clojure.test)
(:use colors))
(deftest pure-colors
(is (= "Red: 5" (color-string (struct color 5 0 0))))
(is (= "Green: 12" (color-string (struct color 0 12 0))))
(is (= "Blue: 40" (color-string (struct color 0 0 40)))))
(deftest varied-colors
(is (= "Red:5, Green: 40, Blue: 6" (color-string (struct color 5 40 6)))))
在清单11中 ,当我用单一颜色调用方法时,它将执行多方法的单一颜色版本。 如果使用复杂的颜色配置文件进行调用,则默认方法将返回所有颜色。
从继承中解耦多态性提供了一种强大的,上下文相关的调度机制。 例如,考虑图像文件格式的问题,每个图像文件都有一组不同的特征来定义类型。 通过使用分派功能 ,Clojure使您能够构建功能强大的分派,就像Java多态性一样,但具有较少的限制。
结论
在本期中,我提供了下一代JVM语言中各种调度机制的快速介绍。 使用调度有限的语言工作时,往往会由于诸如设计模式之类的外在变通方法而使代码混乱。 在新语言中选择以前不存在的替代方法可能很困难,因为您必须转移范式。 这是学习功能思考的一部分。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft15/index.html