Lecture
Modularity — one of the key principles of object-oriented programming that makes it possible to build flexible, scalable systems. It ensures that code is split into independent components, simplifies maintenance and testing, and promotes the reuse of solutions. In today's development world, modularity has become the foundation for architectures that can withstand growing requirements and increasing project complexity.
Modularity in OOP is a principle that states:
every class should form a separate module.
The second [of the rules which I resolved firmly to observe] was to divide each of the difficulties I examined into as many parts as might be necessary in order best to resolve them.
The third was to order my thoughts in a certain way, beginning with the simplest and easiest-to-know objects, and rising gradually, by degrees, to the knowledge of the most complex, assuming an order even among those that do not naturally precede one another.
René Descartes, "Discourse on the Method" (1637)
Software architects mainly use three standard approaches to creating the initial base components (modules).

Essence: the system is described as a sequence of steps (a workflow) that carries data or a task from an initial state to a final one.
Advantages:
Disadvantages:
Essence: the components of the system are defined through external roles (actors) and their actions. Each actor initiates certain operations, and the system responds.
Advantages:
Focus on interaction and behavior.
Convenient for designing interfaces and APIs.
Works well in distributed and asynchronous systems where actors exchange messages.
Disadvantages:
Requires clear identification of actors and their responsibilities.
Can become complicated when there are many roles.
Essence: the design starts by singling out entities (for example, database tables or domain objects), while behavior and processes remain secondary.
Why this is an anti-pattern:
The system ends up “anemic” — lots of data structures, little logic.
Hard to adapt to real business processes.
Leads to excessive model detail without an understanding of behavior.
Extendibility and reusability, the two key quality factors introduced in lecture 1, require a system with a flexible architecture made up of autonomous software components. That is precisely why lecture 1 introduced the term modularity, which combines both factors.
Modular programming used to be understood as assembling programs out of small pieces, usually subroutines. But such an approach cannot ensure genuine extendibility and reuse of a software product unless it also guarantees that the building blocks - the modules - are self-contained and form stable structures. Any sufficiently complete definition of modularity must ensure that these properties are achieved.
Thus, a method for designing a software product is modular if it helps designers build a system made up of autonomous elements with simple, consistent structural relationships between them. The goal of this lecture is to elaborate on this informal definition and to work out exactly what properties a method must have to deserve the name "modular". Our attention will be focused on the design stage, but all the ideas apply equally well to the earlier stages of analysis and specification, and to the later stages of implementation and maintenance.
Let us look at modularity from several angles. We will introduce a set of additional properties: five criteria, five rules, and five principles of modularity that, taken together, ensure that the most important requirements placed on a modular design method are met.
For a practicing software developer, principles and rules matter just as much as criteria. The difference lies only in the causal relationship: the criteria are mutually independent (a method may satisfy one of them while at the same time conflicting with the rest), whereas the rules follow from the criteria, and the principles follow from the rules.
One might expect this lecture to begin with a detailed description of what a module looks like. But that is not the case, and there are good reasons for it. The task of this lecture and the next two is to analyze the properties that a properly designed modular structure must possess. We will get to the question of what modules look like at the end of our discussion, not at the beginning. Until we reach that point, the word "module" will simply mean a component into which the system under discussion is divided. If you are familiar with non-OO methods, you have probably thought of the subroutines found in most programming and design languages, or perhaps of the packages of the Ada language and (admittedly under a different name) of Modula. Finally, in later lectures our discussion will lead us to the OO form of module - the class. Even if you are already familiar with classes and OO methods, you should still read this lecture to understand the requirements placed on classes - it will help you build them correctly.
A design method that can be called "modular" must satisfy five basic requirements:
A design method satisfies the Decomposability criterion if it helps break a problem down into several less complex subproblems, connected by a simple structure and independent enough that work can later proceed on each of them separately.
This process will often be cyclical, since any given subproblem may turn out to be complex enough to require further decomposition.

Figure 3.1. Decomposition
A consequence of the decomposability requirement is division of labor: once a system has been decomposed into subsystems, the work on them should be distributed among different developers or development teams. This is a difficult task, since it requires limiting the possible interdependencies between subsystems:
The most obvious example of the method under discussion3.1) , one satisfying the decomposability criterion, is the method of top-down design. Under this method, the developer must start with the most abstract description of the function performed by the system. This representation is then elaborated in successive steps, breaking each subsystem at every step into a small number of simpler subsystems, until elements are reached whose level of abstraction is low enough that they can be implemented directly. This process can be pictured as a tree.

Figure 3.2. The top-down design hierarchy
A typical counter-example is any method that calls for including a global initialization module in the system being built. Many of a system's modules need initialization - opening files or initializing variables.
Each module must carry out this initialization before it begins performing the operations directly assigned to it. It might seem sensible to gather all such actions for all the system's modules into a single module that initializes everything for everyone at once. Such a module would have good "temporal cohesion" in the sense that all of its actions take place at one stage of the system's operation. But achieving this kind of "temporal cohesion" would require violating the autonomy of the other modules. The initialization module would have to be given access rights to many data structures belonging to various modules of the system, structures requiring specific initialization actions. This means that the author of the initialization module would have to constantly keep track of other modules' data structures and coordinate with their authors. And that is incompatible with the decomposability criterion.
The term "temporal cohesion" comes from a method known as structured design (see the bibliographical notes).
| In the object-oriented method, each module must initialize its own data structures on its own. |
A method satisfies the Modular Composability criterion if it supports the development of software elements that can be freely combined with one another to produce new systems, possibly in an environment different from the one for which those elements were originally developed.
Composability defines the process that is the reverse of decomposition: software elements are extracted from the context for which they were originally intended, so that they can be used again in a different context.

Figure 3.3. Composition
A modular design method eases this process by producing autonomous software elements that are sufficiently independent of the problem originally posed, which makes such extraction possible.
Composability is directly connected with reuse. This criterion reflects an old dream - turning the process of building a software product into a matter of stacking building blocks, so that programs can be built out of factory-made components.
Composability is independent of decomposability. In fact, the two criteria are often in conflict. For example, top-down design, which as already shown satisfies the decomposability criterion, usually produces modules that are not easy to combine with modules obtained from other sources. Under such decomposition, modules tend to be closely tied to the specific requirements that led to their development, and cannot be adapted for use under other conditions. Top-down design gives no guidance on developing modules that satisfy general requirements. It offers no means for such development, and it lets one neither avoid nor even detect redundant code among the modules produced in different parts of the hierarchy.
Both composability and decomposability are part of the requirements placed on a modular design method. A mix of the two design approaches - top-down and bottom-up - is unavoidable. Rene Descartes drew attention to this principle of complementarity almost four centuries ago, as can be seen by comparing the two rules from his Discourse quoted in the epigraph to this lecture.
A method satisfies the Modular Understandability criterion if it helps produce a program such that, by reading it, one can understand the content of each module without knowing the text of the others, or, in the worst case, having only looked through a few of them.
The importance of this criterion follows from its effect on the maintenance process of a software product. Almost every maintenance activity, whether unavoidable or not so unavoidable, is bound up with a deep understanding of the program's elements. A method can hardly be called modular if someone reading the program text is unable to grasp its meaning.

Figure 3.4. Understandability
Like the other four, this criterion applies to modules at any level of system description: analysis, design, or implementation.
In later lectures, the modular understandability criterion will help us look at two important questions: how to document reusable components, and how to index them so that software developers can easily find them through an appropriate query. Under this criterion, information about a component that is useful for documentation or search should, as far as possible, be contained in the text of the component itself, so that documentation, indexing, or search tools can process the component and extract the required information.
Having the needed information right inside each component is preferable to storing it somewhere else, for example in a database that holds information about components.
A method satisfies the Modular Continuity criterion if a small change to the specification of a developed system leads to a change in just one module, or a small number of modules.
This criterion is directly related to the extendibility criterion. As emphasized in the previous lecture, making changes is an inherent part of the software development process. The corresponding requirements on the program will inevitably change over the course of development. Continuity means that small changes will affect only individual modules in the system's structure, not the system as a whole.
The term "continuity" is suggested by analogy with the notion of a continuous function in mathematical analysis. A mathematical function is continuous if (informally) a small change in the argument produces a proportionally small change in the result. In our case, the role of the function is played by the software construction method, which can be regarded as a mechanism that takes a specification as input and returns, as its result, a system satisfying the given requirements:
Software_construction_method: Specification -> System

Figure 3.5. Continuity
This mathematical term is introduced here only by analogy, since there is no formal notion of the size of a specification or a program. One could devise an acceptable measure for defining "small" or "large" changes to a program, but coming up with a similar definition for specifications is a genuine problem. Still, without claiming any rigor, this intuitive definition captures a necessary requirement for any modular method.
A method satisfies the Modular Protection criterion if it results in a system architecture in which an abnormal condition occurring during the execution of a module is confined to that module, or, in the worst case, spreads to only a few neighboring modules.
The question of failures and errors is central to software engineering. Here we are concerned with run-time errors caused by hardware interrupts, incorrect input data, or the exhaustion of some needed resource (for example, insufficient memory). The protection criterion is not aimed at preventing or fixing errors, but at a problem directly tied to modularity - the propagation of errors within a modular system.

