相同的问题,不同的范式
在 函数式思维:函数设计模式,第 1 部分 文章中,我开始研究传统 Gang of Four (GoF) 设计模式(参阅 参考资料)与更加函数式的方法的交叉。在本期文章中,我们将继续讨论这些内容,向您展示用模式、元编程以及函数式组合这 3 种不同范式处理一个常见问题的解决方案。
如果您使用的语言所支持的主要范式是对象,那么从这些方面开始考虑每个问题的解决方案就会很容易。然而,大多数现代语言都支持多范式,意味着它们同时支持对象、元对象、函数式组合等范式。学习针对不同的问题使用不同范式是成为更好开发人员所必经的阶段。
关于本系列
本 系列 的目标是重新调整您对函数思维的认识,帮助您以全新的方式看待常见问题,并提升您的日常编码能力。本系列文章将探讨函数编程概念、允许在 Java™ 语言中进行函数式编程的框架、在 JVM 上运行的函数编程语言,以及语言设计的未来方向。本系列面向那些了解 Java 及其抽象工作原理,但对函数语言不甚了解的开发人员。
在本期文章中,我将专攻由适配器 设计模式解决的传统问题:转换接口使之与不兼容的接口一起工作。首先,此传统方法是使用 Java 编写成的。
Java 中的适配器模式
此适配器模式能将类的接口转换成一个兼容接口。当两个类由于实现细节而不能协同工作、但却又能够在概念形式上协同工作时,就可以使用此适配器模式。在这个示例中,我创建了几个简单的类,建模将方桩装进圆孔的问题。方桩有时能够装进圆孔,如 图 1 中所示,这取决于桩与孔的相对大小:
图 1. 圆孔中的方桩
![圆孔中的方桩的图解](https://i-blog.csdnimg.cn/blog_migrate/972d18ac2d878a4cebe0953427863f35.png)
要确定方形是否可以装进圆形中,我使用 图 2 中显示的公式:
图 2. 确定方形是否可以装进圆形的公式
![确定方形是否可以装进圆形的公式](https://i-blog.csdnimg.cn/blog_migrate/648346d70111579750c8b2df184e53ef.png)
图 2 中的公式将方形的边长除以 2,并求该数的平方,再将结果乘以 2,最后再将所得结果开二次方根。如果该值小于圆形的半径,则桩可完全塞进圆孔中。
我可以使用一个处理转换的简单实用工具类来轻易地解决此方桩/圆孔的问题。但是这体现了一个更大的问题。例如,如果我调整 Button 以使之大小符合一个 Panel 类型,它不是专为该类型而设计但能够与之兼容,那会怎么样?方桩与圆孔的问题是用适配器设计模式解决常见问题的简化版:调整两个互不兼容的接口。要使方桩能够与圆孔一起工作,我需要少量的类和接口来实现此适配器模式,如清单 1 中所示。
清单 1. Java 中的方桩和圆孔
public class SquarePeg { private int width; public SquarePeg(int width) { this.width = width; } public int getWidth() { return width; } } public interface Circularity { public double getRadius(); } public class RoundPeg implements Circularity { private double radius; public double getRadius() { return radius; } public RoundPeg(int radius) { this.radius = radius; } } public class RoundHole { private double radius; public RoundHole(double radius) { this.radius = radius; } public boolean pegFits(Circularity peg) { return peg.getRadius() <= radius; } }
为了减少 Java 代码量,我添加了一个名为 Circularity
的接口来表明实现程序拥有一个半径。这让我可以根据圆形事物编写 RoundHole
代码,而不只是编写 RoundPeg
代码。这是在适配器模式中使类型解析更加简单所做出的常见让步。
要使方桩装进圆孔中,我需要一个适配器,通过公开 getRadius()
方法将 Circularity
添加到 SquarePeg
中,如清单 2 所示:
清单 2. 方桩适配器
public class SquarePegAdaptor implements Circularity { private SquarePeg peg; public SquarePegAdaptor(SquarePeg peg) { this.peg = peg; } public double getRadius() { return Math.sqrt(Math.pow((peg.getWidth()/2), 2) * 2); } }
要测试适配器确实能够让我将大小合适的方桩放进圆孔中,我执行了清单 3 中显示的测试:
清单 3. 测试适配性
@Test public void square_pegs_in_round_holes() { RoundHole hole = new RoundHole(4.0); Circularity peg; for (int i = 3; i <= 10; i++) { peg = new SquarePegAdaptor(new SquarePeg(i)); if (i < 6) assertTrue(hole.pegFits(peg)); else assertFalse(hole.pegFits(peg)); } }
在 清单 3 中,针对于每个假设的宽度,我使用 SquarePegAdaptor
包装 SquarePeg
的创建,启用 hole
的 pegFits()
方法来返回一个关于桩合适性的智能评估。
此代码简洁直观,因为这是在 Java 中实现的一个简单却冗长的模式。很明显,此范式是 GoF 设计模式方法。然而,该模式方法并不是惟一的方法。
Groovy 中的动态适配器
Groovy(参阅 参考资料)支持若干个 Java 中没有的编程范式,所以我将在其余的示例中使用 Groovy。首先,我要实现将 清单 2 中的 “标准” 适配器模式解决方案迁移到 Groovy,如清单 4 所示:
清单 4. Groovy 中的桩、孔和适配器
class SquarePeg { def width } class RoundPeg { def radius } class RoundHole { def radius def pegFits(peg) { peg.radius <= radius } } class SquarePegAdapter { def peg def getRadius() { Math.sqrt(((peg.width/2) ** 2)*2) } }
清单 2 中的 Java 版本与 清单 4 中的 Groovy 版本之间最显著的区别在于冗余性。Groovy 的设计目的是通过动态类型和便利性(比如允许方法中的最后一行自动作为方法的返回值)来移除一些 Java 的重复性,正如 getRadius()
方法所展示的。
清单 5 显示 Groovy 版本的适配器测试:
清单 5. 在 Groovy 中测试传统适配器
@Test void pegs_and_holes() { def hole = new RoundHole(radius:4.0) (4..7).each { w -> def peg = new SquarePegAdapter( peg:new SquarePeg(width:w)) if (w < 6 ) assertTrue hole.pegFits(peg) else assertFalse hole.pegFits(peg) } }
在 清单 5 中,我利用了 Groovy 的另一个便利性,称为名称/值构造函数,这是我在同时构造 RoundHole
、SquarePegAdaptor
和 SquarePeg
时,由 Groovy 自动生成的构造函数。
尽管有糖衣语法 (syntactic sugar),但是该版本与 Java 版本一样,遵循 GoF 设计-模式范式。常见具有 Java 背景的 Groovy 开发人员将他们多年的工作经验转到一个新的语法环境中。然而,Groovy 拥有更好的解决此问题的方法,即使用元编程。
使用元编程实现适配性
Groovy 的出色特性之一是提供对元编程的有力支持。我将使用元编程通过 ExpandoMetaClass
将适配器直接构建到类中。
ExpandoMetaClass
动态语言的一个常见特性是开放类:可以重新打开现有的类(您的类或系统类,比如 String
或 Object
)以添加、移除或更改方法。开放类在特定领域语言 (DSLs) 中运用颇多,常用于构建流畅接口。Groovy 拥有针对开放类的两个机制:categories 和 ExpandoMetaClass
。我的示例只显示 expando 语法。
ExpandoMetaClass
允许您将新方法添加到类或各个对象实例中。对于适配示例,我需要向我的 SquarePeg
添加 “radiusness”,之后我才能查看它是否能装进圆孔中,如清单 6 所示:
清单 6. 使用 ExpandoMetaClass
将半径添加到方桩
static { SquarePeg.metaClass.getRadius = { -> Math.sqrt(((delegate.width/2) ** 2)*2) } } @Test void expando_adapter() { def hole = new RoundHole(radius:4.0) (4..7).each { w -> def peg = new SquarePeg(width:w) if (w < 6) assertTrue hole.pegFits(peg) else assertFalse hole.pegFits(peg) } }
Groovy 中的每个类均拥有一个预定义的 metaClass
属性,公开该类的 ExpandoMetaClass
。在 清单 6 中,我使用该属性来将一个getRadius()
方法添加(使用熟悉的公式)到 SquarePeg
类中。在使用 ExpandoMetaClass
时,时机很重要;我必须确保,在单元测试中尝试调用此方法之前成功添加了该方法。因此,我在测试类的静态初始化器中添加了该新方法,它会在测试类加载时将方法添加到 SquarePeg
中。在将 getRadius()
方法添加到 SquarePeg
之后,我就能够将它传递到 hole.pegFits
方法中,并且 Groovy 的动态类型会负责处理此测试。
使用 ExpandoMetaClass
肯定比使用较长的模式版本更加简洁。而且操作几乎是不可见的,这也是它的一大劣势。在将方法大规模添加到现有类时应该谨慎操作,因为您是在以方便性换取不可见的行为,该行为可能很难进行调试。在一些情况下这是可以接受的,比如在特定域语言中 (DSL) 代表框架对现有基础架构进行普遍变更。
此示例说明了如何使用元编程范式(修改现有类)解决适配器问题。然而,这并不是使用 Groovy 的动态性解决此问题的惟一方法。
动态适配器
Groovy 已得到优化以更好地与 Java 集成,包括 Java 相对严格的地方。例如,动态地生成类在 Java 中非常繁琐,但是 Groovy 却能够轻松实现此操作。这表明我可以动态地生成一个适配器类,如清单 7 所示:
清单 7. 使用动态适配器
def roundPegOf(squarePeg) { [getRadius:{Math.sqrt( ((squarePeg.width/2) ** 2)*2)}] as RoundThing } @Test void functional_adaptor() { def hole = new RoundHole(radius:4.0) (4..7).each { w -> def peg = roundPegOf(new SquarePeg(width:w)) if (w < 6) assertTrue hole.pegFits(peg) else assertFalse hole.pegFits(peg) } }
Groovy 的字面量哈希语法使用方括号,该括号出现在 清单 7 中的 roundPegOf()
方法中。要生成一个实现接口的类,Groovy 允许您创建一个哈希函数,以方法名作为键,以实现代码块作为值。as
运算符使用哈希函数来生成一个实现接口的类,其中使用哈希键名来生成实例方法。因此,在 清单 7 中,roundPegOf()
方法创建了一个单项哈希函数,以 getRadius
作为方法名(Groovy 的哈希键是字符串时不必使用双引号)并以我熟悉的转换代码作为实现代码块。as
运算符将此转换成一个实现 RoundThing
接口的类,充当包装 functional_adaptor()
测试中创建functional_adaptor()
的适配器。
动态生成类可以消除传统模式方法的冗余性和拘泥性。它还比元编程方法更加简明:不向类添加新方法;而是生成一个即时的包装器实现适配性。这使用了设计模式范式(添加一个适配器类),但是最大限度地减少了麻烦和语法的使用。
函数式适配器
当您仅有的工具是锤子时,所有的问题就看起来像钉子。如果您仅有的范式是面向对象,那么您可能会丧失看到其他替代方案的能力。在无一等函数的语言上花费太多时间的一大危害是过度应用模式来解决问题。许多模式(例如 Observer、Visitor 和 Command,等等)实质上是应用可移植代码的机制,使用缺少高阶函数的语言所实现。我可以摈弃大量的对象捕获,并仅编写一个函数来处理该转换。结果表示该方法拥有众多优势。
函数!
如果您拥有一等函数(可以出现在任何其他语言结构能出现的任何地方的函数,包括外部类),您可以为您自己编写一个处理适配性的转换函数,如清单 8 中的 Groovy 代码所示:
清单 8. 使用简单的转换函数
def pegFits(peg, hole) { Math.sqrt(((peg.width/2) ** 2)*2) <= hole.radius } @Test void functional_all_the_way() { def hole = new RoundHole(radius:4.0) (4..7).each { w -> def peg = new SquarePeg(width:w) if (w < 6) assertTrue pegFits(peg, hole) else assertFalse pegFits(peg, hole) } }
在 清单 8 中,我创建了一个能接受 peg
和 hole
的函数,并使用它来检查方桩的合适性。此方法有效,但是无法决定方桩与面向对象认为其所属圆孔的合适性。在某些情况下,具体化该决定比调整类更加明智。这代表函数式范式:接受参数并返回值的纯函数。
组合
在结束函数方法讨论之前,我将向您展示我最喜欢的适配器,它结合了设计模式和函数方法。要阐述使用作为一等函数交付的轻量级动态生成器的优势,请细看清单 9 中的示例:
清单 9. 通过轻量级动态适配器组合函数
class CubeThing { def x, y, z } def asSquare(peg) { [getWidth:{peg.x}] as SquarePeg } def asRound(peg) { [getRadius:{Math.sqrt( ((peg.width/2) ** 2)*2)}] as RoundThing } @Test void mixed_functional_composition() { def hole = new RoundHole(radius:4.0) (4..7).each { w -> def cube = new CubeThing(x:w) if (w < 6) assertTrue hole.pegFits(asRound(asSquare(cube))) else assertFalse hole.pegFits(asRound(asSquare(cube))) } }
在 清单 9 中,我创建了少量返回动态适配器的函数,让我将适配器以一种方便、易读方式相互连接起来。组合 函数允许函数控制和封装其参数所发生的操作,而不必担心谁可能正在将它们用作一个参数。这是一个非常函数式的方法,利用了 Groovy 创建动态包装器类的能力作为实现。
在 Java I/O 库中对比轻量级适配器方法与蹩脚版本的适配器组合, 如清单 10 所示:
清单 10. 蹩脚版本的适配器组合
ZipInputStream zis = new ZipInputStream( new BufferedInputStream( new FileInputStream(argv[0])));
清单 10 中的示例显示了适配器解决的一个常见问题:混合并匹配组合行为的能力。缺少一等函数,Java 不得不通过构造函数来进行组合。使用函数来包装其他函数并修改其返回,这在函数编程中非常常见,但是在 Java 中并不常见,因为该语言以过多语法增加了冲突。
结束语
如果您一直只局限于使用一个范式,那么您就很难看到其他替代方法的益处,因为它不符合您的世界观。现代的混合范式语言提供了设计选择的组合,此外,了解每个范式的工作原理(并与其他范式交互)可帮助您选择更佳的解决方案。在本期中,我阐述了适配性常见问题,并通过 Java 和 Groovy 中的传统适配器设计模式解决了这个问题。接着,使用 Groovy 元编程和 ExpandoMetaClass
解决了此问题,然后展示了动态适配器类。您还看到了,为适配器类使用轻量级语法可支持便捷的函数式组合,而这在 Java 中实现起来非常麻烦。
在下一期,我将继续研究设计模式和函数式编程的交叉。
参考资料
学习
- The Productive Programmer(Neal Ford,O'Reilly Media,2008 年):Neal Ford 的新书讨论了帮助您提高编码效率的工具和实践。
- Design Patterns: Elements of Reusable Object-Oriented Software(Erich Gamma 等人,Addison-Wesley,1994 年):关于 Gang of Four 在设计模式方面的经典之作。
- Design Patterns in Dynamic Languages:Peter Norvig 例证了强大的语言(比如函数语言)对设计模式具有更少需求。
- 适配器模式:适配器是一种著名的 Gang of Four 设计模式。
- Groovy:Groovy 是一种多范式 JVM 语言,其语法与 Java 非常接近。其高级特性包括许多函数式编程元素的增加。
- “实战 Groovy:使用闭包、ExpandoMetaClass 和类别进行元编程”:更多了解 Groovy 在运行时向类动态添加新方法的能力。