6

我正在阅读 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# has partial classes,它提供相同的功能,并且还允许嵌套类。大概其他一些语言对嵌套类以及将一个类拆分为多个文件有类似的支持。

这种模式是否出现在 C# 或任何其他语言的任何地方?我认为许多语言的大型项目都面临抽象模块要解决的问题。这个“抽象类作为抽象模块”的东西不起作用,因此没有被使用,有什么原因吗?在我看来,与提供相同功能的各种 DI 框架相比,它是一个更简洁的解决方案。

4

2 回答 2

8

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

Scala 和 ML 之间的类比是经过深思熟虑的,如果您查看 Scala 编译器的源代码,您会发现 Scala 对象通常使用包含“模块”作为一部分的名称来引用。

于 2012-06-08T08:39:22.150 回答
2

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

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

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

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

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

在命令式语言中,您通常将接口用于相同目的:

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

正如您所提到的,如果您的模块有一个大接口(例如描述一个子系统),那么在一个文件中编写实现丰富接口的类通常是不切实际的。如果该语言不支持将同一类拆分为单独的源(或更准确地说:将同一类的不同部分从不同的源文件“粘合”在一起),解决方案通常是放弃附带的要求并提供一系列指定它们之间交互的接口——因此您获得了子系统的 API(它是最纯粹意义上的 API:它是与子系统交互的接口,还没有实现)。

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

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

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

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

于 2012-06-12T13:39:08.840 回答