软件工程原理,来自Robert C. Martin的《Clean Code》一书,改编为JavaScript。这不是风格指南。它是在 JavaScript 中生成可读、可重用和可重构软件的指南。
并非这里的每一项原则都必须严格遵守,得到普遍同意的原则就更少了。这些是指导方针,仅此而已,但它们是由清洁代码的作者在多年的集体经验中编纂出来的。
我们的软件工程技术只有50多年的历史,我们仍然在学习很多东西。当软件架构和架构本身一样古老时,也许那时我们将有更严格的规则要遵循。现在,让这些准则作为评估你和你的团队生成的 JavaScript 代码质量的试金石。
还有一件事:了解这些不会立即使你成为更好的软件开发人员,与他们合作多年并不意味着你不会犯错误。每一段代码都是从初稿开始的,就像湿粘土被塑造成最终形式一样。最后,当我们与同行一起审查时,我们会凿掉不完美之处。不要因为需要改进的初稿而自责。打败代码吧!
坏:
const yyyymmdstr = moment().format("YYYY/MM/DD");
好:
const currentDate = moment().format("YYYY/MM/DD");
坏:
getUserInfo();
getClientData();
getCustomerRecord();
好:
getUser();
我们将阅读比我们编写的代码更多的代码。重要的是,我们编写的代码是可读和可搜索的。如果不命名最终对理解我们的程序有意义的变量,我们伤害了读者。使你的姓名可搜索。buddy.js 和 ESLint 等工具可以帮助识别未命名的常量。
坏:
// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);
好:
// Declare them as capitalized named constants.
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
setTimeout(blastOff, MILLISECONDS_PER_DAY);
坏:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
好:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
显式总比隐式好。
坏:
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Wait, what is `l` for again?
dispatch(l);
});
好:
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
如果你的类/对象名称告诉你一些东西,不要在你的变量名中重复。
坏:
const Car = {
carMake: "Honda",
carModel: "Accord",
carColor: "Blue"
};
function paintCar(car, color) {
car.carColor = color;
}
好:
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car, color) {
car.color = color;
}
默认参数通常比短路更干净。请注意,如果使用它们,则函数将仅提供参数的默认值。其他“假”值(如、、、、和)不会替换为默认值。
undefined
''
""
false
null
0
NaN
坏:
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
好:
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
限制函数参数的数量非常重要,因为它使函数测试更容易。超过三个会导致组合爆炸,你必须使用每个单独的参数测试大量不同的案例。
一个或两个参数是理想的情况,如果可能的话,应避免三个参数。除此之外的任何事情都应该得到巩固。通常,如果你有两个以上的参数,那么你的函数想做太多。如果不是,大多数情况下,一个更高级别的对象就足以作为参数。
由于 JavaScript 允许你动态创建对象,而无需大量类样板,因此如果你发现自己需要大量参数,则可以使用对象。
为了明确函数所需的属性,可以使用 ES2015/ES6 解构语法。这有几个优点:
坏:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
好:
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
这是迄今为止软件工程中最重要的规则。当函数做不止一件事时,它们更难组合、测试和推理。当你可以将一个函数隔离到一个操作时,可以轻松重构它,并且你的代码将读起来更干净。如果你从本指南中没有其他任何东西,那么你将领先于许多开发人员。
坏:
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
好:
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
坏:
function addToDate(date, month) {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);
好:
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
当你有多个抽象级别时,你的函数通常会做太多。拆分函数可实现可重用性和更轻松的测试。
坏:
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...
});
}
好:
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;
}
尽最大努力避免重复代码。重复的代码是不好的,因为这意味着如果你需要更改某些逻辑,则有多个地方可以更改某些内容。
想象一下,如果你经营一家餐馆,你跟踪你的库存:你所有的西红柿、洋葱、大蒜、香料等。如果你有多个列表,那么当你提供包含西红柿的菜肴时,所有列表都必须更新。如果你只有一个列表,则只有一个地方可以更新!
通常,你有重复的代码,因为你有两个或多个略有不同的东西,它们有很多共同点,但它们的差异迫使你有两个或多个单独的函数来执行许多相同的操作。删除重复的代码意味着创建一个抽象,该抽象可以仅使用一个函数/模块/类来处理这组不同的事情。
正确进行抽象至关重要,这就是为什么你应该遵循类部分中列出的 SOLID 原则。糟糕的抽象可能比重复的代码更糟糕,所以要小心!话虽如此,如果你能做一个好的抽象,那就去做吧!不要重复自己,否则你会发现自己在任何时候想要改变一件事时都会更新多个地方。
坏:
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);
});
}
好:
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);
});
}
坏:
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);
好:
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);
标志告知用户此函数执行多项操作。函数应该做一件事。如果函数遵循基于布尔值的不同代码路径,请拆分函数。
坏:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
好:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
如果函数执行除获取值并返回另一个值或多个值之外的任何操作,则会产生副作用。副作用可能是写入文件,修改某些全局变量,或者不小心将所有钱汇给陌生人。
现在,你确实需要偶尔在程序中产生副作用。与前面的示例一样,你可能需要写入文件。你要做的是集中执行此操作的位置。不要有多个写入特定文件的函数和类。有一个服务可以做到这一点。一个,只有一个。
重点是避免常见的陷阱,例如在没有任何结构的对象之间共享状态,使用可由任何内容写入的可变数据类型,以及不集中发生副作用的位置。如果你能做到这一点,你会比绝大多数其他程序员更快乐。
坏:
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
let name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
好:
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
在 JavaScript 中,有些值是不可更改的(不可变的),有些是可变的(可变的)。对象和数组是两种可变值,因此在将它们作为参数传递给函数时,仔细处理它们非常重要。JavaScript 函数可以更改对象的属性或更改数组的内容,这很容易在其他地方导致错误。
假设有一个函数接受表示购物车的数组参数。如果该函数在该购物车数组中进行更改(例如,通过添加要购买的商品),则使用该相同数组的任何其他函数都将受到此添加的影响。这可能很棒,但也可能是坏事。让我们想象一个糟糕的情况:
cart
用户单击“购买”按钮,该按钮调用生成网络请求并将数组发送到服务器的函数。由于网络连接不良,函数必须不断重试请求。现在,如果用户在网络请求开始之前不小心点击了他们实际上不想要的商品上的“添加到购物车”按钮怎么办?如果发生这种情况并且网络请求开始,则该购买函数将发送意外添加的项目,因为数组已被修改。
purchase
cart
purchase
cart
一个很好的解决方案是让函数始终克隆、编辑它并返回克隆。这将确保仍在使用旧购物车的功能不会受到更改的影响。
addItemToCart
cart
关于这种方法,有两个注意事项:
在某些情况下,你可能实际上想要修改输入对象,但是当你采用此编程实践时,你会发现这些情况非常罕见。大多数东西都可以重构为没有副作用!
克隆大型对象在性能方面可能非常昂贵。幸运的是,这在实践中不是一个大问题,因为有一些很棒的库可以让这种编程方法快速,而不是像手动克隆对象和数组那样占用大量内存。
坏:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
好:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
污染全局变量在 JavaScript 中是一种不好的做法,因为你可能会与另一个库发生冲突,并且你的 API 用户在生产中出现异常之前不会更明智。让我们考虑一个例子:如果你想扩展JavaScript的原生Array方法,有一个可以显示两个数组之间差异的方法,该怎么办?你可以将新函数写入 ,但它可能会与另一个尝试执行相同操作的库发生冲突。如果另一个库只是用来查找数组的第一个和最后一个元素之间的差异怎么办?这就是为什么只使用 ES2015/ES6 类并简单地扩展全局会好得多。
diff
Array.prototype
diff
Array
坏:
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
好:
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
JavaScript不像Haskell那样是一种函数式语言,但它有一种函数式的味道。函数式语言可以更简洁、更易于测试。尽可能支持这种编程风格。
坏:
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;
}
好:
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
);
坏:
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}
好:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
坏:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
好:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
这似乎是一项不可能完成的任务。第一次听到这句话时,大多数人会说,“没有声明,我怎么能做任何事情?答案是,在许多情况下,你可以使用多态性来完成相同的任务。第二个问题通常是,“嗯,这很好,但我为什么要这样做?答案是我们之前学到的干净代码概念:一个函数应该只做一件事。当你拥有具有语句的类和函数时,你是在告诉用户你的函数执行多项操作。记住,只做一件事。
if
if
坏:
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();
}
}
}
好:
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();
}
}
JavaScript 是非类型的,这意味着你的函数可以接受任何类型的参数。有时你会被这种自由所困扰,在你的函数中进行类型检查变得很诱人。有很多方法可以避免这样做。首先要考虑的是一致的 API。
坏:
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"));
}
}
好:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location("texas"));
}
如果你正在使用字符串和整数等基本基元值,并且无法使用多态性,但仍然觉得需要类型检查,则应考虑使用 TypeScript。它是普通JavaScript的绝佳替代品,因为它在标准JavaScript语法之上为你提供了静态类型。手动类型检查普通JavaScript的问题在于,做得好需要太多额外的措辞,以至于你得到的虚假的“类型安全”并不能弥补失去的可读性。保持你的JavaScript干净,写好的测试,并进行良好的代码审查。否则,使用TypeScript(就像我说的,这是一个很好的选择!)。
坏:
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");
}
好:
function combine(val1, val2) {
return val1 + val2;
}
现代浏览器在运行时在后台进行大量优化。很多时候,如果你在优化,那么你只是在浪费你的时间。有很好的资源可以查看缺乏优化的地方。同时瞄准那些,直到它们被修复(如果可以的话)。
坏:
// 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++) {
// ...
}
好:
for (let i = 0; i < list.length; i++) {
// ...
}
死代码和重复代码一样糟糕。没有理由将其保留在你的代码库中。如果没有被调用,请摆脱它!如果你仍然需要它,它在你的版本历史记录中仍然是安全的。
坏:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");
好:
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");
使用 getter 和 setter 访问对象上的数据可能比简单地查找对象上的属性更好。“为什么?”你可能会问。好吧,这里有一个杂乱无章的原因列表:
set
坏:
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
好:
function makeBankAccount() {
// this one is private
let balance = 0;
// a "getter", made public via the returned object below
function getBalance() {
return balance;
}
// a "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 及更低版本)来实现。
坏:
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
好:
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 类的可读类继承、构造和方法定义。如果你需要继承(并且知道你可能不需要),那么最好选择ES2015 / ES6类。但是,更喜欢小函数而不是类,直到你发现自己需要更大更复杂的对象。
坏:
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() {};
好:
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() {
/* ... */
}
}
这种模式在 JavaScript 中非常有用,你可以在许多库(如 jQuery 和 Lodash)中看到它。它允许你的代码富有表现力,而不是冗长。出于这个原因,我说,使用方法链接并查看你的代码将有多干净。在你的类函数中,只需在每个函数的末尾返回,你可以将更多的类方法链接到它上面。
this
坏:
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();
好:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
正如四人帮在《设计模式》中所说的那样,在可能的情况下,你应该更喜欢组合而不是继承。使用继承有很多很好的理由,使用组合有很多很好的理由。这句格言的要点是,如果你的大脑本能地追求遗传,试着想想作文是否可以更好地模拟你的问题。在某些情况下可以。
你可能想知道,“我什么时候应该使用继承?这取决于你手头的问题,但这是一个不错的列表,说明什么时候继承比组合更有意义:
坏:
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
好:
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);
}
// ...
}
正如 Clean Code 中所述,“一个类更改的原因永远不应该超过一个”。挤满一个有很多功能的班级是很诱人的,比如当你在航班上只能携带一个行李箱时。这样做的问题是,你的类在概念上不会有凝聚力,它会给它很多改变的理由。尽量减少需要更改类的次数非常重要。这很重要,因为如果一个类中有太多功能,并且你修改了其中的一部分,则可能很难理解这将如何影响代码库中的其他依赖模块。
坏:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
好:
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所说,“软件实体(类,模块,函数等)应该开放扩展,但关闭修改。这是什么意思呢?这个原则基本上指出,你应该允许用户在不更改现有代码的情况下添加新功能。
坏:
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
}
好:
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 的子类型,那么 T 类型的对象可以替换为 S 类型的对象(即,S 类型的对象可以替换 T 类型的对象),而不会改变该程序的任何理想属性(正确性、执行的任务等)。这是一个更可怕的定义。
对此的最佳解释是,如果你有父类和子类,则基类和子类可以互换使用,而不会得到不正确的结果。这可能仍然令人困惑,所以让我们看一下经典的方形矩形示例。从数学上讲,正方形是一个矩形,但是如果你通过继承使用“is-a”关系对其进行建模,则很快就会遇到麻烦。
坏:
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);
好:
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);
JavaScript 没有接口,所以这个原则不像其他原则那样严格适用。然而,即使JavaScript缺乏类型系统,它也是重要和相关的。
ISP指出,“不应强迫客户端依赖它们不使用的接口。接口是JavaScript中的隐式契约,因为鸭子类型。
在 JavaScript 中演示此原则的一个很好的例子是用于需要大型设置对象的类。不要求客户端设置大量选项是有益的,因为大多数时候他们不需要所有设置。使它们可选有助于防止出现“胖接口”。
坏:
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.
// ...
});
好:
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() {}
}
});
这个原则陈述了两个基本的东西:
起初这可能很难理解,但如果你使用过AngularJS,你已经看到了这个原则以依赖注入(DI)的形式实现。虽然它们不是相同的概念,但 DIP 使高级模块无法了解其低级模块的详细信息并进行设置。它可以通过 DI 实现此目的。这样做的一大好处是它减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使代码难以重构。
如前所述,JavaScript 没有接口,因此依赖的抽象是隐式契约。也就是说,一个对象/类向另一个对象/类公开的方法和属性。在下面的示例中,隐式协定是 的任何 Request 模块都将有一个方法。
InventoryTracker
requestItems
坏:
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();
好:
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();
测试比运输更重要。如果你没有测试或数量不足,那么每次发布代码时,你都无法确定你没有破坏任何东西。决定什么构成足够的金额取决于你的团队,但拥有 100% 的覆盖率(所有报表和分支)是你获得非常高的信心和开发人员安心的方式。这意味着除了拥有一个出色的测试框架之外,你还需要使用一个好的覆盖工具。
没有理由不编写测试。有很多好的JS测试框架,所以找到一个你的团队喜欢的框架。当你找到一个适合你的团队的人时,那么目标是始终为你引入的每个新功能/模块编写测试。如果你的首选方法是测试驱动开发 (TDD),那很好,但重点是确保在启动任何功能或重构现有功能之前达到覆盖目标。
坏:
import assert from "assert";
describe("MomentJS", () => {
it("handles date boundaries", () => {
let date;
date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});
好:
import assert from "assert";
describe("MomentJS", () => {
it("handles 30-day months", () => {
const date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
});
it("handles leap year", () => {
const date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
});
it("handles non-leap year", () => {
const date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});
回调不干净,会导致过多的嵌套。在 ES2015/ES6 中,承诺是内置的全局类型。使用它们!
坏:
import { get } from "request";
import { writeFile } from "fs";
get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
(requestErr, response, body) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile("article.html", body, writeErr => {
if (writeErr) {
console.error(writeErr);
} else {
console.log("File written");
}
});
}
}
);
好:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
承诺是回调的一个非常干净的替代方案,但ES2017 / ES8带来了async和await,它提供了一个更干净的解决方案。你所需要的只是一个以关键字为前缀的函数,然后你可以在没有函数链的情况下命令性地编写逻辑。如果你今天可以利用 ES2017/ES8 功能,请使用此功能!
async
then
坏:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
好:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
async function getCleanCodeArticle() {
try {
const body = await get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
);
await writeFile("article.html", body);
console.log("File written");
} catch (err) {
console.error(err);
}
}
getCleanCodeArticle()
抛出错误是一件好事!它们意味着运行时已成功识别程序中何时出现问题,并通过停止当前堆栈上的函数执行、终止进程(在 Node 中)并在控制台中通过堆栈跟踪通知你来通知你。
对捕获的错误不执行任何操作并不能使你能够修复或对所述错误做出 React 。将错误记录到控制台 () 并没有好多少,因为很多时候它会迷失在打印到控制台的大量东西中。如果你将任何代码包装在一个中,这意味着你认为那里可能会发生错误,因此你应该有一个计划,或者创建一个代码路径,以便在它发生时发生。
console.log
try/catch
坏:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
好:
try {
functionThatMightThrow();
} catch (error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}
出于同样的原因,你不应忽略从 中捕获的错误。
try/catch
坏:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});
好:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
});
格式是主观的。像这里的许多规则一样,没有你必须遵循的硬性规定。重点是不要争论格式。有大量的工具可以自动化这一点。用一个!工程师争论格式是浪费时间和金钱。
对于不属于自动格式设置范围的内容(缩进、制表符与空格、双引号与单引号等),请在此处查看一些指导。
JavaScript 是无类型的,所以大写可以告诉你很多关于你的变量、函数等的信息。这些规则是主观的,因此你的团队可以选择他们想要的任何内容。关键是,无论你们选择什么,都要保持一致。
坏:
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const Artists = ["ACDC", "Led Zeppelin", "The Beatles"];
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Alpaca {}
好:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const ARTISTS = ["ACDC", "Led Zeppelin", "The Beatles"];
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Alpaca {}
如果一个函数调用另一个函数,请将这些函数垂直关闭在源文件中。理想情况下,将呼叫者保持在被呼叫者的正上方。我们倾向于从上到下阅读代码,就像报纸一样。因此,请以这种方式读取代码。
坏:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
lookupPeers() {
return db.lookup(this.employee, "peers");
}
lookupManager() {
return db.lookup(this.employee, "manager");
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getManagerReview() {
const manager = this.lookupManager();
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
好:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
lookupPeers() {
return db.lookup(this.employee, "peers");
}
getManagerReview() {
const manager = this.lookupManager();
}
lookupManager() {
return db.lookup(this.employee, "manager");
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
评论是道歉,而不是要求。好的代码大多是记录自己。
坏:
function hashIt(data) {
// The hash
let hash = 0;
// Length of string
const length = data.length;
// Loop through every character in data
for (let i = 0; i < length; i++) {
// Get character code.
const char = data.charCodeAt(i);
// Make the hash
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash &= hash;
}
}
好:
function hashIt(data) {
let hash = 0;
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data.charCodeAt(i);
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash &= hash;
}
}
版本控制的存在是有原因的。将旧代码保留在历史记录中。
坏:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
好:
doStuff();
请记住,使用版本控制!不需要死代码、注释代码,尤其是期刊注释。用来获取历史!
git log
坏:
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Removed type-checking (LI)
* 2015-03-14: Added combine with type-checking (JR)
*/
function combine(a, b) {
return a + b;
}
好:
function combine(a, b) {
return a + b;
}
他们通常只会增加噪音。让函数和变量名称以及正确的缩进和格式设置为代码提供可视结构。
坏:
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: "foo",
nav: "bar"
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
好:
$scope.model = {
menu: "foo",
nav: "bar"
};
const actions = function() {
// ...
};
这也有其他语言版本: