Skip to content
This repository has been archived by the owner on Nov 18, 2023. It is now read-only.

Commit

Permalink
docs(README): update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jul 4, 2020
1 parent 17b1586 commit 51f4e3b
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 13 deletions.
274 changes: 267 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,281 @@
<div align="center">
<img src="https://res.cloudinary.com/adonisjs/image/upload/q_100/v1557762307/poppinss_iftxlt.jpg" width="600px">
</div>

# Manager Pattern
> Implementation of the Manager pattern used by AdonisJS
[![circleci-image]][circleci-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url]

Manager pattern is a way to ease the construction of objects of similar nature. To understand it better, we will follow an imaginary example through out this document.

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of contents

- [Manager/Builder pattern](#managerbuilder-pattern)
- [Scanerio](#scanerio)
- [Basic implementation](#basic-implementation)
- [Using a Manager Class](#using-a-manager-class)
- [Step1: Define a sample config object.](#step1-define-a-sample-config-object)
- [Step 2: Create manager class and accept mappings config](#step-2-create-manager-class-and-accept-mappings-config)
- [Step 3: Using the config to constructor drivers](#step-3-using-the-config-to-constructor-drivers)
- [Step 4: Move drivers construction into the manager class](#step-4-move-drivers-construction-into-the-manager-class)
- [Usage](#usage)
- [Extending from outside-in](#extending-from-outside-in)
- [Driver Interface](#driver-interface)
- [Defining Drivers Interface](#defining-drivers-interface)
- [Passing interface to Manager](#passing-interface-to-manager)
- [Mappings Type](#mappings-type)
- [Mapping suggestions](#mapping-suggestions)
- [Return type](#return-type)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

<div align="center">
<img src="https://res.cloudinary.com/adonisjs/image/upload/q_100/v1557762307/poppinss_iftxlt.jpg" width="600px">
</div>

# Manager/Builder pattern
> Abstract class to incapsulate the behavior of constructing and re-using named objects.
## Scanerio

[![circleci-image]][circleci-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url]
Let's imagine we are creating a mailing library and it supports multiple drivers like: **SMTP**, **Mailgun**, **PostMark** and so on. Also, we want the users of our library to use each driver for multiple times using different configuration. For example:

Using the **Mailgun driver with different accounts**. Maybe one account for sending promotional emails and another account for sending transactional emails.

## Basic implementation

The simplest way to expose the drivers, is to export them directly and let the consumer construct instances of them. For example:

```ts
import { Mailgun } from 'my-mailer-library'

const promotional = new Mailgun(configForPromtional)
const transactional = new Mailgun(configForTransactional)

promotional.send()
transactional.send()
```

The above approach works perfect, but has few drawbacks

- If the construction of the drivers needs more than one constructor arguments, then it will become cumbersome for the consumer to satisfy all those dependencies.
- They will have to manually manage the lifecycle of the constructed objects. Ie `promotional` and `transactional` in this case.


## Using a Manager Class

What we really need is a Manager to manage and construct these objects in the most ergonomic way.

### Step1: Define a sample config object.

First step is to move the mappings to a configuration file. The config mimics the same behavior we were trying to achieve earlier (in the Basic example), but defines it declaratively this time.

```ts
const mailersConfig = {
default: 'transactional',

list: {
transactional: {
driver: 'mailgun'
},

promotional: {
driver: 'mailgun'
},
}
}
```

### Step 2: Create manager class and accept mappings config

```ts
import { Manager } from '@poppinss/manager'

class MailManager implements Manager {
protected singleton = true

constructor (private config) {
super({})
}
}
```

### Step 3: Using the config to constructor drivers

The **Base Manager** class will do all the heavy lifting for you. However, you will have to define certain methods to resolve the values from the config file.

```ts
import { Manager } from '@poppinss/manager'

class MailManager implements Manager {
protected singleton = true

protected getDefaultMappingName() {
return this.config.default
}

protected getMappingConfig(mappingName: string) {
return this.config.list[mappingName]
}

protected getMappingDriver(mappingName: string) {
return this.config.list[mappingName].driver
}

constructor (private config) {
super({})
}
}
```

### Step 4: Move drivers construction into the manager class

The final step is to write the code for constructing drivers. The **Base Manager** uses a convention for this. Anytime the consumer will ask for the `mailgun` driver, it will invoke `createMailgun` method. So the convention here is to prefix `create` followed by the camelCase driver name.

```ts
import { Manager } from '@poppinss/manager'

class MailManager implements Manager {
// ... The existing code

public createMailgun (mappingName, config) {
return new Mailgun(config)
}

public createSmtp (mappingName, config) {
return new Smtp(config)
}
}
```

## Usage

Once done, the consumer of the Mailer class just needs to define the mappings config and they are good to go.

```ts
const mailersConfig = {
default: 'transactional',

list: {
transactional: {
driver: 'mailgun'
},

promotional: {
driver: 'mailgun'
},
}
}

const mailer = new MailManager(mailersConfig)

mailer.use('transactional').send()
mailer.use('promotional').send()
```

- The lifecycle of the mailers is now encapsulated within the manager class. The consumer can call `mailer.use()` as many times as they want, without worrying about creating too many un-used objects.
- They just need to define the mailers config once and get rid of any custom code required to construct individual drivers.

## Extending from outside-in

The Base Manager class comes with first class support for adding custom drivers from outside-in using the `extend` method.

```ts
const mailer = new MailManager(mailersConfig)

mailer.extend('postmark', (container, mappingName, config) => {
return new PostMark(config)
})
```

The `extend` method receives a total of three arguments:

- The container object is the value you passed to the Base Manager class using the `super({})` keyword. In case of AdonisJS, it is reference to the IoC container.
- The name of the mapping inside the config file.
- The actual configuration object.
- The `callback` should return an instance of the Driver.

## Driver Interface

The Manager class can also leverage static Typescript types to have better intellisense support and also ensure that the drivers added using the `extend` method adhere to a given interface.

### Defining Drivers Interface

Following is a dummy interface, we expect all drivers to adhere too

```ts
interface DriverContract {
send (): Promise<void>
}
```


### Passing interface to Manager

```ts
import { Manager } from '@poppinss/manager'
import { DriverContract } from './Contracts'

class MailManager implements Manager<
DriverContract
> {
}
```

## Mappings Type

The mappings config currently has `any` type and hence, the `mailer.use` method cannot infer correct return types.

In order to improve intellisense for the `use` method. You will have to define a type for the mappings too.

```ts
type MailerMappings = {
transactional: Mailgun,
promotional: Mailgun
}

type MailerConfig = {
default: keyof MailerMappings,
list: {
[K in keyof MailerMappings]: any
}
}

const mailerConfig: MailerConfig = {
default: 'transactional',

list: {
transactional: {
driver: 'mailgun',
// ...
},

promotional: {
driver: 'mailgun',
// ...
},
}
}
```

Finally, pass the `MailerMappings` to the Base Manager class

```ts
import { DriverContract, MailerMappings } from './Contracts'

class MailManager implements Manager<
DriverContract,
DriverContract,
MailerMappings
> {
}
```

Once mailer mappings have been defined, the `use` method will have proper return types.

### Mapping suggestions

![](assets/mappings.png)

### Return type

![](assets/return-type.png)

[circleci-image]: https://img.shields.io/circleci/project/github/poppinss/manager/master.svg?style=for-the-badge&logo=circleci
[circleci-url]: https://circleci.com/gh/poppinss/manager "circleci"
Expand Down
Binary file added assets/mappings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/return-type.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 4 additions & 5 deletions example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ interface Mailgun extends DriverContract {
}

type Mappings = {
smtp: Smtp
mailgun: Mailgun
promotional: Smtp
transactional: Mailgun
}

type Mailer = ManagerContract<any, DriverContract, DriverContract, Mappings>

const a = {} as Mailer
a.use()
const mailer = {} as Mailer

// mailer.use('')
mailer.use('')
2 changes: 1 addition & 1 deletion src/Contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface ManagerContract<
container: Container

/**
* Returns concrete type when binding name is from the mappings lsit
* Returns concrete type when binding name is from the mappings list
*/
use<K extends keyof MappingsList>(name: K): MappingsList[K]

Expand Down

0 comments on commit 51f4e3b

Please sign in to comment.