Skip to content

Instantly share code, notes, and snippets.

@MikeyBurkman
Last active January 2, 2019 22:27
Show Gist options
  • Save MikeyBurkman/f406ed52e363f1fe475eb4af1f597a07 to your computer and use it in GitHub Desktop.
Save MikeyBurkman/f406ed52e363f1fe475eb4af1f597a07 to your computer and use it in GitHub Desktop.
TS Dependency Example
/**
* Dependency Example in TS.
* Instead of dependency injection, this method shows how services can "fetch" their dependencies,
* without being concerned about the lifecycles of their dependencies.
* These services are also isolated from their dependencies, lending themselves to easier testing.
* Does not use classes or decorators or any black magic to accomplish this.
*
* All services are essentially split into 3 pieces:
* 1. The interface that defines the service to other services.
* 2. The service constructing function that takes in all depedencies as arguments.
* 3. The instance getter function, that other services will import in their own instance getter functions
* See comments on each function below for more info.
*/
// Baz service
import { memoize, repeat } from 'lodash';
import { Counter, counter } from './counter';
// Anything that needs a Baz service will ask for this interface in their constructing function.
// This is essentially the service's contract.
export interface Baz {
/**
* Documentation for the shout function goes here.
* This is a silly little function with no real meaning right now. It returns a string...
*/
shout: () => string;
}
// This next function contains all the logic for the Baz service, but has zero dependencies outside
// of its arguments.
// (Exceptions can of course be made, like for a logger.)
// Basically, anything that would need to be mocked in unit tests should be an argument.
// This function would be imported by the unit tests, and could be easily constructed with
// mock values for testing.
// Nothing outside of this file and unit tests will ever call this function, so adding dependencies
// at a later date will not break callers unless it breaks the interface.
export const newBaz = (word: string, counter: Counter): Baz => {
const shout = () => {
counter = counter.increment();
const count = counter.value();
return repeat(word, count);
};
return { shout };
};
// This is a no-arg function that returns the singleton instance.
// This function is imported by other services, but does NOT contain any logic, and will
// NOT be unit tested. (Integration tests will test it. The compiler itself also helps a lot here.)
// This function is all about assembling dependencies, whether they be config
// values, or implementations of other dependencies.
// The memoize() is a bit weird, but it also ensures that it's essentially a lazily-initiallized
// instance. If this instantiation were instead done in its own file, then node's module resolver would
// essentially handle this lazy initialization for us. (It would get created only when that file is imported.)
// (We want it to be lazily-initialized so that unit tests importing this file won't
// inadventently call the instantiation logic, which might lead to undesired side effects. It might not be necessary)
// If your service is stateful for some reason, and you don't want a singleton, just simply leave out the memoize() bit
// and you'll get a new instance every time.
export const baz = memoize(
(): Baz => {
const word = 'bananas '; // This could could come from the config, for instance
const counterInstance = counter(); // Note -- no args ever passed to this function either
return newBaz(word, counterInstance);
}
);
// Counter service
// Follows the same pattern as baz.ts, except this doesn't have any external dependencies
import { memoize } from 'lodash';
export interface Counter {
increment: () => Counter;
value: () => number;
}
export const newCounter = (value: number): Counter => {
return {
increment: () => newCounter(value + 1),
value: () => value
};
};
export const counter = memoize(() => newCounter(0));
// Just a silly file to show how we can import the baz getter and use it
// without knowing anything about Baz's dependencies.
import { baz } from './baz';
console.log(baz().shout()); // "bananas "
console.log(baz().shout()); // "bananas bananas "
console.log(baz().shout()); // "bananas bananas bananas "
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment