UNPKG

cooperate

Version:

Convention based composition tool that let's you compose a series of objects into a single object quickly.

205 lines (138 loc) 6 kB
# cooperate [![Greenkeeper badge](https://badges.greenkeeper.io/midknight41/cooperate.svg)](https://greenkeeper.io/) [![Build Status](https://travis-ci.org/midknight41/cooperate.svg?branch=master)](https://travis-ci.org/midknight41/cooperate) **cooperate** is a convention based composition tool that let's you compose a series of objects into a single object quickly. It does not alter the original objects or prototypes but wraps them in a proxy object. The proxy object will forward the method calls to the appropriate underlying object methods and perserves the appropriate behaviour of your getters and setters too. See [Change Log](./CHANGELOG.md) for changes from previous versions. ## Installation ``` npm install cooperate -S ``` ## Why use it? Inheritance can be a bit of a pain for many reasons and a great alternative is to use composition. This is traditionally done like this: ```js class MainClass { constructor(capability1, capability2) { this.capability1_ = capability1; this.capability2_ = capability2; } doStuff(value) { this.capability1_.doStuff(value); } doMoreStuff(value) { this.capability2_.doMoreStuff(value); } } const capability1 = new Capability1(); const capability2 = new Capability2(); const combined = new MainClass(capability1, capability2); ``` While this approach avoids some of the pain points of inheritance it does require you do a lot of method forwarding which is repetitive, time-consuming and boring. That's where **cooperate** comes in. **cooperate** inspects the objects that you would like to combine and does all that method forwarding for you with a single line of code. ```js import { compose } from "cooperate"; const capability1 = new Capability1(); const capability2 = new Capability2(); const combined = compose([capability1, capability2]); ``` JavaScript does not really have the concept of privacy but it is very common for developers to use an ```_``` to mark methods as private. **cooperate** follows this convention and will not expose anything that begins or ends with an ```_``` on the combined object. ## Detailed Example ```js import { compose } from "cooperate"; import assert from "assert"; class SpecificFeatures { constructor(db) { this._db = db; } getSalesByRegion(regionName) { const query = this.formatQuery_(regionName); return this._db.query(query); } formatQuery_(regionName) { return { region: regionName }; } } class GenericFeatures { constructor(db) { this._db = db; } get connected() { return this._db.connected; } insert(data) { this._db.insert(data); } findById(id) { return this._db.query({ _id: id }); } } const db = {}; const genericFeatures = new GenericFeatures(db); const specificFeatures = new SpecificFeatures(db); const repo = compose([genericFeatures, specificFeatures]); // Creates an object with no shared state assert(repo.findById); assert(repo.getSalesByRegion); assert(repo.insert); assert(Object.getOwnPropertyDescriptor(repo, "connected")); // private members are not exposed assert(repo.formatQuery_ === undefined); assert(Object.getOwnPropertyDescriptor(repo, "_db") === undefined); ``` ## Dealing with naming collisions With any form of composition you will eventually come across a problem with two things that have the same name. **cooperate** will throw an error if you try and do this as it cannot decide what to do without your help. ### Mapping Members To help with this problem, **cooperate** provides the ```mapMembers()``` function. This lets you define your method forward rules per object. #### Example 1 ```js import { compose, mapMembers } from "cooperate"; const genericFeatures = new GenericFeatures(db); const specificFeatures = new SpecificFeatures(db); const genericWithMapping = mapMembers(genericFeatures) .map("findById").to("findSalesById"); const repo = compose([genericWithMapping, specificFeatures]); // Method is remapped assert(repo.findSalesById); assert(repo.findById === undefined); ``` #### Example 2 ```js import { compose, mapMembers } from "cooperate"; import createRepo from "./Repo"; const product = createRepo("products"); const sale = createRepo("sales"); const productWithMapping = mapMembers(product) .map("getItem").to("getProduct") .map("updateItem").to("updateProduct") .map("insertItem").to("insertProduct") .map("deleteItem").to("deleteProduct"); const salesWithMapping = mapMembers(sale) .map("getItem").to("getSale") .map("updateItem").to("updateSale") .map("insertItem").to("insertSale") .map("deleteItem").to("deleteSale"); const combinedRepo = compose([productWithMapping, salesWithMapping]); ``` ### Hiding Members The ```mapMembers()``` can also be used with members that you don't want to expose on the **cooperate** object. This can be achieved with the ```hide()``` method. ```js import { compose, mapMembers } from "cooperate"; import assert from "assert"; const genericFeatures = new GenericFeatures(db); const specificFeatures = new SpecificFeatures(db); const genericWithMapping = mapMembers(genericFeatures) .map("findById").to("findSalesById") .hide("deleteItem"); const repo = compose([genericWithMapping, specificFeatures]); assert(repo.findSalesById); assert(repo.findById === undefined); assert(repo.deleteItem === undefined); ``` If the objects you are trying to compose have a uniform interface then there is also an option on the ```compose()``` method to make this easier. ```js import { compose } from "cooperate"; import assert from "assert"; // Assume all three capabilities here support a property called isWorking. import capability1 from "./ capability1"; import capability2 from "./ capability2"; import capability3 from "./ capability3"; const service = compose([capability1, capability2, capability3], { hide: ["isWorking"] }); // no naming collisions and no isWorking property on the resulting object assert(service.isWorking === undefined); ```