├── .gitignore ├── .solutions ├── stage-1 │ ├── abstraction-with-functions │ │ ├── solution.advanced.js │ │ └── solution.basic.js │ └── closures-and-scope │ │ └── solution.js └── stage-2 │ └── module-pattern │ └── solution.js ├── CONTRIBUTING.md ├── README.md ├── mentor-notes.md ├── package-lock.json ├── package.json ├── stage-1 ├── README.md ├── abstraction-with-functions │ ├── README.md │ ├── exercise.js │ └── exercise.test.js ├── closures-and-scope │ ├── README.md │ ├── exercise.js │ └── exercise.test.js ├── exercise │ ├── README.md │ ├── app.js │ ├── index.html │ ├── main.css │ └── stack-icon.png └── first-class-functions │ └── README.md └── stage-2 ├── README.md └── module-pattern ├── README.md ├── exercise.js └── exercise.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.solutions/stage-1/abstraction-with-functions/solution.advanced.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: Refactor the code! 3 | * 4 | * This file is a collection of functions you've been asked to refactor. 5 | * 6 | * The primary purpose of this exercise is to use your judgement to decide when 7 | * and where to introduce appropriate abstractions, and whether you can use 8 | * either abstractions provided by JavaScript, or write your own. 9 | * 10 | * The command 11 | * npm run s1.functions 12 | * will run tests to ensure the functions do what they should. They should all 13 | * still pass when you've finished refactoring. 14 | * 15 | * Advice: 16 | * + Try to recognise common patterns in the code. 17 | * + When you have recognised a pattern, think about if you could make a 18 | * function to encapsulate it, instead of repeating code in several places. 19 | */ 20 | 'use strict'; 21 | 22 | /* 23 | * More advanced solution 24 | * 25 | * We recognise that every function needs to iterate over the keys and values of 26 | * an object. 27 | * 28 | * We also recognise that every function makes changes to either the keys or the 29 | * values while it iterates. 30 | * 31 | * This is the central pattern that we can write an abstraction for. 32 | */ 33 | 34 | 35 | /* 36 | * This is our central abstraction. It is a higher-order function that takes 37 | * care of iterating over an object and constructing a new one. It accepts two 38 | * functions as arguments that specify how each key and value should be changed. 39 | * The final argument is the object to be mapped over. 40 | */ 41 | function mapObj (keyMap, valueMap, input) { 42 | return Object.keys(input) 43 | .reduce(function (acc, key) { 44 | acc[keyMap(key)] = input[valueMap(key)]; 45 | return acc; 46 | }, {}); 47 | } 48 | 49 | /* 50 | * Now we have a collection of functions that we can pass to our `mapObj` 51 | * function that will change the way that it maps over a given object. 52 | */ 53 | 54 | function identity (x) { 55 | return x; 56 | } 57 | 58 | function capitalise (str) { 59 | return str.slice(0, 1).toUpperCase().concat(str.slice(1)); 60 | } 61 | 62 | function reverse (str) { 63 | return str.split('').reverse().join(''); 64 | } 65 | 66 | function increment (n) { 67 | return n + 1; 68 | } 69 | 70 | 71 | /* 72 | * Now we define the functions we want in terms of the `mapObj` higher-order 73 | * function and the other functions we defined above. 74 | */ 75 | 76 | function capitaliseObjectKeys (input) { 77 | // This says: 78 | // > map over the object 79 | // > use `capitalise` to transform the object keys 80 | // > use `identity` to transform the object values 81 | return mapObj(capitalise, identity, input); 82 | } 83 | 84 | function capitaliseObjectValues (input) { 85 | // This says: 86 | // > map over the object 87 | // > use `identity` to transform the object keys 88 | // > use `capitalise` to transform the object values 89 | return mapObj(identity, capitalise, input); 90 | } 91 | 92 | function incrementObjectValues (input) { 93 | // This says: 94 | // > map over the object 95 | // > use `identity` to transform the object keys 96 | // > use `increment` to transform the object values 97 | return mapObj(identity, increment, input); 98 | } 99 | 100 | function reverseObjectKeys (input) { 101 | // This says: 102 | // > map over the object 103 | // > use `reverse` to transform the object keys 104 | // > use `identity` to transform the object values 105 | return mapObj(reverse, identity, input); 106 | } 107 | 108 | 109 | module.exports = { 110 | capitaliseObjectKeys, 111 | capitaliseObjectValues, 112 | incrementObjectValues, 113 | reverseObjectKeys, 114 | }; 115 | -------------------------------------------------------------------------------- /.solutions/stage-1/abstraction-with-functions/solution.basic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: Refactor the code! 3 | * 4 | * This file is a collection of functions you've been asked to refactor. 5 | * 6 | * The primary purpose of this exercise is to use your judgement to decide when 7 | * and where to introduce appropriate abstractions, and whether you can use 8 | * either abstractions provided by JavaScript, or write your own. 9 | * 10 | * The command 11 | * npm run s1.functions 12 | * will run tests to ensure the functions do what they should. They should all 13 | * still pass when you've finished refactoring. 14 | * 15 | * Advice: 16 | * + Try to recognise common patterns in the code. 17 | * + When you have recognised a pattern, think about if you could make a 18 | * function to encapsulate it, instead of repeating code in several places. 19 | */ 20 | 'use strict'; 21 | 22 | /* 23 | * Basic solution 24 | * 25 | * Replace for-loops with reduce 26 | * Abstracts capitilsation operation 27 | */ 28 | 29 | 30 | function capitalise (str) { 31 | return str.slice(0, 1).toUpperCase().concat(str.slice(1)); 32 | } 33 | 34 | function capitaliseObjectKeys (input) { 35 | return Object.keys(input) 36 | .reduce(function (acc, key) { 37 | acc[capitalise(key)] = input[key]; 38 | return acc; 39 | }, {}); 40 | } 41 | 42 | 43 | function capitaliseObjectValues (input) { 44 | return Object.keys(input) 45 | .reduce(function (acc, key) { 46 | acc[key] = capitalise(input[key]); 47 | return acc; 48 | }, {}); 49 | } 50 | 51 | function incrementObjectValues (input) { 52 | return Object.keys(input) 53 | .reduce(function (acc, key) { 54 | acc[key] = input[key] + 1; 55 | return acc; 56 | }, {}); 57 | } 58 | 59 | function reverseObjectKeys (input) { 60 | return Object.keys(input) 61 | .reduce(function (acc, key) { 62 | const reversedKey = key.split('').reverse().join(''); 63 | acc[reversedKey] = input[key]; 64 | return acc; 65 | }, {}); 66 | } 67 | 68 | 69 | module.exports = { 70 | capitaliseObjectKeys, 71 | capitaliseObjectValues, 72 | incrementObjectValues, 73 | reverseObjectKeys, 74 | }; 75 | -------------------------------------------------------------------------------- /.solutions/stage-1/closures-and-scope/solution.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: The code is broken, fix it! 3 | * 4 | * This exercise is a collection of empty functions; fill them in in order to 5 | * make the tests pass. 6 | * 7 | * When you think you have finished, run the command: 8 | * npm run s1.closures 9 | * This will run a series of tests which should all pass. 10 | */ 11 | 'use strict'; 12 | 13 | 14 | var inc = 1; 15 | 16 | /* 17 | * This function should increase the value passed in as an argument by the 18 | * increment defined by the `inc` variable. For example: 19 | * 20 | * increment(2); // returns 3 21 | */ 22 | function increment (n) { 23 | return n + inc; 24 | } 25 | 26 | 27 | /* 28 | * This function should return a function that increments its argument by one 29 | * plus whatever number is given as an argument. For example: 30 | * 31 | * var incBy3 = createIncrementer(3); 32 | * var incBy2 = createIncrementer(2); 33 | * incBy3(2); // returns 5 34 | * incBy2(2); // returns 4 35 | */ 36 | function createIncrementer (base) { 37 | return function (n) { 38 | return base + n; 39 | } 40 | } 41 | 42 | 43 | /* 44 | * This function should return an object that represents a counter and contains 45 | * three methods: `inc`, which increments the counter by 1, `dec` which 46 | * decrements the counter by 1, and `read`, which returns the current value of 47 | * the counter. For example: 48 | * 49 | * var counter = createCounter(); 50 | * counter.read() // returns 0 51 | * counter.inc() 52 | * counter.read() // returns 1 53 | * counter.dec() 54 | * counter.read() // returns 0 55 | */ 56 | function createCounter () { 57 | var state = 0; 58 | 59 | return { 60 | inc: function (n) { state++; }, 61 | dec: function (n) { state--; }, 62 | read: function (n) { return state; }, 63 | } 64 | } 65 | 66 | 67 | module.exports = { 68 | increment, 69 | createIncrementer, 70 | createCounter, 71 | }; 72 | -------------------------------------------------------------------------------- /.solutions/stage-2/module-pattern/solution.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: Create some modules! 3 | * 4 | * When you think you have finished, run the command: 5 | * npm run s2.modules 6 | * This will run a series of tests which should all pass. 7 | */ 8 | 'use strict'; 9 | 10 | /* 11 | * Create a single module (using an IIFE) which contains functionality to parse 12 | * URLs. 13 | * 14 | * We have started you off with the basic structure. 15 | * 16 | * https :// www.example.com / hello ? foo=1&bar=2 17 | * | | | | | | | | 18 | * | protocol | | domain | | path | | querystring | 19 | */ 20 | var UrlParser = (function () { 21 | var urlRegEx = /^([a-z]+):\/\/([a-z\.]+)\/([^\?]+)\?(.+)$/i; 22 | 23 | function getMatch (str) { 24 | return str.match(urlRegEx); 25 | } 26 | 27 | function nth (n, array) { 28 | return (array || [])[n]; 29 | } 30 | 31 | return { 32 | // a function that takes a URL and returns its protocol 33 | protocol: function getProtocol (url) { 34 | return nth(1, getMatch(url)); 35 | }, 36 | 37 | // a function that takes a URL and returns its domain 38 | domain: function getDomain (url) { 39 | return nth(2, getMatch(url)); 40 | }, 41 | 42 | // a function that takes a URL and returns its path 43 | path: function getPath (url) { 44 | return nth(3, getMatch(url)); 45 | }, 46 | 47 | // a function that takes a URL and returns its query string 48 | querystring: function getQueryString (url) { 49 | return nth(4, getMatch(url)); 50 | }, 51 | }; 52 | })(); 53 | 54 | 55 | /* 56 | * Create a module that can support multiple instances (like in our example). 57 | * The module should be a function with several additional methods attached as 58 | * attributes. 59 | * 60 | * Example: 61 | * var exampleBuilder = createUrlBuilder('https://example.com'); 62 | * 63 | * var url = exampleBuilder({ query: { foo: 1, bar: 2 }, path: 'hello' }); 64 | * 65 | * console.log(url); // https://example.com/hello?foo=1&bar=2 66 | * 67 | * exampleBuilder. 68 | */ 69 | var createUrlBuilder = function (host) { 70 | 71 | function queryObjectToString (query) { 72 | return Object.keys(query) 73 | .map(function (key) { 74 | return key + '=' + query[key]; 75 | }) 76 | .join('&'); 77 | } 78 | 79 | function appendPath (base, path) { 80 | return base + '/' + path; 81 | } 82 | 83 | function appendQueryString (base, query) { 84 | return base + '?' + queryObjectToString(query); 85 | } 86 | 87 | var builder = function (config) { 88 | var url = host; 89 | 90 | if (config.path) { 91 | url = appendPath(url, config.path); 92 | } 93 | 94 | if (config.query) { 95 | url = appendQueryString(url, config.query); 96 | } 97 | 98 | return url; 99 | }; 100 | 101 | builder.path = function (path) { 102 | return appendPath(host, path); 103 | }; 104 | 105 | builder.query = function (query) { 106 | return appendQueryString(host, query); 107 | }; 108 | 109 | return builder; 110 | }; 111 | 112 | 113 | module.exports = { 114 | UrlParser, 115 | createUrlBuilder, 116 | }; 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | For now, see the [Founders and Coders contributing guide](https://github.com/foundersandcoders/master-reference/blob/master/CONTRIBUTING.md) 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elements of Software Design in Javascript 2 | 3 | **Author**: [@eliascodes](https://github.com/eliascodes) 4 | **Maintainer**: [@eliascodes](https://github.com/eliascodes) 5 | 6 | ## Introduction 7 | This is a resource to introduce some of the basic elements of software design to beginners. Created for the [Founders & Coders](https://github.com/foundersandcoders) programme. 8 | 9 | ## Intent 10 | The intent is for this resource: 11 | * to become a stack-agnostic beginners guide to the elements of software design & architecture relevant to beginners and junior developers. 12 | * to be tailored to the particular requirements of the Founders and Coders programme. 13 | * to be structured in a modular enough way that several workshops, pitched at different experience levels, could potentially be constructed from it. 14 | 15 | This is a work in progress. Contributions [are welcome](./CONTRIBUTING.md). 16 | 17 | ## Before Starting 18 | If you intend to attempt the exercises in this repo, first clone the repo: 19 | ``` 20 | $ git clone git@github.com:foundersandcoders/ws-software-design-js.git 21 | ``` 22 | Then install the dependencies 23 | ``` 24 | $ npm install 25 | ``` 26 | 27 | ## Contents 28 | This resource is structured in _stages_, each consisting of multiple topics. It is recommended to be comfortable with the topics in one stage before moving on to another. 29 | 30 | It is advised to spend around 15-20 minutes reading each topic within the stages, after which mentors will go through the material and ask questions to check understanding. 31 | 32 | * [Stage 1](./stage-1) 33 | * [Stage 2](./stage-2) 34 | * Others coming soon... 35 | -------------------------------------------------------------------------------- /mentor-notes.md: -------------------------------------------------------------------------------- 1 | ## Notes for mentors 2 | 3 | ### Structure of Software Design Workshop 4 | 5 | This is a 3 hour workshop with probably more material and exercises to complete than is likely possible to. As such, mentors should let students know in advance that they probably will not complete everything and instead should focus on understanding the material. 6 | 7 | Mentors can help students achieve this by telling them to spend around 15 minutes reading each section in Stage 1. After this mentors should spend 10 minutes talking through the material and lollipop questions (see some examples below). After this, students can either move on to the next section or on to the exercise if the section has one. 8 | 9 | ### Questions 10 | 11 | **First-class functions:** 12 | * What is a first-class function? 13 | * Can you give two examples of how functions can be used? 14 | 15 | **Abstraction with functions** 16 | * Can you give an example of when you might use abstraction? 17 | * Can you give any examples of abstractions that are provided by JavaScript? 18 | * Why is abstraction useful? 19 | 20 | **Closures and Scope** 21 | * What is block scope? 22 | * What is a closure? 23 | * Can you explain the difference between var, let and const? 24 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elements-js-design", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "ansi-regex": { 7 | "version": "2.1.1", 8 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 9 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 10 | "dev": true 11 | }, 12 | "ansi-styles": { 13 | "version": "2.2.1", 14 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 15 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 16 | "dev": true 17 | }, 18 | "balanced-match": { 19 | "version": "1.0.0", 20 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 21 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 22 | "dev": true 23 | }, 24 | "brace-expansion": { 25 | "version": "1.1.8", 26 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 27 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 28 | "dev": true 29 | }, 30 | "chalk": { 31 | "version": "1.1.3", 32 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 33 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 34 | "dev": true 35 | }, 36 | "concat-map": { 37 | "version": "0.0.1", 38 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 39 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 40 | "dev": true 41 | }, 42 | "core-util-is": { 43 | "version": "1.0.2", 44 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 45 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 46 | "dev": true 47 | }, 48 | "deep-equal": { 49 | "version": "1.0.1", 50 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 51 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", 52 | "dev": true 53 | }, 54 | "define-properties": { 55 | "version": "1.1.2", 56 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", 57 | "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", 58 | "dev": true 59 | }, 60 | "defined": { 61 | "version": "1.0.0", 62 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 63 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", 64 | "dev": true 65 | }, 66 | "duplexer": { 67 | "version": "0.1.1", 68 | "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 69 | "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", 70 | "dev": true 71 | }, 72 | "es-abstract": { 73 | "version": "1.7.0", 74 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz", 75 | "integrity": "sha1-363ndOAb/Nl/lhgCmMRJyGI/uUw=", 76 | "dev": true 77 | }, 78 | "es-to-primitive": { 79 | "version": "1.1.1", 80 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", 81 | "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", 82 | "dev": true 83 | }, 84 | "escape-string-regexp": { 85 | "version": "1.0.5", 86 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 87 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 88 | "dev": true 89 | }, 90 | "figures": { 91 | "version": "1.7.0", 92 | "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", 93 | "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", 94 | "dev": true 95 | }, 96 | "for-each": { 97 | "version": "0.3.2", 98 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", 99 | "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", 100 | "dev": true 101 | }, 102 | "foreach": { 103 | "version": "2.0.5", 104 | "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", 105 | "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", 106 | "dev": true 107 | }, 108 | "fs.realpath": { 109 | "version": "1.0.0", 110 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 111 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 112 | "dev": true 113 | }, 114 | "function-bind": { 115 | "version": "1.1.0", 116 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", 117 | "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", 118 | "dev": true 119 | }, 120 | "glob": { 121 | "version": "7.1.2", 122 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 123 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 124 | "dev": true 125 | }, 126 | "has": { 127 | "version": "1.0.1", 128 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 129 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 130 | "dev": true 131 | }, 132 | "has-ansi": { 133 | "version": "2.0.0", 134 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 135 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 136 | "dev": true 137 | }, 138 | "inflight": { 139 | "version": "1.0.6", 140 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 141 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 142 | "dev": true 143 | }, 144 | "inherits": { 145 | "version": "2.0.3", 146 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 147 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 148 | "dev": true 149 | }, 150 | "is-callable": { 151 | "version": "1.1.3", 152 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", 153 | "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", 154 | "dev": true 155 | }, 156 | "is-date-object": { 157 | "version": "1.0.1", 158 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 159 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 160 | "dev": true 161 | }, 162 | "is-finite": { 163 | "version": "1.0.2", 164 | "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", 165 | "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", 166 | "dev": true 167 | }, 168 | "is-function": { 169 | "version": "1.0.1", 170 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", 171 | "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=", 172 | "dev": true 173 | }, 174 | "is-regex": { 175 | "version": "1.0.4", 176 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 177 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 178 | "dev": true 179 | }, 180 | "is-symbol": { 181 | "version": "1.0.1", 182 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", 183 | "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", 184 | "dev": true 185 | }, 186 | "isarray": { 187 | "version": "1.0.0", 188 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 189 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 190 | "dev": true 191 | }, 192 | "lodash": { 193 | "version": "3.10.1", 194 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", 195 | "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", 196 | "dev": true 197 | }, 198 | "minimatch": { 199 | "version": "3.0.4", 200 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 201 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 202 | "dev": true 203 | }, 204 | "minimist": { 205 | "version": "1.2.0", 206 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 207 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 208 | "dev": true 209 | }, 210 | "number-is-nan": { 211 | "version": "1.0.1", 212 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 213 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", 214 | "dev": true 215 | }, 216 | "object-assign": { 217 | "version": "4.1.1", 218 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 219 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 220 | "dev": true 221 | }, 222 | "object-inspect": { 223 | "version": "1.2.2", 224 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.2.2.tgz", 225 | "integrity": "sha1-yCEV5PzIiK6hTWTCLk8X9qcNXlo=", 226 | "dev": true 227 | }, 228 | "object-keys": { 229 | "version": "1.0.11", 230 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", 231 | "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", 232 | "dev": true 233 | }, 234 | "once": { 235 | "version": "1.4.0", 236 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 237 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 238 | "dev": true 239 | }, 240 | "parse-ms": { 241 | "version": "1.0.1", 242 | "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz", 243 | "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=", 244 | "dev": true 245 | }, 246 | "path-is-absolute": { 247 | "version": "1.0.1", 248 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 249 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 250 | "dev": true 251 | }, 252 | "path-parse": { 253 | "version": "1.0.5", 254 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 255 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 256 | "dev": true 257 | }, 258 | "plur": { 259 | "version": "1.0.0", 260 | "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz", 261 | "integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY=", 262 | "dev": true 263 | }, 264 | "pretty-ms": { 265 | "version": "2.1.0", 266 | "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz", 267 | "integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=", 268 | "dev": true 269 | }, 270 | "process-nextick-args": { 271 | "version": "1.0.7", 272 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 273 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", 274 | "dev": true 275 | }, 276 | "re-emitter": { 277 | "version": "1.1.3", 278 | "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.3.tgz", 279 | "integrity": "sha1-+p4xn/3u6zWycpbvDz03TawvUqc=", 280 | "dev": true 281 | }, 282 | "readable-stream": { 283 | "version": "2.3.2", 284 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.2.tgz", 285 | "integrity": "sha1-WgTfBeT1f+Pw3Gj90R3FyXx+b00=", 286 | "dev": true 287 | }, 288 | "repeat-string": { 289 | "version": "1.6.1", 290 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 291 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", 292 | "dev": true 293 | }, 294 | "resolve": { 295 | "version": "1.3.3", 296 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", 297 | "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", 298 | "dev": true 299 | }, 300 | "resumer": { 301 | "version": "0.0.0", 302 | "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", 303 | "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", 304 | "dev": true 305 | }, 306 | "safe-buffer": { 307 | "version": "5.1.1", 308 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 309 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", 310 | "dev": true 311 | }, 312 | "split": { 313 | "version": "1.0.0", 314 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.0.tgz", 315 | "integrity": "sha1-xDlc5oOrzSVLwo/h2rtuXCfc/64=", 316 | "dev": true 317 | }, 318 | "string_decoder": { 319 | "version": "1.0.3", 320 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 321 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 322 | "dev": true 323 | }, 324 | "string.prototype.trim": { 325 | "version": "1.1.2", 326 | "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", 327 | "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", 328 | "dev": true 329 | }, 330 | "strip-ansi": { 331 | "version": "3.0.1", 332 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 333 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 334 | "dev": true 335 | }, 336 | "supports-color": { 337 | "version": "2.0.0", 338 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 339 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 340 | "dev": true 341 | }, 342 | "tap-out": { 343 | "version": "1.4.2", 344 | "resolved": "https://registry.npmjs.org/tap-out/-/tap-out-1.4.2.tgz", 345 | "integrity": "sha1-yQfsG/lAURHQiCY+kvVgi4jLs3o=", 346 | "dev": true 347 | }, 348 | "tap-spec": { 349 | "version": "4.1.1", 350 | "resolved": "https://registry.npmjs.org/tap-spec/-/tap-spec-4.1.1.tgz", 351 | "integrity": "sha1-4unyb1IIIysfViKIyXYk1YqI8Fo=", 352 | "dev": true 353 | }, 354 | "tape": { 355 | "version": "4.7.0", 356 | "resolved": "https://registry.npmjs.org/tape/-/tape-4.7.0.tgz", 357 | "integrity": "sha512-ePzu2KfZYVtq0v+KKGxBJ9HJWYZ4MaQWeGabD+KpVdMKRen3NJPf6EiwA5BxfMkhQPGtCwnOFWelcB39bhOUng==", 358 | "dev": true 359 | }, 360 | "through": { 361 | "version": "2.3.8", 362 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 363 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 364 | "dev": true 365 | }, 366 | "through2": { 367 | "version": "2.0.3", 368 | "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", 369 | "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", 370 | "dev": true 371 | }, 372 | "trim": { 373 | "version": "0.0.1", 374 | "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", 375 | "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", 376 | "dev": true 377 | }, 378 | "util-deprecate": { 379 | "version": "1.0.2", 380 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 381 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 382 | "dev": true 383 | }, 384 | "wrappy": { 385 | "version": "1.0.2", 386 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 387 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 388 | "dev": true 389 | }, 390 | "xtend": { 391 | "version": "4.0.1", 392 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 393 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", 394 | "dev": true 395 | } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elements-js-design", 3 | "version": "1.0.0", 4 | "description": "Introduction to elements of JavaScript design for beginners & junior developers", 5 | "main": "README.md", 6 | "scripts": { 7 | "s1": "tape './stage-1/**/*.test.js' | tap-spec", 8 | "s1.firstclass": "tape './stage-1/first-class-functions/exercise.test.js' | tap-spec", 9 | "s1.closures": "tape './stage-1/closures-and-scope/exercise.test.js' | tap-spec", 10 | "s1.functions": "tape './stage-1/abstraction-with-functions/exercise.test.js' | tap-spec", 11 | "s2": "tape './stage-2/**/*.test.js' | tap-spec", 12 | "s2.module": "tape './stage-2/module-pattern/exercise.test.js' | tap-spec", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/foundersandcoders/workshop-client-side-design.git" 18 | }, 19 | "author": "eliascodes", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/foundersandcoders/workshop-client-side-design/issues" 23 | }, 24 | "homepage": "https://github.com/foundersandcoders/workshop-client-side-design#readme", 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "tap-spec": "^4.1.1", 28 | "tape": "^4.7.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stage-1/README.md: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | This first stage concerns itself with fundamental concepts of JavaScript that most design patterns will be built upon. We recommend that you are comfortable with these concepts before moving on to other stages. 3 | 4 | ## Contents 5 | 1. [Functions as First Class Objects](./first-class-functions) 6 | 2. [Closures and Scope](./closures-and-scope) 7 | 3. [Abstraction with Functions (Part 1)](./abstraction-with-functions) 8 | 4. [Final Exercise](./exercise) 9 | 10 | ## Where to Start? 11 | Start with whichever topic you're most interested in, and feel free to skip any you feel you are already comfortable with. For those new to these topics, we recommend proceeding in the order given by the [contents](#contents) section. 12 | -------------------------------------------------------------------------------- /stage-1/abstraction-with-functions/README.md: -------------------------------------------------------------------------------- 1 | # Abstraction with Functions 2 | 3 | ## Learning Outcomes 4 | * Understand what abstraction is and why it is useful 5 | 6 | ## What is abstraction? 7 | The term "abstraction" as it is used here, is the process by which we allow the developer to work at a higher conceptual level by limiting the amount of low-level complexity we expose them to. Let's illustrate this with an example: 8 | 9 | ```js 10 | // Task: 11 | // Write a function that calculates the square root of each number in a collection of numbers 12 | 13 | // Here is our collection 14 | var list = [1, 2, 3, 4]; 15 | 16 | // This is an example of a solution that would be considered "low-level" 17 | function sqRoot_1 (xs) { 18 | // Here we are telling the computer explicitly _how_ to do the following: 19 | // * (1) Initialise the output 20 | // * (2) Iterate through an array 21 | // * (3) Calculate the result for each item in the input 22 | // * (4) Add results to the output for each item in the input 23 | var result = []; // (1) 24 | for (var i = 0; i < xs.length; i++) { // (2) 25 | var sqrt = xs[i] ** 0.5 // (3) 26 | result.push(sqrt); // (4) 27 | } 28 | return result; 29 | } 30 | 31 | // This is an example of a solution that would be considered to be at a higher conceptual 32 | // level than the previous solution 33 | function sqRoot_2 (xs) { 34 | // Here we use the provided `map` array method. This method abstracts away steps 35 | // (1), (2), and (4) above; the only _how_ we need to specify is (3). 36 | // 37 | // We are simply able to say "I want a new array where each value is the square 38 | // root of the original", and have the language take care of the tedious steps 39 | // of construction and iteration. 40 | return xs.map(function (x) { 41 | return x ** 0.5; 42 | }); 43 | } 44 | ``` 45 | 46 | To be clear: while in the example above the higher-level solution is shorter than the lower-level one, abstraction is strictly _not_ about writing shorter code; it is about hiding unnecessary complexity and writing code in a way that more closely resembles your mental model of the problem. 47 | 48 | ## How can abstraction be useful? 49 | Aside from the kinds of examples above, where we substitute low-level code with abstractions that are provided by the language, it is enormously powerful to be able to create _your own_ abstractions, which are specific to the software you are writing and/or the domain within which you are working. 50 | 51 | For example, think about a typical front-end web application. It will almost certainly have the following elements: 52 | * Attaching event listeners to DOM nodes 53 | * Making AJAX requests 54 | 55 | A naive, low-level implementation will be littered with code that looks something like the following: 56 | 57 | ```js 58 | // ... 59 | 60 | document.querySelector('#foo').addEventListener('submit', function (event) { 61 | var xhr = new XMLHttpRequest(); 62 | 63 | xhr.addEventListener('load', function () { 64 | if (xhr.status === 200) { 65 | var response = JSON.parse(xhr.responseText); 66 | // ... do something with the response 67 | } 68 | }); 69 | 70 | xhr.open('GET', 'https://example.com/search?query=' + event.target[0].value); 71 | xhr.send(); 72 | }); 73 | 74 | document.querySelector('#bar').addEventListener('click', function (event) { 75 | var xhr = new XMLHttpRequest(); 76 | 77 | xhr.addEventListener('load', function () { 78 | if (xhr.status === 200) { 79 | var response = JSON.parse(xhr.responseText); 80 | // ... do something with the response 81 | } 82 | }); 83 | 84 | var element = document.querySelector('#pam'); 85 | xhr.open('GET', 'https://lulz.org/search?query=' + element.value); 86 | xhr.send(); 87 | }); 88 | 89 | // ... 90 | ``` 91 | 92 | That is, we repeatedly instruct the computer _how_ to attach event listeners, _how_ to send AJAX requests, _how_ it should treat the response, and so on. This burdens the developer with remembering all these details each time, and having to wade through lots of code while reading. 93 | 94 | Wouldn't it be better for us to avoid this if possible? Looking again at the elements identified above, let's attempt to abstract away the process of attaching an event listener to a DOM element, and similarly the process of making an AJAX request: 95 | 96 | ```js 97 | // ... 98 | 99 | // First we define some functions that offer simple interfaces for common tasks. 100 | // The functions are abstractions that hide the full complexity of the task from 101 | // the developer. 102 | 103 | function addListener (selector, eventName, callback) { 104 | document.querySelector(selector).addEventListener(eventName, callback); 105 | } 106 | 107 | function fetch (url, callback) { 108 | var xhr = new XMLHttpRequest(); 109 | 110 | xhr.addEventListener('load', function () { 111 | if (xhr.status === 200) { 112 | var response = JSON.parse(xhr.responseText); 113 | return callback(response); 114 | } 115 | }); 116 | 117 | xhr.open('GET', url); 118 | xhr.send(); 119 | } 120 | 121 | // ... then elsewhere in the application, we use these abstractions instead. 122 | // Our code then becomes easier to read and allows the developer to simply add 123 | // listeners or make AJAX requests without worrying about _how_ they are made. 124 | 125 | addListener('#foo', 'submit', function (event) { 126 | var url = 'https://example.com/search?query=' + event.target[0].value; 127 | 128 | fetch(url, function (response) { 129 | // ... do something with the response 130 | }); 131 | }); 132 | 133 | addListener('#bar', 'click', function (event) { 134 | var element = document.querySelector('#pam'); 135 | var url = 'https://lulz.org/search?query=' + element.value; 136 | 137 | fetch(url, function (response) { 138 | // ... do something with the response 139 | }); 140 | }); 141 | 142 | // ... 143 | ``` 144 | 145 | ## Exercises 146 | Attempt `./exercise.js` in this directory. 147 | 148 | You can check your code by running the test suite for this exercise with `npm run s1.functions` 149 | -------------------------------------------------------------------------------- /stage-1/abstraction-with-functions/exercise.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: Refactor the code! 3 | * 4 | * This file is a collection of functions you've been asked to refactor. 5 | * 6 | * The primary purpose of this exercise is to use your judgement to decide when 7 | * and where to introduce appropriate abstractions, and whether you can use 8 | * either abstractions provided by JavaScript, or write your own. 9 | * 10 | * The command 11 | * npm run s1.functions 12 | * will run tests to ensure the functions do what they should. They should all 13 | * still pass when you've finished refactoring. 14 | * 15 | * Advice: 16 | * + Try to recognise common patterns in the code. 17 | * + When you have recognised a pattern, think about if you could make a 18 | * function to encapsulate it, instead of repeating code in several places. 19 | */ 20 | 'use strict'; 21 | 22 | function capitaliseObjectKeys (input) { 23 | const keys = Object.keys(input); 24 | const result = {}; 25 | 26 | for (var ii = 0; ii < keys.length; ii++) { 27 | const capitalisedKey = keys[ii].slice(0, 1).toUpperCase().concat(keys[ii].slice(1)); 28 | result[capitalisedKey] = input[keys[ii]]; 29 | } 30 | 31 | return result; 32 | } 33 | 34 | 35 | function capitaliseObjectValues (input) { 36 | const keys = Object.keys(input); 37 | const result = {}; 38 | 39 | for (var ii = 0; ii < keys.length; ii++) { 40 | const value = input[keys[ii]]; 41 | const capitalisedValue = value.slice(0, 1).toUpperCase().concat(value.slice(1)); 42 | result[keys[ii]] = capitalisedValue; 43 | } 44 | 45 | return result; 46 | } 47 | 48 | function incrementObjectValues (input) { 49 | const keys = Object.keys(input); 50 | const result = {}; 51 | 52 | for (var ii = 0; ii < keys.length; ii++) { 53 | const value = input[keys[ii]]; 54 | result[keys[ii]] = value + 1; 55 | } 56 | 57 | return result; 58 | } 59 | 60 | function reverseObjectKeys (input) { 61 | const keys = Object.keys(input); 62 | const result = {}; 63 | 64 | for (var ii = 0; ii < keys.length; ii++) { 65 | const reversedKey = keys[ii].split('').reverse().join(''); 66 | result[reversedKey] = input[keys[ii]]; 67 | } 68 | 69 | return result; 70 | } 71 | 72 | module.exports = { 73 | capitaliseObjectKeys, 74 | capitaliseObjectValues, 75 | incrementObjectValues, 76 | reverseObjectKeys, 77 | }; 78 | -------------------------------------------------------------------------------- /stage-1/abstraction-with-functions/exercise.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tape'); 4 | const exercise = require('./exercise.js'); 5 | 6 | tape('Abstraction with Functions', function (test) { 7 | test.test('capitaliseObjectKeys', (t) => { 8 | const input = { foo: 'foo', bar: 'bar' }; 9 | 10 | const result = exercise.capitaliseObjectKeys(input); 11 | 12 | t.deepEqual(result, { Foo: 'foo', Bar: 'bar' }); 13 | t.end(); 14 | }); 15 | 16 | test.test('capitaliseObjectValues', (t) => { 17 | const input = { foo: 'foo', bar: 'bar' }; 18 | 19 | const result = exercise.capitaliseObjectValues(input); 20 | 21 | t.deepEqual(result, { foo: 'Foo', bar: 'Bar' }); 22 | t.end(); 23 | }); 24 | 25 | test.test('incrementObjectValues', (t) => { 26 | const input = { foo: 1, bar: 4, baz: -1 }; 27 | 28 | const result = exercise.incrementObjectValues(input); 29 | 30 | t.deepEqual(result, { foo: 2, bar: 5, baz: 0 }); 31 | t.end(); 32 | }); 33 | 34 | test.test('reverseObjectKeys', (t) => { 35 | const input = { foo: 1, bar: 4, baz: -1 }; 36 | 37 | const result = exercise.reverseObjectKeys(input); 38 | 39 | t.deepEqual(result, { oof: 1, rab: 4, zab: -1 }); 40 | t.end(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /stage-1/closures-and-scope/README.md: -------------------------------------------------------------------------------- 1 | # Closures and Scope 2 | 3 | ## Learning Outcomes 4 | * Understand the term 'scope' 5 | * Understand what a closure is and how to create one 6 | 7 | ## What is meant by scope in JavaScript? 8 | The idea of 'scope' refers to what the _execution context_ of a particular piece of code is. One of the most important things this context determines is what variables are available to that piece of code. 9 | 10 | In ES5 JavaScript, scope is exclusively delimited by functions. In ES6 JavaScript, block-scoping has been introduced via the `let` and `const` keywords. Both these things will be explained further on. 11 | 12 | ## Function Scope 13 | The global scope is the default scope for all JavaScript in the browser. Any JavaScript executed by the browser can access variables defined in global scope. 14 | 15 | ```js 16 | // global scope 17 | var foo = 'bar'; 18 | ``` 19 | 20 | Creating a new scope is as simple as creating a new function: 21 | 22 | ```js 23 | // global scope (scope A) 24 | var foo = 'foo'; 25 | 26 | function bar () { 27 | // local scope (scope B) 28 | 29 | var pam = 'pam'; // pam is now available in scope B, but not scope A 30 | var a = foo + pam; // foo is available in scope B, because it was defined in global scope 31 | return a; 32 | } 33 | 34 | console.log(bar()); // 'foopam' 35 | console.log(foo); // 'foo' 36 | console.log(pam); // ReferenceError 37 | ``` 38 | 39 | More formally, each function has access to its own _local_ scope, and also the scope of the function that _encloses_ it (or global scope, if there is no enclosing function). 40 | 41 | ```js 42 | 43 | // global scope (scope A) 44 | var foo = 'foo'; 45 | 46 | function bar () { 47 | // local scope (scope B) 48 | // Has access to: scope B and scope A 49 | var a = 'a'; 50 | 51 | function pam () { 52 | // local scope (scope C) 53 | // Has access to: scope C, scope B and scope A 54 | var b = 'b'; 55 | return foo + b + a; 56 | } 57 | 58 | return pam(); 59 | } 60 | 61 | console.log(foo); // foo 62 | console.log(bar()); // fooba 63 | console.log(a); // ReferenceError 64 | console.log(b); // ReferenceError 65 | ``` 66 | 67 | This relationship is applied recursively, which means that no matter how deeply nested a function is, it will have access to variables defined in all scopes enclosing it (including the global scope). 68 | 69 | ## Block Scope 70 | 71 | Block scoping was introduced to JavaScript via the `let` and `const` keywords, which are used to declare variables in the same way as `var`. The difference (in terms of scoping at least) is that while variables declared with `var` are available within the function in which they're defined (and all sub-functions), variables defined with `let` and `const` are only available within the _block_ they're defined in: 72 | 73 | ```js 74 | // global scope 75 | 76 | var bar = 'bar'; 77 | var array = [1, 2, 3]; 78 | 79 | if (true) { 80 | // local block scope A 81 | let pam = bar + 'pam'; // barpam 82 | const j = 0; 83 | } 84 | 85 | for (let i = 0; i < array.length; i++) { 86 | // local block scope B 87 | console.log(i, array[i]); 88 | 89 | console.log(j); // ReferenceError 90 | console.log(pam); // ReferenceError 91 | } 92 | 93 | console.log(pam); // ReferenceError 94 | console.log(i); // ReferenceError 95 | console.log(j); // ReferenceError 96 | ``` 97 | 98 | The differences between `var`, `let` and `const` can be summarised as follows: 99 | * Variables declared with `var` can be reassigned and are function-scoped. 100 | * Variables declared with `let` can be reassigned and are block-scoped. 101 | * Variables declared with `const` cannot be reassigned and are block-scoped. 102 | 103 | ## What is a Closure? 104 | If you have been writing JavaScript for a while, chances are you will probably have used closures already without realising. 105 | 106 | Simply defined, a closure is a technique for creating scopes that persist even after the function they are defined by has returned. Again, let's illustrate this with an example: 107 | 108 | ```js 109 | // global scope (scope A) 110 | 111 | function createIncrementer () { 112 | // local scope (scope B) 113 | var a = 1; 114 | 115 | return function addOneTo (n) { 116 | // local scope (scope C) 117 | return a + n; 118 | } 119 | } 120 | 121 | var addOneTo = createIncrementer(); 122 | // Now the value of `addOneTo` is the function returned from `createIncrementer`. 123 | // This value exists in the global scope. The body of `addOneTo` references 124 | // variable `a` defined in `scope B`, therefore this variable remains available 125 | // to `addOneTo` even after `createIncrementer` returns. 126 | 127 | console.log(addOneTo(2)); // returns 3 128 | console.log(a); // ReferenceError 129 | ``` 130 | 131 | And that's basically it. Note that the variables defined in `scope B` here are sometimes referred to as being in the _closure scope_ or the _lexical scope_ of `addOneTo`. 132 | 133 | ## Why are closures useful? 134 | Closures are useful because they allow the implementation of all sorts of higher-level abstractions, including: 135 | 136 | * the creation of _private_ variables (or state) 137 | * the _parameterised_ creation of functions 138 | * the creation of _modules_ 139 | 140 | 141 | ## Scope traps 142 | #### Sequential script load order 143 | In the [function scope](#function-scope) section, we remarked: 144 | > Any JavaScript executed by the browser can access variables defined in global scope. 145 | 146 | This is true as far as it goes, but it should be remembered that ` 150 | 151 | ``` 152 | 153 | In this example, all JavaScript in `script.2.js` will have access to the global variables defined in `script.1.js`, but the reverse is not true, because `script.1.js` is executed before `script.2.js`. 154 | 155 | #### Loop variables, `var` vs `let` 156 | TBD 157 | 158 | ## Exercises 159 | Attempt `./exercise.js` in this directory. 160 | 161 | You can check your code by running the test suite for this exercise with `npm run s1.closures` 162 | -------------------------------------------------------------------------------- /stage-1/closures-and-scope/exercise.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: Fill in the functions 3 | * 4 | * This exercise is a collection of empty functions; fill them in in order to 5 | * make the tests pass. 6 | * 7 | * When you think you have finished, run the command: 8 | * npm run s1.closures 9 | * This will run a series of tests which should all pass. 10 | */ 11 | 'use strict'; 12 | 13 | 14 | var inc = 1; 15 | 16 | /* 17 | * This function should increase the value passed in as an argument by the 18 | * increment defined by the `inc` variable. For example: 19 | * 20 | * increment(2); // returns 3 21 | */ 22 | function increment (n) { 23 | // fill in ... 24 | } 25 | 26 | 27 | /* 28 | * This function should return a function that increments its argument by 29 | * whatever number is given as an argument. For example: 30 | * 31 | * var incBy3 = createIncrementer(3); 32 | * var incBy2 = createIncrementer(2); 33 | * incBy3(2); // returns 5 34 | * incBy2(2); // returns 4 35 | */ 36 | function createIncrementer (base) { 37 | // fill in ... 38 | } 39 | 40 | 41 | /* 42 | * This function should return an object that represents a counter and contains 43 | * three methods: `inc`, which increments the counter by 1, `dec` which 44 | * decrements the counter by 1, and `read`, which returns the current value of 45 | * the counter. For example: 46 | * 47 | * var counter = createCounter(); 48 | * counter.read() // returns 0 49 | * counter.inc() 50 | * counter.read() // returns 1 51 | * counter.dec() 52 | * counter.read() // returns 0 53 | */ 54 | function createCounter () { 55 | // fill in ... 56 | } 57 | 58 | 59 | module.exports = { 60 | increment, 61 | createIncrementer, 62 | createCounter, 63 | }; 64 | -------------------------------------------------------------------------------- /stage-1/closures-and-scope/exercise.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tape'); 4 | const exercise = require('./exercise.js'); 5 | 6 | tape('Closures and Scope', function (test) { 7 | test.test('increment', function (t) { 8 | t.equal(exercise.increment(4), 5, 'Expect increment(4) === 5'); 9 | t.equal(exercise.increment(99), 100, 'Expect increment(99) === 100'); 10 | t.equal(exercise.increment(-1), 0, 'Expect increment(-1) === 0'); 11 | t.equal(exercise.increment(-100), -99, 'Expect increment(-100) === -99'); 12 | t.equal(exercise.increment(Infinity), Infinity, 'Expect increment(Infinity) === Infinity'); 13 | t.end(); 14 | }); 15 | 16 | test.test('createIncrementer', function (t) { 17 | const incBy3 = exercise.createIncrementer(3); 18 | const incBy2 = exercise.createIncrementer(2); 19 | const decBy1 = exercise.createIncrementer(-1); 20 | 21 | t.equal(incBy3(5), 8); 22 | t.equal(incBy3(-1), 2); 23 | t.equal(incBy3(-0), 3); 24 | t.equal(incBy2(19), 21); 25 | t.equal(incBy2(-43), -41); 26 | t.equal(incBy2(Infinity), Infinity); 27 | t.equal(decBy1(-4), -5); 28 | t.equal(decBy1(0), -1); 29 | t.equal(decBy1(5), 4); 30 | t.end(); 31 | }); 32 | 33 | test.test('createCounter', function (t) { 34 | const c1 = exercise.createCounter(); 35 | const c2 = exercise.createCounter(); 36 | const c3 = exercise.createCounter(); 37 | 38 | // Initial state 39 | t.equal(c1.read(), 0); 40 | t.equal(c2.read(), 0); 41 | t.equal(c3.read(), 0); 42 | 43 | // Increments are independent 44 | c1.inc(); 45 | t.equal(c1.read(), 1); 46 | t.equal(c2.read(), 0); 47 | t.equal(c3.read(), 0); 48 | 49 | // Decrements are independent 50 | c3.dec(); 51 | t.equal(c1.read(), 1); 52 | t.equal(c2.read(), 0); 53 | t.equal(c3.read(), -1); 54 | 55 | // Operations stack 56 | c2.inc(); 57 | c2.inc(); 58 | c2.inc(); 59 | c2.dec(); 60 | t.equal(c1.read(), 1); 61 | t.equal(c2.read(), 2); 62 | t.equal(c3.read(), -1); 63 | 64 | t.end(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /stage-1/exercise/README.md: -------------------------------------------------------------------------------- 1 | # Stage 1: Final Exercise 2 | 3 | This is the final exercise of stage 1. You have been given a small working app. To try it out, open the `index.html` file in this directory in your browser. 4 | 5 | Your task is to refactor the app's JavaScript using the principles and techniques that have been covered in the rest of stage 1. This is an open-ended exercise, there is no defined solution or "end-point" (unless you run out of time); you must use your judgement to decide when you've arrived at a satisfactory solution. 6 | 7 | First take a few minutes to understand what the code is doing, then use what you have learned in the preceding stage-1 exercises to refactor the app. Take your time, and think about what principles you are trying to apply while you are refactoring. 8 | -------------------------------------------------------------------------------- /stage-1/exercise/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: Refactor the code! 3 | * 4 | * This script file contains a small front-end app that queries the 5 | * StackOverflow API. It works, but the code is not ideal; there is a lot of 6 | * work to do to clean it up. 7 | * 8 | * First take a few minutes to understand what the code is doing, then use what 9 | * you have learned in the preceding stage-1 exercises to refactor the app. 10 | * 11 | * Take your time, and think about what principles you are trying to apply while 12 | * you are refactoring. 13 | */ 14 | 'use strict'; 15 | 16 | document.querySelector('#form-unanswered').addEventListener('submit', function (e) { 17 | e.preventDefault(); 18 | 19 | var form = e.target; 20 | var tags = form.querySelector('input[name=tags]').value; 21 | var url = 'https://api.stackexchange.com/2.2/questions/unanswered?order=desc&sort=activity&site=stackoverflow&tagged=' + tags; 22 | 23 | var xhr = new XMLHttpRequest(); 24 | 25 | xhr.addEventListener('load', function () { 26 | if (xhr.status === 200) { 27 | var response = JSON.parse(xhr.responseText); 28 | 29 | document.querySelector('#results-summary').innerHTML = '' 30 | + '

' 31 | + 'Query of ' + tags + ' returned ' + response.items.length + ' results' 32 | + '

'; 33 | 34 | document.querySelector('#results-body').innerHTML = response.items.map(function (item) { 35 | return '' 36 | + '
' 37 | + '

Title: ' + item.title + '

' 38 | + '

Date: ' + new Date(item.creation_date) + '

' 39 | + '

Link: Click here

' 40 | + '

Owner: ' + item.owner.display_name + '

' 41 | + '
' 42 | }) 43 | .join('
'); 44 | 45 | } else { 46 | console.log('Status Code: ' + xhr.status); 47 | } 48 | }); 49 | 50 | xhr.open('GET', url); 51 | xhr.send(); 52 | }); 53 | 54 | 55 | document.querySelector('#form-answerers').addEventListener('submit', function (e) { 56 | e.preventDefault(); 57 | 58 | var form = e.target; 59 | var tag = form.querySelector('input[name=tags]').value; 60 | var url = 'http://api.stackexchange.com/2.2/tags/' + tag + '/top-answerers/all_time?site=stackoverflow' 61 | 62 | var xhr = new XMLHttpRequest(); 63 | 64 | xhr.addEventListener('load', function () { 65 | if (xhr.status === 200) { 66 | var response = JSON.parse(xhr.responseText); 67 | 68 | document.querySelector('#results-summary').innerHTML = '' 69 | + '

' 70 | + 'Query of ' + tags + ' returned ' + response.items.length + ' results' 71 | + '

'; 72 | 73 | document.querySelector('#results-body').innerHTML = response.items.map(function (item) { 74 | return '' 75 | + '
' 76 | + '

User: ' + item.user.display_name + '

' 77 | + '

Reputation: ' + item.user.reputation + '

' 78 | + '

Profile: Click here

' 79 | + '

Score: ' + item.score + '

' 80 | + '
' 81 | }) 82 | .join('
'); 83 | 84 | } else { 85 | console.log('Status Code: ' + xhr.status); 86 | } 87 | }); 88 | 89 | xhr.open('GET', url); 90 | xhr.send(); 91 | }); 92 | -------------------------------------------------------------------------------- /stage-1/exercise/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rep Builder 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |

StackOverflow Reputation Builder

14 |

This app lets you search by topic for unanswered questions on Stack Overflow to help you build your reputation. Find unanswered questions for a topic you know about, write quality answers, and watch your reputation go up.

15 |

Sometimes, you also need some inspiration. This page also lets you search for the top answerers for a tag. If you want to rise to the top ranks for a topic, see how many reputation points you'll need to aim for!

16 |
17 |
18 |
19 |
20 |

Get Unanswered Questions

21 |

Find unanswered questions by tag. For multiple tags, use a semi-colon to separate.

22 |
23 | 24 | 25 |
26 |

View the Top Answerers for a Tag

27 |
28 | 29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /stage-1/exercise/main.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 3em auto; 3 | width: 600px; 4 | } 5 | 6 | .hidden { 7 | display: none; 8 | } 9 | 10 | .left { 11 | float: left; 12 | } 13 | 14 | .stack-image { 15 | background: url('./stack-icon.png') no-repeat center center; 16 | width: 200px; 17 | height: 200px; 18 | -webkit-background-size: cover; 19 | -moz-background-size: cover; 20 | -o-background-size: cover; 21 | background-size: cover; 22 | margin-right: 1em; 23 | border-radius: 5px; 24 | } 25 | 26 | .intro { 27 | margin-bottom: 1em; 28 | } 29 | 30 | .stack { 31 | clear: both; 32 | } 33 | 34 | .inspiration { 35 | margin-left: 3em; 36 | } 37 | 38 | .result { 39 | border: 1px solid black; 40 | padding: 5px; 41 | margin: 0 auto 1em auto; 42 | } 43 | 44 | .error { 45 | background-color: #F78181; 46 | padding: 1em; 47 | } 48 | -------------------------------------------------------------------------------- /stage-1/exercise/stack-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/ws-software-design-js/e570793e2fa0cb663d785993b3b9ba59d2c13a5b/stage-1/exercise/stack-icon.png -------------------------------------------------------------------------------- /stage-1/first-class-functions/README.md: -------------------------------------------------------------------------------- 1 | # Functions as First Class Objects 2 | 3 | ## Learning Outcomes 4 | * Understand what a First Class Object is 5 | 6 | ## What is a First Class Object? 7 | The simplest explanation might be, an object that behaves as though it is has a value, so that you can treat it _almost_ like any other variable. So, you can: 8 | * create and destroy it (at runtime) 9 | * pass it to functions and return it from functions 10 | * print it 11 | * store it in data structures (like arrays or objects) 12 | 13 | ## How are functions First Class? 14 | Functions in JavaScript are said to be _first class objects_ because they fulfil the above criteria. 15 | 16 | You can store references to functions in variables: 17 | ```js 18 | var foo = function (a) { 19 | return a + 1; 20 | }; 21 | 22 | // or 23 | 24 | function foo (a) { 25 | return a + 1; 26 | } 27 | ``` 28 | 29 | You can store references to functions in other data structures: 30 | 31 | ```js 32 | var object = { 33 | foo: function (a) { 34 | return a + 1; 35 | } 36 | }; 37 | 38 | // or 39 | 40 | var foo = function (a) { 41 | return a + 1; 42 | }; 43 | 44 | var object = { 45 | foo: foo, 46 | }; 47 | 48 | object.foo(3); // gives 4 49 | ``` 50 | 51 | You can pass functions as arguments to functions: 52 | 53 | ```js 54 | function foo (a) { 55 | return a + 1; 56 | } 57 | 58 | function apply (fun, num) { 59 | return fun(num); 60 | } 61 | 62 | apply(foo, 3); // gives 4 63 | ``` 64 | 65 | You can return a function from a function: 66 | 67 | ```js 68 | function foo (a) { 69 | return a + 1; 70 | } 71 | 72 | function bar () { 73 | return foo; 74 | } 75 | 76 | var fun = bar(); 77 | 78 | fun(3); // gives 4 79 | bar()(3); // also gives 4 80 | ``` 81 | 82 | In JavaScript, the `Function` data-type is an instance of the `Object` data-type: 83 | 84 | ```js 85 | function foo (a) { 86 | return a + 1; 87 | } 88 | 89 | foo instanceof Function; // gives `true` 90 | foo instanceof Object; // gives `true` 91 | ``` 92 | 93 | This means you can attach arbitrary attributes to functions: 94 | 95 | ```js 96 | function foo (a) { 97 | return a + 1; 98 | } 99 | 100 | foo.bar = 2; 101 | ``` 102 | 103 | Note that identity of functions works the same way as identity of objects in JavaScript (because instances of `Function` are also instances of `Object`): 104 | 105 | ```js 106 | var foo = function (a) { 107 | return a + 1; 108 | }; 109 | 110 | var bar = function (a) { 111 | return a + 1; 112 | }; 113 | 114 | var pam = foo; 115 | 116 | foo == bar; // gives `false` 117 | bar == pam; // gives `false` 118 | foo == pam; // gives `true` 119 | ``` 120 | 121 | 122 | ## Why is this useful? 123 | First class functions are extremely useful because they allow you to (eventually): 124 | * Create powerful abstractions which reduce and factor out details so that developers can focus on fewer concepts. This is covered in more detail in [abstraction-with-functions](../abstraction-with-functions) 125 | * Break-up and modularise code more easily (particularly asynchronous code) 126 | -------------------------------------------------------------------------------- /stage-2/README.md: -------------------------------------------------------------------------------- 1 | # Stage 2 2 | The second stage concerns itself with exploration of more intermediate level design patterns, utilising the language features and techniques covered in [stage 1](../stage-1). 3 | 4 | ## Contents 5 | 1. [The module pattern](./module-pattern) 6 | 2. Coming soon... 7 | 8 | ## Where to Start? 9 | Start with whichever topic you're most interested in, and feel free to skip any you feel you are already comfortable with. For those new to these topics, we recommend proceeding in the order given by the [contents](#contents) section. 10 | -------------------------------------------------------------------------------- /stage-2/module-pattern/README.md: -------------------------------------------------------------------------------- 1 | # The Module Pattern 2 | 3 | ## What is a Module? 4 | Many languages have the concept of a 'module', or a self-contained bundle of code, built in. This makes it much easier to achieve a modular, robust design. In the browser, Javascript doesn't have this. Any variable or function defined in the global scope of a script file is also in the global scope of any script loaded after it. However, there is a way to avoid this ([without any](https://www.npmjs.com/package/browserify) [additional tooling](https://www.npmjs.com/package/webpack)), and it's called the _module pattern_. 5 | 6 | We will briefly outline the idea here, but for more detail, the following articles are strongly recommend: 7 | * [Mastering the module pattern](https://toddmotto.com/mastering-the-module-pattern/) 8 | * [The module pattern in depth](http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html) 9 | 10 | ## How to create a Module? 11 | The module pattern exploits the technique of creating a [closure](../../stage-1/closures-and-scope) to create variables which can by used by the module, but not directly by any of the code outside it. 12 | 13 | This can be used as a way to build [abstractions](../../stage-1/abstraction-with-functions) which present simple interfaces to the developer and hides away implementation details and complexity. 14 | 15 | We can illustrate this with an example creating a simple calculator: 16 | 17 | ```js 18 | function createCalculator () { 19 | var result = 0; 20 | 21 | function add (n) { 22 | result += n; 23 | } 24 | 25 | function subtract (n) { 26 | result -= n; 27 | } 28 | 29 | function read () { 30 | return result; 31 | } 32 | 33 | function clear () { 34 | result = 0; 35 | } 36 | 37 | return { 38 | add: add, 39 | subtract: subtract, 40 | read: read, 41 | clear: clear, 42 | }; 43 | } 44 | 45 | var calculator = createCalculator(); 46 | var anotherCalculator = createCalculator(); 47 | 48 | calculator.add(3); 49 | calculator.subtract(1); 50 | calculator.read(); // returns 2; 51 | 52 | anotherCalculator.read(); // returns 0; 53 | 54 | calculator.clear(); 55 | calculator.read(); // returns 0; 56 | ``` 57 | 58 | Note that the above form of the module pattern allows for the creation of several instances of the module, but each instance needs to be explicitly created. If multiple instances are not necessary, it is also possible to exploit the **IIFE** (_immediately-invoked-function-expression_) technique to instantiate the module immediately: 59 | 60 | ```js 61 | var Calculator = (function createCalculator () { 62 | // ... module code 63 | })(); 64 | 65 | Calculator.add(3); 66 | Calculator.read(); // returns 3; 67 | ``` 68 | 69 | ## When should I use the module pattern? 70 | The module pattern may be appropriate if you wish to create an abstraction that has the ability to do the following: 71 | * hide complex implementations 72 | * present a simpler interface to the developer than the underlying code 73 | * maintain an internal private state 74 | * keep variables which are irrelevant to the rest of the code within the private scope of the module 75 | 76 | ## When shouldn't I use the module pattern? 77 | If the abstraction you wish to create has no internal state, and the implementation is not too complex, it may often be simpler (and therefore better) to simply write a function. 78 | 79 | ## Exercises 80 | Attempt `./exercise.js` in this directory. 81 | 82 | You can check your code by running the test suite for this exercise with `npm run s2` 83 | -------------------------------------------------------------------------------- /stage-2/module-pattern/exercise.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exercise: Create some modules! 3 | * 4 | * When you think you have finished, run the command: 5 | * npm run s2.modules 6 | * This will run a series of tests which should all pass. 7 | */ 8 | 'use strict'; 9 | 10 | /* 11 | * Create a single module (using an IIFE) which contains functionality to parse 12 | * URLs. 13 | * 14 | * We have started you off with the basic structure. 15 | * 16 | * https :// www.example.com / hello ? foo=1&bar=2 17 | * | | | | | | | | 18 | * | protocol | | domain | | path | | querystring | 19 | */ 20 | var UrlParser = (function () { 21 | // fill in ... 22 | 23 | return { 24 | // a function that takes a URL and returns its protocol 25 | protocol: null, 26 | 27 | // a function that takes a URL and returns its domain 28 | domain: null, 29 | 30 | // a function that takes a URL and returns its path 31 | path: null, 32 | 33 | // a function that takes a URL and returns its query string 34 | query: null, 35 | }; 36 | }); 37 | 38 | 39 | /* 40 | * Create a module that can support multiple instances (like in our example). 41 | * The module should be a function with several additional methods attached as 42 | * attributes. 43 | * 44 | * Example: 45 | * var exampleBuilder = createUrlBuilder('https://example.com'); 46 | * 47 | * var url = exampleBuilder({ query: { foo: 1, bar: 2 }, path: 'hello' }); 48 | * 49 | * console.log(url); // https://example.com/hello?foo=1&bar=2 50 | * 51 | * exampleBuilder. 52 | */ 53 | var createUrlBuilder = function (host) { 54 | // fill in ... 55 | 56 | var builder = function () {} 57 | 58 | return builder; 59 | }; 60 | 61 | 62 | module.exports = { 63 | UrlParser, 64 | createUrlBuilder, 65 | }; 66 | -------------------------------------------------------------------------------- /stage-2/module-pattern/exercise.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tape'); 4 | const exercise = require('../../.solutions/stage-2/module-pattern/solution.js'); 5 | // const exercise = require('./exercise.js'); 6 | 7 | tape('Module Pattern', function (test) { 8 | test.test('UrlParser', function (t) { 9 | const { UrlParser } = exercise; 10 | const url = 'https://example.com/hello?foo=1&bar=2'; 11 | 12 | t.equal(typeof UrlParser, 'object', 'Expect module to be object'); 13 | t.equal(typeof UrlParser.protocol, 'function', 'Expect protocol method'); 14 | t.equal(typeof UrlParser.domain, 'function', 'Expect domain method'); 15 | t.equal(typeof UrlParser.path, 'function', 'Expect path method'); 16 | t.equal(typeof UrlParser.querystring, 'function', 'Expect querystring method'); 17 | 18 | t.equal(UrlParser.protocol(url), 'https'); 19 | t.equal(UrlParser.domain(url), 'example.com'); 20 | t.equal(UrlParser.path(url), 'hello'); 21 | t.equal(UrlParser.querystring(url), 'foo=1&bar=2'); 22 | t.end(); 23 | }); 24 | 25 | test.test('createUrlBuilder', function (t) { 26 | const { createUrlBuilder } = exercise; 27 | const host = 'https://example.com'; 28 | const urlBuilder = createUrlBuilder(host); 29 | 30 | t.equal(typeof urlBuilder, 'function', 'Expect URL builder to be a function'); 31 | t.equal( 32 | urlBuilder({ path: 'hello', query: { foo: 1, bar: 2 } }), 33 | `${host}/hello?foo=1&bar=2`, 34 | 'Expect call to URL builder to build full URL' 35 | ); 36 | t.equal(urlBuilder.path('hello'), `${host}/hello`); 37 | t.equal(urlBuilder.query({ foo: 1, bar: 2 }), `${host}?foo=1&bar=2`); 38 | t.end(); 39 | }); 40 | }); 41 | --------------------------------------------------------------------------------