函数式思维: 函数设计模式,第 2 部分

相同的问题,不同的范式

设计模式仅表现为一种解决问题的方法,然而,如果您主要使用的是面向对象语言,那么您可能会想到 设计模式。在本期的函数式思维 中,Neal Ford 将阐述使用传统设计模式、元编程和函数式组合处理一个常见问题(接口不兼容)的各个解决方案。每种方法都各有利弊,但是考虑解决方案的设计可以帮助您以一种新的方式来看待问题。

Neal Ford, 软件架构师, ThoughtWorks Inc.

2012 年 5 月 03 日

  • +内容

在 函数式思维:函数设计模式,第 1 部分 文章中,我开始研究传统 Gang of Four (GoF) 设计模式(参阅 参考资料)与更加函数式的方法的交叉。在本期文章中,我们将继续讨论这些内容,向您展示用模式、元编程以及函数式组合这 3 种不同范式处理一个常见问题的解决方案。

如果您使用的语言所支持的主要范式是对象,那么从这些方面开始考虑每个问题的解决方案就会很容易。然而,大多数现代语言都支持多范式,意味着它们同时支持对象、元对象、函数式组合等范式。学习针对不同的问题使用不同范式是成为更好开发人员所必经的阶段。

关于本系列

本 系列 的目标是重新调整您对函数思维的认识,帮助您以全新的方式看待常见问题,并提升您的日常编码能力。本系列文章将探讨函数编程概念、允许在 Java™ 语言中进行函数式编程的框架、在 JVM 上运行的函数编程语言,以及语言设计的未来方向。本系列面向那些了解 Java 及其抽象工作原理,但对函数语言不甚了解的开发人员。

在本期文章中,我将专攻由适配器 设计模式解决的传统问题:转换接口使之与不兼容的接口一起工作。首先,此传统方法是使用 Java 编写成的。

Java 中的适配器模式

此适配器模式能将类的接口转换成一个兼容接口。当两个类由于实现细节而不能协同工作、但却又能够在概念形式上协同工作时,就可以使用此适配器模式。在这个示例中,我创建了几个简单的类,建模将方桩装进圆孔的问题。方桩有时能够装进圆孔,如 图 1 中所示,这取决于桩与孔的相对大小:

图 1. 圆孔中的方桩
圆孔中的方桩的图解

要确定方形是否可以装进圆形中,我使用 图 2 中显示的公式:

图 2. 确定方形是否可以装进圆形的公式
确定方形是否可以装进圆形的公式

图 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 的另一个便利性,称为名称/值构造函数,这是我在同时构造 RoundHoleSquarePegAdaptor 和 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 中实现起来非常麻烦。

在下一期,我将继续研究设计模式和函数式编程的交叉。

参考资料

学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
东南亚位于我国倡导推进的“一带一路”海陆交汇地带,作为当今全球发展最为迅速的地区之一,近年来区域内生产总值实现了显著且稳定的增长。根据东盟主要经济体公布的最新数据,印度尼西亚2023年国内生产总值(GDP)增长5.05%;越南2023年经济增长5.05%;马来西亚2023年经济增速为3.7%;泰国2023年经济增长1.9%;新加坡2023年经济增长1.1%;柬埔寨2023年经济增速预计为5.6%。 东盟国家在“一带一路”沿线国家中的总体GDP经济规模、贸易总额与国外直接投资均为最大,因此有着举足轻重的地位和作用。当前,东盟与中国已互相成为双方最大的交易伙伴。中国-东盟贸易总额已从2013年的443亿元增长至 2023年合计超逾6.4万亿元,占中国外贸总值的15.4%。在过去20余年中,东盟国家不断在全球多变的格局里面临挑战并寻求机遇。2023东盟国家主要经济体受到国内消费、国外投资、货币政策、旅游业复苏、和大宗商品出口价企稳等方面的提振,经济显现出稳步增长态势和强韧性的潜能。 本调研报告旨在深度挖掘东南亚市场的增长潜力与发展机会,分析东南亚市场竞争态势、销售模、客户偏好、整体市场营商环境,为国内企业出海开展业务提供客观参考意见。 本文核心内容: 市场空间:全球行业市场空间、东南亚市场发展空间。 竞争态势:全球份额,东南亚市场企业份额。 销售模:东南亚市场销售模、本地代理商 客户情况:东南亚本地客户及偏好分析 营商环境:东南亚营商环境分析 本文纳入的企业包括国外及印尼本土企业,以及相关上下游企业等,部分名单 QYResearch是全球知名的大型咨询公司,行业涵盖各高科技行业产业链细分市场,横跨如半导体产业链(半导体设备及零部件、半导体材料、集成电路、制造、封测、分立器件、传感器、光电器件)、光伏产业链(设备、硅料/硅片、电池片、组件、辅料支架、逆变器、电站终端)、新能源汽车产业链(动力电池及材料、电驱电控、汽车半导体/电子、整车、充电桩)、通信产业链(通信系统设备、终端设备、电子元器件、射频前端、光模块、4G/5G/6G、宽带、IoT、数字经济、AI)、先进材料产业链(金属材料、高分子材料、陶瓷材料、纳米材料等)、机械制造产业链(数控机床、工程机械、电气机械、3C自动化、工业机器人、激光、工控、无人机)、食品药品、医疗器械、农业等。邮箱:market@qyresearch.com

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值