TypeScript 代码整洁之道
中文 | English
将 Clean Code 的概念适用到 TypeScript,灵感来自 clean-code-javascript。
本文翻译自 labs42io/clean-code-typescript,由于译者水平有限,错误之处烦请指明!
目录
- 简介
- 变量
- 函数
- 对象与数据结构
- 类
- SOLID原则
- 测试
- 并发
- 错误处理
- 格式化
- 注释
简介
这不是一份 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个)
限制参数个数,这样函数测试会更容易。超过三个参数会导致测试复杂度激增,需要测试众多不同参数的组合场景。 理想情况,只有一两个参数。如果有两个以上的参数,那么您的函数可能就太过复杂了。
如果需要很多参数,请您考虑使用对象。为了使函数的属性更清晰,可以使用解构,它有以下优点:
当有人查看函数签名时,会立即清楚使用了哪些属性。
解构对传递给函数的参数对象做深拷贝,这可预防副作用。(注意:不会克隆从参数对象中解构的对象和数组)
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' });复制代码
为了避免副作用,不允许显式传递undefined
或null
值。参见 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
,编辑它,并返回克隆。这确保引用购物车的其他函数不会受到任何更改的影响。
注意两点:
在某些情况下,可能确实想要修改输入对象,这种情况非常少见。且大多数可以重构,确保没副作用!(见纯函数)
性能方面,克隆大对象代价确实比较大。还好有一些很好的库,它提供了一些高效快速的方法,且不像手动克隆对象和数组那样占用大量内存。
反例:
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));
复制代码
对象和数据结构
使用getters
和setters
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 中type
和interface
区别的解答 。
反例:
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> {// ...}}复制代码
组合大于继承
正如“四人帮”在设计模式中所指出的那样,您尽可能使用组合而不是继承。组合和继承各有优劣。这个准则的主要观点是,如果你潜意识地倾向于继承,试着想想组合是否能更好地给你的问题建模,在某些情况下可以。
什么时候应该使用继承?这取决于你面临的问题。以下场景使用继承更好:
- 继承代表的是“is-a”关系,而不是“has-a”关系 (人 -> 动物 vs. 用户 -> 用户详情)。
- 可复用基类的代码 (人类可以像所有动物一样移动)。
- 希望通过更改基类对派生类进行全局更改(改变所有动物在运动时的热量消耗)。
反例:
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)
这个原则有两个要点:
- 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
- 抽象不依赖实现,实现应依赖抽象。
一开始这难以理解,但是如果你使用过 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(测试驱动开发)三定律
- 在编写不能通过的单元测试前,不可编写生产代码。
- 只可编写刚好无法通过的单元测试,不能编译也算不过。
- 只可编写刚好足以通过当前失败测试的生产代码。
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/Await
比 Promises
更好
使用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';
)
- Polyfills (例如:
反例:
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
中设置编译器选项的paths
和baseUrl
属性。
这样可以避免导入时使用较长的相对路径。
反例:
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 代码整洁之道相关推荐
- JavaScript 代码整洁之道
目录 介绍 变量 函数 对象和数据结构 类 测试 并发 错误处理 格式化 注释 介绍 本文作者根据 Robert C. Martin <代码整洁之道>总结了适用于 JavaScript 的 ...
- 重读【代码整洁之道】
一.前言 [代码整洁之道]很经典,但也有些过时,翻译上也有些啰嗦,但总体上是好书.通过对本书核心内容的摘抄,结合自己的经验,整理了一些精简的点,这样你就省的去啃那本400多页的书了. 软件质量 = 架 ...
- 《代码整洁之道》(Clean Code)- 读书笔记
一.关于Bob大叔的Clean Code <代码整洁之道>主要讲述了一系列行之有效的整洁代码操作实践.软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关.这一点,无论是敏捷开发流派 ...
- 代码整洁之道(一)最佳实践小结
摘要: Any fool can write code that a computer can understand. Good programmers write code that humans ...
- 2015年第11本:代码整洁之道Clean Code
前一段时间一直在看英文小说,在读到<Before I fall>这本书时,读了40%多实在看不下去了,受不了美国人啰啰嗦嗦的写作风格,还是读IT专业书吧. 从5月9日开始看<代码整洁 ...
- 《代码整洁之道:程序员的职业素养》一一1.5 参考文献
本节书摘来自异步社区出版社<代码整洁之道:程序员的职业素养>一书中的第1章,第1.5节,作者:[美]Robert C. Martin(罗伯特 C. 马丁),更多章节内容可以访问云栖社区&q ...
- 代码整洁之道(Clean Code)- 读书笔记
Sorry, 许久未更新文章了,主要因为刚刚换了一家新公司,忙于组建团队(建设.招聘.流程.框架等)与熟悉公司业务,还有领导给的其他工作等等,实在是没有时间更新了.最近在和团队分享Bob大叔的< ...
- 《代码整洁之道 Clean Architecture》-读书笔记
大家好,我是烤鸭: 关于<代码整洁之道>,记录一下读书笔记. 代码整洁之道 第一章 整洁代码 整洁代码的艺术 第二章 有意义的命名 避免误导 有意义的区分 使用读得出来和可搜索的名字 避免 ...
- 代码整洁之道(一)最佳实践小结 1
摘要: Any fool can write code that a computer can understand. Good programmers write code that humans ...
最新文章
- Silve“.NET研究”rlight 游戏开发小技巧:传说中的透视跑马灯
- 数据结构-串操作应用之词索引表
- C 和 C++ 文件操作详解
- kafka消费报错:org.apache.kafka.common.errors.WakeupException: null
- 数据结构与算法笔记(三) 线性表(链式描述) 链表
- Activity 的窗口去头的方式
- [转]《谁让你是飞鸟我是鱼》by(晨曦)
- 不懂开发的运维,未来该如何发展?
- Video Copilot VCReflect for Mac/win (AE倒影插件) 支持2022多帧渲染​
- 数据库原理及应用教程课后习题答案 第4版 微课版 陈志泊主编
- JAVA(jar)软件_Autojar - 打包工具 - 开发工具 - JAVA开源项目 - 开源吧
- 谷歌学术、github、Sci-Hub镜像网址总结
- 如何快速压缩PPT文件?
- excel查找空值快捷键_Excel之定位和查找(一)
- cmd 如何跨驱动器移动文件夹
- 怎么免费做百度引流?百度免费引流方法有哪些?
- 【c语言】初识c语言-让你对c语言不在感到一无所知
- matlab将彩图转化成灰度图,matlab 如何将彩图转成灰度图
- 蓝桥杯javac组我们的征途是星辰大海
- 【第27天】SQL进阶-查询优化- performance_schema系列实战三:锁问题排查(表级锁)(SQL 小虚竹)
热门文章
- 谈RAM与ROM的区别与理解
- springboot+shiro is not eligible for getting processed by all BeanPostProcessors
- 设置TinyMCE在线HTML编辑控件只读
- 长尾效应(由百度网络资料整理)
- Mysql报表统计常用sql
- 未来人类将被AI取代?一文读懂人工智能类型与发展阶段
- 荣耀全明星不显示服务器,LoveLive!学园偶像祭 全明星无法连接服务器是什么原因...
- 猎聘公司管理系统 ——需求规格说明书
- 强大的密码破解工具:hashcat简介与用法介绍与实例
- 应用机器学习(三):朴素贝叶斯分类器