Skip to content

Commit

Permalink
v0.3.1 (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgdigital authored Oct 23, 2021
1 parent 320a2d3 commit 01339d5
Show file tree
Hide file tree
Showing 24 changed files with 389 additions and 316 deletions.
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# Changelog

## [0.3.1]

- Change the `ContainerModule` type to include a key, allowing modules to be used multiple times without rebuilding
- Simplify interface naming

## [0.2.2]

- Add `containerKeyValues` function

## [0.2.1]

- Change behaviour of `IContainerBuilder.define` so that already-defined services are not overwritten
- Change behaviour of `ContainerBuilder.define` so that already-defined services are not overwritten

## [0.1.16]

- Fix type signature of `IContainerBuilder.use` method
- Fix type signature of `ContainerBuilder.use` method

## [0.1.15]

Expand Down
155 changes: 83 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Several dependency injection solutions exist for TypeScript. Most use either dec

<span style="color:green">&check;</span> Achieving loose coupling in large applications

tsinject works by defining named factory functions in a container builder, with unique symbols mapping services available in the container to their type. These factory functions can return anything, allowing configuration objects, class instances, functions or any other type of value to be defined as a container service. Any code can be containerized without need for modifications such as annotations or static properties.
**tsinject** works by defining named factory functions in a container builder, with unique symbols mapping services available in the container to their type. These factory functions can return anything, allowing configuration objects, class instances, functions or any other type of value to be defined as a container service. Any code can be containerized without need for modifications such as annotations or static properties.

Any application that does something useful needs to cause side effects. These might include:

Expand All @@ -37,7 +37,7 @@ Any application that does something useful needs to cause side effects. These mi
- Asking the user for input
- Logging a message to the console

These capabilities are implemented by components of the application, with some components depending on others, and with the implementation or configuration of components often depending on values read from the environment. The quickest way to allow components to communicate with each other is often via globally defined singleton instances. This increases complexity, making code more difficult to debug, test and maintain. Instead, by building components that have their dependencies injected, we can create complex but decoupled applications.
These capabilities are implemented by components of the application, with some components depending on others, and with the implementation or configuration of components often depending on values read from the environment. The quickest way to allow components to communicate with each other is often via globally defined singleton instances. Importing these global side effects throughout an application can increase complexity, making code more difficult to debug, test and maintain. Instead, by building components that have their dependencies injected, we can create complex but decoupled applications.

## Usage

Expand Down Expand Up @@ -84,35 +84,39 @@ import simpleLogFormatter from '../../logging/simpleLogFormatter'
const loggingModule: ContainerModule<
processEnvModule.services &
LoggingServices
> = builder => builder
// Use another container module that provides services required by this one
.use(processEnvModule.default)
// Define a config object based on environment variables
.define(
keys.loggerConfig,
container => loggerConfigFromEnv(
container.get(processEnvModule.keys.processEnv)
> = {
// Specify a unique key for the module
key: Symbol('loggingModule'),
build: builder => builder
// Use another container module that provides services required by this one
.use(processEnvModule.default)
// Define a config object based on environment variables
.define(
keys.loggerConfig,
container => loggerConfigFromEnv(
container.get(processEnvModule.keys.processEnv)
)
)
)
// Provide a different implementation depending on environment variable configuration
.define(
keys.logFormatter,
container => container.get(keys.loggerConfig).pretty
? prettyLogFormatter
: simpleLogFormatter
)
.define(
keys.logWriter,
() => consoleLogWriter
)
.define(
keys.logger,
container => new Logger(
container.get(keys.logFormatter),
container.get(keys.logWriter),
container.get(keys.loggerConfig).level
// Provide a different implementation depending on environment variable configuration
.define(
keys.logFormatter,
container => container.get(keys.loggerConfig).pretty
? prettyLogFormatter
: simpleLogFormatter
)
)
.define(
keys.logWriter,
() => consoleLogWriter
)
.define(
keys.logger,
container => new Logger(
container.get(keys.logFormatter),
container.get(keys.logWriter),
container.get(keys.loggerConfig).level
)
)
}

export default loggingModule
```
Expand All @@ -132,9 +136,7 @@ const logger = container.get(loggingModule.keys.logger)
logger.info('Logging something!')
```

**Note:** We should only call [IContainer.get](https://mgdigital.github.io/tsinject/interfaces/IContainer.html#get) from within a factory function or from the [composition root](https://freecontent.manning.com/dependency-injection-in-net-2nd-edition-understanding-the-composition-root/), avoiding the [service locator anti-pattern](https://freecontent.manning.com/the-service-locator-anti-pattern/).

**Note:** When defining a service in the container, if that service key is already defined then the key will **not** be overwritten. This allows modules to be used multiple times without introducing unpredictable behaviour when using decorators. For example, if modules A and B both depend on module C, they can both use module C, and then be used together module D, where they will share the same dependency instance from module C. If an already defined service needs to be overwritten, this can be done with a decorator.
**Note:** We should only call [Container.get](https://mgdigital.github.io/tsinject/interfaces/Container.html#get) from within a factory function or from the [composition root](https://freecontent.manning.com/dependency-injection-in-net-2nd-edition-understanding-the-composition-root/), avoiding the [service locator anti-pattern](https://freecontent.manning.com/the-service-locator-anti-pattern/).

### Decorators

Expand All @@ -145,32 +147,35 @@ Decorators allow us to modify an already-defined service. Let's create a custom
import type { ContainerModule } from '@mgdigital/tsinject'
import * as loggingModule from './examples/container/loggingModule'

const myCustomLoggingModule: ContainerModule<
const customLoggingModule: ContainerModule<
loggingModule.services
> = builder => builder
.use(loggingModule.default)
// Decorate the logger config so that output is always pretty
.decorate(
loggingModule.keys.loggerConfig,
factory => container => ({
...factory(container),
pretty: true
})
)
// Decorate the log formatter to append an exclamation mark to all log entries
.decorate(
loggingModule.keys.logFormatter,
factory => container => {
const baseFormatter = factory(container)
return (level, message, data) =>
baseFormatter(level, message, data) + '!'
}
)
// Overwrite the log writer with some other implementation
.decorate(
loggingModule.keys.logWriter,
() => () => myCustomLogWriter
)
> = {
key: Symbol('customLoggingModule'),
build: builder => builder
.use(loggingModule.default)
// Decorate the logger config so that output is always pretty
.decorate(
loggingModule.keys.loggerConfig,
factory => container => ({
...factory(container),
pretty: true
})
)
// Decorate the log formatter to append an exclamation mark to all log entries
.decorate(
loggingModule.keys.logFormatter,
factory => container => {
const baseFormatter = factory(container)
return (level, message, data) =>
baseFormatter(level, message, data) + '!'
}
)
// Overwrite the log writer with some other implementation
.decorate(
loggingModule.keys.logWriter,
() => () => myCustomLogWriter
)
}
```

We can also use decorators to achieve features that aren't explicitly implemented in this library, such as service tagging, which we can do by defining a service as an array:
Expand All @@ -188,24 +193,30 @@ type ServiceMap = {

const myModule: ContainerModule<
ServiceMap
> = builder => builder
.define(
serviceTag,
() => []
)
> = {
key: Symbol('myModule'),
build: builder => builder
.define(
serviceTag,
() => []
)
}

const myOtherModule: ContainerModule<
ServiceMap
> = builder => builder
.use(myModule)
.decorate(
serviceTag,
// Add a service to the array of already defined services
factory => container => [
...factory(container),
{ foo: 'bar' }
]
)
> = {
key: Symbol('myOtherModule'),
build: builder => builder
.use(myModule)
.decorate(
serviceTag,
// Add a service to the array of already defined services
factory => container => [
...factory(container),
{ foo: 'bar' }
]
)
}
```

And that's it - unlike some other DI containers that claim to be lightweight, tsinject really is tiny and has a simple API, allowing large and complex but loosely coupled applications to be built from small, simple and easily testable components.
Expand Down
Loading

0 comments on commit 01339d5

Please sign in to comment.