Skip to content

Instantly share code, notes, and snippets.

@psenger
Last active July 23, 2025 07:57
Show Gist options
  • Select an option

  • Save psenger/cc3d0619709f9004ac38fe0d62a3eb82 to your computer and use it in GitHub Desktop.

Select an option

Save psenger/cc3d0619709f9004ac38fe0d62a3eb82 to your computer and use it in GitHub Desktop.
[Design Pattern: NodeJS Dependency Injection] #NodeJS #JavaScript #Module #DesignPattern

Node.js Dependency Injection Factory Patterns 🏭

Because sometimes your modules need to be more flexible than a yoga instructor! 🧘‍♂️

What's This All About?

Ever found yourself in "require hell" where your Node.js modules are so tightly coupled they might as well be married? Or maybe you've tried to test a function that depends on 17 different external services and your test file looks like a spaghetti monster?

This GIST provides two battle-tested patterns for implementing Dependency Injection with the Factory Pattern in Node.js. No fancy frameworks required - just good ol' JavaScript closures doing what they do best!

The Problem It Solves 🎯

  • Tight Coupling: When your modules are hardwired to specific dependencies
  • Jest Mocking Insanity: Ever spent 3 hours trying to mock a module that imports another module that imports another module? Yeah, we've all been there
  • Testing Nightmares: When you can't test functions without spinning up a database, API server, and sacrificing a rubber duck
  • Inflexibility: When swapping implementations requires rewriting half your codebase
  • Private Function Testing: When you need to test internal logic without exposing it to the world

The Two Patterns

Why Two Examples?

Both patterns solve the same core problems but offer different interfaces for different use cases. They both leverage JavaScript closures to create truly private functions and encapsulated dependencies - something that's surprisingly tricky in Node.js!

1. Object Return Variant (factory-object-return.js)

Returns an object with multiple methods - perfect for utility libraries or service modules.

// What you get:
{
  exportedFunctionOne: Function,
  exportedFunctionTwo: Function,
  _privateFunctionTwo: Function // only in test mode!
}

When to use: When you need a module that exposes multiple related functions as an API (think fs.readFile, fs.writeFile, etc.)

2. Function Return Variant (factory-function-return.js)

Returns a callable function with methods attached - ideal for single-purpose utilities.

// What you get:
exportedFunctionOne() // It's callable!
exportedFunctionOne._privateFunctionTwo // Also has methods in test mode

When to use: When you have one primary function but might need helper methods (think express() which is callable but also has express.static)

The Closure & Private Function Magic 🪄

Both patterns use the same clever tricks:

  1. True Private Functions: Unlike class methods or object properties, functions defined inside the factory are truly private - they literally don't exist outside the closure scope unless explicitly exposed.

  2. Two Levels of Privacy:

    • Module-level private: _privateFunctionOne - Shared across all instances, no access to instance dependencies
    • Instance-level private: _privateFunctionTwo - Has access to injected dependencies through closure
  3. Test-Mode Exposure: Private functions are conditionally exposed only when NODE_ENV='test', keeping your production API clean while making testing possible.

// This is how the magic works:
function createFactory(dependencies) {
  // This function is trapped in the closure!
  function _privateFunctionTwo() {
    // Can access 'dependencies' here thanks to closure
    // This is TRULY private unless we explicitly expose it
  }
  
  // Only expose in test mode
  return {
    publicMethod,
    ...(process.env.NODE_ENV === 'test' ? { _privateFunctionTwo } : {})
  };
}

Without this pattern, you'd have to either:

  • Make everything public (bye bye encapsulation!)
  • Use underscores and hope nobody calls _private methods (spoiler: they will)
  • Try to test private methods through public interfaces only (good luck with complex logic!)

How It Works 🛠️

Both patterns follow the same core principles:

  1. Factory Function: Creates instances with custom dependencies
  2. Dependency Validation: Ensures required dependencies are provided
  3. Partial Overrides: Mix and match custom and default dependencies
  4. Test Mode Magic: Exposes private functions only when NODE_ENV='test'
  5. Closure Power: Each instance maintains its own dependency scope

Usage Examples

Production Usage

// Using the default instance (most common)
const utils = require('./factory-object-return');
utils.exportedFunctionOne(); // Uses default dependencies
utils.exportedFunctionTwo(); // Ready to rock!

// Or for the function variant
const doTheThing = require('./factory-function-return');
doTheThing(); // Just call it!

Custom Dependencies

// Need custom behavior? Use the factory!
const { factory } = require('./factory-object-return');

// Create your custom dependencies
const customDatabase = {
  A: new PostgresClient(),
  B: new RedisCache(),
  C: new Logger('production'),
  D: new MetricsCollector()
};

const customServices = {
  E: new EmailService('SendGrid'),
  F: new PaymentProcessor('Stripe')
};

// Create your custom instance
const customUtils = factory(customDatabase, customServices);
customUtils.exportedFunctionOne(); // Now using YOUR dependencies!

Testing Usage

// In your test file
process.env.NODE_ENV = 'test';
const { factory } = require('./factory-object-return');

describe('My Awesome Module', () => {
  it('should test private functions', () => {
    // Create test instance with mocked dependencies
    const testInstance = factory(
      {
        A: mockDatabase,
        B: mockCache,
        C: mockLogger,
        D: mockMetrics
      },
      {
        E: mockEmailService,
        F: mockPaymentProcessor
      }
    );
    
    // Now you can test private functions! 
    expect(testInstance._privateFunctionTwo()).toBe('awesome');
    
    // Module-level private functions are also available
    const module = require('./factory-object-return');
    expect(module._privateFunctionOne()).toBe('also awesome');
  });
});

The Jest Mocking Nightmare vs. The DI Dream

❌ The Jest Mocking Horror Show

// someModule.js - tightly coupled nightmare
const database = require('./database');
const emailService = require('./emailService');
const logger = require('./logger');

function processUser(userId) {
  const user = database.getUser(userId);
  logger.log('Processing user:', userId);
  if (user.isActive) {
    emailService.sendWelcome(user.email);
  }
  return user;
}

// someModule.test.js - Welcome to mock hell! 👹
jest.mock('./database');
jest.mock('./emailService');
jest.mock('./logger');

const database = require('./database');
const emailService = require('./emailService');
const logger = require('./logger');
const { processUser } = require('./someModule');

describe('processUser with Jest mocks', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    // Oh, you wanted to reset the module? Here's more boilerplate!
    jest.resetModules();
  });

  it('should send email to active users', () => {
    // Setting up mocks is like solving a Rubik's cube blindfolded
    const mockUser = { id: 1, isActive: true, email: 'test@example.com' };
    database.getUser.mockReturnValue(mockUser);
    
    processUser(1);
    
    // Did it work? Who knows! Check your module import order!
    expect(emailService.sendWelcome).toHaveBeenCalledWith('test@example.com');
    
    // Oh, you changed the implementation? Time to rewrite ALL your mocks!
  });
});

✅ The DI Pattern Dream

// processUserModule.js - Using our DI pattern
const { factory } = require('./factory-object-return');

function createProcessUser(deps) {
  const { database, emailService, logger } = deps;
  
  return {
    processUser(userId) {
      const user = database.getUser(userId);
      logger.log('Processing user:', userId);
      if (user.isActive) {
        emailService.sendWelcome(user.email);
      }
      return user;
    }
  };
}

// Export default instance and factory
module.exports = createProcessUser({
  database: require('./database'),
  emailService: require('./emailService'),
  logger: require('./logger')
});
module.exports.factory = createProcessUser;

// processUser.test.js - Testing bliss! 🌈
const { factory } = require('./processUserModule');

describe('processUser with DI', () => {
  it('should send email to active users', () => {
    // Create simple mock objects - no Jest magic needed!
    const mockUser = { id: 1, isActive: true, email: 'test@example.com' };
    const mockDeps = {
      database: {
        getUser: jest.fn().mockReturnValue(mockUser)
      },
      emailService: {
        sendWelcome: jest.fn()
      },
      logger: {
        log: jest.fn()
      }
    };
    
    // Create instance with mocks - so clean!
    const { processUser } = factory(mockDeps);
    
    processUser(1);
    
    // Assertions are straightforward and predictable
    expect(mockDeps.emailService.sendWelcome).toHaveBeenCalledWith('test@example.com');
    expect(mockDeps.logger.log).toHaveBeenCalledWith('Processing user:', 1);
  });
  
  it('should not send email to inactive users', () => {
    // Different test? Just create different mocks!
    const mockUser = { id: 2, isActive: false, email: 'inactive@example.com' };
    const mockDeps = {
      database: {
        getUser: jest.fn().mockReturnValue(mockUser)
      },
      emailService: {
        sendWelcome: jest.fn()
      },
      logger: {
        log: jest.fn()
      }
    };
    
    const { processUser } = factory(mockDeps);
    processUser(2);
    
    expect(mockDeps.emailService.sendWelcome).not.toHaveBeenCalled();
  });
});

