Factory Function Pattern In-Depth

Source: https://medium.com/@pyrolistical/factory-functions-pattern-in-depth-356d14801c9

There are plenty great introductions to Factory Functions, but few describe the details of the pattern. If you don’t know what a Factory Function is, please watch Factory Functions in Javascript or read the transcription.

This article will describe the smaller patterns within the overall Factory Functions Pattern. All code snippets are written in ECMAScript 2015. As of January 2016 these code snippets will require Babel until Node.js has full ECMAScript 2015 support.

The Basics

A Factory Function is just a Function that creates something. It is usually an Object, but it can be anything, a String, an Array, or even another Function. In this article we will focus on Factory Functions as a replacement for ECMAScript 2015 Class.

Here is a simple Factory Function.

// greeter.js

export default () => {

    return {

        greet() {

            console.log('Hello World!');

        }

    };

}

That just defines the Factory Function but does not create an instance of the object. This is done is another file, typically the main file.

// main.js

import Greeter from './greeter';

const greeter = Greeter();

greeter.greet();  // prints Hello World!

The convention for Factory Functions is to capitalize the name. This way you can think of it like a class but you don’t use new. This is very similar to Scala Case Classes.

Dependency Injection

Let’s say we want to greet to a file or to an API. We will refactor the previous example such that we are not greeting directly to console.log.

// greeter.js

export default (outputStream) => {

    return {

        greet() {

            outputStream.send(‘Hello World!’);

        }

    };

}

Notice greeter does not need to know how outputStream is implemented.

// main.js

import Greeter from './greeter';

import ConsoleOutputStream from './console-output-stream';

const consoleOutputStream = ConsoleOutputStream();

const greeter = Greeter(consoleOutputStream);

greeter.greet();  // prints Hello World!

We inject output stream in the main file. This is Dependency Injection. What is console output stream? It’s just another Factory Function!

// console-output-stream.js

export default () => {

    return {

        send(line) {

            console.log(line);

        }

    };

}

Unit Testing

Greeter no longer depends on any global references (i.e. console.log), which means we can now unit test it without any dirty tricks.

We want to assert that the output stream was called with “Hello World!”. This is easy to do when we are in full control of the dependencies.

In JavaScript, it is easy to mock objects with an object literal. This technique is use to mock the output stream and pass it into greeter.

Mocha and Chai are the only tools we need.

// test/greeter.js

import { expect } from 'chai';

import Greeter from '../greeter';

describe('greeter', () => {

    it('should send a greeting to output stream', () => {

        const outputStream = {

            send(line) {

                expect(line).to.equal('Hello World!');

        }

    };

    const greeter = Greeter(outputStream);

    greeter.greet();

    });

});

No mocking library required!

Encapsulation

Private data is required for Encapsulation. With ECMAScript 2015 classes, private data is possible, but awkward. Let’s extend our example and make greeter stateful. We will allow others to configure the greeting message, but we will keep the data private.

// greeter.js

export default (outputStream) => {

    let _message = 'Hello World!';

    return {

        greet() {

            outputStream.send(_message);

        },

        set message(message) {

            _message = message;

        },

        get message() {

            return _message;

        }

    };

}

That set/get syntax might look foreign to you, but it is actually just combining ECMAScript 5.1 getters/setters with ECMAScript 2015 Enhanced Object Literals.

We can then usemessagelike a normal property.

// main.js

import Greeter from './greeter';

import ConsoleOutputStream from './console-output-stream';

const consoleOutputStream = ConsoleOutputStream();

const greeter = Greeter(consoleOutputStream);

greeter.message = 'Salutations Earth.';

greeter.greet(); // prints Salutations Earth.

It is impossible to access the _message variable from outside of the Factory Function. Data is kept private. Difference instances of greeter will have their own private copy of _message and will not conflict.

Composition

If we keep the objects created using Factory Functions small, we can use composition to create new objects from smaller components. Let’s add some additional functionality so we have something to compose. We will add a wave gesture.

// gesturer.js

export default (outputStream) => {

    return {

        wave() {

            outputStream.send('*Waves hand*');

        }

    };

}

To create a waving greeter, we simply create the two smaller components, then use Object.assign to compose into a single object.

// main.js

import Greeter from './greeter';

import Gesturer from './gesturer';

import ConsoleOutputStream from './console-output-stream';

const consoleOutputStream = ConsoleOutputStream();

const greeter = Greeter(consoleOutputStream);

const gesturer = Gesturer(consoleOutputStream);

const wavingGreeter = Object.assign({}, greeter, gesturer);

