中文 | English

将 Clean Code 的概念适用到 TypeScript,灵感来自 clean-code-javascript。

本文翻译自 labs42io/clean-code-typescript,由于译者水平有限,错误之处烦请指明!

目录

  1. 简介
  2. 变量
  3. 函数
  4. 对象与数据结构
  5. SOLID原则
  6. 测试
  7. 并发
  8. 错误处理
  9. 格式化
  10. 注释

简介

这不是一份 TypeScript 设计规范,而是将 Robert C. Martin 的软件工程著作 《Clean Code》 适用到 TypeScript,指导读者使用 TypeScript 编写易读、可复用和易重构的软件。

并不是每个原则都要严格遵守,被广泛认同的就更少了。虽然这只是一份指导原则,但却是Clean Code 作者对多年编程经验的凝练。

软件工程技术已有50多年的历史了,我们仍然要学习很多的东西。当软件架构和架构本身一样古老的时候,也许我们会有更严格的规则要遵守。现在,让这些指导原则作为评估您和您的团队代码质量的试金石。

另外,理解这些原则不会立即让您成为优秀的程序员,也不意味着工作多年不会犯错。每一段代码都是从不完美开始的,通过走查不断趋于完美,就像黏土制作成陶艺一样,享受这个过程吧!

变量

变量名要有意义

做有意义的区分,让读者更容易理解变量的含义。

反例:


function between<T>(a1: T, a2: T, a3: T) {return a2 <= a1 && a1 <= a3;}复制代码

正例:


function between<T>(value: T, left: T, right: T) {return left <= value && value <= right;}复制代码

变量名可拼读出来

如果你不能读出它,你在讨论它时听起来就会像个白痴。

反例:


class DtaRcrd102 {private genymdhms: Date;private modymdhms: Date;private pszqint = '102';}复制代码

正例:


class Customer {private generationTimestamp: Date;private modificationTimestamp: Date;private recordId = '102';}复制代码

对功能一致的变量采用统一命名

反例:


function getUserInfo(): User;function getUserDetails(): User;function getUserData(): User;复制代码

正例:


function getUser(): User;复制代码

使用可检索的名字

我们读代码要比写的多,所以易读性和可检索非常重要。如果不抽取并命名有意义的变量名,那就坑了读代码的人。代码要可检索,TSLint 就可以帮助识别未命名的常量。

反例:


// What the heck is 86400000 for?setTimeout(restart, 86400000);复制代码

正例:


// Declare them as capitalized named constants.const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;setTimeout(restart, MILLISECONDS_IN_A_DAY);复制代码

使用自解释的变量名

反例:


