ryanmcdermottμ clean-code-javascriptλ₯Ό λ²μν ν¬μ€νΈμ λλ€.
ν΄λ¦° μ½λ(clean code)λ₯Ό μ¬μ©νλ©΄ μ½λμ κ°λ μ±κ³Ό μ¬μ¬μ©μ±μ λμΌμ μμΌλ©° 리ν©ν λ§νκΈ° μ¬μ΄ νλ‘κ·Έλ¨μ κ°λ°ν μ μλ€.
-
Bad
const yyyymmdstr = moment().format("YYYY/MM/DD");
-
Good
const currentDate = moment().format("YYYY/MM/DD");
-
Bad
getUserInfo(); getClientData(); getCustomerRecord();
-
Good
getUser();
μμ±λ μ½λμ 리뷰λ κ²μμ μν΄μ νλ‘κ·Έλ¨μ μ½κ² μ΄ν΄ν μ μλ μλ―Έλ₯Ό λ΄κ³ μλ λ³μλͺ μ μ¬μ©ν΄μΌνλ€.
-
Bad
// 86400000κ° λλ체 λμ?! setTimeout(blastOff, 86400000);
-
Good
// λλ¬Έμλ‘ μ μΈλ μμ μ΄λ¦μ ν΅ν΄ λ³μκ° λ¬΄μμ μλ―Ένλμ§ μ μ μλ€. const MILLISECONDS_IN_A_DAY = 60 * 60 * 24 * 1000; //86400000; setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
-
Bad
const address = "One Infinite Loop, Cupertino 95014"; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; saveCityZipCode( address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2] );
-
Good
const address = "One Infinite Loop, Cupertino 95014"; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; const [_, city, zipCode] = address.match(cityZipCodeRegex) || []; saveCityZipCode(city, zipCode);
λͺ μμ μΈ(explicit)κ²μ΄ μμμ μΈ(implicit)κ²λ³΄λ€ λ«λ€.
-
Bad
const locations = ["Austin", "New York", "San Francisco"]; locations.forEach(l => { doStuff(); doSomeOtherStuff(); // ... // ... // ... // 1μ΄ λλ체 λμ?! dispatch(l); });
-
Good
const locations = ["Austin", "New York", "San Francisco"]; locations.forEach(location => { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch(location); });
ν΄λμ€λ κ°μ²΄μ μ΄λ¦μ΄ 무μΈκ°λ₯Ό μλ―Ένκ³ μλ€λ©΄, νλ‘νΌν°μ ν΄λμ€/κ°μ²΄μ μ΄λ¦μ λ°λ³΅ν νμ μλ€.
-
Bad
const Car = { carMake: "Honda", carModel: "Accord", carColor: "Blue" }; function paintCar(car) { car.carColor = "Red"; }
-
Good
const Car = { make: "Honda", model: "Accord", color: "Blue" }; function paintCar(car) { car.color = "Red"; }
-
Bad
function createMicrobrewery(name) { const breweryName = name || "Hipster Brew Co."; // ... }
-
Good
function createMicrobrewery(name = "Hipster Brew Co.") { // ... }
λ κ°μ μΈμκ° κ°μ₯ μ΄μμ μ΄λ©° ν¨μμ μΈμμ κ°μλ₯Ό μ΅μννλ©΄ ν¨μλ₯Ό λ μ½κ² ν μ€νΈν μ μλ€. μΈμκ° λ§μκ²½μ° ν μ€νΈ μΌμ΄μ€κ° λ§μμ Έ λΉν¨μ¨μ μ΄λ€.
-
Bad
function createMenu(title, body, buttonText, cancellable) { // ... } createMenu("Foo", "Bar", "Baz", true);
-
Good
function createMenu({ title, body, buttonText, cancellable }) { // ... } createMenu({ title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true });
λ κ°μ§ μ΄μμ μΌμ μννλ ν¨μλ€μ λΉκ΅νκ±°λ ν μ€νΈν λμ μ΄λ €μμ΄ μλ€. ν¨μλ₯Ό νλμ μμ μΌλ‘ λΆλ¦¬ν μ μμΌλ©΄ μ½κ² 리ν©ν λ§ν μ μμΌλ©° κ°λ μ±μ λμΌ μ μλ€.
-
Bad
function emailClients(clients) { clients.forEach(client => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }
-
Good
function emailActiveClients(clients) { clients.filter(isActiveClient).forEach(email); } function isActiveClient(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); }
-
Bad
function addToDate(date, month) { // ... } const date = new Date(); // μ°, μ, μΌ μ€ λ¬΄μμ λνλ ν¨μμΈκ° μκΈ° μ΄λ €μ addToDate(date, 1);
-
Good
function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date);
λ λ¨κ³ μ΄μμ μΆμνκ° μ μ©λ κ²½μ° λ§μ μμ μ μνν¨μΌλ‘ λΉν¨μ¨μ μ΄λ€.
-
Bad
function parseBetterJSAlternative(code) { const REGEXES = [ // ... ]; const statements = code.split(" "); const tokens = []; REGEXES.forEach(REGEX => { statements.forEach(statement => { // ... }); }); const ast = []; tokens.forEach(token => { // lex... }); ast.forEach(node => { // parse... }); }
-
Good
function parseBetterJSAlternative(code) { const tokens = tokenize(code); const syntaxTree = parse(tokens); syntaxTree.forEach(node => { // parse... }); } function tokenize(code) { const REGEXES = [ // ... ]; const statements = code.split(" "); const tokens = []; REGEXES.forEach(REGEX => { statements.forEach(statement => { tokens.push(/* ... */); }); }); return tokens; } function parse(tokens) { const syntaxTree = []; tokens.forEach(token => { syntaxTree.push(/* ... */); }); return syntaxTree; }
μ€λ³΅λ μ½λλ€μ μΌλΆ μ½λμ λ‘μ§μ λ³κ²½ν λ λ€λ₯Έ μ½λλ€μ λ‘μ§λ λ³κ²½λ κ°λ₯μ±μ΄ μμΌλ―λ‘ μ€λ³΅μ μ΅μν νλ κ²μ΄ μ’λ€.
μ€λ³΅λ μ½λλ₯Ό μ κ±°νλ€λ κ²μ λ€μν μμλ€μ μ²λ¦¬ ν μ μλ ν¨μ/λͺ¨λ/ν΄λμ€ λ±μ μΆμνλ₯Ό λ§λλ κ²μ μλ―Ένλ€. μΆμνλ₯Ό ν λλ Classes μΉμ μ λμ¬ SOLID μμΉμ λ°λΌ μ¬λ°λ₯΄κ² μμ±νλ κ²μ΄ μ€μνλ€.
-
Bad
function showDeveloperList(developers) { developers.forEach(developer => { const expectedSalary = developer.calculateExpectedSalary(); const experience = developer.getExperience(); const githubLink = developer.getGithubLink(); const data = { expectedSalary, experience, githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach(manager => { const expectedSalary = manager.calculateExpectedSalary(); const experience = manager.getExperience(); const portfolio = manager.getMBAProjects(); const data = { expectedSalary, experience, portfolio }; render(data); }); }
-
Good
function showEmployeeList(employees) { employees.forEach(employee => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); const data = { expectedSalary, experience }; switch (employee.type) { case "manager": data.portfolio = employee.getMBAProjects(); break; case "developer": data.githubLink = employee.getGithubLink(); break; } render(data); }); }
-
Bad
const menuConfig = { title: null, body: "Bar", buttonText: null, cancellable: true }; function createMenu(config) { config.title = config.title || "Foo"; config.body = config.body || "Bar"; config.buttonText = config.buttonText || "Baz"; config.cancellable = config.cancellable !== undefined ? config.cancellable : true; } createMenu(menuConfig);
-
Good
const menuConfig = { title: "Order", // User did not include 'body' key buttonText: "Send", cancellable: true }; function createMenu(config) { let finalConfig = Object.assign( { title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true }, config ); return finalConfig // config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} // ... } createMenu(menuConfig);
νλκ·Έ λ§€κ°λ³μλ₯Ό μ¬μ©ν΄ 쑰건 λΆκΈ°λ₯Ό ν΅ν΄ ν¨μκ° λ€λ₯Έ λμμ μννλ κ²½μ° ν¨μλ₯Ό λΆλ¦¬μμΌμΌ νλ€.
-
Bad
function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } }
-
Good
function createFile(name) { fs.create(name); } function createTempFile(name) { createFile(`./temp/${name}`); }
ν¨μλ κ°μ κ°μ Έμ λ€λ₯Έ κ°μΌλ‘ λ°ννλ κ² μ΄μΈμ μμ μ μννλ©΄ Side effectκ° λ°μνλ€. μ΄λ₯Ό λ°©μ§νκΈ° μν΄μ ꡬ쑰 μμ΄ κ°μ²΄μ μνλ₯Ό 곡μ , μ΄λ€κ²μΌλ‘λ κΈ°λ‘λ μ μλ κ°λ³μ μΈ λ°μ΄ν° νμ λ€μ μ¬μ©, Side effectκ° λ°μνλ κ³³μ λΉμ§μ€ν λ±μ νΌν΄μΌνλ€.
-
Bad
// λ§μ½ μ΄ nameμ μ¬μ©νλ λ€λ₯Έ ν¨μκ° μμ΄ μ΄ λ³μκ° κ³΅μ λλ©΄, μλμΉμκ² λ³μμ κ°μ΄ λ³ν μ μλ€. let name = "Ryan McDermott"; function splitIntoFirstAndLastName() { name = name.split(" "); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott'];
-
Good
function splitIntoFirstAndLastName(name) { return name.split(" "); } const name = "Ryan McDermott"; const newName = splitIntoFirstAndLastName(name); console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott'];
μλ°μ€ν¬λ¦½νΈμμ μΌλΆ κ°λ€μ λ³κ²½μ΄ λΆκ°νλ©°(immutable) λ€λ₯Έ μΌλΆ κ°λ€μ λ³κ²½μ΄ κ°λ₯νλ€(mutable). κ°μ²΄μ λ°°μ΄ λ μ’ λ₯λ λ³κ²½ κ°λ₯ν κ°μ΄λ―λ‘ ν¨μμ λ§€κ°λ³μλ‘ μ λ¬ν λ μ£Όμν΄μΌνλ€. μλ°μ€ν¬λ¦½νΈ ν¨μλ κ°μ²΄μ νλ‘νΌν°μ λ°°μ΄μ μμμ κ°μ λ°κΏ μ μμΌλ©° μ΄κ²μ΄ λ²κ·Έμ μμΈμ΄ λλ€.
-
Bad
const addItemToCart = (cart, item) => { cart.push({ item, date: Date.now() }); }; // μΌνμΉ΄νΈμ μΆκ°λμ΄μλ itemμ λν λ°°μ΄(cart)μ λ°νλμ§ μλλ€.
-
Good
const addItemToCart = (cart, item) => { return [...cart, { item, date: Date.now() }]; }; // addItemToCartκ° λ°νν λ cartλ₯Ό 볡μ λ° μλ‘μ΄ itemμ μΆκ°ν ν λ°ννκΈ° λλ¬Έμ μ΄μ μΌνμΉ΄νΈμ μΆκ°ν΄λμ μμ΄ν μ μν₯μ μ£Όμ§ μλλ€.
μ μ λ²μλ₯Ό μμ±νλ κ²μ(polluting globals)μ λμ μ΅κ΄μ΄λ€. λ€λ₯Έ λΌμ΄λΈλ¬λ¦¬μ μΆ©λν κ°λ₯μ±μ΄ μκ³ API μ¬μ©μκ° νλ‘λμ
μ μμΈκ° λ°μν λκΉμ§ λͺ¨λ₯Όμλ μλ€. μλ₯Ό λ€μ΄ Array λ©μλλ₯Ό νμ₯ν΄ λ λ°°μ΄κ°μ μ°¨μ΄λ₯Ό λνλΌμ μλ diff λ©μλλ₯Ό λ§λ€λ €κ³ ν λ, Array.prototypeμ μ¬μ©ν κ²½μ° λ€λ₯Έ λΌμ΄λΈλ¬λ¦¬μ μΆ©λν κ°λ₯μ±μ΄ μλ€. μ΄ κ²½μ° Array.prototypeμ΄ μλ ES6μ class ν€μλλ₯Ό μ¬μ©ν μ μλ€.
-
Bad
Array.prototype.diff = function diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); };
-
Good
class SuperArray extends Array { diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); } }
-
Bad
const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; let totalOutput = 0; for (let i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; }
-
Good
const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; const totalOutput = programmerOutput.reduce( (totalLines, output) => totalLines + output.linesOfCode, 0 );
-
Bad
if (fsm.state === "fetching" && isEmpty(listNode)) { // ... }
-
Good
function shouldShowSpinner(fsm, listNode) { return fsm.state === "fetching" && isEmpty(listNode); } if (shouldShowSpinner(fsmInstance, listNodeInstance)) { // ... }
-
Bad
function isDOMNodeNotPresent(node) { // ... } if (!isDOMNodeNotPresent(node)) { // ... }
-
Good
function isDOMNodePresent(node) { // ... } if (isDOMNodePresent(node)) { // ... }
λ€μμ μ‘°κ±΄λ¬Έμ΄ νμν κ²½μ° λ€νμ± μ¦, class μμ(extend)μ μ΄μ©νλκ²μ΄ μ’λ€.
-
Bad
class Airplane { // ... getCruisingAltitude() { switch (this.type) { case "777": return this.getMaxAltitude() - this.getPassengerCount(); case "Air Force One": return this.getMaxAltitude(); case "Cessna": return this.getMaxAltitude() - this.getFuelExpenditure(); } } }
-
Good
class Airplane { // ... } class Boeing777 extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude() - this.getPassengerCount(); } } class AirForceOne extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude(); } } class Cessna extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude() - this.getFuelExpenditure(); } }
μλ°μ€ν¬λ¦½νΈλ μΈμμ μ΄λ ν νμ λ μ¬μ©ν μ μλ untyped μΈμ΄μ΄λ€. μ΄λ‘μΈν΄ νμ κ²μ¬λ₯Ό ν΄μΌνλ κ²½μ°κ° μκΈΈμλ μλ€. μ΄λ₯Ό νΌνκΈ° μν λ§μ λ°©λ² μ€ κ°μ₯ λ¨Όμ κ³ λ €ν΄μΌν λ°©λ²μ μΌκ΄λ APIλ₯Ό λ§λλ κ²μ΄λ€.
-
Bad
function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.pedal(this.currentLocation, new Location("texas")); } else if (vehicle instanceof Car) { vehicle.drive(this.currentLocation, new Location("texas")); } }
-
Good
function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location("texas")); }
λ¬Έμμ΄μ΄λ μ μν κ°μ κΈ°λ³Έ μμμ μΈ νμ μ κ°λ€λ‘ μμ μνλ€λ©΄ λ€νμ±μ μ¬μ©ν μ μλ€. μ΄ κ²½μ° νμ κ²μ¬κ° νμνλ€λ©΄ νμ μ€ν¬λ¦½νΈλ₯Ό μ¬μ©νλ κ²μ κ³ λ €ν΄μΌνλ€. νμ μ€ν¬λ¦½νΈλ μλ°μ€ν¬λ¦½νΈ ꡬ문μ μ μ νμ΄νμ μ 곡νλ νλ₯ν λμμ΄λ€.
-
Bad
function combine(val1, val2) { if ( (typeof val1 === "number" && typeof val2 === "number") || (typeof val1 === "string" && typeof val2 === "string") ) { return val1 + val2; } throw new Error("Must be of type String or Number"); }
-
Good
function combine(val1, val2) { return val1 + val2; }
λͺ¨λ λΈλΌμ°μ λ€μ λ°νμμ 보μ΄μ§ μμ§λ§ λ΄λΆμ μΌλ‘(under-the-hood) λ§μ μ΅μ νλ₯Ό μννλ€. λ°λΌμ νμμ΄μμ μ§λμΉ μ΅μ νλ μκ° λλΉμ΄λ€.
-
Bad
// On old browsers, each iteration with uncached `list.length` would be costly // because of `list.length` recomputation. In modern browsers, this is optimized. for (let i = 0, len = list.length; i < len; i++) { // ... }
-
Good
for (let i = 0; i < list.length; i++) { // ... }
λμ΄μ μ¬μ©νμ§μλ μ½λ(dead code)λ€μ μ€λ³΅ μ½λλ€λ§νΌ λμλ©° μ½λλ² μ΄μ€μ μ μ₯ν μ΄μ κ° μλ€. νΈμΆλμ§μλ μ½λλ€μ μ κ±°ν΄μΌνλ€.
-
Bad
function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker("apples", req, "www.inventory-awesome.io");
-
Good
function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker("apples", req, "www.inventory-awesome.io");
getterμ setterλ₯Ό μ¬μ©ν΄ κ°μ²΄λ₯Ό μ‘μΈμ€νλ κ²μ΄ κ°μ²΄μ νλ‘νΌν°λ₯Ό μ°Ύλκ²λ³΄λ€ λ λ«λ€. κ·Έ μ΄μ λ μλμ κ°λ€.
- κ°μ²΄μ μμ±μ μ»λ κ² μ΄μμ μμ μ΄ νμν λ λͺ¨λ μ κ·Όμλ€μ λ³κ²½ν νμκ° μλ€.
setμ μ¬μ©ν΄ μ½κ² μ ν¨μ± κ²μ¦ μ μ°¨λ₯Ό λ§λ€ μ μλ€.- λ΄λΆμ μΌλ‘ μΊ‘μνκ° κ°λ₯νλ€.
- μ½λ λ‘κΉ κ³Ό μλ¬ μ²λ¦¬κ° κ°νΈνλ€.
- μλ²μμλΆν° κ°μ²΄μ νλ‘νΌν°λ₯Ό κ°μ Έμ¬λ lazy loadκ° κ°λ₯νλ€.
- Bad
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;- Good
function makeBankAccount() {
// balanceλ privateν λ©€λ² λ³μμ΄λ€.
let balance = 0;
// "getter", made public via the returned object below
function getBalance() {
return balance;
}
// "setter", made public via the returned object below
function setBalance(amount) {
// ... validate before updating the balance
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);ν΄λ‘μ (ES5 μ΄ν)λ₯Ό μ¬μ©ν΄ privateν λ©€λ² λ³μλ₯Ό λ§λ€ μ μλ€.
- Bad
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined- Good
function makeEmployee(name) {
return {
getName() {
return name;
}
};
}
const employee = makeEmployee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John DoeκΈ°μ‘΄ ES5 ν΄λμ€μμλ ν΄λμ€ μμ, μμ±μ λ©μλλ₯Ό ꡬννλ κ²μ΄ μ΄λ ΅λ€. λ§μ½ μμμ΄ νμν κ²½μ° ES6μ ν΄λμ€λ₯Ό μ¬μ©νλ κ²μ΄ μ’λ€. κ·Έλ¬λ λ ν¬κ³ 볡μ‘ν κ°μ²΄κ° νμνλ€λ©΄ ν΄λμ€λ³΄λ€ μμ ν¨μλ₯Ό μ¬μ©νλ κ²μ΄ λ λ«λ€.
- Bad
const Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}
this.age = age;
};
Animal.prototype.move = function move() {};
const Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error("Instantiate Mammal with `new`");
}
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
const Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error("Instantiate Human with `new`");
}
Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};- Good
class Animal {
constructor(age) {
this.age = age;
}
move() {
/* ... */
}
}
class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}
liveBirth() {
/* ... */
}
}
class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}
speak() {
/* ... */
}
}λ©μλ 체μ΄λμ μλ°μ€ν¬λ¦½νΈμμ λ§€μ° μ μ©ν ν¨ν΄μ΄λ©° μ μ΄μΏΌλ¦¬λ lodash κ°μ λ§μ λΌμ΄λΈλ¬λ¦¬μμ μ¬μ©λλ€. λ©μλ 체μ΄λμ μ¬μ©νλ©΄ μ½λλ₯Ό μ ννν μ μμΌλ©° κ°κ²°νκ² λ§λ€μ΄μ€λ€. ν΄λμ€ ν¨μμμ κ°λ¨ν λͺ¨λ ν¨μμ thisλ₯Ό λ°ννλκ²λ§μΌλ‘ ν΄λμ€ λ©μλλ₯Ό μ°κ²°ν μ μλ€.
- Bad
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();- Good
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// 체μ΄λμ μν this λ°ν
return this;
}
setModel(model) {
this.model = model;
// 체μ΄λμ μν this λ°ν
return this;
}
setColor(color) {
this.color = color;
// 체μ΄λμ μν this λ°ν
return this;
}
save() {
console.log(this.make, this.model, this.color);
// 체μ΄λμ μν this λ°ν
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();μμμ μ¬μ©νμλλ³΄λ€ μ»΄ν¬μ§μ μ μ¬μ©ν λμ μ΄λμ΄ λ§κΈ° λλ¬Έμ κ°λ₯νλ€λ©΄ μμ보λ€λ μ»΄ν¬μ§μ μ μ¬μ©ν΄μΌ νλ€. (Design pattern by Gang of Four)
κ·Έλ¬λ μμμ κ΄κ³κ° "has-a"(μ¬μ©μ -> μ¬μ©μμ 보)κ° μλ "is-a" (μ¬λ -> λλ¬Ό)μΌλ, κΈ°λ°μ΄ λλ λ² μ΄μ€ ν΄λμ€λ₯Ό μ¬μ¬μ©ν μ μμΌλ©° λ² μ΄μ€ ν΄λμ€λ₯Ό μμ ν΄ νμλ ν΄λμ€λ€μ μμ νκ³ μΆμλλ μμμ μ¬μ©νλκ²μ΄ λ λ«λ€.
- Bad
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Employeesκ° tax dataλ₯Ό κ°μ§κ³ μκΈ° λλ¬Έμ μ΄ ν΄λμ€λ μ’μ§μλ€.
// EmployeeTaxDataλ Employee νμ
μ΄ μλλ€.
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}- Good
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}ν΄λ¦° μ½λμμλ ν΄λμ€λ₯Ό μμ νλ μ΄μ κ° ν κ°μ§λ₯Ό λμΌλ©΄ μλλ€κ³ λ§νλ€. μ΄κ²μ νλμ ν΄λμ€μ λ무 λ§μ κΈ°λ₯μ κ°λ μ±μ°λ κ²(jam-pack)κ³Ό κ°λ€. μ΄ λ¬Έμ λ ν΄λμ€κ° κ°λ μ μΌλ‘ ꡬμ±λμ΄μμ§ μμΌλ©° ν΄λμ€λ₯Ό λ°κΏμΌν λ§μ μ΄μ κ° λλ€. νλμ ν΄λμ€μ λ무 λ§μ κΈ°λ₯λ€μ΄ μκ³ μ΄λ¬ν μμ κΈ°λ₯λ€μ μμ μ΄ λ€λ₯Έ λͺ¨λλ€μ μ΄λ€ μν₯μ λΌμΉμ§λμ§ μκΈ° μ΄λ ΅κΈ° λλ¬Έμ ν΄λμ€μ μμ μκ°μ μ΅μν νλκ²μ΄ μ€μνλ€.
- Bad
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}- Good
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}Bertrand Meyerμ μνλ©΄ μννΈμ¨μ΄ μν°ν°λ€(ν΄λμ€, λͺ¨λ, ν¨μ λ±)μ νμ₯μ μν΄μ κ°λ°©μ μ΄μ΄μΌνλ©° μμ μμλ νμμ μ΄μ΄μΌ νλ€. μ΄ μμΉμ κΈ°λ³Έμ μΌλ‘ μ‘΄μ¬νλ μ½λμ μμ μμ΄ μ¬μ©μκ° μλ‘μ΄ κΈ°λ₯λ€μ μΆκ°ν μ μλλ‘ νμ©ν΄μΌνλ€λ κ²μ μλ―Ένλ€.
- Bad
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}- Good
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// transform response and return
});
}
}리μ€μ½ν μΉν μμΉμ λ§€μ° κ°λ¨νμ§λ§ κ°λ ₯ν κ°λ μ΄λ€. μ΄ μμΉμ μ μλ "λ§μ½ Sκ° Tμ νμ νμ (sub type)μ΄λ©΄, νλ‘κ·Έλ¨ μμ±μ λ³κ²½ μμ΄ T νμ μ κ°μ²΄λ S νμ μ κ°μ²΄λ‘ μΉνλ μ μλ€. "λ κ²μ΄λ€.
μλ₯Ό λ€μ΄, λΆλͺ¨ ν΄λμ€μ μμ ν΄λμ€κ° μλ κ²½μ° λ² μ΄μ€ ν΄λμ€μ μμ ν΄λμ€λ μλͺ»λ κ²°κ³Ό μμ΄ κ΅νν΄μ μ¬μ©ν μ μλ€. λ€λ₯Έ μλ‘ μνμ μΌλ‘ μ μ¬κ°νμ μ§μ¬κ°νμ΄μ§λ§ μμμ ν΅ν΄ "is-a" κ΄κ³λ‘ λͺ¨λΈλ§ν κ²½μ° λ¬Έμ κ° λ°μνλ€.
- Bad
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);- Good
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);μλ°μ€ν¬λ¦½νΈμλ μΈν°νμ΄μ€κ° μκΈ° λλ¬Έμ λ€λ₯Έ μμΉλ€ λ§νΌ μ격νκ² μ μ©λμ§ μμ§λ§ νμ μμ€ν μ΄ μλλΌλ μ€μνκ³ κ΄λ ¨μλ μμΉμ΄λ€.
μΈν°νμ΄μ€ λΆλ¦¬ μμΉμ "ν΄λΌμ΄μΈνΈκ° μ¬μ©νμ§ μλ μΈν°νμ΄μ€μ μμ‘΄νλλ‘ κ°μν΄μλ μλλ€." κ·μ νκ³ μμΌλ©° μΈν°νμ΄μ€λ€μ λνμ΄ν λλ¬Έμ μμμ μΈ contractμ΄λ€.
μΈν°νμ΄μ€ λΆλ¦¬ μμΉμ μ’μ μλ λ§μ μμ μ€μ κ°μ²΄(large settings objects)κ° νμν ν΄λμ€μ΄λ€. λλΆλΆ λͺ¨λ μ€μ λ€μ μΈν μ μꡬνλκ²μ΄ μλκΈ°λλ¬Έμ ν΄λΌμ΄μΈνΈκ° λ§μ μμ μ΅μ μ μ νν νμκ° μλ€. μ νμ μΈ μ€μ μ ν΅ν΄ fat μΈν°νμ΄μ€λ₯Ό λ§λλ κ²μ λ°©μ§ν μ μλ€.
- Bad
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});- Good
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
options: {
animationModule() {}
}
});μ΄ μμΉμ λκ°μ§μ νμ μμλ₯Ό κ°μ§κ³ μλ€.
- μμ λ 벨μ λͺ¨λμ νμ λ 벨μ λͺ¨λμ μμ‘΄ν΄μλ μλλ€. λ λͺ¨λ λ€ μΆμνμ μμ‘΄ν΄μΌ νλ€.
- μΆμνλ μΈλΆμ¬νμ μμ‘΄ν΄μλ μλλ€. μΈλΆμ¬νμ μΆμνμ μν΄ λ¬λΌμ ΈμΌνλ€.
μλ°μ€ν¬λ¦½νΈμμλ μΈν°νμ΄μ€κ° μκΈ°λλ¬Έμ μΆμνμ μμ‘΄νλκ²μ μμμ μΈ contractλ€. μ¦, λ€λ₯Έ κ°μ²΄μ ν΄λμ€μ λ
ΈμΆλλ λ©μλμ νλ‘νΌν°λ€μ΄ μμμ μΈ contractκ° λλ€. μλμ μμ μ½λμμμ μμμ μΈ contractλ InventoryTrackerμ λν μμ² λͺ¨λλ€μ΄ requestItems λ©μλλ₯Ό κ°μ§κ² λλ€λ κ²μ΄λ€.
- Bad
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();- Good
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();Reference