《Journal of Computer Languages》:Design pattern for reusing immutable methods in object-oriented languages
编辑推荐:
不可变编程在面向对象语言中的挑战与解决方案。研究通过几何库示例,揭示OOP语言继承非破坏性mutators导致的代码重复与扩展性问题,提出基于工厂方法模式的设计方案,实现跨语言(Clojure、C#、Java等8种)的通用解决方案,并论证功能更新与动态类型等语言特征对模式实现的影响,最后在Common Lisp中通过扩展语言功能验证解决方案的有效性。
威廉·弗拉格奥尔(William Flageol)|扬-加埃尔·盖埃内克(Yann-Ga?l Guéhéneuc)|斯特凡·蒙尼尔(Stefan Monnier)|穆拉德·巴德里(Mourad Badri)
加拿大魁北克省特鲁瓦里维埃大学(Université du Québec à Trois Rivières, QC, Canada)
摘要
函数式编程中的编程概念越来越受欢迎,并已被引入到面向对象编程中。在这些概念中,不可变性是函数式编程的核心概念,它为软件开发带来了优势。然而,在面向对象编程语言中引入不可变性会面临一些挑战。
问题:
其中一个挑战出现在继承概念中,即非破坏性修改器(non-destructive mutators)的覆盖问题。修改器在软件运行过程中用于更新数据。非破坏性修改器作用于不可变对象,它们返回新对象而不是修改原始对象。当继承非破坏性修改器时,简单的实现方式会导致代码重复,并引发代码可扩展性问题。
贡献:
我们分析了一个非破坏性修改器覆盖的例子,讨论了相关挑战,并提出了一种新的设计模式作为解决方案,该模式受到“工厂方法模式”(Factory Method pattern)的启发。我们还讨论了这种模式的优点和局限性,以及在八种语言(Clojure、Common Lisp、C#、Java、Kotlin、OCaml、Rust 和 Scala)中的实现方式。最后,我们识别并讨论了主要影响该模式实现的语言特性,并通过扩展 Common Lisp 语言来展示这些特性对实现的影响。
结论:
我们提出的设计模式有助于减少代码重复,并提高继承的非破坏性修改器的可扩展性。然而,要完全解决代码可扩展性问题,需要使用具备函数式更新功能的语言。我们认为面向对象编程语言应该考虑在其语言特性中加入函数式更新功能,以更好地支持新的函数式编程特性。
引言
近年来,受函数式编程启发的编程特性在面向对象编程(OOP)语言中变得越来越普遍。Java 语言在 2014 年通过 Java 8 引入了闭包(closures),2018 年通过 Java 11 引入了模式匹配(pattern matching),2021 年通过 Java 16 引入了记录类型(record types)。C# 语言在 2007 年(C# 3)引入了闭包,在 2019 年(C# 8)引入了模式匹配,在 2020 年(C# 9)引入了记录类型和函数式更新功能。其他面向对象语言,如 Kotlin 和 Scala,也具备许多函数式编程的特性。
在函数式编程的众多概念中,不可变性是一个核心概念。开发者的经验表明[1],[2],不可变性在软件开发中具有优势。不可变性有助于管理竞态条件(race conditions),因为它使开发者能够更清晰地理解代码逻辑。此外,它还有助于测试和维护,因为它可以消除与修改相关的错误(例如,当两个对象引用相同的数据,其中一个对象修改了该数据时,可能会导致另一个对象的行为意外改变)。因此,现代面向对象语言试图引入便于使用不可变性的特性。
然而,在通常依赖状态的面向对象编程中引入不可变性会带来一些挑战,其中许多挑战在第二节中进行了讨论。这些挑战涉及继承、方法覆盖(method overriding)和返回值多态性(return-value polymorphism)。
继承和子类型(subtyping)是面向对象编程中代码重用的主要机制。由于类可以从父类继承方法和属性,修改器(修改接收对象的方法)仍然可以正常工作,因为它们的上下文(属性和相关方法)通过继承得以保留。然而,根据定义,不可变类不能使用传统的修改器,必须使用非破坏性修改器来更新对象的状态。非破坏性修改器返回新对象而不是修改原始对象。使用继承来实现非破坏性修改器具有挑战性,因为它们需要创建与方法接收对象运行时类型相关的新对象,而这在当前的静态类型面向对象语言中是不可能的(这一挑战及其他挑战在第三节 3.2 节中有进一步说明)。
在这项研究中,我们分析了一个在开发几何库时使用不可变性的例子,其中类表示点(points)、大小(sizes)和矩形(rectangles)。矩形具有位置(由点表示)和范围(由大小表示),因此应该能够重用这些类定义的操作。尽管这个例子很简单,但它展示了在面向对象编程中使用不可变性的诸多挑战。
这些挑战与代码重复和代码可扩展性有关。代码重复是一种众所周知的代码问题,会对软件的质量属性(如可维护性和可重用性)产生负面影响[3],[4],[5]。此外,在对程序进行扩展时,某些程序可能需要更多的工作量。我们将扩展程序所需的工作量称为代码可扩展性。
在不可变环境下,有时很难实现代码重用,而简单的子类型方法会导致代码重复(代码不具备可扩展性)。这些挑战是面向对象编程中不可变性的固有特性,因为对象的身份(描述对象的行为)和状态之间存在矛盾,而不可变性禁止对对象状态进行修改。在面向对象编程中修改对象时,对象的身份得以保留;而在使用非破坏性修改时,则必须创建新对象,身份无法保留。当向面向对象编程语言中添加不可变性时,这种矛盾不可避免,因为目前没有机制可以在多个不同对象之间保持对象的身份。因此,遵循软件工程的传统,在编程语言特性或概念之间存在内在矛盾时,我们提出了一种新的设计模式来缓解这种矛盾和部分挑战。
在介绍解决方案之前,我们首先详细描述了这些挑战,我们将其称为不可变工厂方法设计模式(Immutable Factory Method Design Pattern)。我们还讨论了这种设计模式的优点和局限性,并展示了其在 Clojure、Common Lisp、C#、Java、Kotlin、OCaml、Rust 和 Scala 中的实现变体。我们还讨论了可以改进这些实现的语言特性。具体来说,我们确定了函数式更新(functional updating)和动态类型(dynamic typing)这两个特性对设计模式实现的影响。我们进一步通过扩展一个已经具有动态类型的编程语言(Common Lisp)并添加新的函数式更新功能来展示这些特性的必要性,从而简化了设计模式的实现。
通过这项研究和新的设计模式,我们进一步推动了在面向对象编程中引入不可变性的研究,并解决了这种组合方式下的常见挑战。我们也帮助了那些在面向对象程序中使用不可变性时会遇到这些挑战的软件开发者,并向语言设计者提供了有助于更好地支持不可变性的具体语言特性。
本文的贡献如下:
- 1. 通过一个实际例子描述了结合不可变性和面向对象编程子类型时的挑战。
- 2. 提出了一种新的设计模式,用于解决代码重复和代码可扩展性相关的挑战。
- 3. 展示了该设计模式在 Clojure、Common Lisp、C#、Java、Kotlin、OCaml、Rust 和 Scala 中的实现变体。
- 4. 讨论了影响设计模式实现的具体特性(函数式更新和动态类型)。
- 5. 对 Common Lisp 进行了扩展,以展示该功能如何简化设计模式的实现。
文章的其余部分安排如下:第二节详细介绍了面向对象编程中的不可变性背景,并将本研究置于相应的背景中。第三节提出了一个类似于 Gamma 等人[6] 使用的 Coplien 形式的新的设计模式。它包括一个贯穿整个研究过程的挑战示例,并展示了基于工厂方法模式[6] 的解决方案,以及该模式在多种语言中的实现。第四节总结了解决方案,并讨论了主要影响实现的语言特性。第五节展示了该设计模式在 Common Lisp 中的实现,并进一步展示了如何通过添加函数式更新功能来解决相关挑战。第六节概述了不同语言实现中的各种措施,并进一步讨论了这些措施。第七节讨论了与本文相关的类似和相关工作。最后,第八节总结了本研究并讨论了未来的工作方向。
本文是我们 2023 年 EuroPLoP 发表的同名论文[7]的扩展。我们重新组织了设计模式的描述,采用了 Gamma 等人在其书籍[6]中使用的 Coplien 形式。我们还在其他编程语言(如 Scala、OCaml 和 C#)中添加了示例代码,并进一步讨论了各种实现之间的差异。最后,我们添加了一个新的部分,其中展示了为 Common Lisp 添加的新功能,以实现本文提出的挑战解决方案。
背景
不可变性
在本节中,我们定义并讨论了不可变性与面向对象编程的关系。我们概述了关于不可变性的文献,并总结了主要的研究方向。然后,我们将我们的工作与现有文献进行了对比,解释了本研究与其他工作的不同之处。
不可变工厂模式
虽然不可变性带来了许多好处,并且在许多编程语言中都可用,但在同时利用面向对象编程特性的同时实现不可变性也可能具有挑战性。在本节中,我们讨论了在结合不可变性和面向对象编程子类型时出现的挑战。使用面向对象编程中的基本特性很难解决这些挑战,同时还要保持代码的可重用性和避免代码重复。
我们首先介绍了这些挑战,并展示了……(原文此处内容不完整)
讨论
我们现在讨论所提出的设计模式的一般形式、其优点和局限性,以及它如何受到某些特定语言特性的影响。
扩展 Common Lisp
在识别和讨论了各种面向对象编程和函数式语言如何处理我们的设计模式及其挑战之后,我们现在通过扩展第三节中展示的一种语言来展示这些特性的影响。我们选择 Common Lisp 作为目标语言,因为它具有元编程(meta-programming)能力。Common Lisp 通过宏支持元编程,这使我们能够在不安装第三方工具(例如 Java 中的 Lombok 框架)的情况下扩展语言。
在本节中,我们……(原文此处内容不完整)
评估
在本节中,我们概述了第三节中介绍的各种语言中该模式的实现情况,收集了一些比较这些实现的指标,并讨论了结果。
相关工作
有许多研究探讨了语言特性对面向对象编程设计模式的影响,并试图改进这些设计模式。其中许多研究提出了其他范式中可用的特性。
大量关于改进面向对象编程设计模式的研究集中在面向方面编程(Aspect-Oriented Programming, AOP)[21]上。Hanneman 等人[22]提出了一种使用 AOP 对 GoF 设计模式进行更新的方案。他们在 23 个更新后的模式中的 17 个模式中发现了模块性的改进。Dürschmid[23] 提出了一种构建工具……(原文此处内容不完整)
结论
不可变性是许多面向对象编程语言(如 C#、Java 和 Kotlin)中越来越常用的特性。不可变性的强制实施也是当前研究的重点。在本研究中,我们讨论了在面向对象编程和不可变性结合使用时遇到的挑战,特别是关于通过子类型重用不可变方法的问题。
我们分析了一个实际示例的实现情况,发现其中存在与代码重复和代码可扩展性相关的挑战,这些问题是由于需要多重继承和返回值多态性造成的。
CRediT 作者贡献声明
威廉·弗拉格奥尔(William Flageol):撰写 – 审稿与编辑、撰写 – 原稿编写、软件实现、方法论、调查、形式分析、概念化。
扬-加埃尔·盖埃内克(Yann-Ga?l Guéhéneuc):撰写 – 审稿与编辑、监督。
斯特凡·蒙尼尔(Stefan Monnier):撰写 – 审稿与编辑、监督。
利益冲突声明
作者声明以下可能被视为潜在利益冲突的财务利益/个人关系:威廉·弗拉格奥尔报告称获得了魁北克自然与技术研究基金(Fonds de Recherche du Québec en Nature et Technologies)的财务支持;扬-加埃尔·盖埃内克报告称获得了加拿大研究主席计划(Canada Research Chairs Program)的财务支持。如果还有其他作者,他们声明没有已知的可能会影响工作的财务利益或个人关系。
致谢
本项工作得到了魁北克自然与技术研究基金(Fonds de Recherche du Québec en Nature et Technologies)(资助编号:273582)和物联网领域的一级加拿大研究主席职位(Canada Research Chair Tier 1 on Empirical Software Engineering)的支持。