Why This Is So Much Better 🎉

  1. No Module Mocking: No more jest.mock() at the top of every test file
  2. No Import Order Issues: Jest mocks are hoisted and can cause bizarre issues
  3. Explicit Dependencies: You see exactly what each function needs
  4. Easy Test Isolation: Each test can have completely different mocks
  5. No Global State: No need for beforeEach with jest.clearAllMocks()
  6. Refactoring Freedom: Change your dependencies without breaking tests

Key Features 🌟

  • Zero Dependencies: No DI frameworks needed
  • Type Safe-ish: Validates required dependencies at runtime
  • Test Friendly: Private functions accessible in test mode
  • Flexible: Use defaults or inject custom dependencies
  • Clean API: Production code stays clean, test helpers only appear in tests

When to Use Which Pattern?

  • Object Return: When you need multiple equally important methods
  • Function Return: When you have one primary function with helper methods

Pro Tips 💡

  1. Always provide all required properties (A, B, C, D) together - it's all or nothing!
  2. Optional properties (E, F) can be overridden individually
  3. Each factory instance maintains its own closure scope - no shared state surprises
  4. The default instance is created at module load time for convenience

Code Standards

This code follows some opinionated standards:

  • Single quotes only (because we're not monsters)
  • No semicolons (they're so 2010)
  • Spaces around operators: a + b not a+b
  • Spaces inside object braces: { key: value } not {key: value}

TL;DR

These patterns let you write testable, flexible Node.js modules without selling your soul to a DI framework. Pick the pattern that fits your use case, inject your dependencies, and test with confidence!

Happy coding! 🚀

/**
* Factory Pattern with Dependency Injection Template - Object Return Variant
*
* This module implements a Factory Pattern with Dependency Injection that returns an OBJECT
* containing multiple public methods. This pattern provides a traditional API surface with
* multiple equally-important methods exposed as object properties.
*
* Design Patterns and Intent:
* The code implements a Factory Pattern with Dependency Injection to create utility API objects
* with customizable dependencies and testable private methods. Key distinguishing aspects:
*
* - Factory Return Type: Returns an OBJECT with multiple public methods (exportedFunctionOne, exportedFunctionTwo)
* - Multiple Public Methods: Provides multiple co-equal functions as object properties
* - Conditional Property Spread: Uses spread operator to conditionally include private methods in test mode
* - Use Case: Ideal for creating traditional APIs with multiple related methods
*
* Architecture Notes:
* - The factory returns an object literal with public methods as properties
* - Private function _privateFunctionTwo is conditionally spread into the returned object
* - Module-level function _privateFunctionOne is exposed on module.exports
* - Pattern suitable for creating cohesive APIs with multiple related functions
*
* Testing Strategy:
* - _privateFunctionOne: Module-level, accessible via module.exports._privateFunctionOne
* - _privateFunctionTwo: Instance-level, included in returned object via conditional spread
*
* Code Standards:
* - Validates that `dependencyInjectedOne` provides required properties (A, B, C, D)
* - Uses spread operator for safe partial overrides of `dependencyInjectedTwo`
* - Exposes private functions for testing only in `NODE_ENV='test'`
* - Uses closures for instance-specific private functions and dependency encapsulation
* - Single quotes and not double quotes
* - No semicolon
* - Spaces around operators like =, +, -, etc. Example: a + b instead of a+b
* - No spaces inside parentheses Example: func(arg) instead of func( arg )
* - Spaces inside the curly braces of an object Example: { key: value } instead of {key: value}
*
* @module factory-object-return
* @requires ./somethingOne - Default implementation for first dependency set
* @requires ./somethingTwo - Default implementation for second dependency set
*/
const DefaultSomethingOne = require('./somethingOne')
const DefaultSomethingTwo = require('./somethingTwo')
/**
* Private utility function for internal use and testing, defined at the module level.
*
* This function is shared across all factory instances and does not have access to instance-specific
* dependencies or context. It is exposed for testing only when `NODE_ENV='test'`.
*
* @private
* @returns {void}
*/
function _privateFunctionOne() {}
/**
* Factory function implementing the Module Factory Pattern with Dependency Injection (DI) to create
* utility API instances with customizable dependencies and testable private methods.
*
* This template encapsulates object creation (Module Factory Pattern) and supports Dependency Injection
* for flexible dependency management. It returns an object with public utility functions, and in test
* environments (NODE_ENV='test'), private functions for unit testing. Each instance uses closures to
* encapsulate dependencies and maintain isolation. The template enforces dependency validation for
* robustness and uses conditional exports to keep the production API clean.
*
* @param {Object} [dependencyInjectedOne] - Optional first dependency object containing required utilities A, B, C, D.
* @param {any} [dependencyInjectedOne.A] - Utility A, required for functionality.
* @param {any} [dependencyInjectedOne.B] - Utility B, required for functionality.
* @param {any} [dependencyInjectedOne.C] - Utility C, required for functionality.
* @param {any} [dependencyInjectedOne.D] - Utility D, required for functionality.
* @param {Object} [dependencyInjectedTwo] - Optional second dependency object containing utilities E, F.
* @param {any} [dependencyInjectedTwo.E] - Utility E, optional with default from `somethingTwo`.
* @param {any} [dependencyInjectedTwo.F] - Utility F, optional with default from `somethingTwo`.
* @returns {Object} An object containing public utility functions and, in test environments, a private function.
* @returns {Function} .exportedFunctionOne - Public utility function for action one, using injected dependencies.
* @returns {Function} .exportedFunctionTwo - Public utility function for action two, using injected dependencies.
* @returns {Function} [_privateFunctionTwo] - Private utility function for testing, instance-specific (only in NODE_ENV='test').
* @throws {Error} If `dependencyInjectedOne` is provided but lacks required properties A, B, C, or D.
* @example
* // Using default instance
* const utils = require('./factory');
* utils.exportedFunctionOne(); // Calls action one with default dependencies
* utils.exportedFunctionTwo(); // Calls action two with default dependencies
* @example
* // Creating custom instance
* const { factory } = require('./factory');
* const customInstance = factory(
* { A: 'customA', B: 'customB', C: 'customC', D: 'customD' },
* { E: 'customE' }
* );
* customInstance.exportedFunctionOne(); // Uses custom dependencies
*/
function createFactory( dependencyInjectedOne, dependencyInjectedTwo ) {
/**
* Destructure all-or-nothing properties from the first dependency, with defaults from './somethingOne'.
* Validates required properties to ensure robustness.
* @private
*/
const depOne = dependencyInjectedOne || DefaultSomethingOne
if (!depOne.A || !depOne.B || !depOne.C || !depOne.D) {
throw new Error('First dependency must provide A, B, C, and D')
}
const { A, B, C, D, } = depOne
/**
* Destructure partial properties from the second dependency, with defaults from './somethingTwo'.
* Uses spread operator for safe merging of optional properties.
* @private
*/
const { E, F, } = { ...DefaultSomethingTwo, ...dependencyInjectedTwo }
/**
* Private utility function for internal use and testing, scoped to this factory instance.
* Uses closure to access instance-specific dependencies (A, B, C, D, E, F).
* Exposed for testing only when `NODE_ENV='test'`.
* @private
* @returns {void}
*/
function _privateFunctionTwo() {}
/**
* Public utility function for performing action one.
* Intended to use injected dependencies (A, B, C, D, E, F) for functionality.
* @returns {void}
*/
function exportedFunctionOne() {}
/**
* Public utility function for performing action two.
* Intended to use injected dependencies (A, B, C, D, E, F) for functionality.
* @returns {void}
*/
function exportedFunctionTwo() {}
/**
* Returns the public API with conditional exposure of private function for testing.
* Breaks traditional OOP encapsulation for testing purposes, only in test environments.
*/
return {
exportedFunctionOne,
exportedFunctionTwo,
...( process.env.NODE_ENV === 'test' ? { _privateFunctionTwo }: {} )
}
}
// Create the default instance with default dependencies
const defaultInstance = createFactory()
/**
* Default export: The utility API object from the default factory instance, created with default dependencies.
* Provides public utility functions for use with default or injected dependencies. In test environments
* (NODE_ENV='test'), includes a private function for testing. Implements the Module Factory Pattern
* with Dependency Injection.
*
* @type {Object}
* @property {Function} exportedFunctionOne - Public utility function for action one.
* @property {Function} exportedFunctionTwo - Public utility function for action two.
* @property {Function} [_privateFunctionTwo] - Private utility function for testing, instance-specific (only in NODE_ENV='test').
* @example
* const utils = require('./factory');
* utils.exportedFunctionOne(); // Calls action one with default dependencies
* utils.exportedFunctionTwo(); // Calls action two with default dependencies
*/
module.exports = defaultInstance
/**
* Named export: The factory function for creating new utility API instances with custom dependencies.
* Implements the Module Factory Pattern with Dependency Injection.
*
* @type {Function}
* @property {Function} [_privateFunctionOne] - Private utility function for testing, module-level (only in NODE_ENV='test').
* @example
* const { factory } = require('./factory');
* const customInstance = factory(
* { A: 'customA', B: 'customB', C: 'customC', D: 'customD' },
* { E: 'customE' }
* );
* customInstance.exportedFunctionOne(); // Uses custom dependencies
*/
module.exports._privateFunctionOne = process.env.NODE_ENV === 'test' ? _privateFunctionOne : undefined
/**
* Named export: The factory function for creating new utility API instances with custom dependencies.
* Implements the Module Factory Pattern with Dependency Injection (DI) to encapsulate object creation
* and enable flexible dependency management. Returns an object with public utility functions and, in
* test environments (NODE_ENV='test'), a private function for unit testing.
*
* @type {Function}
* @param {Object} [dependencyInjectedOne] - Optional first dependency object with required utilities A, B, C, D.
* @param {any} [dependencyInjectedOne.A] - Utility A, required for functionality.
* @param {any} [dependencyInjectedOne.B] - Utility B, required for functionality.
* @param {any} [dependencyInjectedOne.C] - Utility C, required for functionality.
* @param {any} [dependencyInjectedOne.D] - Utility D, required for functionality.
* @param {Object} [dependencyInjectedTwo] - Optional second dependency object with utilities E, F.
* @param {any} [dependencyInjectedTwo.E] - Utility E, optional with default from `somethingTwo`.
* @param {any} [dependencyInjectedTwo.F] - Utility F, optional with default from `somethingTwo`.
* @returns {Object} An object with public utility functions and, in test environments, a private function.
* @returns {Function} .exportedFunctionOne - Public utility function for action one, using injected dependencies.
* @returns {Function} .exportedFunctionTwo - Public utility function for action two, using injected dependencies.
* @returns {Function} [_privateFunctionTwo] - Private utility function for testing, instance-specific (only in NODE_ENV='test').
* @throws {Error} If `dependencyInjectedOne` is provided but lacks required properties A, B, C, or D.
* @example
* const { factory } = require('./factory');
* const customInstance = factory(
* { A: 'customA', B: 'customB', C: 'customC', D: 'customD' },
* { E: 'customE' }
* );
* customInstance.exportedFunctionOne(); // Uses custom dependencies
* customInstance.exportedFunctionTwo(); // Uses custom dependencies
* if (process.env.NODE_ENV === 'test') {
* customInstance._privateFunctionTwo(); // Available for testing
* }
*/
module.exports.factory = createFactory
/**
* Factory Pattern with Dependency Injection Template - Function Return Variant
*
* This module implements a Factory Pattern with Dependency Injection that returns a FUNCTION
* with attached methods, rather than an object. This pattern is useful when the primary export
* needs to be callable while still providing additional methods as properties.
*
* Design Patterns and Intent:
* The code implements a Factory Pattern with Dependency Injection to create utility functions with
* customizable dependencies and testable private methods. Key distinguishing aspects:
*
* - Factory Return Type: Returns a FUNCTION (exportedFunctionOne) with methods attached as properties
* - Single Primary Function: Focuses on one main callable function as the interface
* - Method Attachment: Private test methods are attached directly to the function in test environments
* - Use Case: Ideal when you need a primary callable interface with auxiliary methods
*
* Architecture Notes:
* - The factory returns exportedFunctionOne as a callable function, not an object
* - Private function _privateFunctionTwo is attached as a property to exportedFunctionOne
* - Module-level function _privateFunctionOne is exposed on module.exports
* - Pattern suitable for utilities that are primarily functions with helper methods
*
* Testing Strategy:
* - _privateFunctionOne: Module-level, accessible via module.exports._privateFunctionOne
* - _privateFunctionTwo: Instance-level, accessible via returnedFunction._privateFunctionTwo
*
* Code Standards:
* - Validates that `dependencyInjectedOne` provides required properties (A, B, C, D)
* - Uses spread operator for safe partial overrides of `dependencyInjectedTwo`
* - Exposes private functions for testing only in `NODE_ENV='test'`
* - Uses closures for instance-specific private functions and dependency encapsulation
* - Single quotes and not double quotes
* - No semicolon
* - Spaces around operators like =, +, -, etc. Example: a + b instead of a+b
* - No spaces inside parentheses Example: func(arg) instead of func( arg )
* - Spaces inside the curly braces of an object Example: { key: value } instead of {key: value}
*
* @module factory-function-return
* @requires ./somethingOne - Default implementation for first dependency set
* @requires ./somethingTwo - Default implementation for second dependency set
*/
const DefaultSomethingOne = require('./somethingOne')
const DefaultSomethingTwo = require('./somethingTwo')
/**
* Private utility function for internal use and testing, defined at the module level.
*
* This function is shared across all factory instances and does not have access to instance-specific
* dependencies or context. It operates at the module scope and is exposed for testing purposes
* only when NODE_ENV is set to 'test'.
*
* @private
* @function _privateFunctionOne
* @memberof module:factory
* @returns {void}
* @example
* // In test environment (NODE_ENV='test')
* const factory = require('./factory');
* if (factory._privateFunctionOne) {
* factory._privateFunctionOne(); // Available for testing
* }
*/
function _privateFunctionOne() {}
/**
* Factory function implementing the Factory Pattern with Dependency Injection (DI) to create utility
* function instances with customizable dependencies and testable private methods.
*
* This function uses the Module Factory Pattern with Dependency Injection to manage dependencies flexibly. Each instance maintains its own state and closure scope, preserving access to injected dependencies. Private functions are optionally exposed for testing, supporting both production and test use cases. While this example returns a function (not an object) with methods attached as properties, it can be changed to return an object as it is merely a template and example.
*
* @function createFactory
* @param {Object} [dependencyInjectedOne] - Optional first dependency object with utilities A, B, C, D.
* @param {Object} [dependencyInjectedOne.A] - Utility A from the first dependency, required for core functionality.
* @param {Object} [dependencyInjectedOne.B] - Utility B from the first dependency, required for core functionality.
* @param {Object} [dependencyInjectedOne.C] - Utility C from the first dependency, required for core functionality.
* @param {Object} [dependencyInjectedOne.D] - Utility D from the first dependency, required for core functionality.
* @param {Object} [dependencyInjectedTwo] - Optional second dependency object with utilities E, F.
* @param {Object} [dependencyInjectedTwo.E] - Utility E from the second dependency, required for core functionality.
* @param {Object} [dependencyInjectedTwo.F] - Utility F from the second dependency, required for core functionality.
* @returns {Object} An object containing public utility functions and, in test environments, private functions.
* @returns {Function} .exportedFunctionOne - Public utility function for action one, using injected dependencies and instance context.
* @returns {Function} [_privateFunctionOne] - Private utility function for testing (only in NODE_ENV='test').
* @returns {Function} [_privateFunctionTwo] - Private utility function for testing (only in NODE_ENV='test').
* @throws {Error} If dependencyInjectedOne is provided but lacks required properties A, B, C, or D.
* @example
* // Creating instance with default dependencies
* const { factory } = require('./factory');
* const defaultUtil = factory();
* defaultUtil(); // Uses default dependencies
*
* @example
* // Creating instance with custom dependencies
* const { factory } = require('./factory');
* const customUtil = factory(
* { A: customA, B: customB, C: customC, D: customD },
* { E: customE, F: customF }
* );
* customUtil(); // Uses custom dependencies
*
* @example
* // Accessing private functions in test environment
* process.env.NODE_ENV = 'test';
* const { factory } = require('./factory');
* const testUtil = factory();
* testUtil._privateFunctionTwo(); // Available for testing
*/
function createFactory(dependencyInjectedOne, dependencyInjectedTwo) {
/**
* Validate and destructure all-or-nothing properties from the first dependency.
* Uses default values from DefaultSomethingOne if no custom dependency is provided.
* Ensures all required properties are present to maintain consistency.
*
* @private
* @type {Object}
*/
const depOne = dependencyInjectedOne || DefaultSomethingOne
if (!depOne.A || !depOne.B || !depOne.C || !depOne.D) {
throw new Error('First dependency must provide A, B, C, and D')
}
const { A, B, C, D, } = depOne
/**
* Destructure partial properties from the second dependency with safe merging.
* Uses spread operator to allow partial overrides while maintaining defaults.
* Individual properties can be overridden without providing the complete object.
*
* @private
* @type {Object}
*/
const { E, F, } = { ...DefaultSomethingTwo, ...dependencyInjectedTwo }
/**
* Private utility function for internal use and testing, scoped to this factory instance.
*
* This function has access to the injected dependencies (A, B, C, D, E, F) through closure.
* It is instance-specific and will be attached to exportedFunctionOne for testing purposes.
* Each factory instance creates its own version of this function with its own closure scope.
*
* @private
* @function _privateFunctionTwo
* @returns {void}
*/
function _privateFunctionTwo() {
// Instance-specific implementation with access to A, B, C, D, E, F
}
/**
* Public utility function for performing action one.
*
* This is the main exported function that utilizes the injected dependencies.
* It serves as the primary interface for the factory-created utility.
* In test environments, private functions are attached as properties.
*
* @public
* @function exportedFunctionOne
* @returns {void}
*/
function exportedFunctionOne() {
// Implementation using injected dependencies A, B, C, D, E, F
}
// Conditionally attach private function for testing
if (process.env.NODE_ENV === 'test') {
exportedFunctionOne._privateFunctionTwo = _privateFunctionTwo
}
// Return the utility function (not an object)
return exportedFunctionOne
}
// Create the default instance with default dependencies
const defaultInstance = createFactory()
/**
* Default export: The utility function from the default factory instance, created with default dependencies.
*
* This provides a ready-to-use utility function without requiring manual factory instantiation.
* It uses DefaultSomethingOne and DefaultSomethingTwo as dependencies. In test environments,
* private functions are accessible as properties on this function.
*
* @type {Function}
* @property {Function} [_privateFunctionTwo] - Instance-specific private function (only in NODE_ENV='test')
* @example
* const utility = require('./factory');
* utility(); // Calls the default instance with default dependencies
*
* @example
* // In test environment
* process.env.NODE_ENV = 'test';
* const utility = require('./factory');
* utility._privateFunctionTwo(); // Access private function for testing
*/
module.exports = defaultInstance
/**
* Module-level private function exposed for testing.
*
* This function is attached directly to the module exports and is available
* across all instances. It does not have access to instance-specific dependencies.
* Only exposed when NODE_ENV is set to 'test'.
*
* @type {Function|undefined}
* @memberof module:factory
* @private
*/
module.exports._privateFunctionOne = process.env.NODE_ENV === 'test' ? _privateFunctionOne : undefined
/**
* Named export: The factory function for creating new utility function instances with custom dependencies.
*
* This export provides access to the factory function itself, allowing consumers to create
* custom instances with their own dependency configurations. It implements the Factory Pattern
* with Dependency Injection for maximum flexibility and testability.
*
* @type {Function}
* @property {Function} factory - The createFactory function for creating custom instances
* @see {@link createFactory} for detailed parameter and return documentation
* @example
* const { factory } = require('./factory');
* const customInstance = factory(
* { A: 'customA', B: 'customB', C: 'customC', D: 'customD' },
* { E: 'customE' }
* );
* customInstance(); // Uses custom dependencies
*
* @example
* // Creating multiple instances with different configurations
* const { factory } = require('./factory');
* const instance1 = factory({ A: 'a1', B: 'b1', C: 'c1', D: 'd1' });
* const instance2 = factory({ A: 'a2', B: 'b2', C: 'c2', D: 'd2' });
* // Each instance has its own dependency configuration
*/
module.exports.factory = createFactory
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment