Home Design Patterns Module
Module
Modern PracticeGroup related things behind one deliberately narrow public surface.
Purpose
A Module is a unit of code with two properties: the things inside it belong together, and most of them cannot be named from outside. The first half is cohesion and it is a matter of taste. The second half is the pattern, and it is enforceable. A module is defined by what it refuses to export.
This is the Facade instinct applied to a whole unit of code rather than to a handful of collaborating objects. A Facade offers a convenient door and leaves the rest of the subsystem reachable. A Module makes the rest unreachable, so the narrow surface is not a convenience, it is the contract. That difference matters, because what you can change safely tomorrow is exactly the set of things nobody could name today.
Design
In practice you do not build this out of classes. You use whatever construct
the language provides, because every mainstream language now has one: a .NET
assembly with internal, a Java package or a Java 9 module with an
exports clause, an ES module where anything without
export is simply unreachable, a Python module with
__all__ and the leading-underscore convention, a C++20
export module. The pattern is a description of what those constructs
are for, not a recipe for reimplementing them.
Vision all point at things
Reporting cannot mention. The public surface is two operations
wide; the module behind it is three classes deep and free to become five.The design work is choosing the boundary, and the useful rule is Parnas's from 1972: draw the line around a decision that is likely to change, not around a step in the process. A module named after a stage of the pipeline splits along the flow of data, so a change to a decision cuts across every module at once. A module named after the decision absorbs the change entirely.
Example
Autobot Robotics's vision system started as a set of classes in a shared project: a frame grabber, a blob finder, a lens distortion model, and a coordinate transform. Everything was public, because nothing had a reason not to be.
Then the office-side reporting tool wanted a picture of the part next to each
inspection result, and the quickest way to get one was to call
FrameGrabber.grab() directly. Some months later the vision team
changed the grabber so that a returned frame stayed valid only until the next
capture, which was correct for the vision pipeline and fine for every caller they
knew about. Reporting was not a caller they knew about. It started writing torn
images into customer reports, and nobody connected the two changes for a fortnight
because there was no reason to look at reporting when the vision code changed.
The team marked the grabber, the blob finder and the lens model internal, and
published locate, calibrate and a new
snapshot that returned a frame the caller owned. Reporting's genuine
need became one documented entry point instead of an accident. The important
result is not that the bug was fixed; it is that the vision team can now change
buffer ownership again and the compiler will tell them who is affected.
namespace Autobot.Sensing
{
// The entire public surface: one class, three operations.
public static class Vision
{
public static Pose locate(Part p) { /* some code */ }
public static void calibrate() { /* some code */ }
public static Frame snapshot() { /* some code */ }
}
// internal: nameable inside this assembly, invisible outside it.
internal class FrameGrabber
{
public Frame grab() { /* some code */ }
}
internal class BlobFinder { /* some code */ }
internal class LensModel { /* some code */ }
}
// A different assembly in the same solution.
namespace Autobot.Reporting
{
class InspectionReport
{
public void build()
{
Frame f = Autobot.Sensing.Vision.snapshot();
// FrameGrabber g = new FrameGrabber();
// does not compile: FrameGrabber is inaccessible
// due to its protection level
}
}
}
internal, repeated three times,
and the fact that the public surface is three operations rather than four
classes. Nothing else here is doing any work.Seen in the Wild
Java 9 added a real module system: module-info.java declares
requires and exports, and a package that is not exported
cannot be read by another module even by reflection, which is what finally made
sun.misc.Unsafe hard to reach. .NET has assemblies plus the
internal modifier, with InternalsVisibleTo as the
deliberate escape hatch for test projects. ES modules make it structural: a
binding without export is not merely discouraged, it does not exist
outside the file. Python is the weakest of the four, relying on
__all__ and a leading underscore that nothing enforces. The idea
itself is older than all of them: Parnas's 1972 paper "On the Criteria To Be Used
in Decomposing Systems into Modules" is still the clearest statement of why the
boundary should follow a design decision rather than a processing step.
Comparisons
- Facade is the same instinct at object scale. A Facade simplifies access and leaves the subsystem reachable; a Module makes the inside unreachable, so it can enforce what a Facade can only suggest.
- Extensibility is what you get when the modules are designed to be added to rather than only to be hidden behind.
- Aggregate draws the same kind of boundary around data rather than around code, with one entry point and one consistency rule.
- Singleton is what a module turns into when you give it mutable state. A module of pure functions is fine to reach globally; a module holding a connection is a global variable with a namespace on it.
- Dependency Injection is how a module gets its collaborators without naming them, which is what keeps the dependency graph between modules acyclic.
Why It Exists
My take
Every symbol that can be reached is a symbol that will be reached. That is the entire pressure behind this pattern and it is not a statement about discipline, it is a statement about time. In a codebase that lives ten years, the question "who calls this function" has an answer you cannot know: it is whoever found it in autocomplete on a Tuesday in a year you have forgotten. The vision bug in the example above is the normal case, not an unusual one. Nobody did anything wrong; the grabber was public, so it was part of the contract, and nobody had ever decided that.
I learned this the hard way in C, where the language gives you nothing. The
token-ring stack I wrote had modules, but they were an act of will: a public
header with the declarations you were meant to use, a private header for the
rest, static on every file-scope function that was not exported, and
a naming prefix on everything that was, because there were no namespaces and two
modules with a function called reset() is a link error. That was the
whole enforcement mechanism, and it was weak. Anything not marked
static was reachable from anywhere in the image by simply declaring
it yourself, and people did, because the alternative was waiting for someone to
add an accessor. The linker was the only thing standing between a layered design
and a plate of spaghetti, and the linker did not care.
So my honest position is that Module is the pattern you should almost never
implement, because you should be using the one your language already has. If you
find yourself writing a class called SomethingModule whose job is to
gather up other classes, stop: you have reinvented Facade, or Singleton, and you
are getting none of the enforcement that made the pattern worth naming. The
useful skill here is not construction, it is knowing where to draw the line and
then having the nerve to keep things behind it when someone asks nicely.
Criticisms
Module boundaries get drawn in the wrong place more often than not, and the
wrong place has a recognizable smell: a module named after a layer or a noun
rather than a decision. utils, helpers,
common and core are modules with no design decision
inside them, so nothing about them can be changed without affecting everybody, and
because everything depends on them they sit at the bottom of the dependency graph
where nothing can be removed. Cohesion is not "these things are all small".
Narrow surfaces also only ever widen. Every export exists because somebody had a
real need, and no one has ever been rewarded for deleting one. The first version
publishes three operations, then a legitimate case appears and it is four, and the
count only goes in one direction because removing an export breaks a caller you
cannot see. Java's sun.misc.Unsafe is the extreme version: never
intended as public API, depended on by so much of the ecosystem that removing it
took a decade and a whole new module system.
Finally, the enforcement is usually softer than it looks. internal
plus InternalsVisibleTo is a boundary with a documented hole in it,
Python's underscore is pure convention, and reflection defeats most of it in most
languages. And there is a granularity trap at the other end: split a system into
sixty modules and the dependency graph between them becomes the thing you cannot
understand, which is the problem you were trying to solve.