Completed
Push — master ( 5b5b28...e6d81f )
by Esaú
01:58
created

hierarchy-helper.js ➔ instanceDefinedOrNull   C

Complexity

Conditions 10
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
c 1
b 0
f 0
nc 4
dl 0
loc 22
rs 6.1368
nop 8

How to fix   Complexity    Many Parameters   

Complexity

Complex classes like hierarchy-helper.js ➔ instanceDefinedOrNull often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
// spec/helpers/hierarchy-helper.js
2
"use strict";
3
4
// :: DEPENDENCIES
5
6
const path = require("path");
7
const root = path.dirname(path.dirname(__dirname));
8
9
module.exports = (hierarchy) => {
10
    // check parameters
11
    if (!Array.isArray(hierarchy)) {
12
        throw new Error("hierarchy must be an array");
13
    }
14
15
    // load dependencies
16
    let deps      = hierarchy.map(value => require(path.join(root, value + ".js")));
17
    let klassName = hierarchy.pop();
18
    let Klass     = deps.pop();
19
    const first   = (hierarchy.length === 0);
20
    const third   = (hierarchy.length >= 3);
21
22
    // :: TESTING
23
24
    // test the last class in the hierarchy tree
25
    describe(klassName, () => {
26
27
        // :: INHERITED PROTOTYPE
28
29
        // all inherit from Object
30
        it("should inherit from 'Object'", () => {
31
            expect(new Klass()).toEqual(jasmine.any(Object));
32
        });
33
34
        // check the hierarchy tree
35
        for (let i = 0; i < hierarchy.length; i += 1) {
36
            it("should inherit from '" + hierarchy[i] + "'", () => {
37
                expect(new Klass()).toEqual(jasmine.any(deps[i]));
38
            });
39
        }
40
41
        // check inherited properties
42
        if (!first) {
43
            it("should have a prototype property named 'name'", () => {
44
                expect(Klass.prototype).toHaveString("name");
45
            });
46
47
            it("should have a prototype property named 'message'", () => {
48
                expect(Klass.prototype).toHaveString("message");
49
            });
50
51
            it("should have a prototype property named 'code'", () => {
52
                expect(Klass.prototype).toHaveMember("code");
53
            });
54
        }
55
56
        // check Object methods
57
        it("should have a prototype method named 'toString()'", () => {
58
            expect(Klass.prototype).toHaveMethod("toString");
59
        });
60
61
        // check inherited methods
62
        if (!first) {
63
            it("should have a prototype method named 'native()'", () => {
64
                expect(Klass.prototype).toHaveMethod("native");
65
            });
66
        }
67
68
        // :: EXTENDED PROTOTYPE
69
70
        // check extended properties
71
        if (first) {
72
            it("should have a prototype property named 'name'", () => {
73
                expect(Klass.prototype).toHaveString("name");
74
            });
75
76
            it("should have a prototype property named 'message'", () => {
77
                expect(Klass.prototype).toHaveString("message");
78
            });
79
80
            it("should have a prototype property named 'code'", () => {
81
                expect(Klass.prototype).toHaveMember("code");
82
            });
83
84
            it("should have a prototype method named 'native()'", () => {
85
                expect(Klass.prototype).toHaveMethod("native");
86
            });
87
        }
88
89
        // :: PROTOTYPE VALUES
90
91
        it("should have the 'class' name in the prototype property named 'name'", () => {
92
            expect(Klass.prototype.name).toEqual(klassName);
93
        });
94
95
        it("should have a dummy default value as message", () => {
96
            expect(Klass.prototype.message).toEqual("thrown");
97
        });
98
99
        it("should have a null default value as code", () => {
100
            expect(Klass.prototype.code).toBeNull();
101
        });
102
103
        // :: CONSTRUCTOR
104
105
        it("should instantiate without parameters", () => {
106
            let arg1, arg2, arg3, test;
107
            if (third) {
108
                test = (() => new Klass(arg1, arg2));
109
            } else {
110
                test = (() => new Klass(arg1, arg2, arg3));
111
            }
112
            for (let i = 0; i < 2; i += 1) {
113
                arg1 = (i % 2 === 0 ? undefined : null);
114
                for (let j = 0; j < 2; j += 1) {
115
                    arg2 = (j % 2 === 0 ? undefined : null);
116
                    if (third) {
117
                        expect(test).not.toThrowError("parameter 'name' must be a 'string'");
118
                        expect(test).not.toThrowError("parameter 'message' must be a 'string'");
119
                        expect(test).not.toThrowError("parameter 'code' must be a 'number'");
120
                    } else {
121
                        for (let e = 0; e < 2; e += 1) {
122
                            arg3 = (e % 2 === 0 ? undefined : null);
123
                            expect(test).not.toThrowError("parameter 'name' must be a 'string'");
124
                            expect(test).not.toThrowError("parameter 'message' must be a 'string'");
125
                            expect(test).not.toThrowError("parameter 'code' must be a 'number'");
126
                        }
127
                    }
128
                }
129
            }
130
            test = (() => new Klass());
131
            expect(test).not.toThrowError("parameter 'name' must be a 'string'");
132
            expect(test).not.toThrowError("parameter 'message' must be a 'string'");
133
            expect(test).not.toThrowError("parameter 'code' must be a 'number'");
134
        });
135
136
        it("should instantiate with parameters", () => {
137
            let arg1, arg2, arg3, test1, test2, test3, args1, args2, args3;
138
            test1 = (() => new Klass(arg1));
139
            test2 = (() => new Klass(arg1, arg2));
140
            test3 = (() => {
141
            });
142
            if (!third) {
143
                test3 = (() => new Klass(arg1, arg2, arg3));
144
                args1 = [undefined, null, Klass.prototype.name];
145
                args2 = [undefined, null, Klass.prototype.message];
146
                args3 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
147
            } else {
148
                args1 = [undefined, null, Klass.prototype.message];
149
                args2 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
150
                args3 = [];
151
            }
152
            for (let i = 0; i < args1.length; i += 1) {
153
                arg1 = args1[i];
154
                expect(test1).not.toThrowError("parameter 'name' must be a 'string'");
155
                expect(test1).not.toThrowError("parameter 'message' must be a 'string'");
156
                expect(test1).not.toThrowError("parameter 'code' must be a 'number'");
157
                for (let j = 0; j < args2.length; j += 1) {
158
                    arg2 = args2[j];
159
                    expect(test2).not.toThrowError("parameter 'name' must be a 'string'");
160
                    expect(test2).not.toThrowError("parameter 'message' must be a 'string'");
161
                    expect(test2).not.toThrowError("parameter 'code' must be a 'number'");
162
                    if (!third) {
163
                        for (let e = 0; e < args3.length; e += 1) {
164
                            arg3 = args3[e];
165
                            expect(test3).not.toThrowError("parameter 'name' must be a 'string'");
166
                            expect(test3).not.toThrowError("parameter 'message' must be a 'string'");
167
                            expect(test3).not.toThrowError("parameter 'code' must be a 'number'");
168
                        }
169
                    }
170
                }
171
            }
172
        });
173
174
        it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
175
            let arg1, arg2, arg3, test31, test32, test33, test21, test22, test11;
176
            const noStr   = [{}, true, false, 42, 3.1416, -42, -3.1416, () => null];
177
            const noNmb   = [{}, true, false, '', "qwerty", () => null];
178
            const noStrSz = noStr.length;
179
            const noNmbSz = noNmb.length;
180
            if (!third) {
181
                test33 = (() => new Klass(arg1, arg2, arg3));
182
                test32 = (() => new Klass(null, arg2, arg3));
183
                test31 = (() => new Klass(null, null, arg3));
184
            } else {
185
                test33 = (() => null);
186
                test32 = (() => null);
187
                test31 = (() => null);
188
            }
189
            test22 = (() => new Klass(arg1, arg2));
190
            test21 = (() => new Klass(null, arg2));
191
            test11 = (() => new Klass(arg1));
192
            if (typeof Symbol === "function") {
193
                noStr.push(Symbol("symbol"));
194
                noNmb.push(Symbol("symbol"));
195
            }
196
            for (let i = 0; i < noStrSz; i += 1) {
197
                arg1 = noStr[i];
198
                expect(test11).toThrowError("parameter '" + (third ? "message" : "name") + "' must be a 'string'");
199
                for (let j = 0; j < (third ? noNmbSz : noStrSz); j += 1) {
200
                    if (third) {
201
                        arg2 = noNmb[j];
202
                        expect(test22).toThrowError("parameter 'message' must be a 'string'");
203
                        expect(test21).toThrowError("parameter 'code' must be a 'number'");
204
                    } else {
205
                        arg2 = noStr[j];
206
                        expect(test22).toThrowError("parameter 'name' must be a 'string'");
207
                        expect(test21).toThrowError("parameter 'message' must be a 'string'");
208
                        for (let e = 0; e < noNmb.length; e += 1) {
209
                            arg3 = noNmb[e];
210
                            expect(test33).toThrowError("parameter 'name' must be a 'string'");
211
                            expect(test32).toThrowError("parameter 'message' must be a 'string'");
212
                            expect(test31).toThrowError("parameter 'code' must be a 'number'");
213
                        }
214
                    }
215
                }
216
            }
217
        });
218
219
        // :: MEMBER PROPERTIES
220
221
        const name    = (third ? null : "qwerty");
222
        const message = "asdf";
223
        const code    = Math.round(Math.random() * 0xFFFFFFFF);
224
225
        it("should have all correct properties once instantiated", () => {
226
            instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
227
                if (even) {
228
                    if (third) {
229
                        expect(instance.name).toEqual(Klass.prototype.name);
230
                        expect(instance.message).toEqual(message);
231
                    } else {
232
                        expect(instance.name).toEqual(name);
233
                        expect(instance.message).toEqual(Klass.prototype.message);
234
                    }
235
                } else {
236
                    expect(instance.name).toEqual(Klass.prototype.name);
237
                    expect(instance.message).toEqual(Klass.prototype.message);
238
                }
239
                expect(instance.code).toBeNull();
240
            }, (instance, even1, even2) => {
241
                if (third) {
242
                    expect(instance.name).toEqual(Klass.prototype.name);
243
                    expect(instance.message).toEqual(even1 ? message : Klass.prototype.message);
244
                    expect(instance.code).toEqual(even2 ? code : null);
245
                } else {
246
                    expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
247
                    expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
248
                    expect(instance.code).toBeNull();
249
                }
250
            }, (instance, even1, even2, even3) => {
251
                expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
252
                expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
253
                expect(instance.code).toEqual(even3 ? code : null);
254
            }, third);
255
        });
