358 lines
9.3 KiB
Markdown
358 lines
9.3 KiB
Markdown
# Module Pattern
|
|
|
|
> Encapsulate related functionality, data, and behavior into a single unit with a well-defined public API, hiding internal implementation details.
|
|
|
|
## Problem
|
|
|
|
You have a set of related utility functions and state (e.g., a string utility library with internal helpers). Without encapsulation, all functions and variables pollute the global namespace, internal helpers are exposed, and there is no clear boundary between public and private.
|
|
|
|
## Solution
|
|
|
|
Group related functionality into a module that exposes only a public API. Internal state and helper functions are hidden. In Python this is a module with `__all__`; in Go it's a package with unexported identifiers; in JavaScript it's a closure/IIFE or ES module with selective exports.
|
|
|
|
```
|
|
┌──────────────────────────────┐
|
|
│ StringUtils Module │
|
|
│ │
|
|
│ Public API: │
|
|
│ + capitalize(s) │
|
|
│ + slug(s) │
|
|
│ + word_count(s) │
|
|
│ + stats() │
|
|
│ │
|
|
│ Internal (hidden): │
|
|
│ - _call_count │
|
|
│ - _track_call() │
|
|
│ - _normalize_whitespace() │
|
|
└──────────────────────────────┘
|
|
```
|
|
|
|
## When to Use
|
|
|
|
- You want to group related functions and state with a clear public/private boundary.
|
|
- You need to avoid polluting the global namespace.
|
|
- You want to enforce encapsulation without full OOP class hierarchies.
|
|
- You are building a utility library or service layer.
|
|
|
|
## When to Avoid
|
|
|
|
- The functionality is a single function — just export the function directly.
|
|
- You need multiple instances with separate state — use a class or factory instead.
|
|
- The language has first-class module support and you'd just be wrapping it redundantly.
|
|
|
|
## Pseudocode
|
|
|
|
```
|
|
module StringUtils:
|
|
// Private state
|
|
_call_count = 0
|
|
|
|
// Private helper
|
|
function _track_call():
|
|
_call_count += 1
|
|
|
|
function _normalize_whitespace(s):
|
|
return collapse multiple spaces in s to single spaces, trimmed
|
|
|
|
// Public API
|
|
function capitalize(s):
|
|
_track_call()
|
|
return first letter uppercase + rest of s
|
|
|
|
function slug(s):
|
|
_track_call()
|
|
return _normalize_whitespace(s).lower().replace(" ", "-")
|
|
|
|
function word_count(s):
|
|
_track_call()
|
|
return number of words in _normalize_whitespace(s)
|
|
|
|
function stats():
|
|
return { calls: _call_count }
|
|
|
|
// Usage
|
|
print StringUtils.capitalize("hello world")
|
|
print StringUtils.slug("Hello Big World")
|
|
print StringUtils.word_count("one two three")
|
|
print StringUtils.stats()
|
|
```
|
|
|
|
## Python
|
|
|
|
```python
|
|
"""
|
|
string_utils module — demonstrates the Module pattern.
|
|
|
|
Only symbols listed in __all__ are part of the public API.
|
|
"""
|
|
|
|
# --- Private state ---
|
|
_call_count = 0
|
|
|
|
|
|
# --- Private helpers ---
|
|
def _track_call() -> None:
|
|
global _call_count
|
|
_call_count += 1
|
|
|
|
|
|
def _normalize_whitespace(s: str) -> str:
|
|
"""Collapse multiple spaces and strip."""
|
|
return " ".join(s.split())
|
|
|
|
|
|
# --- Public API ---
|
|
|
|
def capitalize(s: str) -> str:
|
|
"""Capitalize the first letter of the string."""
|
|
_track_call()
|
|
if not s:
|
|
return s
|
|
return s[0].upper() + s[1:]
|
|
|
|
|
|
def slug(s: str) -> str:
|
|
"""Convert to a URL-friendly slug."""
|
|
_track_call()
|
|
return _normalize_whitespace(s).lower().replace(" ", "-")
|
|
|
|
|
|
def word_count(s: str) -> int:
|
|
"""Count words in a string."""
|
|
_track_call()
|
|
normalized = _normalize_whitespace(s)
|
|
return len(normalized.split()) if normalized else 0
|
|
|
|
|
|
def stats() -> dict:
|
|
"""Return usage statistics."""
|
|
return {"total_calls": _call_count}
|
|
|
|
|
|
__all__ = ["capitalize", "slug", "word_count", "stats"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=== Module Pattern: StringUtils ===")
|
|
print()
|
|
|
|
result1 = capitalize("hello world")
|
|
print(f"capitalize('hello world') => '{result1}'")
|
|
|
|
result2 = slug("Hello Big World")
|
|
print(f"slug('Hello Big World') => '{result2}'")
|
|
|
|
result3 = word_count(" one two three ")
|
|
print(f"word_count(' one two three ') => {result3}")
|
|
|
|
result4 = slug("Another Test String")
|
|
print(f"slug('Another Test String') => '{result4}'")
|
|
|
|
print(f"\nstats() => {stats()}")
|
|
|
|
# Show that private members exist but are conventionally hidden
|
|
print(f"\n--- Internal state (not part of public API) ---")
|
|
print(f"_call_count = {_call_count}")
|
|
print(f"_normalize_whitespace(' a b ') = '{_normalize_whitespace(' a b ')}'")
|
|
```
|
|
|
|
**Output:**
|
|
```
|
|
=== Module Pattern: StringUtils ===
|
|
|
|
capitalize('hello world') => 'Hello world'
|
|
slug('Hello Big World') => 'hello-big-world'
|
|
word_count(' one two three ') => 3
|
|
slug('Another Test String') => 'another-test-string'
|
|
|
|
stats() => {'total_calls': 4}
|
|
|
|
--- Internal state (not part of public API) ---
|
|
_call_count = 4
|
|
_normalize_whitespace(' a b ') = 'a b'
|
|
```
|
|
|
|
## Go
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// ============================================================
|
|
// stringutils "module" — unexported identifiers are private.
|
|
// In a real project this would be its own package.
|
|
// ============================================================
|
|
|
|
// private state
|
|
var callCount int
|
|
|
|
// private helper
|
|
func trackCall() {
|
|
callCount++
|
|
}
|
|
|
|
func normalizeWhitespace(s string) string {
|
|
return strings.Join(strings.Fields(s), " ")
|
|
}
|
|
|
|
// --- Public API (exported) ---
|
|
|
|
func Capitalize(s string) string {
|
|
trackCall()
|
|
if len(s) == 0 {
|
|
return s
|
|
}
|
|
runes := []rune(s)
|
|
runes[0] = unicode.ToUpper(runes[0])
|
|
return string(runes)
|
|
}
|
|
|
|
func Slug(s string) string {
|
|
trackCall()
|
|
return strings.ReplaceAll(strings.ToLower(normalizeWhitespace(s)), " ", "-")
|
|
}
|
|
|
|
func WordCount(s string) int {
|
|
trackCall()
|
|
normalized := normalizeWhitespace(s)
|
|
if normalized == "" {
|
|
return 0
|
|
}
|
|
return len(strings.Fields(normalized))
|
|
}
|
|
|
|
type Stats struct {
|
|
TotalCalls int
|
|
}
|
|
|
|
func GetStats() Stats {
|
|
return Stats{TotalCalls: callCount}
|
|
}
|
|
|
|
// ============================================================
|
|
|
|
func main() {
|
|
fmt.Println("=== Module Pattern: StringUtils ===")
|
|
fmt.Println()
|
|
|
|
r1 := Capitalize("hello world")
|
|
fmt.Printf("Capitalize(\"hello world\") => '%s'\n", r1)
|
|
|
|
r2 := Slug("Hello Big World")
|
|
fmt.Printf("Slug(\"Hello Big World\") => '%s'\n", r2)
|
|
|
|
r3 := WordCount(" one two three ")
|
|
fmt.Printf("WordCount(\" one two three \") => %d\n", r3)
|
|
|
|
r4 := Slug("Another Test String")
|
|
fmt.Printf("Slug(\"Another Test String\") => '%s'\n", r4)
|
|
|
|
fmt.Printf("\nGetStats() => %+v\n", GetStats())
|
|
}
|
|
```
|
|
|
|
**Output:**
|
|
```
|
|
=== Module Pattern: StringUtils ===
|
|
|
|
Capitalize("hello world") => 'Hello world'
|
|
Slug("Hello Big World") => 'hello-big-world'
|
|
WordCount(" one two three ") => 3
|
|
Slug("Another Test String") => 'another-test-string'
|
|
|
|
GetStats() => {TotalCalls:4}
|
|
```
|
|
|
|
## JavaScript
|
|
|
|
```javascript
|
|
// Module pattern using an IIFE to create a closure with private scope.
|
|
|
|
const StringUtils = (() => {
|
|
// --- Private state ---
|
|
let callCount = 0;
|
|
|
|
// --- Private helpers ---
|
|
function trackCall() {
|
|
callCount++;
|
|
}
|
|
|
|
function normalizeWhitespace(s) {
|
|
return s.trim().replace(/\s+/g, " ");
|
|
}
|
|
|
|
// --- Public API ---
|
|
return {
|
|
capitalize(s) {
|
|
trackCall();
|
|
if (!s) return s;
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
},
|
|
|
|
slug(s) {
|
|
trackCall();
|
|
return normalizeWhitespace(s).toLowerCase().replace(/ /g, "-");
|
|
},
|
|
|
|
wordCount(s) {
|
|
trackCall();
|
|
const normalized = normalizeWhitespace(s);
|
|
return normalized === "" ? 0 : normalized.split(" ").length;
|
|
},
|
|
|
|
stats() {
|
|
return { totalCalls: callCount };
|
|
},
|
|
};
|
|
})();
|
|
|
|
// --- Main ---
|
|
console.log("=== Module Pattern: StringUtils ===\n");
|
|
|
|
const r1 = StringUtils.capitalize("hello world");
|
|
console.log(`capitalize('hello world') => '${r1}'`);
|
|
|
|
const r2 = StringUtils.slug("Hello Big World");
|
|
console.log(`slug('Hello Big World') => '${r2}'`);
|
|
|
|
const r3 = StringUtils.wordCount(" one two three ");
|
|
console.log(`wordCount(' one two three ') => ${r3}`);
|
|
|
|
const r4 = StringUtils.slug("Another Test String");
|
|
console.log(`slug('Another Test String') => '${r4}'`);
|
|
|
|
console.log(`\nstats() => ${JSON.stringify(StringUtils.stats())}`);
|
|
|
|
// Demonstrate that internals are truly hidden
|
|
console.log(`\n--- Attempting to access private state ---`);
|
|
console.log(`StringUtils.callCount => ${StringUtils.callCount}`); // undefined
|
|
console.log(`StringUtils.trackCall => ${StringUtils.trackCall}`); // undefined
|
|
```
|
|
|
|
**Output:**
|
|
```
|
|
=== Module Pattern: StringUtils ===
|
|
|
|
capitalize('hello world') => 'Hello world'
|
|
slug('Hello Big World') => 'hello-big-world'
|
|
wordCount(' one two three ') => 3
|
|
slug('Another Test String') => 'another-test-string'
|
|
|
|
stats() => {"totalCalls":4}
|
|
|
|
--- Attempting to access private state ---
|
|
StringUtils.callCount => undefined
|
|
StringUtils.trackCall => undefined
|
|
```
|
|
|
|
## Related Patterns
|
|
|
|
- **Facade** — a facade provides a simplified interface over a subsystem; a module encapsulates a subsystem with explicit public/private boundaries.
|
|
- **Singleton** — a module is effectively a singleton in most module systems (loaded and cached once).
|
|
- **Namespace** — modules provide namespacing as a side effect, preventing name collisions in the global scope.
|