process.global
for Universals: Only for xLog
, getConfig
, etc.switch
statements in factories.interface
or JSDoc @interface
.The Self-Configuring Polymorphic Architecture pattern (v2) creates robust, enterprise-grade applications by composing self-configuring components that present formal, uniform interfaces. This version refines the original pattern with clearer guidelines for dependency management, scalability, and safety.
To the maximum extent possible, all information, code, config, and resources needed to understand, debug, and extend a module should be present in that module.
This remains the cornerstone of the architecture. Its goal is to enable a developer to open a single file and gain a complete understanding of that module's behavior and dependencies.
Never use common words (value, data, library, etc.) for names except for minimal local variables used within a few lines.
This principle is critical for maintaining clarity in a system composed of many small modules. Names must be descriptive, uniquely searchable, and specific.
While global state is generally an anti-pattern, a pragmatic exception can be made for a small, immutable set of truly universal utilities. Functions for logging and configuration access are candidates for this approach.
By placing these on process.global
, they become available throughout the codebase without the need for repetitive dependency injection.
Crucial Safety Guideline: If using this pattern, the global object must be frozen at application startup to prevent accidental modification.
// main.js - At startup
const { xLog, getConfig, commandLineParameters } = require('./lib/setup-env');
// Define the global context
process.global = { xLog, getConfig, commandLineParameters };
// Freeze it to prevent any modification
Object.freeze(process.global);
// Now any module can safely use it
const { xLog, getConfig } = process.global;
Modules should instantiate their own stateless dependencies locally. However, any dependency that is stateful, expensive, or manages a shared resource pool must be injected by the orchestrator. This is the key distinction between a shared resource (like a DB connection) and a global utility (like a logger).
Components with varying implementations must adhere to a formally defined interface. This provides a strong contract for developers and allows the orchestrator to use components interchangeably.
In TypeScript, this should be enforced with interface
:
// /interfaces/DataExtractor.ts
export interface DataExtractor {
extract(source: string): Array<any>;
getSourceType(): string;
describe(): string;
}
In JavaScript, use JSDoc for static analysis. This provides type-checking hints to editors and documents the expected contract.
/**
* @interface DataExtractor
* @property {function(string): Array<Object>} extract - Extracts data from a source string.
* @property {function(): string} getSourceType - Returns the type of the source (e.g., 'json', 'csv').
* @property {function(): string} describe - Provides a human-readable description of the extractor.
*/
To avoid violating the Open/Closed Principle, use a Registry Pattern instead of a switch
statement to select a component's implementation. This allows new implementations to be added without modifying the factory logic.
A typical directory structure for this pattern would be:
/fact-extractor
├── index.js # The factory/registry
└── /extractors
├── json-extractor.js
└── csv-extractor.js
The factory (index.js
) dynamically selects the correct implementation:
// /fact-extractor/index.js - The Factory using a Registry
const implementations = {
json: require('./extractors/json-extractor'),
csv: require('./extractors/csv-extractor')
// To extend, add a new file and a new line here. No logic change needed.
};
const moduleFunction = (injectedDependencies) => {
const { getConfig } = process.global;
const localConfig = getConfig('fact-extractor');
const extractionMode = localConfig.defaultExtractionMode;
if (!implementations[extractionMode]) {
throw new Error(`Unknown extraction mode: ${extractionMode}`);
}
// Return the configured implementation, passing through any injected resources
return implementations[extractionMode](injectedDependencies);
};
module.exports = moduleFunction;
A dependency must be externalized and injected if it manages:
process.global
context for convenience.// ✅ CORRECT: The orchestrator manages the shared resource.
// main.js
const db = require('better-sqlite3')(process.env.DB_PATH); // Create once
const reporter = require('./reporter')({ db }); // Inject resource
// reporter.js
const moduleFunction = ({ db }) => { // Receive shared DB handle
const { getConfig, xLog } = process.global; // Use global utilities
const localConfig = getConfig('reporter');
const generateReport = (params) => {
xLog.info(`Generating report with query: ${localConfig.query}`);
const data = db.prepare(localConfig.query).all();
// ...
};
return { generateReport };
};
process.global
: The global context is exclusively for a small number of frozen, universal utilities. All other dependencies must be instantiated locally or injected.switch
statement factory: This violates the Open/Closed principle. Use the Registry Pattern.getConfig
utility.The Self-Configuring Polymorphic Architecture (v2) provides a robust framework for building clean, maintainable, and scalable applications by enforcing:
The key insight is refined: A developer should be able to open a single file and understand everything about how that module works, knowing that its only hidden dependencies are a small, predictable set of frozen global utilities.