256
257
        // :: MEMBER METHODS
258
259
        it("#toString()", () => {
260
            for (let i = 0; i < 2; i += 1) {
261
                let exp1;
262
                const even1   = (i % 2 === 0);
263
                const arg1    = (even1 ? (third ? message : name) : null);
264
                const source1 = new Klass(arg1);
265
                if (even1) {
266
                    exp1 = (third ? Klass.prototype.name : name);
267
                    exp1 += ": ";
268
                    exp1 += (third ? message : Klass.prototype.message);
269
                    exp1 += '.';
270
                } else {
271
                    exp1 = Klass.prototype.name + ": " + Klass.prototype.message + '.';
272
                }
273
                expect(source1.toString()).toEqual(exp1);
274
                for (let j = 0; j < 2; j += 1) {
275
                    let exp2;
276
                    const even2   = (j % 2 === 0);
277
                    const arg2    = (even2 ? (third ? code : message) : null);
278
                    const source2 = new Klass(arg1, arg2);
279
                    if (third) {
280
                        exp2 = Klass.prototype.name;
281
                        exp2 += (even2 ? " (0x" + code.toString(16) + "):" : ':' ) + ' ';
282
                        exp2 += (even1 ? message : Klass.prototype.message) + '.';
283
                    } else {
284
                        exp2 = (even1 ? name : Klass.prototype.name) + ':';
285
                        exp2 += ' ' + (even2 ? message : Klass.prototype.message) + '.';
286
                    }
287
                    expect(source2.toString()).toEqual(exp2);
288
                    if (!third) {
289
                        for (let e = 0; e < 2; e += 1) {
290
                            let exp3;
291
                            const even3   = (e % 2 === 0);
292
                            const arg3    = (even3 ? code : null);
293
                            const source3 = new Klass(arg1, arg2, arg3);
294
                            exp3  = (even1 ? name : Klass.prototype.name);
295
                            exp3 += (even3 ? " (0x" + code.toString(16) + "):" : ':') + ' ';
296
                            exp3 += (even2 ? message : Klass.prototype.message) + '.';
297
                            expect(source3.toString()).toEqual(exp3);
298
                        }
299
                    }
300
                }
301
            }
302
        });
