This paper has been submitted for publication in The Smalltalk Report. Once published, copyright transfers to that journal. Authorization for copies should be obtained there. This early draft is being made available for professional peer communication.
This article proposes a new view of modules and how they may be added to the Smalltalk programming system. Modules provide a way to control the visibility of shared names. Modules also provide a way to hide the detailed collaborations among a group of Smalltalk classes that are organized as a subsystem. The organizing principles of classes and modules are orthogonal. Thus, modules can also be used to safely extend existing baseline classes.
The concept of a module and modular software development have existed for many years. A variety of programming systems have provided support for using separate name spaces to control the visibility of the names used in a program. Exemplars include Modula-2 [8] and Ada [1].
Smalltalk systems use classes to encapsulate the structure and state of objects. Because Smalltalk classes can hide their internal state and serve as centers around which program behavior may be organized, they may also be considered to be modular. But, while Smalltalk classes can encapsulate the state of their instances, they do not encapsulate the behavior of their instances.
By convention, some messages are designated as "private" for the private use of the class and its instances. However, the Smalltalk system does not enforce the designated message privacy. It is not always clear what such privacy means anyway. For example, should subclasses be restricted from using private messages they inherit from their superclasses?
Because classes are globals in the Smalltalk system dictionary, they are all visible to all other classes. This visibility is excessive. It can contribute to information overload for novice Smalltalk programmers. It can also cause class naming conflicts when a team of developers integrate their separately developed components.
This article attempts to deal with these issues in a manner that is relatively non-intrusive and that does not sacrifice any of the flexibility and power offered by existing Smalltalk systems.
In their work on Modular Smalltalk [5], Allen Wirfs-Brock and Brian Wilkerson describe the essential features of modules:
Modules are program units that manage the visibility and accessibility of names...
A module typically groups a set of class definitions and objects to implement some service or abstraction. A module will frequently be the unit of division of responsibility within a programming team....
A module provides an independent naming environment that is separate from other modules within the program....
Modules support team engineering by providing isolated name spaces...
While providing many potential improvements to Smalltalk, the Modular Smalltalk system does not implement modules as first-class objects. Like many other programming systems, the Modular Smalltalk system uses modules only for organizational purposes. This article proposes a different view of modules as a special kind of Smalltalk class.
The definition of a normal Smalltalk class includes a reference to a superclass, the name of the new subclass, and the names of any new instance and class variables added by the new subclass. Class variables are shared by all the instances of a class, and are visible to all the methods of the class and its subclasses, if any.
In addition, the new subclass can provide its methods with access to named objects that are shared on a subscription basis. Certain names in the Smalltalk system dictionary are bound to global pool dictionaries that contain these sharable named objects. The new subclass can subscribe to these objects by including selected global names in its list of pool dictionaries. For example, a File class might be defined using the following message:
Object subclass: #File
instanceVariableNames:
'directory fileId name'
classVariableNames: 'PageSize'
poolDictionaries:
'CharacterConstants'!
Modules may be added to Smalltalk in a relatively straightforward manner. The details of how this can be done are presented in a later section. For now, we can say that each module is a class that contains a name space, called its domain, instead of simply a pool of class variables.
Their are several new messages for defining modules and the private classes contained in their domains. The definition of a module for managing an inventory might use the following message:
Object moduleSubclass: #InventoryManager
instanceVariableNames:
''
classVariableNames: ''
poolDictionaries: ''!
A new private class can be added to the domain of the InventoryManager class using the message:
Object subclass: #InventoryItem in: InventoryManager
instanceVariableNames:
'partNumber partName quantity'
classVariableNames: ''
poolDictionaries:
''!
In order to add a new private subclass of InventoryItem, we send the name of the private class (#InventoryItem) as a message to the InventoryManager module:
InventoryManager InventoryItem subclass: #FloorItem
instanceVariableNames:
'storeLocation'
classVariableNames: ''
poolDictionaries: ''!
The issues involved in this breaking of the module encapsulation will be considered further in a later section.
Modules can be used to create nested subsystems. The following message creates a nested module for managing the accounts in the InventoryManager module class.
Object moduleSubclass: #AccountManager in:
InventoryManager
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries:
''!
Figure 1 depicts the structural relationships between the classes in the InventoryManager module. Note that the graphic design notation of OMT [3] has been extended slightly to show what classes are encapsulated inside a module class. The rounded rectangles represent module domains. Note that the Smalltalk system dictionary is also considered to be the system domain.
Modules provide three ways of encapsulating private behavior, all of which are based on their ability to encapsulate private classes:
Each of these options will be discussed in the following sections.
One advantage of modules is that they provide a way for developers to package systems of components. During the design of a system of objects, groups of classes often know of each other explicitly and collaborate closely to produce some complex behavior. Such subsystems are described informally in [7] on page 135:
Subsystems are groups of classes, or groups of classes and other subsystems, that collaborate among themselves to support a set of contracts. From outside the subsystem, the group of classes can be viewed as working closely together to provide a clearly delimited unit of functionality. From inside, subsystems reveal themselves to have complex structure. They consist of classes and subsystems that collaborate with each other to support distinct contracts that contribute to the overall behavior of the system...
Subsystems are identified by finding a group of classes, each of which fulfills different responsibilities, such that each collaborates closely with other classes in the group in order to cumulatively fulfill a greater responsibility...
There is no conceptual difference between the responsibilities of a class, a subsystem of classes, and even an entire application; it is simply a matter of scale, and the amount of richness and detail in your model...
In this article, we go beyond the conceptual and assert that there is not even any practical difference between the responsibilities of a class and a subsystem of classes when the subsystem is implemented as a module. The module class acts as a capsule around the subsystem of classes enclosed within the module domain.
Such packaging supports some of the practices of good software engineering. Implementation details can be localized, encapsulated, and scoped. Just as good object designs organize state and behavior into classes, systems of objects that are closely coupled, or that cooperate to provide some overall set of services, can be organized into modules.
Several examples of object system design based on responsibilities are given in [7]. Two of the examples in this article are derived from those in [7]. Only the class definitions for these examples are given here. The first example has already been presented. The InventoryManager depicted in figure 1 was derived from the Inventory subsystem described on pages 146-148 of [7]. Pages 151-152 describe the organization of a subsystem for managing transactions against financial accounts. Figure 2 shows how this subsystem might be organized as a module. The classes for this system could be defined using the following messages:
Object moduleSubclass: #FinancialManager
instanceVariableNames:
''
classVariableNames: ''
poolDictionaries: ''!
Object subclass: #Account in: FinancialManager
instanceVariableNames:
'accountID balance'
classVariableNames: ''
poolDictionaries: ''!
Object subclass: #Transaction in: FinancialManager
instanceVariableNames:
'account'
classVariableNames: ''
poolDictionaries: ''!
FinancialManager
Transaction subclass:
#BalanceInquiry
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries:
''!
FinancialManager
Transaction subclass:
#FundsDeposit
instanceVariableNames: 'amount'
classVariableNames:
''
poolDictionaries: ''!
FinancialManager
Transaction subclass:
#FundsWithdrawal
instanceVariableNames: 'amount'
classVariableNames:
''
poolDictionaries: ''!
FinancialManager
Transaction subclass:
#FundsTransfer
instanceVariableNames: 'amount targetAccount'
classVariableNames:
''
poolDictionaries: ''!
Modules provide a safe way to extend and package changes to the baseline classes in the Smalltalk system domain. Figure 3 shows how a private version of the String class can transparently subclass the baseline version of the String class in order to extend it.
ModuleA is a moduleSubclass of class Object and SubclassB is a private class inside the domain of ModuleA. The private String class inside the domain of ModuleA is a private subclass of the baseline String class:
Object moduleSubclass: #ModuleA
instanceVariableNames:
''
classVariableNames: ''
poolDictionaries: ''!
Object subclass: #SubclassB
in: ModuleA
instanceVariableNames:
''
classVariableNames: ''
poolDictionaries: ''!
String subclass: #String
in: ModuleA
instanceVariableNames:
''
classVariableNames: ''
poolDictionaries: ''!
The private String class extensions are visible to methods in both ModuleA and SubclassB, but not to classes outside of ModuleA in the Smalltalk system domain, such as SubclassC.
One drawback exists in the above example. The compiler creates constants for literals using the baseline classes: SmallInteger, Float, String, Symbol, and Array. Unlike Objectworks/Smalltalk, Smalltalk/V does not presently include the source code for its compiler. Because the Smalltalk/V compiler has not been extended to use the privatized versions of baseline classes it uses for literals, the private String class needs to create instances by copying baseline strings. For example, if we want SubclassB to use a private String for some operation, it will need to create it using:
"private" String copyFrom: 'a constant string'
The compiler creates a constant string that is an instance of the baseline String class. The private String class creates an instance of itself that is a copy of this string constant. Given an instance of the private String class, the extended private string operations may be performed on it.
Given access to the source for the compiler, this small defect could be rectified. Then all the baseline classes, including those that the compiler uses for literals, could be extended transparently by private subclasses.
Modules can be used to hide the private methods of a class. In order to do this, a pair of classes are used to divide the public methods from the private ones. The public class is a module whose methods provide its public interface. The private methods are hidden in a private class inside the module domain. The private class can have the same name as the module class.
Figure 4 depicts an example of how this principle can be applied. Because of its simplicity, the full code for this example can be found in the appendix. The module class ClassFiler is derived from the standard Smalltalk class ClassReader. This class is used to file Smalltalk source code in and out of the system, usually using an instance of class FileStream.
The ClassFiler module class has a single instance variable: privateSelf. When an instance of the module class is created, privateSelf is set to reference an instance of the private ClassFiler class. All of the public methods of the module delegate private messages to privateSelf. The instances of the module class serve as proxies that hide the private behavior of the module class.
In order to maintain encapsulation, the public methods in the module class can check the answers that come back from privateSelf. Any answer that is identical to privateSelf should be answered as self (the module instance) instead.
This technique provides true encapsulation of the private methods of the class with a small amount of overhead in time (the delegation and answer checking) and space (the extra instance privateSelf).
Where a normal Smalltalk class uses a Dictionary for its pool of class variables, a module class uses a ModuleDictionary for its domain. The ModuleDictionary class is similar to the SystemDictionary class. Like the Smalltalk system dictionary, each module domain can contain shared objects, including other Smalltalk classes. In addition, each module domain keeps track of the names of the module class variables.
Each class contained in a module domain needs to know what module contains it. For this reason, each class contained inside a module domain is associated with an EncapsulatedMetaClass rather than a MetaClass. The class EncapsulatedMetaClass extends the class MetaClass by adding a reference to the module whose domain contains the encapsulated class.
Figure 5 depicts the classes changed to extend Smalltalk/V. Rectangles with doubled borders indicate the new classes.
Smalltalk methods use names that start with lower case for private names, including instance variable names, method arguments, and block temporaries. Smalltalk methods can also reference shared objects whose names are capitalized.
The visibility of these shared names depends on where they are located in the system. Shared names can be found in class variable pools, global pool dictionaries, and the Smalltalk system dictionary. During method compilation, references to shared names are resolved by searching dictionaries in the following order:
the class variable pools of the class and its superclasses up through the class Object.
the pool dictionaries to which the class subscribes from the Smalltalk system dictionary.
the Smalltalk system dictionary itself.
Extending the visibility rules of the compiler is the key to adding modules to Smalltalk. The Smalltalk system dictionary is the enclosing domain for classes not contained in a module. As such, it is also considered to be the system domain. Because a module contains a name space in its domain, references to shared names are resolved by searching dictionaries in the following order:
the class variable pools of the class and its superclasses up through the class Object.
the pool dictionaries to which the class subscribes in the module domains enclosing the class up through the Smalltalk system domain.
the module domains enclosing the class up through the Smalltalk system domain.
Because these new visibility rules subsume the existing rules, the semantics of normal classes continue to be supported.
Because modules enclose and encapsulate their private classes, the programming tools need a way to break the encapsulation of the module to create new classes inside the module. For this reason, a change has been made to class Class.
When a module class #doesNotUnderstand: aMessage, the message selector is checked to see if it is a capitalized unary selector that may be the name of a private class inside the module. If so, the message answers the requested private class from the module. Otherwise, the message is dealt with using the existing #doesNotUnderstand: behavior.
This revised behavior is provided expressly for the compiler and development tools. This service breaks the encapsulation of the module similar to the way that #instVarAt: breaks the encapsulation of an object.
In order to enforce the encapsulation of a finished module, the module can be closed by adding another version of #doesNotUnderstand: to the module class, overriding the one in class Class. This can be accomplished simply by sending the message #closeModule to the module class:
ModuleA closeModule.
This forces other classes outside of the module scope to use the publicly defined interface to the module.
The module that encloses a group of private classes can provide either direct or indirect access to the services of those classes. If the module grants direct access to an enclosed class by publishing it, then all the services of that class are directly available.
A module can provide direct access to an enclosed private class by supplying an accessing message as part of the public interface to the module. Suppose we want to give direct access to SubclassB in figure 3. We could give ModuleA a class method named #SubclassB that answers SubclassB:
!ModuleA class methods !
SubclassB
"Publish SubclassB."^SubclassB! !
However, modules provide their greatest advantage when they hide or limit the visibility of their internals. This visibility is determined by what information (objects) is revealed by the module class and its instances (if any). The module forms the public interface to the classes inside the module domain.
Several other works [4][5][7] suggest that modules are not first-class and have no direct representation in an active system of objects. They suggest that modules only serve as name spaces for controlling the visibility of shared names. This article has presented a different viewpoint, advocating the inclusion of modules as a special kind of class.
Using a responsibility-driven approach [6][7], the design of an object system can achieve a high degree of encapsulation and reusability. Classes help to maintain encapsulation when they limit access to their variables. Modules can help to maintain a higher degree of encapsulation by limiting access to the private behavior of subsystems.
The Law of Demeter [2] suggests that object systems can best realize the benefits of reuse by strictly limiting the visibility of objects to those other objects in the system that require such visibility. With classes and modules, visibility is controlled by the system designer.
This article shows how modules can be made first-class within Smalltalk systems. Modules provide a natural way of packaging object systems and give object system designers more options for controlling the visibility of a system's implementation details. Modules reduce the possibility of naming conflicts between separable systems of objects.
Just as classes form a hierarchy for the inheritance of structure and behavior, modules can be used to form a nested hierarchy of name spaces (domains). The organizing principles of classes and modules are orthogonal and complement each other.
Classes can be imported into modules by adding a private subclass of the same name to the module domain. However, given the new visibility rules for shared names, this kind of transparent subclassing may be the only reason for explicitly importing classes from outside a module.
Classes can be exported from a module by providing a message for accessing the class by name. However, this kind of revelation on the part of a module is discouraged, because it leads to dependencies on the module's internals.
[1] Grady Booch. Software Engineering with Ada, Benjamin/Cummings, Menlo Park, CA 1983.
[2] Karl L. Lieberherr and Ian Holland. Formulations and Benefits of the Law of Demeter. In SIGPLAN Notices, v24#3, pages 67-78, March 1989.
[3] James Rumbaugh, Michael Blaha, William Premerlani, Frederick Eddy, William Lorensen. Object-Oriented Modeling and Design, Prentice-Hall, Inc., Englewood Cliffs, NJ, 1991.
[4] Clemens A. Szyperski. Import is Not Inheritance, Why We Need Both: Modules and Classes. In ECOOP 1992 Proceedings, pages 19-32, June/July 1992.
[5] Allen Wirfs-Brock and Brian Wilkerson. An Overview of Modular Smalltalk. In OOPSLA 1988 Proceedings, pages 123-134, September 1988.
[6] Rebecca Wirfs-Brock and Brian Wilkerson. Object-Oriented Design: A Responsibility-Driven Approach. In OOPSLA 1989 Proceedings, pages 71-75, October 1989.
[7] Rebecca Wirfs-Brock, Brian Wilkerson, Lauren Wiener. Designing Object-Oriented Software. Prentice-Hall, Inc., Englewood Cliffs, NJ, 1990.
[8] Niklaus Wirth. Programming in Modula-2. In Texts and Monographs in Computer Science, 2nd Edition, David Gries, Springer-Verlag, Berlin 1984.
Macintosh is a trademark of Apple Computer, Inc. PARTS Workbench is a trademark of Digitalk, Inc. Smalltalk/V is a trademark of Digitalk, Inc. Objectworks/Smalltalk is a trademark of ParcPlace Systems, Inc.