导出功能设计模式

    技术2024-01-26  113

    关于本系列

    本系列旨在将您的观点重新定位为实用的心态,帮助您以新的方式看待常见问题并找到改善日常编码的方式。 它探讨了函数式编程的概念,允许使用Java™语言进行函数式编程的框架,在JVM上运行的函数式编程语言以及语言设计的一些未来方向。 该系列面向那些了解Java及其抽象如何工作,但很少或没有使用功能语言经验的开发人员。

    函数世界中的某些人员声称设计模式的概念是有缺陷的,并且在函数式编程中不需要。 可以在狭窄的模式定义下为该视图辩护-但这更多是关于语义而不是使用的争论。 设计模式的概念是对一个常见问题的命名,分类解决方案,它仍然有效。 但是,模式有时在不同的范式下具有不同的外观。 因为在功能世界中解决问题的基础和方法是不同的,所以一些传统的“四人帮”模式(请参阅参考资料 )消失了,而另一些保留了问题却从根本上解决了问题。 本期和下一部分将研究一些传统的设计模式,并以实用的方式重新思考它们。

    在功能编程世界中,传统设计模式通常以以下三种方式之一体现:

    语言吸收了模式。 模式解决方案仍存在于功能范式中,但实现细节有所不同。 该解决方案是使用其他语言或范例所缺乏的功能来实现的。 (例如,许多使用元编程的解决方案简洁明了,而在Java中是不可能的。)

    我将依次从这部分开始研究这三种情况,并从一些熟悉的模式开始,其中大多数全部或部分地被现代语言所包含。

    工厂和咖喱

    咖喱是许多功能语言的功能。 以数学家Haskell Curry(也称为Haskell编程语言)命名,currying转换多参数函数,以便可以将其称为单参数函数链。 与之密切相关的部分应用程序 ,用于分配一个固定值到一个或多个的参数的函数,由此产生更小的元数的另一功能的技术。 (Arity是函数的参数数量。)我在“ 功能性思考,第3部分 ”中讨论了这两种技术。

    在设计模式的上下文中,currying充当功能的工厂。 函数式编程语言的一个共同特征是一等(或更高阶)函数,该函数允许函数充当任何其他数据结构。 借助此功能,我可以轻松创建基于某些条件返回其他功能的功能,这是工厂的本质。 例如,如果您有一个将两个数字相加的通用函数,则可以将currying用作工厂来创建一个始终向其参数添加一个数字的函数-增量器,如清单1所示,用Groovy实现:

    清单1.将函数工厂化
    def adder = { x, y -> return x + y } def incrementer = adder.curry(1) println "increment 7: ${incrementer(7)}" // prints "increment 7: 8"

    在清单1中 ,我将第一个参数设为1 ,返回一个接受单个参数的函数。 本质上,我已经创建了一个函数工厂。

    当您的语言本地支持这种行为时,它往往被用作其他事物的构建块,无论大小。 例如,考虑清单2所示的Scala示例:

    清单2. Scala对curry的“随意”使用
    object CurryTest extends Application { def filter(xs: List[Int], p: Int => Boolean): List[Int] = if (xs.isEmpty) xs else if (p(xs.head)) xs.head :: filter(xs.tail, p) else filter(xs.tail, p) def dividesBy(n: Int)(x: Int) = ((x % n) == 0) val nums = List(1, 2, 3, 4, 5, 6, 7, 8) println(filter(nums, dividesBy(2))) println(filter(nums, dividesBy(3))) }

    清单2中的代码是Scala文档中递归和currying的示例之一(请参阅参考资料 )。 filter()方法通过参数p递归过滤整数列表。 p是谓词函数 —在函数世界中布尔函数的常用术语。 filter()方法检查列表是否为空,如果为空,则简单地返回。 否则,它将通过谓词检查列表中的第一个元素( xs.head ),以查看是否应将其包括在过滤后的列表中。 如果它通过谓词,则返回的是一个新列表,其头部位于最前面,而经过过滤的尾部则为其余。 如果第一个元素未通过谓词测试,则返回仅成为列表中经过过滤的其余部分。

    从模式的角度来看, 清单2中有趣的是在dividesBy()方法中“临时”使用了dividesBy() 。 请注意, dividesBy()接受两个参数,并根据第二个参数是否均匀地划分为第一个参数来返回true或false 。 但是,在调用filter()方法的一部分时调用此方法时,仅使用一个参数来调用该方法-其结果是一个咖喱函数,该函数随后用作filter()方法中的谓词。

    该示例说明了模式在函数式编程中体现的前两种方式,正如我在本文开头列出的那样。 首先,currying内置在语言或运行时中,因此功能工厂的概念已根深蒂固,不需要额外的结构。 其次,它说明了我关于不同实现的观点。 对于典型的Java程序员来说,使用清单2中的 curring可能永远不会发生。 我们从来没有真正拥有可移植的代码,当然也从未考虑过从更通用的功能构造特定功能。 实际上,大多数命令开发人员可能不会在这里考虑使用设计模式,因为从通用方法中创建特定的dividesBy()方法似乎是一个小问题,而设计模式–主要依靠结构来解决问题,因此需要大量的开销来实施-似乎是解决大问题的方法。 按原样使用currying并不能证明一种特殊名称的形式性,而不是已经具有的名称。

    一流的功能和设计模式

    具有一流的功能大大简化了许多常用的设计模式。 (命令设计模式甚至消失了,因为您不再需要对象包装来实现可移植功能。)

    模板方法

    一流的函数使Template Method设计模式(请参阅参考资料 )更易于实现,因为它们消除了可能不必要的结构。 模板方法定义了方法中算法的框架,将某些步骤推迟到子类中,并强迫它们在不更改算法结构的情况下定义这些步骤。 Groovy的清单3中显示了Template方法的典型实现:

    清单3.“标准”模板方法实现
    abstract class Customer { def plan def Customer() { plan = [] } def abstract checkCredit() def abstract checkInventory() def abstract ship() def process() { checkCredit() checkInventory() ship() } }

    在清单3中 , process()方法依赖于checkCredit() , checkInventory()和ship()方法,它们的定义必须由子类提供,因为它们是抽象方法。

    因为一流的函数可以充当任何其他数据结构,所以我可以使用代码块重新定义清单3中的示例,如清单4所示:

    清单4.具有一流功能的模板方法
    class CustomerBlocks { def plan, checkCredit, checkInventory, ship def CustomerBlocks() { plan = [] } def process() { checkCredit() checkInventory() ship() } } class UsCustomerBlocks extends CustomerBlocks{ def UsCustomerBlocks() { checkCredit = { plan.add "checking US customer credit" } checkInventory = { plan.add "checking US warehouses" } ship = { plan.add "Shipping to US address" } } }

    在清单4中 ,算法中的步骤仅仅是该类的属性,可以像其他任何属性一样进行分配。 这是一个示例,其中语言功能主要吸收实现细节。 将这种模式作为解决问题的方法(将步骤推迟到后续处理程序)仍然很有用,但是实现起来更简单。

    这两种解决方案并不相同。 在清单3的“传统” Template Method示例中,抽象类需要子类来实现相关方法。 当然,子类可能只是创建一个空的方法主体,但是抽象方法定义形成了一种文档,提醒子类创建者将其考虑在内。 另一方面,在需要更大灵活性的情况下,方法声明的严格性可能不合适。 例如,我可以创建我的Customer类的版本,该版本接受任何要处理的方法列表。

    对代码块等功能的深入支持使语言易于开发。 考虑您想允许子类继承者跳过某些步骤的情况。 Groovy有一个特殊的受保护的访问运算符( ?. ),可确保在调用对象上的方法之前该对象不为null。 考虑清单5中的process()定义:

    清单5.为代码块调用添加保护
    def process() { checkCredit?.call() checkInventory?.call() ship?.call() }

    在清单5中 ,实现子类的任何人都可以选择将哪个子方法分配代码,而将其他子方法安全地留为空白。

    战略

    通过一流功能简化的另一种流行设计模式是“策略”模式。 策略定义了一系列算法,将每个算法封装在一起并使其可互换。 它使算法独立于使用它的客户端而变化。 一流的功能使构建和操作策略变得简单。

    清单6中显示了用于计算数字乘积的Strategy设计模式的传统实现:

    清单6.将策略设计模式用于两个数字的乘积
    interface Calc { def product(n, m) } class CalcMult implements Calc { def product(n, m) { n * m } } class CalcAdds implements Calc { def product(n, m) { def result = 0 n.times { result += m } result } }

    在清单6中 ,我为两个数字的乘积定义一个接口。 我用两种不同的具体类(策略)实现接口:一种使用乘法,另一种使用加法。 为了测试这些策略,我创建了一个测试用例,如清单7所示:

    清单7.测试产品策略
    class StrategyTest { def listOfStrategies = [new CalcMult(), new CalcAdds()] @Test public void product_verifier() { listOfStrategies.each { s -> assertEquals(10, s.product(5, 2)) } } }

    如清单7所示 ,两种策略都返回相同的值。 使用代码块作为一流的功能,我可以减少前面示例中的许多仪式。 考虑清单8所示的求幂策略的情况:

    清单8.用更少的仪式测试指数
    @Test public void exp_verifier() { def listOfExp = [ {i, j -> Math.pow(i, j)}, {i, j -> def result = i (j-1).times { result *= i } result }] listOfExp.each { e -> assertEquals(32, e(2, 5)) assertEquals(100, e(10, 2)) assertEquals(1000, e(10, 3)) } }

    在清单8中 ,我使用Groovy代码块直接定义了两种内联求幂策略。 就像在“ 模板方法”示例中一样 ,为了方便起见,我以形式交易。 传统方法会围绕每个策略强制使用名称和结构,这有时是可取的。 但是,请注意,我可以选择向清单8中的代码添加更严格的保护措施,而我不能轻易绕过更传统的方法所施加的限制-这种方法更多的是动态对静态参数,而不是功能-编程与设计模式之一。

    受一等函数的存在影响的模式主要是语言吸收的模式的示例。 接下来,我将展示一个保留语义但更改实现的方法。

    跳线和备忘录

    Flyweight模式是一种优化技术,它使用共享来支持大量细粒度的对象引用。 您保持对象池可用,并在池中为特定视图创建引用。 Flyweight使用规范对象的概念-代表该类型的所有其他对象的单个代表性对象。 例如,如果您有特定的消费产品,则该产品的规范版本表示该类型的所有产品。 在应用程序中,您无需为每个用户创建产品列表,而是创建一个规范产品列表,并且每个用户在其产品列表中都有一个引用。

    考虑清单9中的类,它们模拟计算机类型:

    清单9.对计算机类型进行建模的简单类
    class Computer { def type def cpu def memory def hardDrive def cd } class Desktop extends Computer { def driveBays def fanWattage def videoCard } class Laptop extends Computer { def usbPorts def dockingBay } class AssignedComputer { def computerType def userId public AssignedComputer(computerType, userId) { this.computerType = computerType this.userId = userId } }

    在这些类中,假设所有计算机都具有相同的规范,那么为每个用户创建一个新的Computer实例效率不高。 AssignedComputer将计算机与用户关联。

    使此代码更有效的常见方法是将Factory模式和Flyweight模式结合在一起。 考虑一下用于生成规范计算机类型的单例工厂,如清单10所示:

    清单10. flyweight计算机实例的Singleton工厂
    class ComputerFactory { def types = [:] static def instance; private ComputerFactory() { def laptop = new Laptop() def tower = new Desktop() types.put("MacBookPro6_2", laptop) types.put("SunTower", tower) } static def getInstance() { if (instance == null) instance = new ComputerFactory() instance } def ofType(computer) { types[computer] } }

    ComputerFactory类构建可能的计算机类型的缓存,然后通过其ofType()方法传递适当的实例。 这是传统的单例工厂,就像您用Java编写的那样。

    但是,Singleton也是一种设计模式(请参阅参考资料 ),它是运行时吸收的模式的另一个很好的例子。 考虑简化的ComputerFactory ,使用Groovy提供的@Singleton批注,如清单11所示:

    清单11.简化的单例工厂
    @Singleton class ComputerFactory { def types = [:] private ComputerFactory() { def laptop = new Laptop() def tower = new Desktop() types.put("MacBookPro6_2", laptop) types.put("SunTower", tower) } def ofType(computer) { types[computer] } }

    为了测试工厂是否返回规范实例,我编写了一个单元测试,如清单12所示:

    清单12.测试规范类型
    @Test public void flyweight_computers() { def bob = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), "Bob") def steve = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), "Steve") assertTrue(bob.computerType == steve.computerType) }

    在实例之间保存公共信息是一个好主意,这是我进入函数式编程时要保留的一个主意。 但是,实现细节完全不同。 这是在更改(最好简化)实现的同时保留模式语义的示例。

    在上一期中 ,我介绍了memoization ,它是一种编程语言中内置的功能,该功能可自动缓存重复出现的函数返回值。 换句话说,记忆功能允许运行时为您缓存值。 支持Groovy记忆化的最新版本(参见相关主题 )。 考虑清单13中定义的函数:

    清单13.权重的记忆化
    def computerOf = {type -> def of = [MacBookPro6_2: new Laptop(), SunTower: new Desktop()] return of[type] } def computerOfType = computerOf.memoize()

    在清单13中 ,规范类型在computerOf函数中定义。 要创建该函数的备注化实例,我只需调用Groovy运行时定义的memoize()方法即可。

    清单14显示了比较两种方法的调用的单元测试:

    清单14.比较方法
    @Test public void flyweight_computers() { def bob = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), "Bob") def steve = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), "Steve") assertTrue bob.computerType == steve.computerType def sally = new AssignedComputer(computerOfType("MacBookPro6_2"), "Sally") def betty = new AssignedComputer(computerOfType("MacBookPro6_2"), "Betty") assertTrue sally.computerType == betty.computerType }

    最终结果是相同的,但是请注意实现细节方面的巨大差异。 对于“传统”设计模式,我创建了一个新类来充当工厂,实现了两种模式。 对于功能版本,我实现了一个方法,然后返回了已记录的版本。 将诸如缓存之类的细节卸载到运行时意味着手写实现失败的机会更少。 在这种情况下,我保留了Flyweight模式的语义,但是使用了非常简单的实现。

    结论

    在本期中,我介绍了在功能编程中设计模式的语义体现的三种方式。 首先,它们可以被语言或运行时所吸收。 我展示了使用Factory,Strategy,Singleton和Template Method模式的示例。 其次,模式可以保留其语义,但实现方式却完全不同。 我展示了使用类而不是使用记忆的Flyweight模式的示例。 第三,功能语言和运行时可以具有完全不同的功能,从而使它们能够以完全不同的方式解决问题。

    在下一部分中,我将继续研究设计模式与函数式编程的交集,并展示第三种方法的示例。


    翻译自: https://www.ibm.com/developerworks/java/library/j-ft10/index.html

    相关资源:微信小程序源码-合集6.rar
    Processed: 0.026, SQL: 9