Figure 3.6. A violation of protection
The criteria examined above give rise to five rules that must be followed in order to achieve modularity:
The first rule concerns the relationship between the outside system and the software. The next four rules deal with a common problem - how modules communicate with one another. Achieving a good modular architecture calls for a controlled and rigorous method of handling inter-module communication.
Every application system aims to meet the needs of some problem domain. If a good model exists for describing that problem domain, it is desirable to ensure a clear mapping from the structure of the problem, as described by the model, onto the structure of the system. This gives rise to the first rule:
The modular structure produced during software construction should remain compatible with the modular structure produced while modeling the problem domain.
This recommendation follows, in particular, from two of the modularity criteria:
The Few Interfaces rule limits the total number of communication channels connecting the modules of a system:
Every module should communicate with as few other modules as possible.
Communication between modules can take many forms. Modules may call one another (if they are procedures), share data structures, and so on. The Few Interfaces rule limits the number of such connections.

Figure 3.7. Types of inter-module connection structures
In a system made up of n modules, the number of inter-module connections should be much closer to the minimum value n-1, as shown in figure (A), than to the maximum n (n - 1)/2, as shown in figure (B).
This rule follows, in particular, from the continuity and protection criteria: if there are too many interconnections between modules, the effect of a change or an error can spread to a large number of modules. It is also related to the composability criterion (for a module to be usable in a new software environment, it must not depend on too many other modules), as well as to understandability and decomposability.
Option (A) in the last figure shows how to achieve the minimal number of connections, n-1, by means of a highly centralized structure: one main module, with all the others communicating only with it. But there are far more "democratic" structures, such as (C), that contain almost the same number of connections. In this scheme, each module communicates directly with its two nearest neighbors, and there is no central authority. This approach to program construction may at first seem a little surprising, since it does not fit the traditional top-down design model. But it can lead to reliable, extendible solutions. This is exactly the kind of structure that the OO method, applied sensibly, tends to produce.
The Small Interfaces rule concerns the amount of information exchanged, not the number of connections:
If two modules communicate with each other, they should exchange as little information as possible.
An electrical engineer would say that the communication channels between modules should have limited bandwidth:

Figure 3.8. A communication channel between modules
The requirement for Small Interfaces follows, in particular, from the continuity and protection criteria.
A particularly notable counter-example is a construct from Fortran, known to some readers as the "garbage common block" ("garbage common block"). A common block in Fortran's is a directive of the form:
COMMON /common_name/ variable1 : variableN.
The variables listed in the block are accessible in every module that contains a COMMON directive with the same common_name. It is not uncommon to find Fortran's programs in which every module contains the very same huge COMMON directive listing all the significant variables and arrays, so that every module can directly access any data in the program.
The trouble that arises here is that any of the modules can misuse the shared data, and the modules end up tightly coupled to one another; as a result, achieving continuity (limiting the spread of changes) and protection (limiting the spread of errors) becomes extremely difficult. Nevertheless, this time-honored technique remains a favorite of many programmers, even though it leads to long nights of debugging.
Developers using languages with nested structures run into the same difficulties. Given the block structure introduced in Algol and supported, in a more limited form, in Pascal, one can "nest" blocks contained within begin ... end pairs inside other blocks. Moreover, each block can introduce its own variables, which are meaningful only within the syntactic scope of that block. For example:
local -- Start of block B1
x, y: INTEGER
do
... Instructions of block B1 ...
local -- Start of block B2
z: BOOLEAN
do
... Instructions of block B2 ...
end -- End of block B2
local -- Start of block B3
y, z: INTEGER
do
... Instructions of block B3 ...
end -- End of block B3
... Instructions of block B1 (continued) ...
end -- End of block B1
Variable x is accessible to all the instructions in this program fragment, whereas the scopes of the two variables named z (one of type BOOLEAN, the other of type INTEGER) are confined to blocks B2 and B3 respectively. Like x, variable y is declared at the level of block B1, but its scope does not include block B3, where another variable with the same name and the same type locally takes priority over the nearest enclosing variable y. In Pascal's, this kind of block structure exists only for blocks associated with subprograms (procedures and functions).3.4)
Given block structure, the equivalent of Fortran's "garbage" common block is declaring all variables at the very top (global) level. In C-based languages, the equivalent is declaring all variables external. (On clusters, see the course "Fundamentals of Object-Oriented Design". An alternative to nesting is discussed in the section "The architectural role of selective exports (selective exports)".)
Using a block structure is an appealing idea, but it can lead to a violation of the Small Interfaces rule. For this reason we will refrain from using it in the object-oriented notation developed later in this course. The Simula language - an object-oriented derivative of Algol's - supports a block structure for classes. Experience with it has shown that the ability to create nested classes is unnecessary given certain facilities provided by the inheritance mechanism. The structure of object-oriented software has three levels: a system is a set of clusters; a cluster is a set of classes; a class is a set of components (attributes and routines). Clusters, which are more of an organizational device than a linguistic construct, can be nested, letting a project manager structure a large system into as many levels as needed; but classes, like components, have a flat, single-level structure, since nesting at either of these levels would lead to needless complexity.
The fourth rule is another step toward tightening the totalitarian regime of the module society: it is not enough that any negotiation be limited to just a few participants and be kept brief; such negotiations must also be public and conducted in the open!
Any communication between two modules A and B must be obvious and must be reflected in the text of A and/or B.
Behind this rule lie the criteria of:
One of the problems that arises when applying the Explicit Interfaces rule is that inter-module communication can occur through means other than a procedure call; a source of indirect coupling can be, for example, data sharing:

Figure 3.9. Data sharing
Suppose module A modifies some data, and module B uses that same data item x. Then A and B end up effectively coupled through x, even though there may be no explicit relationship between them, such as a procedure call.
The Information Hiding rule can be stated as follows:
The developer of each module must select some subset of the module's properties as the module's official information, accessible to the authors of client modules.
Applying this rule means that every module is known to all the others (that is, to the developers of other modules) through some official description, or so-called public properties.
Of course, that description could be the whole text of the module (the program text, the design text): it does give a correct picture of the module, since it is the module! But the Information Hiding rule states that, in general, this is not required: the description need only include some of the module's properties. The remaining properties should stay non-public, or secret. Instead of the terms public and secret properties, the terms exported and private (hidden) properties are also used. A module's public properties are also known as the module's interface (not to be confused with the user interface of a programming system).
The Information Hiding rule is grounded in the continuity criterion. Suppose that changes occur in some module affecting only its hidden elements and not touching its public properties; then the other modules that reference it, called its clients, will not be affected by these changes. The smaller the public part, the greater the chance that changes to a module will stay confined to its hidden part.
A module that follows the Information Hiding rule can be pictured as an iceberg; only its tip - the interface - is visible to clients.

Figure 3.10. A module under information hiding
As a characteristic example, consider a procedure that searches by key for attributes stored in a table, such as a personnel file or a compiler's identifier table. This procedure depends heavily on how the table is represented - a sequential array or file, a hash table, a binary or index (B-Tree) tree, and so on. Information hiding means that whatever implementation is chosen for the table does not affect how the procedure is used. Client modules must not suffer from any changes to the program's implementation.
The Information Hiding rule places special emphasis on separating the description of a function from its implementation - what a function does and how it does it are different things. Besides the continuity criterion, this rule is also connected with the decomposability, composability, and understandability criteria. One cannot independently develop the modules of a system, combine existing modules, or understand the behavior of individual modules unless one knows precisely what each of them may (or may not) expect from the other modules.
So which of a module's properties should be public, and which should be hidden? As a rule, the public part should include the functionality specified by the module, while everything related to implementing that functionality should be hidden, shielding other modules from later changes to the program's implementation.
However, this recommendation is imprecise, since no definition has been given for specification or implementation. Indeed, one could be tempted to turn the definition on its head and claim that the specification consists of a module's public properties, while the implementation consists of its hidden properties! The OO approach will provide much more precise guidance, based on the theory of abstract data types. (See, in particular, "Abstract Data Types and Information Hiding".)
To properly understand the meaning of information hiding and apply this rule correctly, it is important to avoid a widespread misinterpretation. Despite its name, information hiding does not mean protecting information in the sense of secrecy - forbidding the authors of client modules from accessing the text of the supplier module. In fact, the authors of client modules have access to all the details that interest them. In some cases it might be sensible to deny them that access, but such a decision, which of course project management can make, does not follow from the information hiding rule. As a technical requirement, information hiding means only that client modules (regardless of whether their authors are permitted access to the hidden properties of supplier modules) must rely only on the public properties of the supplier module. More precisely, it must be impossible to build client modules whose correct functioning depends on hidden information.
| Under a formal approach to software development, this definition could be stated as follows. Proving a module's correctness requires making certain assumptions about the properties of its supplier modules. Information hiding means that the proof may rely only on the suppliers' public properties, and in no way on their hidden properties. |
Let us again consider the example of a module implementing a table search algorithm. Some client module, which might be part of a spreadsheet system, calls our module to search the table for a particular element. Suppose further that our search algorithm is based on a binary search tree implementation, but this property is hidden - it is not reflected in the interface. The author of the table search module can decide for themselves whether to tell the author of the spreadsheet program how the search algorithm is implemented. That decision belongs to project management or, possibly (in the case of commercially released software), is a marketing-level decision; either way, it has nothing to do with information hiding.
Information hiding means something else: even if the author of the spreadsheet program knows that the search is based on a binary search tree, they should not write a client module that works correctly only with this particular search implementation - one that would stop working if the search algorithm were replaced by some other one, for example, hash-based search.
One reason for the misunderstanding mentioned above is the very term "information hiding" ("information hiding"), which suggests physical protection. In this sense, the term "encapsulation" ("encapsulation"), sometimes used as a synonym for information hiding, would seem preferable; however, our discussion will continue to use the general term "information hiding".
This discussion shows that the key to information hiding lies not in decisions about how access to a module's source text is organized within project management or marketing policy, but in strict language rules that determine what access rights to a module follow from the properties of its source. The next lecture will show that the first steps in this direction have been implemented in "languages with encapsulation" such as Ada and Modula-2. Object-oriented programming technology will lead to a more complete solution to the problem.3.5)
From the preceding rules and, indirectly, from the criteria, five principles of software construction follow:
The Principle of Linguistic Modular Units states that the formalism used to describe software at various levels (specification, design, implementation) must support modularity:
Linguistic Modular Units Principle
Modules must correspond to the syntactic units of the language used.
The language referred to above may be a programming language, a design language, a language for specifying technical requirements, and so on. In the case of a programming language, modules must be independently compilable.
At any level (analysis, design, implementation), this principle rules out combining a method based on the concept of modularity with a language that lacks the corresponding modular constructs. Indeed, one often encounters firms that, at the design stage, apply certain methodological approaches — for example, using Ada modules — but then implement their designs in a programming language such as Pascal or C, which does not support these approaches. Such an approach violates several of the criteria of modularity:
Like the Information Hiding rule, the Principle of Self-Documentation determines how modules should be documented:
Self-Documentation Principle
The developer of a module must strive to ensure that all information about the module is contained within the module itself.
Implementation of this principle is usually hindered by the generally accepted practice of placing information about a module in separate design documents.
| The documentation discussed here is internal documentation about software components. User documentation for a released software product may be a separate document, produced as printed text or made available on CD-ROM or web pages on the Internet. As noted in the discussion of software quality, a consequence of the general principle of self-documentation is the currently observed trend toward greater use of interactive online help facilities. (See "On Documentation" Lecture 1 ) |
The most obvious justification for the need for the Principle of Self-Documentation is the criterion of modular understandability. It appears, however, that more important is the fact that this principle helps to implement the criterion of continuity. If the software and its documentation are regarded as separate objects, it is difficult to guarantee that they will remain consistent — that they will change in step with every change to the system. However, if everything is kept in one place, this, while not providing a full guarantee, will still help maintain consistency.
This principle, harmless at first glance, contradicts much of what is usually recommended for practical application in the software development literature. The prevailing opinion is that a software developer — a software engineer — must do what, apparently, other engineers are obliged to do: produce a kilogram of paper for every gram of product actually created. The suggestion to keep a record of the software development process is not bad advice, but it by no means follows from this that the program and its documentation are different products.
Such an approach ignores a characteristic property of software that has been repeatedly discussed here: its capacity for change. If the program and its documentation are regarded as two separate products, one can soon find oneself in a situation where the documentation states one thing while the program does something else. And incorrect documentation is far worse than no documentation at all.
| The major achievement of the last few years has been the emergence of software quality standards. ISO certifications have been developed, along with the "2167" standard and its successors, and the Capability Maturity Model (Capability Maturity Model), proposed by the Software Engineering Institute (Software Engineering Institute). But because they originated from models used in other fields of knowledge, they come with an extensive "tail" of paper documentation. Some of these standards could have had a considerably greater effect on software quality, (besides giving software product administrators a means of justifying themselves in the event of subsequent operational problems) if they had incorporated the Principle of Self-Documentation. |
In this course, a consequence of the Principle of Self-Documentation is the method of documenting classes — modules in OO software construction — which calls for including the documentation within the module itself. This by no means implies that the module itself is its own documentation: program text usually contains too much detail (this is precisely the argument in favor of information hiding). The module simply must contain its documentation. (See "Using Class Assertions (assertions) for Documentation" in . See the course "Fundamentals of Object-Oriented Design" and the last two exercises in it.)
With this approach, the software becomes a single software product that provides its various representations, or views (views). One view, suitable for compilation and execution, is the module's complete source text. Another is the documentation, which specifies the module's abstract interface, allowing software developers to create client modules without becoming familiar with the contents of the source module — which corresponds to the Information Hiding rule. Other representations are possible as well.
Although at first it may seem that the Principle of Uniform Access is aimed only at solving problems related to the notation adopted, in reality it sets a design rule that affects many aspects of OO software development. The principle follows from the criterion of Continuity; it can also be regarded as a special case of the Information Hiding rule.3.6)
Let x be a name used to access some data item, which we will subsequently call an object. Let f be the name of a component (feature) applicable to x. A component is understood to mean some operation; this term will be defined in more detail later. For example, x might be a variable representing a bank account, and f a component giving the current balance of that account (account's current balance). Uniform Access is aimed at resolving the question of what the notation for applying f to x should be, without containing any premature commitment to how f is implemented.
In many design and programming languages, the expression describing the application of f to x depends on the implementation of f chosen by the developer. It may be a property stored together with x, or a method called whenever it is needed. In the example with bank accounts and account balances, both approaches are possible:
In the conventional notation of languages such as Pascal, Ada, C, C++, and Java, the notation x.f is used for case A1 and f(x) for case A2.

Fig. 3.11. Two representations of a bank account
The choice between representations A1 and A2 is a trade-off between "space and time": the first saves on computation, the second on memory. The decision to choose one of the variants is a typical example of a decision that the developer changes at least once during the lifetime of a project. Therefore, in order to maintain continuity, it is desirable to have a notation for accessing a component that does not depend on the choice of one of the two representations. If the way x's are implemented is changed at some stage of the project's development, this will not require any changes in the modules that use the call to f.
We have examined an example of the Principle of Uniform Access. In general form, the principle can be formulated as follows:
Uniform Access Principle
All the facilities offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.
Few languages satisfy this principle. The oldest of them was Algol W, in which both a function call and field access were written as a(x). The first OO language to satisfy the Principle of Uniform Access was Simula 67, which used the notation x.f in both cases. The notation proposed in Lectures 7-18 of this course will support this same convention.
Any method of modular decomposition must satisfy the semaphore principle: Open-Closed:
Open-Closed Principle
Modules should be both open and closed.
The contradiction is only apparent, since the terms correspond to different goals:
The need to close modules and the need to leave them open arise from different causes. For software developers, the natural state of a module is to be open, since it is almost impossible to foresee in advance all the elements — data, operations — that may be needed in the course of creating the module. Developers therefore try to keep the software flexible enough to allow for later changes and additions. But it is necessary, especially from the project manager's point of view, to close modules. In a system consisting of many modules, most modules are dependent on one another. For example, a user-interface module may depend on a parsing module (parsing module) — a syntax analyzer — and on a graphics module. The syntax analyzer may depend on a lexical analysis module, and so on. If a module is not closed until there is certainty that it already contains all the necessary components, it will be impossible to complete the development of a multi-module program: each developer will be forced to wait for all the others to finish their work.
With traditional methodology, the two goals just considered turn out to be incompatible. Either the module remains open, which prevents everyone else from using it, or it is closed, in which case any change or addition can set off an unpleasant chain reaction of laborious changes in many other modules that directly or indirectly depend on this original module.
The two figures below illustrate a situation in which it is difficult to reconcile the need for open and closed states of a module. In the first figure, module A is used by client modules B, C, D, which may themselves have their own clients — E, F, and so on.

Fig. 3.12. Module A and its clients
As time passes, the situation changes and new clients appear — F and others — who need an extended or adapted version of module A, which we can call A':

Fig. 3.13. Old and new clients
With non-OO methods, only two solutions to this problem are possible, both equally unsatisfactory:
The possible catastrophic consequences of solution N1 are obvious. Module A may have been in use for a long time and have many clients, such as B, C, and D. The rework needed to satisfy the requirements of the new clients may violate the assumptions on which the old clients relied when using module A; in that case, changes to A can "trigger" a catastrophic chain of changes in the clients, in the clients of those clients, and so on. For a project manager, this is a genuine nightmare: suddenly, entire parts of software that had long been considered finished and deployed turn out to be reopened, which "triggers" a new cycle of development, testing, debugging, and documentation. How many software project managers would want to see themselves in the role of Sisyphus — condemned forever to roll a stone to the top of a mountain only to watch it roll back down each time — all because of problems caused by the need to reopen previously closed modules.
At first glance, solution N2 seems better: it avoids the Sisyphus syndrome, since it does not require modifying software that already exists (shown in the upper part of the last figure). But in reality, this solution can have even worse consequences, since it merely postpones the hour of reckoning. Let us extrapolate the effect of this solution to a large number of modules — many modifications will be needed, taking a long time. In the end, the consequences turn out to be dreadful: an explosive growth in the number of variants of the original modules, many of which are very similar, though not entirely identical.
For many software development organizations, such an abundance of modules, out of proportion to the number of functions actually performed (many variants that appear different turn out, in essence, to be clones), creates a serious software configuration management problem. This problem is usually tackled by using sophisticated tools. Useful in themselves, these tools try to "cure" the program in situations where the first of the solutions discussed would have been preferable. It is, after all, better to avoid redundancy than to create it.
| Configuration management will undoubtedly prove useful, but only if it is possible to find the modules that need to be reopened after changes have occurred while at the same time avoiding recompilation of modules that do not need it. (Exercise E3.6 asks you to work out what the need for configuration management will be in an object-oriented programming environment.) |
But how can one obtain modules that are simultaneously open and closed? Is it possible to leave module A and all its clients in the upper part of the figure unchanged, while at the same time providing module A' to the clients in the lower part, avoiding duplication of software? Thanks to the mechanism of inheritance (inheritance), the OO approach provides a particularly elegant contribution to solving this problem.
The mechanism of inheritance is examined in detail in later lectures; here only a general idea of it is given. To resolve the dilemma — modify or redo — inheritance makes it possible to define a new module A' on the basis of an existing module A, stating only the differences between them. Let us describe A' as
class A' inherit
A
redefine f, g, ... end
feature
f is ...
g is ...
...
u is ...
...
end
where the feature clause contains both the definition of new components specific to A', such as u, and the redefinition of those components (such as f, g, ...) whose representation in A' differs from what they had in A.
To illustrate inheritance graphically, an arrow is used from the "heir" (heir) (the new class A') to the "parent" (parent) (class A):

Fig. 3.14. Adapting a module to new clients
Thanks to the mechanism of OO inheritance, developers can pursue a much more consistent approach to software development than was possible with earlier methods. One way of describing the Open-Closed principle and the OO methods that follow from it is to view them as organized hacking. "Hacking" here is understood as a slipshod (slipshod) approach to assembling and modifying a program (and not at all as unauthorized, and certainly impermissible, intrusion into computer networks). A hacker may be considered a bad person, but his intentions are often pure. He may spot a useful piece of a program that is almost suitable for meeting current needs, needs far exceeding those envisioned when the program was originally developed. Inspired by the laudable wish not to recreate what can be reused, our hacker begins modifying the program's source text, adding facilities to it for performing new tasks. Of course, such an impulse is not bad in itself, but the result is often that the program becomes "cluttered" with numerous expressions of the form: if(this_special_case) then. After several repetitions, possibly carried out by different hackers, the program begins to resemble a slice of Swiss cheese left too long in the August heat (the tastelessness of this metaphor is justified by how well it reproduces the appearance, in such a program, of both "holes" and "growths").
This organized form of hacking makes it possible to adapt to the changing structure of the problems being solved without violating the consistency of the original version.
A brief warning: what is being proposed here is not unorganized hacking. In particular:
The last of the five principles of modularity can be regarded as a consequence of both the Open-Closed principle and the Information Hiding rule.
Before examining the Single Choice principle in detail, let us look at a typical example. Suppose we are building a system for working with a library (in the non-programming sense of the word: a collection of books and other publications, not program modules). This system will process data structures representing various publications. We can declare the corresponding type in Pascal-Ada syntax:
type PUBLICATION =
record
author, title: STRING;
publication_year: INTEGER
case pubtype:(book, journal, conference_proceedings) of
book:(publisher: STRING);
journal:(volume, issue: STRING);
proceedings:(editor, place: STRING) -- Conference proceedings
end
Here we have used a "record type with variants" (record type with variants) to describe sets of data structures with fields, some of which (in this example author, title, publication_year) are common to all cases, while others are specific to particular data variants.
| The use of a particular syntactic construct is not essential here. The programming languages Algol 68 and C provide the same capability by means of the "union" (union) type. A union type is a type T defined as the union of previously existing types A, B, ...: a value of type T is either a value of type A or a value of type B, .... The advantage of record types with variants is that each variant is explicitly associated with a tag (tag), for example book, journal, conference_proceedings. |
Let A be the module that contains the type declaration described above. As long as module A is considered open, fields can be added to it or new variants introduced into it. But once module A is handed over to clients, it should be closed, which by default means that all the essential fields and variants have already been listed in it. Now let B be a typical client of module A. B will manipulate publications through some variable, for example:
p: PUBLICATION
In order to perform any useful actions using p, it is necessary to explicitly distinguish the various cases:
case p of
book:... Instructions which may access the field p.publisher...
journal:... Instructions which may access fields p.volume, p.issue...
proceedings:... Instructions which may access fields p.editor, p.place...
end
Here the case selection statement from Pascal and Ada proved convenient; its syntax mirrors the definition of the record type with variants. In Fortran's and C this can be imitated by repeated use of the unconditional goto statement (switch in C). In these and other languages, the same result can be obtained using nested conditional statements (if ... then ... elseif ... elseif ... else ... end).
It should be noted that, regardless of the syntactic construct used, in order to make such a choice, every client module must know the complete list of representation variants for a publication supported by module A. The consequences of this are not hard to foresee. A moment will come when a new variant is needed — for example, technical reports from companies and universities. It will then be necessary to extend the definition of the PUBLICATION type in module A to take the new case into account. This is entirely logical and unavoidable: if the definition of the notion of a publication has changed, the corresponding type declaration must be updated too. It is, however, much harder to find a justification for the other consequence: any client of module A, such as B, will also require updating if it used the structure discussed above, based on the complete list of cases for p. And this will obviously be the case for most clients.
So we observe a very dangerous kind of change in the program: a simple and natural addition can trigger a chain reaction of changes in many client modules.
This problem will arise whenever some notion admits multiple variants. Here that notion was "publication" ("publication"), and its initial variants were: book (book), journal article (journal article), conference proceedings (conference proceedings); other typical examples might be:
In any such case, it is necessary to allow for the possibility that the list of variants, given and known at some stage of program development, may later be changed by adding or removing variants. To support this approach to the software development process, a way must be found to protect the structure of the program from the effects of such changes. From this follows the Single Choice principle:
Single Choice Principle
Whenever a software system must support a set of alternatives, their complete list should be known to only one module of the system.
The requirement that the list of choices be known to only one module provides preparation for subsequent changes: when variants are added, only the module that contains this information will need to be updated — that is the essence of single choice. All the other modules, in particular its clients, will be able to continue operating as before.
Thus, as the example of the publication library shows, traditional methods do not provide a solution to the problem, whereas object technology makes it possible to obtain a solution thanks to two techniques associated with inheritance: polymorphism (polymorphism) and dynamic binding (dynamic binding). However, the preliminary discussion given here is not sufficient; these techniques can be properly understood only in the context of the whole method of inheritance. (See "Dynamic Binding" )
The Single Choice principle calls for a few more comments:
The design method known as "structured design" [Yourdon 1979] places special emphasis on the importance of using modular structures. This method was based on an analysis of module "coupling" and "cohesion". But the implicit notion of a module in structured design was based on the traditional concept of a subroutine, which limited the scope of the discussion. The principle of uniform access was originally proposed (under the name "uniform reference") in [Geschke 1975]. In discussing uniform access, mention was made of the Algol W language, a successor to Algol 60 and a predecessor of Pascal (in which some interesting mechanisms were proposed that were not preserved in Pascal's), developed by Wirth and Hoare, and described in [Hoare 1966].
Information hiding was proposed in two seminal papers by David Parnas [Parnas 1972] [Parnas 1972a].
Configuration management tools, which recompile modules affected by changes in other modules based on a detailed list of dependencies between modules, are based on the concepts of the Make utility, originally developed for Unix [Feldman 1979]. Modern utilities — and there are many of them on the software market — have significantly extended the functionality of the original ideas.
Some of the exercises given below ask you to devise metrics for quantitatively assessing the various informal modularity criteria formulated in this lecture. Some results relating to OO metrics can be found in the work of Christine Mingins (Christine Mingins) [Mingins 1993] [Mingins 1995] and Brian Henderson-Sellers (Brian Henderson-Sellers) [Henderson-Sellers 1996a].
E3.1 Modularity in programming languages
Study the modular structures of any programming language with which you are well acquainted, and assess how well they satisfy the criteria and principles set out in this lecture.
E3.2 The Open-Closed principle (for Lisp programmers)
Many implementations of Lisp's bind specific functions to their names not statically but at run time. Does this mean that Lisp supports the Open-Closed principle better than statically typed languages?
E3.3 Limitations on information hiding
Can you imagine circumstances under which information hiding should not be applied to the relations between modules?
E3.4 Metrics for modularity (a research report)
The criteria, rules, and principles of modularity have been described in this lecture through qualitative definitions. However, some of them lend themselves to quantitative analysis. These might include:
Investigate the possibility of devising modularity metrics for assessing how modular the architecture of a software system is with respect to some of these notions. The metrics should be size-independent: increasing the size of a system without changing its modular structure should not change the measures of its complexity (see the next exercise).
E3.5 Modularity of existing systems
Apply the criteria, rules, and principles of modularity from this lecture to assess a system to which you have access. If you solved the previous exercise, apply any of the modularity metrics you proposed.
Can you establish any interdependencies between the results of this analysis (qualitative, quantitative, or both) and assessments of the structural complexity of the system under study, based either on an informal analysis of it or, if possible, on actual measurements of the cost of debugging and maintaining it?
E3.6 Configuration management and inheritance
This exercise assumes knowledge of the inheritance mechanism, described later in this course. It is not worth attempting yet if you have reached this lecture by studying the course sequentially.
The discussion of the Open-Closed principle showed that the absence of inheritance in non-OO methods causes excessive costs in developing configuration management tools, since the desire to avoid reopening closed modules can lead to the creation of too many module variants. Determine what role remains for configuration management tools in an OO environment that has an inheritance mechanism, and more generally, how the use of object technology affects configuration management.
If you are familiar with specific configuration management tools, find out how they interact with the inheritance mechanism and with other principles of OO software development.
Modularity in OOP — is not just a convenient technique, but a strategic approach to designing software systems. It helps developers manage complexity, increases reliability, and speeds up the process of introducing new features. Mastering the principles of modularity opens the way to building robust, elegant architectures that remain relevant even as technologies and business objectives change.
Comments