C# 或其他语言中的 Scala 样式抽象模块

本文关键字:Scala 样式 抽象 模块 其他 语言 | 更新日期: 2023-09-27 18:36:43

我正在阅读Martin Odersky的书Programming in Scala,其中有关于抽象模块的部分,以及他的论文Scalable Component Abstractions:

http://lampwww.epfl.ch/~odersky/papers/ScalableComponent.pdf

我的结论是,通过让你的模块抽象而不是对象(或像Java中的经典静态全局模块):

abstract class myModule{ 
    // this is effectively an abstract module, whose concrete 
    // components can be configured by subclassing and instantiating it
    class thing{}
    class item{}
    object stuff{}
    class element{}
    object utils{}
}

您可以实例化具有不同具体特征的模块的多个子类和实例。这使您可以根据情况以不同的方式配置模块(例如,在测试期间替换数据库组件,或在开发环境中替换 IO 组件),以及实例化多个模块,每个模块都有自己的一组模块范围的可变状态。

据我所知,在基本层面上,唯一的硬要求是你可以有嵌套类,这样封闭类就可以充当模块。

另一个实际要求是您可以将类定义分散到多个文件上,因为包含一堆类的模块可能比大多数在单个源文件中接受的代码行数要多。

Scala通过Traits做到了这一点,它带来了一些其他的好东西,这些好东西很好,但不是整个跨多个源文件传播抽象模块类的想法的核心。C# 具有 partial classes ,它们提供相同的功能,并且还允许嵌套类。据推测,其他一些语言对嵌套类也有类似的支持,以及将一个类拆分到多个文件中。

这种模式是否出现在 C# 或任何其他语言中的任何位置?我认为多种语言的大型项目都面临着抽象模块要解决的问题。有什么理由这个"抽象类作为抽象模块"的东西不起作用,因此没有被使用?在我看来,这是一个比提供相同功能的各种 DI 框架更干净的解决方案。

C# 或其他语言中的 Scala 样式抽象模块

通常的比较是与 ML 模块,其中 Scala 特征(或抽象类)扮演 ML 签名的角色,它们的具体实现(通常是 Scala 对象)扮演 ML 结构的角色。这里对 ML 模块的讨论应该使连接相当清晰。

Scala

和ML之间的类比是故意的,如果你看一下Scala编译器的源代码,你会发现Scala对象经常使用包含"模块"作为一部分的名称来引用。

您描述的抽象模块具有以下核心属性:

  • 它是一个封闭的模块,这意味着它提供了允许与模块交互的所有接口
  • 您可以指定对模块的操作
  • 您可以指定属于模块的类型 - 通常这些类型由上述模块操作
  • 没有提供任何实现 - 这是抽象的部分:可以有许多具体的实现,程序中实际使用的实现将由程序指定和选择,因为它最适合需要

能够使用多个源文件指定模块的功能不是核心要求,但它肯定会派上用场。

在其最基本的形式中,模块描述抽象数据类型(例如队列):哪些操作可用于与数据类型交互,以及交互所需的任何辅助类型。

以更复杂的形式,它可以描述整个子系统(例如网络)。

在命令式语言中,您通常使用接口来实现相同的目的:

  • 它是封闭的
  • 您可以指定操作
  • 您可以指定属于接口的类型
  • 无实现

正如你所提到的,如果你的模块有一个大接口(例如描述一个子系统),那么在一个文件中编写实现丰富接口的类通常是不切实际的。 如果该语言不支持将同一类拆分为单独的源(或者更准确地说:将来自不同源文件的同一类的单独部分"粘合"在一起),解决方案通常是丢失随附的需求并提供一系列指定它们之间交互的接口 - 因此您可以获得子系统的 API(它是最纯粹意义上的 API: 它是一个与子系统交互的接口,尚未实现)。

在某些方面,后一种方法可能比封闭类型更通用(在你可以用它实现的意义上是通用的):你可以提供来自不同作者的各种子类型(通过接口指定)的实现:只要子类型只依赖于指定的接口进行交互,这种混合n-match方法就会起作用。

大多数函数式编程语言的优势之一是参数化数据类型,其中您可以使用另一个作为其参数(例如整数队列)实例化一个dayatype。 Java/C# 中的泛型(以及 C++ 中的模板)也实现了同样的灵活性。 当然,确切的含义和表达能力可能因语言的类型系统而异。

整个讨论是单独的依赖注入(DI)形式,它试图通过显式提供所需的部分(而不是让实现选择)来放松类型的具体实现与其支持部分之间的强依赖关系,因为类型的用户可能会更好地了解这些部分的实现最适合实现其目标 - 例如,为测试功能提供模拟实现。

DI

试图解决的问题仅限于命令式语言,在函数式语言中也可能遇到相同的依赖问题:抽象模块的实现可能会选择使用子类型的特定实现(从而将自身耦合到这些实现),而不是将子类型实现作为参数(这是 DI 的目标)