wavingGreeter.message = 'Salutations Earth.';

wavingGreeter.greet(); // prints Salutations Earth.

wavingGreeter.wave(); // prints *Waves hand*

The waving greeter shares the same state as the components it is made up from. We can set a new message on greeter and waving greeter will use it.

Object.assign assigns properties from left to right. In the example, it assigns greet to the empty object, then wave to an object that contains greet.

If there are conflicts, the right most object wins. Or in other words calling a conflicting method is the same as calling the method on the right most object in the list. You can prevent methods from conflicting by wrapping objects and renaming methods before composing them.

Calling Sibling Methods

In all our previous examples we immediately returned the objects we constructed. In more complex objects we might want a method to call another method on the same object. This is possible, but we need to get a reference to the self.

// head-scratcher.js

export default () => {

    const self = {

        scratch(location) {

            console.log(`scratching ${location}`);

        },

        confused() {

            self.scratch('head');

        }

    };

    return self;

}

Bounded Method References

One of the downsides of ECMAScript 2015 Classes is methods depend on the this reference. Unfortunately if you want to use a class method in function libraries such as Ramda, you will end up having to bind this back to the class instance.

Let’s see what this looks like with ECMAScript 2015 Classes.

// offset.js

export default class Offset {

    constructor(delta) {

        this.delta = delta;

    }

    add(value) {

        return value + this.delta;

    }

}

// main.js

import Offset from './offset';

const increment = new Offset(1);

console.log([1, 2, 3]

    .map(increment.add.bind(increment)));  // prints [ 2, 3, 4 ]

Objects created using Factory Functions don’t have this problem, since there is nothisreference. Let’s see that same example with Factory Functions.

// offset.js

export default (delta) => {

    return {

        add(value) {

            return value + delta;

        }

    };

}

// main.js

import Offset from './offset';

const increment = Offset(1);

console.log([1, 2, 3]

    .map(increment.add));  // prints [ 2, 3, 4 ]

Notice how much shorter and simpler the code is.

Types

Factory Functions can create informally typed objects. We simply add a getter type property to the object we wish to type.

// duck.js

export default () => {

    return {

        get type() {

            return 'duck';

        },

        speak() {

            return 'quack';

        }

    };

}

In place of instanceOf, we just use the type property.

// main.js

import Duck from './duck';

import Dragon from './dragon';

const animals = [Duck(), Dragon()];

animals.forEach((animal) => {

    switch(animal.type) {

        case 'duck':

            console.log('safe to pet');

            break;

        case 'dragon':

            console.log('run away');

            break;

        default:

            console.log('be careful');

    }

}); // prints safe to pet, then run away

Self-Instantiating Dependencies Anti-Pattern

The final section will talk about Factory Function Anti-Patterns. The first one is related to Dependency Injection. Before we can understand the Anti-Pattern, we need to understand Dependency Injection in more depth. It actually has two parts, components and injection. Components are only allowed to depend on other components. Injection happens once during application startup.

In our case Function Factories create the components. The Function Factories are used during injection to create the application.

The Self-Instantiating Dependencies Anti-Pattern is when a component creates a dependency on its own outside of injection. Let’s see this in code. We will take our last component gesturer and have it create its own output stream.

// gesturer.js

import ConsoleOutputStream from './console-output-stream';

export default () => {

    const outputStream = ConsoleOutputStream();

    return {

        wave() {

            outputStream.send('*Waves hand*');

        }

    };

}

If you run main, the code still works, so what is the problem? There are many problems. We are violating the Single Responsibility Principle by having gesturer knowing about how to create a output stream. Gesturer doesn’t care how output stream is implemented, yet we have a hard link to the specific implementation.

The problem is not very obvious when we have a simple example like this, but imagine output stream requires many other dependencies. Things get complicated fast if you don’t know who creates your components and don’t know when they are created.

Another problem arises when we attempt to unit test gesturer. How do you mock out the console output stream? Well, you just can’t with ECMAScript 2015 modules (but you can if you are using require and proxyquire).

The solution is to always create all components during the initialization of your application, and inject everything. This keeps everything unit testable and prevents the need to pass down dependencies many layers just for something else to create a component. If you have the situation where you cannot create a component during application initialization but still require many dependencies, then create a factory service. Initialize your factory service like any other component and spawn new objects with normal methods.

I hope this gives you a better appreciation of Factory Functions. This is a living document and will grow over time more Factory Function Patterns get discovered. If you think I missed anything with Factory Functions, please tweet me.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容