303
304
        it("#native()", () => {
305
            instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
306
                    const exp1 = (even && third ? message : Klass.prototype.message);
307
                    expect(instance.native()).toEqual(new Error(exp1));
308
                }, (instance, even1, even2) => {
309
                    const even = (third ? even1 : even2);
310
                    const exp  = (even ? message : Klass.prototype.message);
311
                    expect(instance.native()).toEqual(new Error(exp));
312
                }, (instance, even1, even2) => {
313
                    const exp = (even2 ? message : Klass.prototype.message);
314
                    expect(instance.native()).toEqual(new Error(exp));
315
                }, third);
316
        });
317
    });
318
319
};
320
321
// Does a loop for each argument of the Klass constructor.
322
// If the iteration is even, the parameter is defined.
323
// If the iteration is odd, the parameter is null.
324
function instanceDefinedOrNull(Klass, name, message, code, fn1, fn2, fn3, third) {
325
    for (let i = 0; i < 2; i += 1) {
326
        const even1   = (i % 2 === 0);
327
        const arg1    = (even1 ? (third ? message : name) : null);
328
        const source1 = new Klass(arg1);
329
        fn1(source1, even1);
330
        for (let j = 0; j < 2; j += 1) {
331
            const even2   = (j % 2 === 0);
332
            const arg2    = (even2 ? (third ? code : message) : null);
333
            const source2 = new Klass(arg1, arg2);
334
            fn2(source2, even1, even2);
335
            if (!third) {
336
                for (let e = 0; e < 2; e += 1) {
337
                    const even3   = (e % 2 === 0);
338
                    const arg3    = (even3 ? code : null);
339
                    const source3 = new Klass(arg1, arg2, arg3);
340
                    fn3(source3, even1, even2, even3);
341
                }
342
            }
343
        }
344
    }
345
}