I was going through the Martin Odersky's book Programming in Scala with it's section on abstract modules, and his paper Scalable Component Abstractions:
http://lampwww.epfl.ch/~odersky/papers/ScalableComponent.pdf
My takeaway is that by making your modules abstract classes instead of objects (or classic static, global modules like in 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{}
}
you can the instantiate multiple subclasses and instances of a module with different concrete characteristics. This lets you configure the module differently depending on the circumstances (e.g. substituting the database component during testing, or the IO component when in a dev environment) as well as instantiating multiple modules each with their own set of module-scoped mutable state.
From what I can understand, on a basic level, it's only hard requirement is that you can have nested classes, such that the enclosing class can act as a module.
It's other practical requirement is that you can spread out the class definition over multiple files, since a module with a bunch of classes in it is probably more lines of code than most would accept in a single source file.
Scala does this with Traits, which bring some other goodies which are nice but not central to the whole spread abstract-module-class over multiple source files idea. C# has partial classes
, which provide the same functionality, and also allows nested classes. Presumably some other languages have similar support for nested classes, as well as splitting a class over multiple files.
Does this sort of pattern occur anywhere in C# or any other languages? I would think large projects in many languages face the problem that abstract modules are meant to solve. Is there any reason this "abstract-class as abstract-module" thing doesn't work, and therefore is not used? It seems to me a much cleaner solution than the various DI frameworks with offer kind of the same functionality.
The usual comparison is with ML modules, where Scala traits (or abstract classes) play the role of ML signatures and their concrete implementations (typically Scala objects) play the role of ML structures. The discussion of ML modules here should make the connection reasonably clear.
The analog between Scala and ML is deliberate and if you look at the source of the Scala compiler you'll see that Scala objects are often referred to using names that contain "Module" as a part.
The abstract module you describe has the following core properties:
The feature to be able to specify the module using more than one source file is not a core requirement, but it can certainly come in handy.
In its most basic form, a module is describing an abstract datatype (e.g. queue): what operations are available to interact with the datatype, and any auxiliary types that are needed for the interaction.
In a more complex form, it can describe a whole subsystem (e.g. networking).
In imperative languages you usually use an interface for the same purpose:
As you mentioned, if your module has a large interface (e.g. describes a subsystem), it is usually impractical to write the classes implementing the rich interface in one file. If the language does not provide support for splitting the same class into separate sources (or more precisly: for "gluing" separate parts of the same class from different source files together), the solution usually is to lose the enclosed requirement and provide a series of interfaces that specify interactions among them -- thus you get an API for the subsystem (it is an API in its purest sense: it is an interface to interact with the subsystem, with no implementation yet).
In some ways this latter approach can be more generic (generic in the sense of what you can achieve with it) than an enclosed type: you can provide implementations of the various sub-types (specified via interfaces) from different authors: as long as the subtypes only rely on the specified interface for interaction with each other, this mix-n-match approach will work.
One of the strengths of most functional programming languages are parametrized datatypes, where you instantiate a dayatype with another as its parameter (e.g. a queue of integers). The same flexibility is achieved by Generics in Java/C# (and templates in C++). Of course the exact implications and expressive power can differ among the languages based on their type system.
This whole discussion is separate form Dependency Injection (DI), which tries to loosen the strong dependency between a concrete implementation of a type and its supporting parts by explicitly providing the needed parts (as opposed to have the implementation choose), as the user of the type might have a better understanding of what implementation of those parts are best to achieve its goal -- e.g. providing mock implementations for testing functionality.
The problem DI tries to solve is exclusive to imperative languages, you can have the same dependency problems in functional languages as well: the implementation of an abstract module might choose to use a specific implementation of sub-types (thus coupling itself to those implementations) instead of taking the sub-type implementations as parameters (which is what DI is aiming for)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With