将源自 Robert C. Martin 的 Clean Code 的软件工程原则适配到 JavaScript 。 这不是一个代码风格指南, 它是一个使用 JavaScript 来生产 可读的, 可重用的, 以及可重构的软件的指南。
这里的每一项原则都不是必须遵守的, 甚至只有更少的能够被广泛认可。 这些仅仅是指南而已, 但是却是 Clean Code 作者多年经验的结晶。
我们的软件工程行业只有短短的 50 年, 依然有很多要我们去学习。 当软件架构与建筑架构一样古老时, 也许我们将会有硬性的规则去遵守。 而现在, 让这些指南做为你和你的团队生产的 JavaScript 代码的 质量的标准。
还有一件事: 知道这些指南并不能马上让你成为一个更加出色的软件开发者, 并且使用它们工作多年也并 不意味着你不再会犯错误。 每一段代码最开始都是草稿, 像湿粘土一样被打造成最终的形态。 最后当我们 和搭档们一起审查代码时清除那些不完善之处, 不要因为最初需要改善的草稿代码而自责, 而是对那些代 码下手。
不好的:
constyyyymmdstr=moment().format('YYYY/MM/DD');好的:
constcurrentDate=moment().format('YYYY/MM/DD');不好的:
getUserInfo();getClientData();getCustomerRecord();好的:
getUser();我们要阅读的代码比要写的代码多得多, 所以我们写出的代码的可读性和可搜索性是很重要的。 使用没有 意义的变量名将会导致我们的程序难于理解, 将会伤害我们的读者, 所以请使用可搜索的变量名。 类似 buddy.js 和 ESLint 的工具可以帮助我们找到未命名的常量。
不好的:
// 艹, 86400000 是什么鬼?setTimeout(blastOff,86400000);好的:
// 将它们声明为全局常量 `const` 。constMILLISECONDS_IN_A_DAY=86400000;setTimeout(blastOff,MILLISECONDS_IN_A_DAY);不好的:
constaddress='One Infinite Loop, Cupertino 95014';constcityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;saveCityZipCode(address.match(cityZipCodeRegex)[1],address.match(cityZipCodeRegex)[2]);好的:
constaddress='One Infinite Loop, Cupertino 95014';constcityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;const[,city,zipCode]=address.match(cityZipCodeRegex)||[];saveCityZipCode(city,zipCode);显示比隐式更好
不好的:
constlocations=['Austin','New York','San Francisco'];locations.forEach((l)=>{doStuff();doSomeOtherStuff();// ...// ...// ...// 等等, `l` 是啥?dispatch(l);});好的:
constlocations=['Austin','New York','San Francisco'];locations.forEach((location)=>{doStuff();doSomeOtherStuff();// ...// ...// ...dispatch(location);});如果你的类名/对象名有意义, 不要在变量名上再重复。
不好的:
constCar={carMake: 'Honda',carModel: 'Accord',carColor: 'Blue'};functionpaintCar(car){car.carColor='Red';}好的:
constCar={make: 'Honda',model: 'Accord',color: 'Blue'};functionpaintCar(car){car.color='Red';}不好的:
functioncreateMicrobrewery(name){constbreweryName=name||'Hipster Brew Co.';// ...}好的:
functioncreateMicrobrewery(breweryName='Hipster Brew Co.'){// ...}限制函数参数的个数是非常重要的, 因为这样将使你的函数容易进行测试。 一旦超过三个参数将会导致组 合爆炸, 因为你不得不编写大量针对每个参数的测试用例。
没有参数是最理想的, 一个或者两个参数也是可以的, 三个参数应该避免, 超过三个应该被重构。 通常, 如果你有一个超过两个函数的参数, 那就意味着你的函数尝试做太多的事情。 如果不是, 多数情况下一个 更高级对象可能会满足需求。
由于 JavaScript 允许我们不定义类型/模板就可以创建对象, 当你发现你自己需要大量的参数时, 你 可以使用一个对象。
不好的:
functioncreateMenu(title,body,buttonText,cancellable){// ...}好的:
constmenuConfig={title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true};functioncreateMenu(config){// ...}这是软件工程中最重要的一条规则, 当函数需要做更多的事情时, 它们将会更难进行编写、 测试和推理。 当你能将一个函数隔离到只有一个动作, 他们将能够被容易的进行重构并且你的代码将会更容易阅读。 如 果你严格遵守本指南中的这一条, 你将会领先于许多开发者。
不好的:
functionemailClients(clients){clients.forEach((client)=>{constclientRecord=database.lookup(client);if(clientRecord.isActive()){email(client);}});}好的:
functionemailClients(clients){clients.filter(isClientActive).forEach(email);}functionisClientActive(client){constclientRecord=database.lookup(client);returnclientRecord.isActive();}不好的:
functionaddToDate(date,month){// ...}constdate=newDate();// 很难从函数名看出加了什么addToDate(date,1);好的:
functionaddMonthToDate(month,date){// ...}constdate=newDate();addMonthToDate(1,date);当在你的函数中有多于一个抽象级别时, 你的函数通常做了太多事情。 拆分函数将会提升重用性和测试性。
不好的:
functionparseBetterJSAlternative(code){constREGEXES=[// ...];conststatements=code.split(' ');consttokens=[];REGEXES.forEach((REGEX)=>{statements.forEach((statement)=>{// ...});});constast=[];tokens.forEach((token)=>{// lex...});ast.forEach((node)=>{// parse...});}好的:
functiontokenize(code){constREGEXES=[// ...];conststatements=code.split(' ');consttokens=[];REGEXES.forEach((REGEX)=>{statements.forEach((statement)=>{tokens.push(/* ... */);});});returntokens;}functionlexer(tokens){constast=[];tokens.forEach((token)=>{ast.push(/* ... */);});returnast;}functionparseBetterJSAlternative(code){consttokens=tokenize(code);constast=lexer(tokens);ast.forEach((node)=>{// parse...});}竭尽你的全力去避免冗余代码。 冗余代码是不好的, 因为它意味着当你需要修改一些逻辑时会有多个地方 需要修改。
想象一下你在经营一家餐馆, 你需要记录所有的库存西红柿, 洋葱, 大蒜, 各种香料等等。 如果你有多 个记录列表, 当你用西红柿做一道菜时你得更新多个列表。 如果你只有一个列表, 就只有一个地方需要更 新!
你有冗余代码通常是因为你有两个或多个稍微不同的东西, 它们共享大部分, 但是它们的不同之处迫使你使 用两个或更多独立的函数来处理大部分相同的东西。 移除冗余代码意味着创建一个可以处理这些不同之处的 抽象的函数/模块/类。
让这个抽象正确是关键的, 这是为什么要你遵循 Classes 那一章的 SOLID 的原因。 不好的抽象比冗 余代码更差, 所以要谨慎行事。 既然已经这么说了, 如果你能够做出一个好的抽象, 才去做。 不要重复 你自己, 否则你会发现当你要修改一个东西时时刻需要修改多个地方。
不好的:
functionshowDeveloperList(developers){developers.forEach((developer)=>{constexpectedSalary=developer.calculateExpectedSalary();constexperience=developer.getExperience();constgithubLink=developer.getGithubLink();constdata={
expectedSalary,
experience,
githubLink
};render(data);});}functionshowManagerList(managers){managers.forEach((manager)=>{constexpectedSalary=manager.calculateExpectedSalary();constexperience=manager.getExperience();constportfolio=manager.getMBAProjects();constdata={
expectedSalary,
experience,
portfolio
};render(data);});}好的:
functionshowList(employees){employees.forEach((employee)=>{constexpectedSalary=employee.calculateExpectedSalary();constexperience=employee.getExperience();letportfolio=employee.getGithubLink();if(employee.type==='manager'){portfolio=employee.getMBAProjects();}constdata={
expectedSalary,
experience,
portfolio
};render(data);});}不好的:
constmenuConfig={title: null,body: 'Bar',buttonText: null,cancellable: true};functioncreateMenu(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);好的:
constmenuConfig={title: 'Order',// User did not include 'body' keybuttonText: 'Send',cancellable: true};functioncreateMenu(config){config=Object.assign({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true},config);// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}// ...}createMenu(menuConfig);标记位是告诉你的用户这个函数做了不只一件事情。 函数应该只做一件事情。 如果你的函数因为一个布尔值 出现不同的代码路径, 请拆分它们。
不好的:
functioncreateFile(name,temp){if(temp){fs.create(`./temp/${name}`);}else{fs.create(name);}}好的:
functioncreateFile(name){fs.create(name);}functioncreateTempFile(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.// 如果我们有另一个函数使用这个 name , 现在它应该是一个数组, 这可能会出现错误。letname='Ryan McDermott';functionsplitIntoFirstAndLastName(){name=name.split(' ');}splitIntoFirstAndLastName();console.log(name);// ['Ryan', 'McDermott'];好的:
functionsplitIntoFirstAndLastName(name){returnname.split(' ');}constname='Ryan McDermott';constnewName=splitIntoFirstAndLastName(name);console.log(name);// 'Ryan McDermott';console.log(newName);// ['Ryan', 'McDermott'];污染全局在 JavaScript 中是一个不好的做法, 因为你可能会和另外一个类库冲突, 你的 API 的用户
可能不够聪明, 直到他们得到在生产环境得到一个异常。 让我们来考虑这样一个例子: 假设你要扩展
JavaScript 的 原生 Array , 添加一个可以显示两个数组的不同之处的 diff 方法, 你可以在
Array.prototype 中写一个新的方法, 但是它可能会和尝试做相同事情的其它类库发生冲突。 如果有
另外一个类库仅仅使用 diff 方法来查找数组的第一个元素和最后一个元素之间的不同之处呢? 这就是
为什么使用 ES2015/ES6 的类是一个更好的做法的原因, 只要简单的扩展全局的 Array 即可。
不好的:
Array.prototype.diff=functiondiff(comparisonArray){consthash=newSet(comparisonArray);returnthis.filter(elem=>!hash.has(elem));};好的:
classSuperArrayextendsArray{diff(comparisonArray){consthash=newSet(comparisonArray);returnthis.filter(elem=>!hash.has(elem));}}JavaScript 不是 Haskell 那种方式的函数式语言, 但是它有它的函数式风格。 函数式语言更加简洁 并且更容易进行测试, 当你可以使用函数式编程风格时请尽情使用。
不好的:
constprogrammerOutput=[{name: 'Uncle Bobby',linesOfCode: 500},{name: 'Suzie Q',linesOfCode: 1500},{name: 'Jimmy Gosling',linesOfCode: 150},{name: 'Gracie Hopper',linesOfCode: 1000}];lettotalOutput=0;for(leti=0;i<programmerOutput.length;i++){totalOutput+=programmerOutput[i].linesOfCode;}好的:
constprogrammerOutput=[{name: 'Uncle Bobby',linesOfCode: 500},{name: 'Suzie Q',linesOfCode: 1500},{name: 'Jimmy Gosling',linesOfCode: 150},{name: 'Gracie Hopper',linesOfCode: 1000}];consttotalOutput=programmerOutput.map((programmer)=>programmer.linesOfCode).reduce((acc,linesOfCode)=>acc+linesOfCode,0);不好的:
if(fsm.state==='fetching'&&isEmpty(listNode)){// ...}好的:
functionshouldShowSpinner(fsm,listNode){returnfsm.state==='fetching'&&isEmpty(listNode);}if(shouldShowSpinner(fsmInstance,listNodeInstance)){// ...}不好的:
functionisDOMNodeNotPresent(node){// ...}if(!isDOMNodeNotPresent(node)){// ...}好的:
functionisDOMNodePresent(node){// ...}if(isDOMNodePresent(node)){// ...}这看起来似乎是一个不可能的任务。 第一次听到这个时, 多数人会说: “没有 if 语句还能期望我干
啥呢”, 答案是多数情况下你可以使用多态来完成同样的任务。 第二个问题通常是 “好了, 那么做很棒,
但是我为什么想要那样做呢”, 答案是我们学到的上一条代码整洁之道的理念: 一个函数应当只做一件事情。
当你有使用 if 语句的类/函数是, 你在告诉你的用户你的函数做了不止一件事情。 记住: 只做一件
事情。
不好的:
classAirplane{// ...getCruisingAltitude(){switch(this.type){case'777':
returnthis.getMaxAltitude()-this.getPassengerCount();case'Air Force One':
returnthis.getMaxAltitude();case'Cessna':
returnthis.getMaxAltitude()-this.getFuelExpenditure();}}}好的:
classAirplane{// ...}classBoeing777extendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getPassengerCount();}}classAirForceOneextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude();}}classCessnaextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getFuelExpenditure();}}JavaScript 是无类型的, 这意味着你的函数能接受任何类型的参数。 但是有时又会被这种自由咬伤, 于是又尝试在你的函数中做类型检查。 有很多种方式来避免这个, 第一个要考虑的是一致的 API 。
不好的:
functiontravelToTexas(vehicle){if(vehicleinstanceofBicycle){vehicle.peddle(this.currentLocation,newLocation('texas'));}elseif(vehicleinstanceofCar){vehicle.drive(this.currentLocation,newLocation('texas'));}}好的:
functiontravelToTexas(vehicle){vehicle.move(this.currentLocation,newLocation('texas'));}如果你使用原始的字符串、 整数和数组, 并且你不能使用多态, 但是你依然感觉到有类型检查的需要, 你应该考虑使用 TypeScript 。 它是一个常规 JavaScript 的优秀的替代品, 因为它在标准的 JavaScript 语法之上为你提供静态类型。 对常规 JavaScript 做人工类型检查的问题是需要大量的冗词来仿造类型安 全而不缺失可读性。 保持你的 JavaScript 简洁, 编写良好的测试, 并有良好的代码审阅, 否则使用 TypeScript (就像我说的, 它是一个伟大的替代品)来完成这些。
不好的:
functioncombine(val1,val2){if(typeofval1==='number'&&typeofval2==='number'||typeofval1==='string'&&typeofval2==='string'){returnval1+val2;}thrownewError('Must be of type String or Number');}好的:
functioncombine(val1,val2){returnval1+val2;}现代化浏览器运行时在幕后做大量的优化, 在大多数的时间, 做优化就是在浪费你的时间。 这些是好的 资源, 用来 查看那些地方需要优化。 为这些而优化, 直到他们被修正。
不好的:
// On old browsers, each iteration with uncached `list.length` would be costly// because of `list.length` recomputation. In modern browsers, this is optimized.// 在旧的浏览器上, 每次循环 `list.length` 都没有被缓存, 会导致不必要的开销, 因为要重新计// 算 `list.length` 。 在现代化浏览器上, 这个已经被优化了。for(leti=0,len=list.length;i<len;i++){// ...}好的:
for(leti=0;i<list.length;i++){// ...}僵死代码和冗余代码同样糟糕。 没有理由在代码库中保存它。 如果它不会被调用, 就删掉它。 当你需要 它时, 它依然保存在版本历史记录中。
不好的:
functionoldRequestModule(url){// ...}functionnewRequestModule(url){// ...}constreq=newRequestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');好的:
functionnewRequestModule(url){// ...}constreq=newRequestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');JavaScript 没有接口或类型, 所以坚持这个模式是非常困难的, 因为我们没有 public 和 private
关键字。 正因为如此, 使用 getters 和 setters 来访问对象上的数据比简单的在一个对象上查找属性
要好得多。 “为什么?” 你可能会问, 好吧, 原因请看下面的列表:
- 当你想在获取一个对象属性的背后做更多的事情时, 你不需要在代码库中查找和修改每一处访问;
- 使用
set可以让添加验证变得容易; - 封装内部实现;
- 使用 getting 和 setting 时, 容易添加日志和错误处理;
- 继承这个类, 你可以重写默认功能;
- 你可以延迟加载对象的属性, 比如说从服务器获取。
不好的:
classBankAccount{constructor(){this.balance=1000;}}constbankAccount=newBankAccount();// Buy shoes...bankAccount.balance-=100;好的:
classBankAccount{constructor(balance=1000){this._balance=balance;}// It doesn't have to be prefixed with `get` or `set` to be a getter/settersetbalance(amount){if(verifyIfAmountCanBeSetted(amount)){this._balance=amount;}}getbalance(){returnthis._balance;}verifyIfAmountCanBeSetted(val){// ...}}constbankAccount=newBankAccount();// Buy shoes...bankAccount.balance-=shoesPrice;// Get balanceletbalance=bankAccount.balance;这个可以通过闭包来实现(针对 ES5 或更低)。
不好的:
constEmployee=function(name){this.name=name;};Employee.prototype.getName=functiongetName(){returnthis.name;};constemployee=newEmployee('John Doe');console.log(`Employee name: ${employee.getName()}`);// Employee name: John Doedeleteemployee.name;console.log(`Employee name: ${employee.getName()}`);// Employee name: undefined好的:
constEmployee=function(name){this.getName=functiongetName(){returnname;};};constemployee=newEmployee('John Doe');console.log(`Employee name: ${employee.getName()}`);// Employee name: John Doedeleteemployee.name;console.log(`Employee name: ${employee.getName()}`);// Employee name: John Doe很难为经典的 ES5 类创建可读的的继承、 构造和方法定义。 如果你需要继承(并且感到奇怪为啥你不需 要), 则优先用 ES2015/ES6的类。 不过, 短小的函数优先于类, 直到你发现你需要更大并且更复杂的 对象。
不好的:
constAnimal=function(age){if(!(thisinstanceofAnimal)){thrownewError('Instantiate Animal with `new`');}this.age=age;};Animal.prototype.move=functionmove(){};constMammal=function(age,furColor){if(!(thisinstanceofMammal)){thrownewError('Instantiate Mammal with `new`');}Animal.call(this,age);this.furColor=furColor;};Mammal.prototype=Object.create(Animal.prototype);Mammal.prototype.constructor=Mammal;Mammal.prototype.liveBirth=functionliveBirth(){};constHuman=function(age,furColor,languageSpoken){if(!(thisinstanceofHuman)){thrownewError('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=functionspeak(){};好的:
classAnimal{constructor(age){this.age=age;}move(){/* ... */}}classMammalextendsAnimal{constructor(age,furColor){super(age);this.furColor=furColor;}liveBirth(){/* ... */}}classHumanextendsMammal{constructor(age,furColor,languageSpoken){super(age,furColor);this.languageSpoken=languageSpoken;}speak(){/* ... */}}这个模式在 JavaScript 中是非常有用的, 并且你可以在许多类库比如 jQuery 和 Lodash 中见到。
它使你的代码变得富有表现力, 并减少啰嗦。 因为这个原因, 我说, 使用方法链然后再看看你的代码
会变得多么简洁。 在你的类/方法中, 简单的在每个方法的最后返回 this , 然后你就能把这个类的
其它方法链在一起。
不好的:
classCar{constructor(){this.make='Honda';this.model='Accord';this.color='white';}setMake(make){this.make=make;}setModel(model){this.model=model;}setColor(color){this.color=color;}save(){console.log(this.make,this.model,this.color);}}constcar=newCar();car.setColor('pink');car.setMake('Ford');car.setModel('F-150');car.save();好的:
classCar{constructor(){this.make='Honda';this.model='Accord';this.color='white';}setMake(make){this.make=make;// NOTE: Returning this for chainingreturnthis;}setModel(model){this.model=model;// NOTE: Returning this for chainingreturnthis;}setColor(color){this.color=color;// NOTE: Returning this for chainingreturnthis;}save(){console.log(this.make,this.model,this.color);// NOTE: Returning this for chainingreturnthis;}}constcar=newCar().setColor('pink').setMake('Ford').setModel('F-150').save();正如设计模式四人帮所述, 如果可能, 你应该优先使用组合而不是继承。 有许多好的理由去使用继承, 也有许多好的理由去使用组合。这个格言 的重点是, 如果你本能的观点是继承, 那么请想一下组合能否更好的为你的问题建模。 很多情况下它真的 可以。
那么你也许会这样想, “我什么时候改使用继承?” 这取决于你手上的问题, 不过这儿有一个像样的列表说 明什么时候继承比组合更好用:
- 你的继承表示"是一个"的关系而不是"有一个"的关系(人类->动物 vs 用户->用户详情);
- 你可以重用来自基类的代码(人可以像所有动物一样行动);
- 你想通过基类对子类进行全局的修改(改变所有动物行动时的热量消耗);
不好的:
classEmployee{constructor(name,email){this.name=name;this.email=email;}// ...}// 不好是因为雇员“有”税率数据, EmployeeTaxData 不是一个 Employee 类型。classEmployeeTaxDataextendsEmployee{constructor(ssn,salary){super();this.ssn=ssn;this.salary=salary;}// ...}好的:
classEmployeeTaxData{constructor(ssn,salary){this.ssn=ssn;this.salary=salary;}// ...}classEmployee{constructor(name,email){this.name=name;this.email=email;}setTaxData(ssn,salary){this.taxData=newEmployeeTaxData(ssn,salary);}// ...}正如代码整洁之道所述, “永远不要有超过一个理由来修改一个类”。 给一个类塞满许多功能, 就像你在航 班上只能带一个行李箱一样, 这样做的问题你的类不会有理想的内聚性, 将会有太多的理由来对它进行修改。 最小化需要修改一个类的次数时很重要的, 因为如果一个类拥有太多的功能, 一旦你修改它的一小部分, 将会很难弄清楚会对代码库中的其它模块造成什么影响。
不好的:
classUserSettings{constructor(user){this.user=user;}changeSettings(settings){if(this.verifyCredentials()){// ...}}verifyCredentials(){// ...}}好的:
classUserAuth{constructor(user){this.user=user;}verifyCredentials(){// ...}}classUserSettings{constructor(user){this.user=user;this.auth=newUserAuth(user);}changeSettings(settings){if(this.auth.verifyCredentials()){// ...}}}Bertrand Meyer 说过, “软件实体 (类, 模块, 函数等) 应该为扩展开放, 但是为修改关闭。” 这 是什么意思呢? 这个原则基本上说明了你应该允许用户添加功能而不必修改现有的代码。
不好的:
classAjaxAdapterextendsAdapter{constructor(){super();this.name='ajaxAdapter';}}classNodeAdapterextendsAdapter{constructor(){super();this.name='nodeAdapter';}}classHttpRequester{constructor(adapter){this.adapter=adapter;}fetch(url){if(this.adapter.name==='ajaxAdapter'){returnmakeAjaxCall(url).then((response)=>{// transform response and return});}elseif(this.adapter.name==='httpNodeAdapter'){returnmakeHttpCall(url).then((response)=>{// transform response and return});}}}functionmakeAjaxCall(url){// request and return promise}functionmakeHttpCall(url){// request and return promise}好的:
classAjaxAdapterextendsAdapter{constructor(){super();this.name='ajaxAdapter';}request(url){// request and return promise}}classNodeAdapterextendsAdapter{constructor(){super();this.name='nodeAdapter';}request(url){// request and return promise}}classHttpRequester{constructor(adapter){this.adapter=adapter;}fetch(url){returnthis.adapter.request(url).then((response)=>{// transform response and return});}}这是针对一个非常简单的里面的一个恐怖意图, 它的正式定义是: “如果 S 是 T 的一个子类型, 那么类 型为 T 的对象可以被类型为 S 的对象替换(例如, 类型为 S 的对象可作为类型为 T 的替代品)而不需 要修改目标程序的期望性质 (正确性、 任务执行性等)。” 这甚至是个恐怖的定义。
最好的解释是, 如果你有一个基类和一个子类, 那个基类和字类可以互换而不会产生不正确的结果。 这可 能还有有些疑惑, 让我们来看一下这个经典的正方形与矩形的例子。 从数学上说, 一个正方形是一个矩形, 但是你用 "is-a" 的关系用继承来实现, 你将很快遇到麻烦。
不好的:
classRectangle{constructor(){this.width=0;this.height=0;}setColor(color){// ...}render(area){// ...}setWidth(width){this.width=width;}setHeight(height){this.height=height;}getArea(){returnthis.width*this.height;}}classSquareextendsRectangle{setWidth(width){this.width=width;this.height=width;}setHeight(height){this.width=height;this.height=height;}}functionrenderLargeRectangles(rectangles){rectangles.forEach((rectangle)=>{rectangle.setWidth(4);rectangle.setHeight(5);constarea=rectangle.getArea();// BAD: Will return 25 for Square. Should be 20.rectangle.render(area);});}constrectangles=[newRectangle(),newRectangle(),newSquare()];renderLargeRectangles(rectangles);好的:
classShape{setColor(color){// ...}render(area){// ...}}classRectangleextendsShape{constructor(width,height){super();this.width=width;this.height=height;}getArea(){returnthis.width*this.height;}}classSquareextendsShape{constructor(length){super();this.length=length;}getArea(){returnthis.length*this.length;}}functionrenderLargeShapes(shapes){shapes.forEach((shape)=>{constarea=shape.getArea();shape.render(area);});}constshapes=[newRectangle(4,5),newRectangle(4,5),newSquare(5)];renderLargeShapes(shapes);JavaScript 没有接口, 所以这个原则不像其它语言那么严格。 不过, 对于 JavaScript 这种缺少类 型的语言来说, 它依然是重要并且有意义的。
接口隔离原则说的是 “客户端不应该强制依赖他们不需要的接口。” 在 JavaScript 这种弱类型语言中, 接口是隐式的契约。
在 JavaScript 中能比较好的说明这个原则的是一个类需要一个巨大的配置对象。 不需要客户端去设置大 量的选项是有益的, 因为多数情况下他们不需要全部的设置。 让它们变成可选的有助于防止出现一个“胖接 口”。
不好的:
classDOMTraverser{constructor(settings){this.settings=settings;this.setup();}setup(){this.rootNode=this.settings.rootNode;this.animationModule.setup();}traverse(){// ...}}const$=newDOMTraverser({rootNode: document.getElementsByTagName('body'),animationModule(){}// Most of the time, we won't need to animate when traversing.// ...});好的:
classDOMTraverser{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$=newDOMTraverser({rootNode: document.getElementsByTagName('body'),options: {animationModule(){}}});这个原则阐述了两个重要的事情:
- 高级模块不应该依赖于低级模块, 两者都应该依赖与抽象;
- 抽象不应当依赖于具体实现, 具体实现应当依赖于抽象。
这个一开始会很难理解, 但是如果你使用过 Angular.js , 你应该已经看到过通过依赖注入来实现的这 个原则, 虽然他们不是相同的概念, 依赖反转原则让高级模块远离低级模块的细节和创建, 可以通过 DI 来实现。 这样做的巨大益处是降低模块间的耦合。 耦合是一个非常糟糕的开发模式, 因为会导致代码难于 重构。
如上所述, JavaScript 没有接口, 所以被依赖的抽象是隐式契约。 也就是说, 一个对象/类的方法和
属性直接暴露给另外一个对象/类。 在下面的例子中, 任何一个 Request 模块的隐式契约 InventoryTracker
将有一个 requestItems 方法。
不好的:
classInventoryRequester{constructor(){this.REQ_METHODS=['HTTP'];}requestItem(item){// ...}}classInventoryTracker{constructor(items){this.items=items;// 不好的: 我们已经创建了一个对请求的具体实现的依赖, 我们只有一个 requestItems 方法依// 赖一个请求方法 'request'this.requester=newInventoryRequester();}requestItems(){this.items.forEach((item)=>{this.requester.requestItem(item);});}}constinventoryTracker=newInventoryTracker(['apples','bananas']);inventoryTracker.requestItems();好的:
classInventoryTracker{constructor(items,requester){this.items=items;this.requester=requester;}requestItems(){this.items.forEach((item)=>{this.requester.requestItem(item);});}}classInventoryRequesterV1{constructor(){this.REQ_METHODS=['HTTP'];}requestItem(item){// ...}}classInventoryRequesterV2{constructor(){this.REQ_METHODS=['WS'];}requestItem(item){// ...}}// 通过外部创建依赖项并将它们注入, 我们可以轻松的用一个崭新的使用 WebSockets 的请求模块进行// 替换。constinventoryTracker=newInventoryTracker(['apples','bananas'],newInventoryRequesterV2());inventoryTracker.requestItems();测试比发布更加重要。 如果你没有测试或者测试不够充分, 每次发布时你就不能确认没有破坏任何事情。 测试的量由你的团队决定, 但是拥有 100% 的覆盖率(包括所有的语句和分支)是你为什么能达到高度自信 和内心的平静。 这意味着需要一个额外的伟大的测试框架, 也需要一个好的覆盖率工具。
没有理由不写测试。 这里有大量的优秀的 JS 测试框架, 选一个适合你的团队的即可。 当为团队选择了测试框架之后, 接下来的目标是为生产的每一个新的功能/模 块编写测试。 如果你倾向于测试驱动开发(TDD), 那就太棒了, 但是要点是确认你在上线任何功能或者重 构一个现有功能之前, 达到了需要的目标覆盖率。
不好的:
constassert=require('assert');describe('MakeMomentJSGreatAgain',()=>{it('handles date boundaries',()=>{letdate;date=newMakeMomentJSGreatAgain('1/1/2015');date.addDays(30);date.shouldEqual('1/31/2015');date=newMakeMomentJSGreatAgain('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);date=newMakeMomentJSGreatAgain('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});好的:
constassert=require('assert');describe('MakeMomentJSGreatAgain',()=>{it('handles 30-day months',()=>{constdate=newMakeMomentJSGreatAgain('1/1/2015');date.addDays(30);date.shouldEqual('1/31/2015');});it('handles leap year',()=>{constdate=newMakeMomentJSGreatAgain('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);});it('handles non-leap year',()=>{constdate=newMakeMomentJSGreatAgain('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});回调不够简洁, 因为他们会产生过多的嵌套。 在 ES2015/ES6 中, Promises 已经是内置的全局类型 了,使用它们吧!
不好的:
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin',(requestErr,response)=>{if(requestErr){console.error(requestErr);}else{require('fs').writeFile('article.html',response.body,(writeErr)=>{if(writeErr){console.error(writeErr);}else{console.log('File written');}});}});好的:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin').then((response)=>{returnrequire('fs-promise').writeFile('article.html',response);}).then(()=>{console.log('File written');}).catch((err)=>{console.error(err);});Promises 是回调的一个非常简洁的替代品, 但是 ES2017/ES8 带来的 async 和 await 提供了一个
更加简洁的解决方案。 你需要的只是一个前缀为 async 关键字的函数, 接下来就可以不需要 then
函数链来编写逻辑了。 如果你能使用 ES2017/ES8 的高级功能的话, 今天就使用它吧!
不好的:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin').then((response)=>{returnrequire('fs-promise').writeFile('article.html',response);}).then(()=>{console.log('File written');}).catch((err)=>{console.error(err);});好的:
asyncfunctiongetCleanCodeArticle(){try{constresponse=awaitrequire('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');awaitrequire('fs-promise').writeFile('article.html',response);console.log('File written');}catch(err){console.error(err);}}抛出错误是一件好事情! 他们意味着当你的程序有错时运行时可以成功确认, 并且通过停止执行当前堆栈 上的函数来让你知道, 结束当前进程(在 Node 中), 在控制台中用一个堆栈跟踪提示你。
对捕捉到的错误不做任何处理不能给你修复错误或者响应错误的能力。 向控制台记录错误 (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 是无类型的, 所以大小写告诉你关于你的变量、 函数等的很多事情。 这些规则是主观的, 所以你的团队可以选择他们想要的。 重点是, 不管你们选择了什么, 要保持一致。
不好的:
constDAYS_IN_WEEK=7;constdaysInMonth=30;constsongs=['Back In Black','Stairway to Heaven','Hey Jude'];constArtists=['ACDC','Led Zeppelin','The Beatles'];functioneraseDatabase(){}functionrestore_database(){}classanimal{}classAlpaca{}好的:
constDAYS_IN_WEEK=7;constDAYS_IN_MONTH=30;constsongs=['Back In Black','Stairway to Heaven','Hey Jude'];constartists=['ACDC','Led Zeppelin','The Beatles'];functioneraseDatabase(){}functionrestoreDatabase(){}classAnimal{}classAlpaca{}如果一个函数调用另一个, 则在代码中这两个函数的竖直位置应该靠近。 理想情况下,保持被调用函数在被 调用函数的正上方。 我们倾向于从上到下阅读代码, 就像读一章报纸。 由于这个原因, 保持你的代码可 以按照这种方式阅读。
不好的:
classPerformanceReview{constructor(employee){this.employee=employee;}lookupPeers(){returndb.lookup(this.employee,'peers');}lookupManager(){returndb.lookup(this.employee,'manager');}getPeerReviews(){constpeers=this.lookupPeers();// ...}perfReview(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();}getManagerReview(){constmanager=this.lookupManager();}getSelfReview(){// ...}}constreview=newPerformanceReview(user);review.perfReview();好的:
classPerformanceReview{constructor(employee){this.employee=employee;}perfReview(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();}getPeerReviews(){constpeers=this.lookupPeers();// ...}lookupPeers(){returndb.lookup(this.employee,'peers');}getManagerReview(){constmanager=this.lookupManager();}lookupManager(){returndb.lookup(this.employee,'manager');}getSelfReview(){// ...}}constreview=newPerformanceReview(employee);review.perfReview();注释是代码的辩解, 不是要求。 多数情况下, 好的代码就是文档。
不好的:
functionhashIt(data){// The hashlethash=0;// Length of stringconstlength=data.length;// Loop through every character in datafor(leti=0;i<length;i++){// Get character code.constchar=data.charCodeAt(i);// Make the hashhash=((hash<<5)-hash)+char;// Convert to 32-bit integerhash&=hash;}}好的:
functionhashIt(data){lethash=0;constlength=data.length;for(leti=0;i<length;i++){constchar=data.charCodeAt(i);hash=((hash<<5)-hash)+char;// Convert to 32-bit integerhash&=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) */functioncombine(a,b){returna+b;}好的:
functioncombine(a,b){returna+b;}它们仅仅添加了干扰。 让函数和变量名称与合适的缩进和格式化为你的代码提供视觉结构。
不好的:
////////////////////////////////////////////////////////////////////////////////// Scope Model Instantiation////////////////////////////////////////////////////////////////////////////////$scope.model={menu: 'foo',nav: 'bar'};////////////////////////////////////////////////////////////////////////////////// Action setup////////////////////////////////////////////////////////////////////////////////constactions=function(){// ...};好的:
$scope.model={menu: 'foo',nav: 'bar'};constactions=function(){// ...};This is also available in other languages:
Brazilian Portuguese: fesnt/clean-code-javascript
Chinese:
German: marcbruederlin/clean-code-javascript
Korean: qkraudghgh/clean-code-javascript-ko
Russian:
Vietnamese: hienvd/clean-code-javascript/