declare const users:Map<string, User>;for (const keyValue of users) {// iterate through users map}复制代码

正例:


declare const users:Map<string, User>;for (const [id, user] of users) {// iterate through users map}复制代码

避免思维映射

不要让人去猜测或想象变量的含义,明确是王道.

反例:


const u = getUser();const s = getSubscription();const t = charge(u, s);复制代码

正例:


const user = getUser();const subscription = getSubscription();const transaction = charge(user, subscription);复制代码

不添加无用的上下文

如果类名或对象名已经表达了某些信息,在内部变量名中不要再重复表达。

反例:


type Car = {carMake: string;carModel: string;carColor: string;}function print(car: Car): void {console.log(`${this.carMake} ${this.carModel} (${this.carColor})`);}复制代码

正例:


type Car = {make: string;model: string;color: string;}function print(car: Car): void {console.log(`${this.make} ${this.model} (${this.color})`);}复制代码

使用默认参数,而非短路或条件判断

通常,默认参数比短路更整洁。

反例:


function loadPages(count: number) {const loadCount = count !== undefined ? count : 10;// ...}复制代码

正例:


function loadPages(count: number = 10) {// ...}复制代码

函数

参数越少越好 (理想情况不超过2个)

限制参数个数,这样函数测试会更容易。超过三个参数会导致测试复杂度激增,需要测试众多不同参数的组合场景。 理想情况,只有一两个参数。如果有两个以上的参数,那么您的函数可能就太过复杂了。

如果需要很多参数,请您考虑使用对象。为了使函数的属性更清晰,可以使用解构,它有以下优点:

  1. 当有人查看函数签名时,会立即清楚使用了哪些属性。

  2. 解构对传递给函数的参数对象做深拷贝,这可预防副作用。(注意:不会克隆从参数对象中解构的对象和数组)

  3. TypeScript 会对未使用的属性显示警告。

反例:


function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {// ...}createMenu('Foo', 'Bar', 'Baz', true);复制代码

正例:


function createMenu(options: {title: string, body: string, buttonText: string, cancellable: boolean}) {// ...}createMenu({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true});复制代码

通过 TypeScript 的类型别名,可以进一步提高可读性。


type MenuOptions = {title: string, body: string, buttonText: string, cancellable: boolean};function createMenu(options: MenuOptions) {// ...}createMenu({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true});复制代码

只做一件事

这是目前软件工程中最重要的规则。如果函数做不止一件事,它就更难组合、测试以及理解。反之,函数只有一个行为,它就更易于重构、代码就更清晰。如果您只从本指南中了解到这一点,那么您就领先多数程序员了。

反例:


function emailClients(clients: Client) {clients.forEach((client) => {const clientRecord = database.lookup(client);if (clientRecord.isActive()) {email(client);}});}复制代码

正例:


function emailClients(clients: Client) {clients.filter(isActiveClient).forEach(email);}function isActiveClient(client: Client) {const clientRecord = database.lookup(client);return clientRecord.isActive();}复制代码

名副其实

函数名就可以展示出函数实现的功能。

反例:


function addToDate(date: Date, month: number): Date {// ...}const date = new Date();// It's hard to tell from the function name what is addedaddToDate(date, 1);复制代码

正例:


function addMonthToDate(date: Date, month: number): Date {// ...}const date = new Date();addMonthToDate(date, 1);复制代码

每个函数只包含同一个层级的抽象

当有多个抽象级别时,函数应该是做太多事了。拆分函数以便可复用,也让测试更容易。

反例:


function parseCode(code:string) {const REGEXES = [ /* ... */ ];const statements = code.split(' ');const tokens = [];REGEXES.forEach((regex) => {statements.forEach((statement) => {// ...});});const ast = [];tokens.forEach((token) => {// lex...});ast.forEach((node) => {// parse...});}复制代码

正例:


const REGEXES = [ /* ... */ ];function parseCode(code:string) {const tokens = tokenize(code);const syntaxTree = parse(tokens);syntaxTree.forEach((node) => {// parse...});}function tokenize(code: string):Token[] {const statements = code.split(' ');const tokens:Token[] = [];REGEXES.forEach((regex) => {statements.forEach((statement) => {tokens.push( /* ... */ );});});return tokens;}function parse(tokens: Token[]): SyntaxTree {const syntaxTree:SyntaxTree[] = [];tokens.forEach((token) => {syntaxTree.push( /* ... */ );});return syntaxTree;}复制代码

删除重复代码

重复乃万恶之源!重复意味着如果要修改某个逻辑,需要修改多处代码:cry:。 想象一下,如果你经营一家餐厅,要记录你的库存:所有的西红柿、洋葱、大蒜、香料等等。如果要维护多个库存列表,那是多么痛苦的事!

存在重复代码,是因为有两个或两个以上很近似的功能,只有一点不同,但是这点不同迫使你用多个独立的函数来做很多几乎相同的事情。删除重复代码,则意味着创建一个抽象,该抽象仅用一个函数/模块/类就可以处理这组不同的东西。

合理的抽象至关重要,这就是为什么您应该遵循SOLID原则。糟糕的抽象可能还不如重复代码,所以要小心!话虽如此,还是要做好抽象!尽量不要重复。

反例:


function showDeveloperList(developers: Developer[]) {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: Manager[]) {managers.forEach((manager) => {const expectedSalary = manager.calculateExpectedSalary();const experience = manager.getExperience();const portfolio = manager.getMBAProjects();const data = {expectedSalary,experience,portfolio};render(data);});}复制代码

正例:


class Developer {// ...getExtraDetails() {return {githubLink: this.githubLink,}}}class Manager {// ...getExtraDetails() {return {portfolio: this.portfolio,}}}function showEmployeeList(employee: Developer | Manager) {employee.forEach((employee) => {const expectedSalary = developer.calculateExpectedSalary();const experience = developer.getExperience();const extra = employee.getExtraDetails();const data = {expectedSalary,experience,extra,};render(data);});}复制代码

有时,在重复代码和引入不必要的抽象而增加的复杂性之间,需要做权衡。当来自不同领域的两个不同模块,它们的实现看起来相似,复制也是可以接受的,并且比抽取公共代码要好一点。因为抽取公共代码会导致两个模块产生间接的依赖关系。

使用Object.assign解构来设置默认对象

反例:


type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};function createMenu(config: MenuConfig) {config.title = config.title || 'Foo';config.body = config.body || 'Bar';config.buttonText = config.buttonText || 'Baz';config.cancellable = config.cancellable !== undefined ? config.cancellable : true;}const menuConfig = {title: null,body: 'Bar',buttonText: null,cancellable: true};createMenu(menuConfig);复制代码

正例:


type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};function createMenu(config: MenuConfig) {const menuConfig = Object.assign({title: 'Foo',body: 'Bar',buttonText: 'Baz',cancellable: true}, config);}createMenu({ body: 'Bar' });复制代码

或者,您可以使用默认值的解构:


type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};function createMenu({title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true}: MenuConfig) {// ...}createMenu({ body: 'Bar' });复制代码

为了避免副作用,不允许显式传递undefinednull值。参见 TypeScript 编译器的--strictnullcheck选项。

不要使用Flag参数

Flag参数告诉用户这个函数做了不止一件事。如果函数使用布尔值实现不同的代码逻辑路径,则考虑将其拆分。

反例:


function createFile(name:string, temp:boolean) {if (temp) {fs.create(`./temp/${name}`);} else {fs.create(name);}}复制代码

正例:


function createFile(name:string) {fs.create(name);}function createTempFile(name:string) {fs.create(`./temp/${name}`);}复制代码

避免副作用 (part1)

当函数产生除了“一个输入一个输出”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。

在某些情况下,程序需要一些副作用。如先前例子中的写文件,这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。

重点是要规避常见陷阱,比如,在无结构对象之间共享状态、使用可变数据类型,以及不确定副作用发生的位置。如果你能做到这点,你才可能笑到最后!

反例:


// 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 = 'Robert C. Martin';function toBase64() {name = btoa(name);}toBase64(); // produces side effects to `name` variableconsole.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='复制代码

正例:


// 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.const name = 'Robert C. Martin';function toBase64(text:string):string {return btoa(text);}const encodedName = toBase64(name);console.log(name);复制代码

避免副作用 (part2)

在 JavaScript 中,原类型是值传递,对象、数组是引用传递。

有这样一种情况,如果您的函数修改了购物车数组,用来添加购买的商品,那么其他使用该cart数组的函数都将受此添加操作的影响。想象一个糟糕的情况:

用户点击“购买”按钮,该按钮调用purchase函数,函数请求网络并将cart数组发送到服务器。由于网络连接不好,购买功能必须不断重试请求。恰巧在网络请求开始前,用户不小心点击了某个不想要的项目上的“Add to Cart”按钮,该怎么办?而此时网络请求开始,那么purchase函数将发送意外添加的项,因为它引用了一个购物车数组,addItemToCart函数修改了该数组,添加了不需要的项。

一个很好的解决方案是addItemToCart总是克隆cart,编辑它,并返回克隆。这确保引用购物车的其他函数不会受到任何更改的影响。

注意两点:

  1. 在某些情况下,可能确实想要修改输入对象,这种情况非常少见。且大多数可以重构,确保没副作用!(见纯函数)

  2. 性能方面,克隆大对象代价确实比较大。还好有一些很好的库,它提供了一些高效快速的方法,且不像手动克隆对象和数组那样占用大量内存。

反例:


function addItemToCart(cart: CartItem[], item:Item):void {cart.push({ item, date: Date.now() });};复制代码

正例:


function addItemToCart(cart: CartItem[], item:Item):CartItem[] {return [...cart, { item, date: Date.now() }];};复制代码

不要写全局函数

在 JavaScript 中污染全局的做法非常糟糕,这可能导致和其他库冲突,而调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。

考虑这样一个例子:如果想要扩展 JavaScript 的 Array,使其拥有一个可以显示两个数组之间差异的 diff方法,该怎么做呢?可以将新函数写入Array.prototype ,但它可能与另一个尝试做同样事情的库冲突。如果另一个库只是使用diff来查找数组的第一个元素和最后一个元素之间的区别呢?

更好的做法是扩展Array,实现对应的函数功能。

反例:


declare global {interface Array<T> {diff(other: T[]): Array<T>;}}if (!Array.prototype.diff){Array.prototype.diff = function <T>(other: T[]): T[] {const hash = new Set(other);return this.filter(elem => !hash.has(elem));};}复制代码

正例:


class MyArray<T> extends Array<T> {diff(other: T[]): T[] {const hash = new Set(other);return this.filter(elem => !hash.has(elem));};}复制代码

函数式编程优于命令式编程

尽量使用函数式编程!

反例:


const contributions = [{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 < contributions.length; i++) {totalOutput += contributions[i].linesOfCode;}复制代码

正例:


const contributions = [{name: 'Uncle Bobby',linesOfCode: 500}, {name: 'Suzie Q',linesOfCode: 1500}, {name: 'Jimmy Gosling',linesOfCode: 150}, {name: 'Gracie Hopper',linesOfCode: 1000}];const totalOutput = contributions.reduce((totalLines, output) => totalLines + output.linesOfCode, 0)复制代码

封装判断条件

反例:


if (subscription.isTrial || account.balance > 0) {// ...}复制代码

正例:


function canActivateService(subscription: Subscription, account: Account) {return subscription.isTrial || account.balance > 0}if (canActivateService(subscription, account)) {// ...}复制代码

避免“否定”的判断

反例:


function isEmailNotUsed(email: string) {// ...}if (isEmailNotUsed(email)) {// ...}复制代码

正例:


function isEmailUsed(email) {// ...}if (!isEmailUsed(node)) {// ...}复制代码

避免判断条件

这看起来似乎不太可能完成啊。大多数人听到后第一反应是,“没有if语句怎么实现功能呢?” 在多数情况下,可以使用多态性来实现相同的功能。接下来的问题是 “为什么要这么做?” 原因就是之前提到的:函数只做一件事。

反例:


class Airplane {private type: string;// ...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();default:throw new Error('Unknown airplane type.');}}}复制代码

正例:


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();}}复制代码

避免类型检查

TypeScript 是 JavaScript 的一个严格的语法超集,具有静态类型检查的特性。所以指定变量、参数和返回值的类型,以充分利用此特性,能让重构更容易。

反例:


function travelToTexas(vehicle: Bicycle | Car) {if (vehicle instanceof Bicycle) {vehicle.pedal(this.currentLocation, new Location('texas'));} else if (vehicle instanceof Car) {vehicle.drive(this.currentLocation, new Location('texas'));}}复制代码

正例:


type Vehicle = Bicycle | Car;function travelToTexas(vehicle: Vehicle) {vehicle.move(this.currentLocation, new Location('texas'));}复制代码

不要过度优化

现代浏览器在运行时进行大量的底层优化。很多时候,你做优化只是在浪费时间。有些优秀资源可以帮助定位哪里需要优化,找到并修复它。

反例:


// 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: string) {// ...}function requestModule(url: string) {// ...}const req = requestModule;inventoryTracker('apples', req, 'www.inventory-awesome.io');复制代码

正例:


function requestModule(url: string) {// ...}const req = requestModule;inventoryTracker('apples', req, 'www.inventory-awesome.io');复制代码

使用迭代器和生成器

像使用流一样处理数据集合时,请使用生成器和迭代器。

理由如下:

  • 将调用者与生成器实现解耦,在某种意义上,调用者决定要访问多少项。
  • 延迟执行,按需使用。
  • 内置支持使用for-of语法进行迭代
  • 允许实现优化的迭代器模式

反例:

function fibonacci(n: number): number[] {if (n === 1) return [0];if (n === 2) return [0, 1];const items: number[] = [0, 1];while (items.length < n) {items.push(items[items.length - 2] + items[items.length - 1]);}return items;
}function print(n: number) {fibonacci(n).forEach(fib => console.log(fib));
}// Print first 10 Fibonacci numbers.
print(10);
复制代码

正例:

// Generates an infinite stream of Fibonacci numbers.
// The generator doesn't keep the array of all numbers.
function* fibonacci(): IterableIterator<number> {let [a, b] = [0, 1];while (true) {yield a;[a, b] = [b, a + b];}
}function print(n: number) {let i = 0;for (const fib in fibonacci()) {if (i++ === n) break;  console.log(fib);}
}// Print first 10 Fibonacci numbers.
print(10);
复制代码

有些库通过链接“map”、“slice”、“forEach”等方法,达到与原生数组类似的方式处理迭代。参见 itiriri 里面有一些使用迭代器的高级操作示例(或异步迭代的操作 itiriri-async)。

import itiriri from 'itiriri';function* fibonacci(): IterableIterator<number> {let [a, b] = [0, 1];while (true) {yield a;[a, b] = [b, a + b];}
}itiriri(fibonacci()).take(10).forEach(fib => console.log(fib));
复制代码

对象和数据结构

使用getterssetters

TypeScript 支持 getter/setter 语法。使用 getter 和 setter 从对象中访问数据比简单地在对象上查找属性要好。原因如下:

  • 当需要在获取对象属性之前做一些事情时,不必在代码中查找并修改每个访问器。
  • 执行set时添加验证更简单。
  • 封装内部表示。
  • 更容易添加日志和错误处理。
  • 可以延迟加载对象的属性,比如从服务器获取它。

反例:


class BankAccount {balance: number = 0;// ...}const value = 100;const account = new BankAccount();if (value < 0) {throw new Error('Cannot set negative balance.');}account.balance = value;复制代码

正例:


class BankAccount {private accountBalance: number = 0;get balance(): number {return this.accountBalance;}set balance(value: number) {if (value < 0) {throw new Error('Cannot set negative balance.');}this.accountBalance = value;}// ...}const account = new BankAccount();account.balance = 100;复制代码

让对象拥有 private/protected 成员

TypeScript 类成员支持 public(默认)protected 以及 private的访问限制。

反例:


class Circle {radius: number;constructor(radius: number) {this.radius = radius;}perimeter(){return 2 * Math.PI * this.radius;}surface(){return Math.PI * this.radius * this.radius;}}复制代码

正例:


class Circle {constructor(private readonly radius: number) {}perimeter(){return 2 * Math.PI * this.radius;}surface(){return Math.PI * this.radius * this.radius;}}复制代码

不变性

TypeScript 类型系统允许将接口、类上的单个属性设置为只读,能以函数的方式运行。

还有个高级场景,可以使用内置类型Readonly,它接受类型 T 并使用映射类型将其所有属性标记为只读。

反例:


interface Config {host: string;port: string;db: string;}复制代码

正例:


interface Config {readonly host: string;readonly port: string;readonly db: string;}复制代码

类型 vs 接口

当可能需要联合或交集时,请使用类型。如果需要扩展实现,请使用接口。然而,没有严格的规则,只有适合的规则。

详细解释参考关于 Typescript 中typeinterface区别的解答 。

反例:


interface EmailConfig {// ...}interface DbConfig {// ...}interface Config {// ...}//...type Shape {// ...}复制代码

正例:


type EmailConfig {// ...}type DbConfig {// ...}type Config  = EmailConfig | DbConfig;// ...interface Shape {}class Circle implements Shape {// ...}class Square implements Shape {// ...}复制代码

小、小、小!要事情说三遍

类的大小是由它的职责来度量的。按照单一职责原则,类要小。

反例:


class Dashboard {getLanguage(): string { /* ... */ }setLanguage(language: string): void { /* ... */ }showProgress(): void { /* ... */ }hideProgress(): void { /* ... */ }isDirty(): boolean { /* ... */ }disable(): void { /* ... */ }enable(): void { /* ... */ }addSubscription(subscription: Subscription): void { /* ... */ }removeSubscription(subscription: Subscription): void { /* ... */ }addUser(user: User): void { /* ... */ }removeUser(user: User): void { /* ... */ }goToHomePage(): void { /* ... */ }updateProfile(details: UserDetails): void { /* ... */ }getVersion(): string { /* ... */ }// ...}复制代码

正例:


class Dashboard {disable(): void { /* ... */ }enable(): void { /* ... */ }getVersion(): string { /* ... */ }}// split the responsibilities by moving the remaining methods to other classes// ...复制代码

高内聚低耦合

内聚:定义了类成员之间相互关联的程度。理想情况下,高内聚类的每个方法都应该使用类中的所有字段,实际上这不可能也不可取。但我们依然提倡高内聚。

耦合:指的是两个类之间的关联程度。如果其中一个类的更改不影响另一个类,则称为低耦合类。

好的软件设计具有高内聚性低耦合性

反例:


class UserManager {// Bad: each private variable is used by one or another group of methods.// It makes clear evidence that the class is holding more than a single responsibility.// If I need only to create the service to get the transactions for a user,// I'm still forced to pass and instance of emailSender.constructor(private readonly db: Database,private readonly emailSender: EmailSender) {}async getUser(id: number): Promise<User> {return await db.users.findOne({ id })}async getTransactions(userId: number): Promise<Transaction[]> {return await db.transactions.find({ userId })}async sendGreeting(): Promise<void> {await emailSender.send('Welcome!');}async sendNotification(text: string): Promise<void> {await emailSender.send(text);}async sendNewsletter(): Promise<void> {// ...}}复制代码

正例:


class UserService {constructor(private readonly db: Database) {}async getUser(id: number): Promise<User> {return await db.users.findOne({ id })}async getTransactions(userId: number): Promise<Transaction[]> {return await db.transactions.find({ userId })}}class UserNotifier {constructor(private readonly emailSender: EmailSender) {}async sendGreeting(): Promise<void> {await emailSender.send('Welcome!');}async sendNotification(text: string): Promise<void> {await emailSender.send(text);}async sendNewsletter(): Promise<void> {// ...}}复制代码

组合大于继承

正如“四人帮”在设计模式中所指出的那样,您尽可能使用组合而不是继承。组合和继承各有优劣。这个准则的主要观点是,如果你潜意识地倾向于继承,试着想想组合是否能更好地给你的问题建模,在某些情况下可以。

什么时候应该使用继承?这取决于你面临的问题。以下场景使用继承更好:

  1. 继承代表的是“is-a”关系,而不是“has-a”关系 (人 -> 动物 vs. 用户 -> 用户详情)。
  2. 可复用基类的代码 (人类可以像所有动物一样移动)。
  3. 希望通过更改基类对派生类进行全局更改(改变所有动物在运动时的热量消耗)。

反例:


class Employee {constructor(private readonly name: string, private readonly email:string) {}// ...}// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employeeclass EmployeeTaxData extends Employee {constructor(name: string, email:string,private readonly ssn: string, private readonly salary: number) {super(name, email);}// ...}复制代码

正例:


class Employee {private taxData: EmployeeTaxData;constructor(private readonly name: string, private readonly email:string) {}setTaxData(ssn: string, salary: number): Employee {this.taxData = new EmployeeTaxData(ssn, salary);return this;}// ...}class EmployeeTaxData {constructor(public readonly ssn: string, public readonly salary: number) {}// ...}复制代码

使用方法链

非常有用的模式,在许多库中都可以看到。它让代码表达力更好,也更简洁。

反例:


class QueryBuilder {private collection: string;private pageNumber: number = 1;private itemsPerPage: number = 100;private orderByFields: string[] = [];from(collection: string): void {this.collection = collection;}page(number: number, itemsPerPage: number = 100): void {this.pageNumber = number;this.itemsPerPage = itemsPerPage;}orderBy(...fields: string[]): void {this.orderByFields = fields;}build(): Query {// ...}}// ...const query = new QueryBuilder();query.from('users');query.page(1, 100);query.orderBy('firstName', 'lastName');const query = queryBuilder.build();复制代码

正例:


class QueryBuilder {private collection: string;private pageNumber: number = 1;private itemsPerPage: number = 100;private orderByFields: string[] = [];from(collection: string): this {this.collection = collection;return this;}page(number: number, itemsPerPage: number = 100): this {this.pageNumber = number;this.itemsPerPage = itemsPerPage;return this;}orderBy(...fields: string[]): this {this.orderByFields = fields;return this;}build(): Query {// ...}}// ...const query = new QueryBuilder().from('users').page(1, 100).orderBy('firstName', 'lastName').build();复制代码

SOLID原则

单一职责原则 (SRP)

正如 Clean Code 中所述,“类更改的原因不应该超过一个”。将很多功能打包在一个类看起来很诱人,就像在航班上您只能带一个手提箱。这样带来的问题是,在概念上类不具有内聚性,且有很多原因去修改类。而我们应该尽量减少修改类的次数。如果一个类功能太多,修改了其中一处很难确定对代码库中其他依赖模块的影响。

反例:


class UserSettings {constructor(private readonly user: User) {}changeSettings(settings: UserSettings) {if (this.verifyCredentials()) {// ...}}verifyCredentials() {// ...}}复制代码

正例:


class UserAuth {constructor(private readonly user: User) {}verifyCredentials() {// ...}}class UserSettings {private readonly auth: UserAuth;constructor(private readonly user: User) {this.auth = new UserAuth(user);}changeSettings(settings: UserSettings) {if (this.auth.verifyCredentials()) {// ...}}}复制代码

开闭原则 (OCP)

正如 Bertrand Meyer 所说,“软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。” 换句话说,就是允许在不更改现有代码的情况下添加新功能。

反例:


class AjaxAdapter extends Adapter {constructor() {super();}// ...}class NodeAdapter extends Adapter {constructor() {super();}// ...}class HttpRequester {constructor(private readonly adapter: Adapter) {}async fetch<T>(url: string): Promise<T> {if (this.adapter instanceof AjaxAdapter) {const response = await makeAjaxCall<T>(url);// transform response and return} else if (this.adapter instanceof NodeAdapter) {const response = await makeHttpCall<T>(url);// transform response and return}}}function makeAjaxCall<T>(url: string): Promise<T> {// request and return promise}function makeHttpCall<T>(url: string): Promise<T> {// request and return promise}复制代码

正例:


abstract class Adapter {abstract async request<T>(url: string): Promise<T>;}class AjaxAdapter extends Adapter {constructor() {super();}async request<T>(url: string): Promise<T>{// request and return promise}// ...}class NodeAdapter extends Adapter {constructor() {super();}async request<T>(url: string): Promise<T>{// request and return promise}// ...}class HttpRequester {constructor(private readonly adapter: Adapter) {}async fetch<T>(url: string): Promise<T> {const response = await this.adapter.request<T>(url);// transform response and return}}复制代码

里氏替换原则 (LSP)

对一个非常简单的概念来说,这是个可怕的术语。

它的正式定义是:“如果 S 是 T 的一个子类型,那么类型 T 的对象可以被替换为类型 S 的对象,而不会改变程序任何期望的属性(正确性、执行的任务等)“。这是一个更可怕的定义。

更好的解释是,如果您有一个父类和一个子类,那么父类和子类可以互换使用,而不会出现问题。这可能仍然令人困惑,所以让我们看一看经典的正方形矩形的例子。从数学上讲,正方形是矩形,但是如果您通过继承使用 “is-a” 关系对其建模,您很快就会遇到麻烦。

反例:


class Rectangle {constructor(protected width: number = 0, protected height: number = 0) {}setColor(color: string) {// ...}render(area: number) {// ...}setWidth(width: number) {this.width = width;}setHeight(height: number) {this.height = height;}getArea(): number {return this.width * this.height;}}class Square extends Rectangle {setWidth(width: number) {this.width = width;this.height = width;}setHeight(height: number) {this.width = height;this.height = height;}}function renderLargeRectangles(rectangles: Rectangle[]) {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);复制代码

正例:


abstract class Shape {setColor(color: string) {// ...}render(area: number) {// ...}abstract getArea(): number;}class Rectangle extends Shape {constructor(private readonly width = 0, private readonly height = 0) {super();}getArea(): number {return this.width * this.height;}}class Square extends Shape {constructor(private readonly length: number) {super();}getArea(): number {return this.length * this.length;}}function renderLargeShapes(shapes: Shape[]) {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);复制代码

接口隔离原则 (ISP)

“客户不应该被迫依赖于他们不使用的接口。” 这一原则与单一责任原则密切相关。这意味着不应该设计一个大而全的抽象,否则会增加客户的负担,因为他们需要实现一些不需要的方法。

反例:


interface ISmartPrinter {print();fax();scan();}class AllInOnePrinter implements ISmartPrinter {print() {// ...}  fax() {// ...}scan() {// ...}}class EconomicPrinter implements ISmartPrinter {print() {// ...}  fax() {throw new Error('Fax not supported.');}scan() {throw new Error('Scan not supported.');}}复制代码

正例:


interface IPrinter {print();}interface IFax {fax();}interface IScanner {scan();}class AllInOnePrinter implements IPrinter, IFax, IScanner {print() {// ...}  fax() {// ...}scan() {// ...}}class EconomicPrinter implements IPrinter {print() {// ...}}复制代码

依赖反转原则(Dependency Inversion Principle)

这个原则有两个要点:

  1. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
  2. 抽象不依赖实现,实现应依赖抽象。

一开始这难以理解,但是如果你使用过 Angular,你就会看到以依赖注入(DI)的方式实现了这一原则。虽然概念不同,但是 DIP 阻止高级模块了解其低级模块的细节并进行设置。它可以通过 DI 实现这一点。这样做的一个巨大好处是减少了模块之间的耦合。耦合非常糟糕,它让代码难以重构。

DIP 通常是通过使用控制反转(IoC)容器来实现的。比如:TypeScript 的 IoC 容器 InversifyJs

反例:


import { readFile as readFileCb } from 'fs';import { promisify } from 'util';const readFile = promisify(readFileCb);type ReportData = {// ..}class XmlFormatter {parse<T>(content: string): T {// Converts an XML string to an object T}}class ReportReader {// BAD: We have created a dependency on a specific request implementation.// We should just have ReportReader depend on a parse method: `parse`private readonly formatter = new XmlFormatter();async read(path: string): Promise<ReportData> {const text = await readFile(path, 'UTF8');return this.formatter.parse<ReportData>(text);}}// ...const reader = new ReportReader();await report = await reader.read('report.xml');复制代码

正例:


import { readFile as readFileCb } from 'fs';import { promisify } from 'util';const readFile = promisify(readFileCb);type ReportData = {// ..}interface Formatter {parse<T>(content: string): T;}class XmlFormatter implements Formatter {parse<T>(content: string): T {// Converts an XML string to an object T}}class JsonFormatter implements Formatter {parse<T>(content: string): T {// Converts a JSON string to an object T}}class ReportReader {constructor(private readonly formatter: Formatter){}async read(path: string): Promise<ReportData> {const text = await readFile(path, 'UTF8');return this.formatter.parse<ReportData>(text);}}// ...const reader = new ReportReader(new XmlFormatter());await report = await reader.read('report.xml');// or if we had to read a json report:const reader = new ReportReader(new JsonFormatter());await report = await reader.read('report.json');复制代码

测试

测试比发货更重要。如果没有测试或数量不足,那么每次发布代码时都无法确保不引入问题。怎样才算是足够的测试?这取决于团队,但是拥有100%的覆盖率(所有语句和分支)会让团队更有信心。这一切都要基于好的测试框架以及覆盖率工具。

没有任何理由不编写测试。有很多优秀的 JS 测试框架都支持 TypeScript,找个团队喜欢的。然后为每个新特性/模块编写测试。如果您喜欢测试驱动开发(TDD),那就太好了,重点是确保在开发任何特性或重构现有特性之前,代码覆盖率已经达到要求。

TDD(测试驱动开发)三定律

  1. 在编写不能通过的单元测试前,不可编写生产代码。
  2. 只可编写刚好无法通过的单元测试,不能编译也算不过。
  3. 只可编写刚好足以通过当前失败测试的生产代码。

F.I.R.S.T.准则

整洁的测试应遵循以下准则:

  • 快速(Fast),测试应该快(及时反馈出业务代码的问题)。
  • 独立(Independent),每个测试流程应该独立。
  • 可重复(Repeatable),测试应该在任何环境上都能重复通过。
  • 自我验证(Self-Validating),测试结果应该明确通过或者失败
  • 及时(Timely),测试代码应该在产品代码之前编写。

单一的测试每个概念

测试也应该遵循单一职责原则,每个单元测试只做一个断言。

反例:


import { assert } from 'chai';describe('AwesomeDate', () => {it('handles date boundaries', () => {let date: AwesomeDate;date = new AwesomeDate('1/1/2015');date.addDays(30);assert.equal('1/31/2015', date);date = new AwesomeDate('2/1/2016');date.addDays(28);assert.equal('02/29/2016', date);date = new AwesomeDate('2/1/2015');date.addDays(28);assert.equal('03/01/2015', date);});});复制代码

正例:


import { assert } from 'chai';describe('AwesomeDate', () => {it('handles 30-day months', () => {const date = new AwesomeDate('1/1/2015');date.addDays(30);assert.equal('1/31/2015', date);});it('handles leap year', () => {const date = new AwesomeDate('2/1/2016');date.addDays(28);assert.equal('02/29/2016', date);});it('handles non-leap year', () => {const date = new AwesomeDate('2/1/2015');date.addDays(28);assert.equal('03/01/2015', date);});});复制代码

测试用例名称应该显示它的意图

当测试失败时,出错的第一个迹象可能就是它的名字。

反例:


describe('Calendar', () => {it('2/29/2020', () => {// ...});it('throws', () => {// ...});});复制代码

正例:


describe('Calendar', () => {it('should handle leap year', () => {// ...});it('should throw when format is invalid', () => {// ...});});复制代码

并发

用 Promises 替代回调

回调不够整洁而且会导致过多的嵌套*(回调地狱)*。

有些工具使用回调的方式将现有函数转换为 promise 对象:

  • Node.js 参见util.promisify
  • 通用参见 pify, es6-promisify

反例:


import { get } from 'request';import { writeFile } from 'fs';function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void){get(url, (error, response) => {if (error) {callback(error);} else {writeFile(saveTo, response.body, (error) => {if (error) {callback(error);} else {callback(null, response.body);}});}})}downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {if (error) {console.error(error);} else {console.log(content);}});复制代码

正例:


import { get } from 'request';import { writeFile } from 'fs';import { promisify } from 'util';const write = promisify(writeFile);function downloadPage(url: string, saveTo: string): Promise<string> {return get(url).then(response => write(saveTo, response))}downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html').then(content => console.log(content)).catch(error => console.error(error));  复制代码

Promise 提供了一些辅助方法,能让代码更简洁:

方法 描述
Promise.resolve(value) 返回一个传入值解析后的 promise 。
Promise.reject(error) 返回一个带有拒绝原因的 promise 。
Promise.all(promises) 返回一个新的 promise,传入数组中的每个 promise 都执行完成后返回的 promise 才算完成,或第一个 promise 拒绝而拒绝。
Promise.race(promises) 返回一个新的 promise,传入数组中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

Promise.all在并行运行任务时尤其有用,Promise.race让为 Promise 更容易实现超时。

Async/AwaitPromises 更好

使用async/await语法,可以编写更简洁、更易理解的链式 promise 的代码。一个函数使用async关键字作为前缀,JavaScript 运行时会暂停await关键字上的代码执行(当使用 promise 时)。

反例:


import { get } from 'request';import { writeFile } from 'fs';import { promisify } from 'util';const write = util.promisify(writeFile);function downloadPage(url: string, saveTo: string): Promise<string> {return get(url).then(response => write(saveTo, response))}downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html').then(content => console.log(content)).catch(error => console.error(error));  复制代码

正例:


import { get } from 'request';import { writeFile } from 'fs';import { promisify } from 'util';const write = promisify(writeFile);async function downloadPage(url: string, saveTo: string): Promise<string> {const response = await get(url);await write(saveTo, response);return response;}// somewhere in an async functiontry {const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');console.log(content);} catch (error) {console.error(error);}复制代码

错误处理

抛出错误是件好事!它表示着运行时已经成功识别出程序中的错误,通过停止当前堆栈上的函数执行,终止进程(在Node.js),以及在控制台中打印堆栈信息来让你知晓。

抛出Error或 使用reject

JavaScript 和 TypeScript 允许你 throw 任何对象。Promise 也可以用任何理由对象拒绝。

建议使用 Error 类型的 throw 语法。因为你的错误可能在写有 catch语法的高级代码中被捕获。在那里捕获字符串消息显得非常混乱,并且会使调试更加痛苦。出于同样的原因,也应该在拒绝 promise 时使用 Error类型。

反例:


function calculateTotal(items: Item[]): number {throw 'Not implemented.';}function get(): Promise<Item[]> {return Promise.reject('Not implemented.');}复制代码

正例:


function calculateTotal(items: Item[]): number {throw new Error('Not implemented.');}function get(): Promise<Item[]> {return Promise.reject(new Error('Not implemented.'));}// or equivalent to:async function get(): Promise<Item[]> {throw new Error('Not implemented.');}复制代码

使用 Error 类型的好处是 try/catch/finally 语法支持它,并且隐式地所有错误都具有 stack 属性,该属性对于调试非常有用。

另外,即使不用 throw 语法而是返回自定义错误对象,TypeScript在这块更容易。考虑下面的例子:


type Failable<R, E> = {isError: true;error: E;} | {isError: false;value: R;}function calculateTotal(items: Item[]): Failable<number, 'empty'> {if (items.length === 0) {return { isError: true, error: 'empty' };}// ...return { isError: false, value: 42 };}复制代码

详细解释请参考原文。

别忘了捕获错误

捕获错误而不处理实际上也是没有修复错误,将错误记录到控制台(console.log)也好不到哪里去,因为它常常丢失在控制台大量的日志之中。如果将代码写在try/catch 中,说明那里可能会发生错误,因此应该考虑在错误发生时做一些处理。

反例:


try {functionThatMightThrow();} catch (error) {console.log(error);}// or even worsetry {functionThatMightThrow();} catch (error) {// ignore error}复制代码

正例:


import { logger } from './logging'try {functionThatMightThrow();} catch (error) {logger.log(error);}复制代码

不要忽略被拒绝的 promises

理由和不能在try/catch中忽略Error一样。

反例:


getUser().then((user: User) => {return sendEmail(user.email, 'Welcome!');}).catch((error) => {console.log(error);});复制代码

正例:


import { logger } from './logging'getUser().then((user: User) => {return sendEmail(user.email, 'Welcome!');}).catch((error) => {logger.log(error);});// or using the async/await syntax:try {const user = await getUser();await sendEmail(user.email, 'Welcome!');} catch (error) {logger.log(error);}复制代码

格式化

就像这里的许多规则一样,没有什么是硬性规定,格式化也是。重点是不要争论格式,使用自动化工具实现格式化。对于工程师来说,争论格式就是浪费时间和金钱。通用的原则是保持一致的格式规则

对于 TypeScript ,有一个强大的工具叫做 TSLint。它是一个静态分析工具,可以帮助您显著提高代码的可读性和可维护性。项目中使用可以参考以下 TSLint 配置:

  • TSLint Config Standard - 标准格式规则

  • TSLint Config Airbnb - Airbnb 格式规则

  • TSLint Clean Code - 灵感来自于Clean Code: A Handbook of Agile Software Craftsmanship 的 TSLint 规则。

  • TSLint react - React 相关的Lint规则

  • TSLint + Prettier - Prettier 代码格式化相关的 lint 规则

  • ESLint rules for TSLint - TypeScript 的 ESLint

  • Immutable - 在 TypeScript 中禁用 mutation 的规则

还可以参考TypeScript 风格指南和编码约定的源代码。

大小写一致

大写可以告诉你很多关于变量、函数等的信息。这些都是主观规则,由你的团队做选择。关键是无论怎么选,都要一致

反例:


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 Container {}复制代码

正例:


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 Container {}复制代码

类名、接口名、类型名和命名空间名最好使用“帕斯卡命名”。

变量、函数和类成员使用“驼峰式命名”。

调用函数的函数和被调函数应靠近放置

当函数间存在相互调用的情况时,应将两者靠近放置。最好是应将调用者写在被调者的上方。这就像读报纸一样,我们都是从上往下读,那么读代码也是。

反例:


class PerformanceReview {constructor(private readonly employee: Employee) {}private lookupPeers() {return db.lookup(this.employee.id, 'peers');}private lookupManager() {return db.lookup(this.employee, 'manager');}private getPeerReviews() {const peers = this.lookupPeers();// ...}review() {this.getPeerReviews();this.getManagerReview();this.getSelfReview();// ...}private getManagerReview() {const manager = this.lookupManager();}private getSelfReview() {// ...}}const review = new PerformanceReview(employee);review.review();复制代码

正例:


class PerformanceReview {constructor(private readonly employee: Employee) {}review() {this.getPeerReviews();this.getManagerReview();this.getSelfReview();// ...}private getPeerReviews() {const peers = this.lookupPeers();// ...}private lookupPeers() {return db.lookup(this.employee.id, 'peers');}private getManagerReview() {const manager = this.lookupManager();}private lookupManager() {return db.lookup(this.employee, 'manager');} private getSelfReview() {// ...}}const review = new PerformanceReview(employee);review.review();复制代码

组织导入

使用整洁且易于阅读的import语句,您可以快速查看当前代码的依赖关系。导入语句应遵循以下做法:

  • Import语句应该按字母顺序排列和分组。
  • 应该删除未使用的导入语句。
  • 命名导入必须按字母顺序(例如:import {A, B, C} from 'foo';)。
  • 导入源必须在组中按字母顺序排列。 例如: import * as foo from 'a'; import * as bar from 'b';
  • 导入组用空行隔开。
  • 组内按照如下排序:
    • Polyfills (例如: import 'reflect-metadata';)
    • Node 内置模块 (例如: import fs from 'fs';)
    • 外部模块 (例如: import { query } from 'itiriri';)
    • 内部模块 (例如: import { UserService } from 'src/services/userService';)
    • 父目录中的模块 (例如: import foo from '../foo'; import qux from '../../foo/qux';)
    • 来自相同或兄弟目录的模块 (例如: import bar from './bar'; import baz from './bar/baz';)

反例:

import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';
复制代码

正例:

import 'reflect-metadata';import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';
复制代码

使用 typescript 别名

为了创建整洁漂亮的导入语句,可以在tsconfig.json中设置编译器选项的pathsbaseUrl属性。

这样可以避免导入时使用较长的相对路径。

反例:

import { UserService } from '../../../services/UserService';
复制代码

正例:

import { UserService } from '@services/UserService';
复制代码
// tsconfig.json
..."compilerOptions": {..."baseUrl": "src","paths": {"@services": ["services/*"]}...}
...
复制代码

注释

写注释意味着没有注释就无法表达清楚,而最好用代码去表达。

不要注释坏代码,重写吧!— Brian W. Kernighan and P. J. Plaugher

代码自解释而不是用注释

代码即文档。

反例:


// Check if subscription is active.if (subscription.endDate > Date.now) {  }复制代码

正例:


const isSubscriptionActive = subscription.endDate > Date.now;if (isSubscriptionActive) { /* ... */ }复制代码

不要将注释掉的代码留在代码库中

版本控制存在的一个理由,就是让旧代码成为历史。

反例:


class User {name: string;email: string;// age: number;// jobPosition: string;}复制代码

正例:


class User {name: string;email: string;}复制代码

不要像写日记一样写注释

记住,使用版本控制!不需要保留无用代码、注释掉的代码,尤其像日记一样的注释。使用git log来获取历史。

反例:


/*** 2016-12-20: Removed monads, didn't understand them (RM)* 2016-10-01: Improved using special monads (JP)* 2016-02-03: Added type-checking (LI)* 2015-03-14: Implemented combine (JR)*/function combine(a:number, b:number): number {return a + b;}复制代码

正例:


function combine(a:number, b:number): number {return a + b;}复制代码

避免使用注释标记位置

它们常常扰乱代码。要让代码结构化,函数和变量要有合适的缩进和格式。

另外,你可以使用支持代码折叠的IDE (看下 Visual Studio Code 代码折叠).

反例:


// Client classclass Client {id: number;name: string;address: Address;contact: Contact;// public methodspublic describe(): string {// ...}// private methodsprivate describeAddress(): string {// ...}private describeContact(): string {// ...}};复制代码

正例:


class Client {id: number;name: string;address: Address;contact: Contact;public describe(): string {// ...}private describeAddress(): string {// ...}private describeContact(): string {// ...}};复制代码

TODO 注释

当发现自己需要在代码中留下注释,以提醒后续改进时,使用// TODO注释。大多数IDE都对这类注释提供了特殊的支持,你可以快速浏览整个TODO列表。

但是,请记住TODO注释并不是坏代码的借口。

反例:

function getActiveSubscriptions(): Promise<Subscription[]> {// ensure `dueDate` is indexed.return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
复制代码

正例:

function getActiveSubscriptions(): Promise<Subscription[]> {// TODO: ensure `dueDate` is indexed.return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
复制代码

转载于:https://juejin.im/post/5cfb2b4951882576be276247

TypeScript 代码整洁之道相关推荐

  1. JavaScript 代码整洁之道

    目录 介绍 变量 函数 对象和数据结构 类 测试 并发 错误处理 格式化 注释 介绍 本文作者根据 Robert C. Martin <代码整洁之道>总结了适用于 JavaScript 的 ...

  2. 重读【代码整洁之道】

    一.前言 [代码整洁之道]很经典,但也有些过时,翻译上也有些啰嗦,但总体上是好书.通过对本书核心内容的摘抄,结合自己的经验,整理了一些精简的点,这样你就省的去啃那本400多页的书了. 软件质量 = 架 ...

  3. 《代码整洁之道》(Clean Code)- 读书笔记

    一.关于Bob大叔的Clean Code <代码整洁之道>主要讲述了一系列行之有效的整洁代码操作实践.软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关.这一点,无论是敏捷开发流派 ...

  4. 代码整洁之道(一)最佳实践小结

    摘要: Any fool can write code that a computer can understand. Good programmers write code that humans ...

  5. 2015年第11本:代码整洁之道Clean Code

    前一段时间一直在看英文小说,在读到<Before I fall>这本书时,读了40%多实在看不下去了,受不了美国人啰啰嗦嗦的写作风格,还是读IT专业书吧. 从5月9日开始看<代码整洁 ...

  6. 《代码整洁之道:程序员的职业素养》一一1.5 参考文献

    本节书摘来自异步社区出版社<代码整洁之道:程序员的职业素养>一书中的第1章,第1.5节,作者:[美]Robert C. Martin(罗伯特 C. 马丁),更多章节内容可以访问云栖社区&q ...

  7. 代码整洁之道(Clean Code)- 读书笔记

    Sorry, 许久未更新文章了,主要因为刚刚换了一家新公司,忙于组建团队(建设.招聘.流程.框架等)与熟悉公司业务,还有领导给的其他工作等等,实在是没有时间更新了.最近在和团队分享Bob大叔的< ...

  8. 《代码整洁之道 Clean Architecture》-读书笔记

    大家好,我是烤鸭: 关于<代码整洁之道>,记录一下读书笔记. 代码整洁之道 第一章 整洁代码 整洁代码的艺术 第二章 有意义的命名 避免误导 有意义的区分 使用读得出来和可搜索的名字 避免 ...

  9. 代码整洁之道(一)最佳实践小结 1

    摘要: Any fool can write code that a computer can understand. Good programmers write code that humans ...

最新文章

  1. Silve“.NET研究”rlight 游戏开发小技巧:传说中的透视跑马灯
  2. 数据结构-串操作应用之词索引表
  3. C 和 C++ 文件操作详解
  4. kafka消费报错:org.apache.kafka.common.errors.WakeupException: null
  5. 数据结构与算法笔记(三) 线性表(链式描述) 链表
  6. Activity 的窗口去头的方式
  7. [转]《谁让你是飞鸟我是鱼》by(晨曦)
  8. 不懂开发的运维,未来该如何发展?
  9. Video Copilot VCReflect for Mac/win (AE倒影插件) 支持2022多帧渲染​
  10. 数据库原理及应用教程课后习题答案 第4版 微课版 陈志泊主编
  11. JAVA(jar)软件_Autojar - 打包工具 - 开发工具 - JAVA开源项目 - 开源吧
  12. 谷歌学术、github、Sci-Hub镜像网址总结
  13. 如何快速压缩PPT文件?
  14. excel查找空值快捷键_Excel之定位和查找(一)
  15. cmd 如何跨驱动器移动文件夹
  16. 怎么免费做百度引流?百度免费引流方法有哪些?
  17. 【c语言】初识c语言-让你对c语言不在感到一无所知
  18. matlab将彩图转化成灰度图,matlab 如何将彩图转成灰度图
  19. 蓝桥杯javac组我们的征途是星辰大海
  20. 【第27天】SQL进阶-查询优化- performance_schema系列实战三:锁问题排查(表级锁)(SQL 小虚竹)

热门文章

  1. 谈RAM与ROM的区别与理解
  2. springboot+shiro is not eligible for getting processed by all BeanPostProcessors
  3. 设置TinyMCE在线HTML编辑控件只读
  4. 长尾效应(由百度网络资料整理)
  5. Mysql报表统计常用sql
  6. 未来人类将被AI取代?一文读懂人工智能类型与发展阶段
  7. 荣耀全明星不显示服务器,LoveLive!学园偶像祭 全明星无法连接服务器是什么原因...
  8. 猎聘公司管理系统 ——需求规格说明书
  9. 强大的密码破解工具:hashcat简介与用法介绍与实例
  10. 应用机器学习(三):朴素贝叶斯分类器