TS代码整洁之道——"净"
maxueming|2022-10
3. 对象和数据结构
3.1 使用 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;
3.2 让对象拥有 private/protected 成员
TypeScript 类成员支持 public(默认)、protected 以及 private 的访问限制。
反例:
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
surface() {
return Math.PI * this.radius * this.radius;
}
}
正例:
class Circle {
constructor(private readonly radius: number) {
}
surface(){
return Math.PI * this.radius * this.radius;
}
}
3.3 不变性
TypeScript 类型系统允许将接口、类上的单个属性设置为只读,能以函数的方式运行。
还有个高级场景,可以使用内置类型 Readonly,它接受类型 T 并使用映射类型将其所有属性标记为只读。
反例:
interface Config {
host: string;
port: string;
db: string;
}
正例:
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
3.4 类型 vs 接口
定义组合类型,交叉类型和原始类型,请使用 type。如果需要扩展或实现,请使用 interface。然而,没有最好,只有是否适合。type 和 interface 区别,详细参考 Stack Overflow 上的解答 。
示例:
interface Shape {
}
class Circle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
4. 类的设计以及 SOLID 原则
首先,类一定要小、小、小!重要的事情说三遍!类的大小是由它的职责来度量的,按照单一职责原则,类要小。
另外,好的设计要高内聚低耦合:
- 内聚:定义类成员之间相互关联的程度。理想情况下,高内聚类的每个方法都应该使用类中的所有字段,实际这不可能也不可取。但依然提倡高内聚。
- 耦合:指的是两个类之间的关联程度。如果其中一个类的更改不影响另一个类,则称为低耦合类。
这些都是老生常谈的原则,这里不举例说明了。
4.1 组合大于继承
“四人帮”在《设计模式》中指出:尽可能使用组合而不是继承。如果你默认倾向于继承,那么考虑下组合是否能更好的解决问题。
何时使用继承?需要因地制宜:
- 继承代表的是 is-a 关系,而不是 has-a 关系(人 -> 动物 vs. 用户 -> 用户详情)。
- 可复用基类的代码 。
- 希望通过更改基类对派生类进行全局更改。
设计模式四人帮,又称 Gang of Four,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人的《设计模式》,原名 Design Patterns: Elements of Reusable Object-Oriented Software,第一次将设计模式提升到理论高度,并将之规范化。
反例:
class User {
constructor(
private readonly name: string,
private readonly id: string) {
}
// ...
}
// 用户工作信息并不是一类用户,这种继承有问题
class UserJob extends User {
constructor(
name: string,
id: string,
private readonly company: string,
private readonly salary: number) {
super(name, id);
}
// ...
}
正例:
class User {
private job: UserJob; // 使用组合
constructor(
private readonly name: string,
private readonly id: string) {
}
setJob(company: string, salary: number): User {
this.job = new UserJob(company, salary);
return this;
}
// ...
}
class UserJob {
constructor(
public readonly company: string,
public readonly salary: number) {
}
// ...
}
4.2 链式调用
非常有用且表达力非常好的一种写法,代码也看起来更简洁。
反例:
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();
4.3 SOLID 原则
4.3.1 单一职责原则(Single Responsibility Principle)
类更改的原因不应该超过一个。
如果把很多不相关的功能都放在一个类中,看起来似乎很方便。却导致可能有很多原因去修改它,应该尽量减少修改类的次数。且修改了其中一处很难确定对其他依赖模块的影响。
反例:
class UserSettings {
constructor(private readonly user: User) {
}
changeSettings(settings: UserSettings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// 和 UserSettings 没有关系的逻辑
// ...
}
}
正例:
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()) {
// ...
}
}
}
4.3.2 开闭原则(Open Closed Principle)
正如 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> {
// 对于不同的 adapter 都要做不同处理,如果新增一类 adapter 需要在此处新增处理逻辑
if (this.adapter instanceof AjaxAdapter) {
const response = await makeAjaxCall<T>(url);
} else if (this.adapter instanceof NodeAdapter) {
const response = await makeHttpCall<T>(url);
}
}
}
function makeAjaxCall<T>(url: string): Promise<T> {
// 请求并返回 promise
}
function makeHttpCall<T>(url: string): Promise<T> {
// 请求并返回 promise
}
正例:
abstract class Adapter {
abstract async request<T>(url: string): Promise<T>;
}
class AjaxAdapter extends Adapter {
// ...
async request<T>(url: string): Promise<T>{
// 请求并返回 promise
}
}
class NodeAdapter extends Adapter {
//...
async request<T>(url: string): Promise<T>{
// 请求并返回 promise
}
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {}
// 新增一类 adapter,此处代码不需要做任何处理
async fetch<T>(url: string): Promise<T> {
const response = await this.adapter.request<T>(url);
}
}
4.3.3 里氏替换原则(Liskov Substitution Principle)
听起来有点懵?是的!
这个原则的定义是:“如果 S 是 T 的一个子类型,那么类型 T 的对象可以被替换为类型 S 的对象,而不会改变程序的正确性”。这听起来似乎还是有点懵了。
讲人话就是,如果有一个父类和一个子类,那么父类和子类可以互换使用,而代码不会出现问题。
看下经典的正方形矩形的例子。从数学上讲,正方形是矩形,但是如果通过继承使用 is-a 关系对其建模,很快就会遇到麻烦。
反例:
class Rectangle {
constructor(
protected width: number = 0,
protected height: number = 0) {
}
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(); // 当传入的是 Square 时,返回了 25,应该是 20。没有遵守里氏替换原则
// ...
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
子类必须实现父类的抽象方法,但最好不要重写父类的非抽象方法。
正例:
abstract class Shape {
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();
// ...
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
4.3.4 接口隔离原则(Interface Segregation Principle)
“要设计小并且具体的接口,而非大而全!”这一原则与单一责任原则密切相关。试想,如果一个接口是一个大而全的抽象,那么实现这个接口就会成为一种负担,因为需要实现一些不需要的方法。
反例:
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() {
// ...
}
}
4.3.5 依赖倒置原则(Dependency Inversion Principle)
这个原则有两个要点:
- 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
- 抽象不依赖实现,实现应依赖抽象。
一开始这难以理解,但是如果你使用过 Angular,你就会看到以依赖注入(DI)的方式实现了这一原则。虽然概念不同,但是 DIP 阻止高级模块了解其低级模块的实现细节,这样做的一个巨大好处是减少了模块之间的耦合。模块间的高耦合非常麻烦,它让代码难以重构。
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 {
// 转换 XML 字符串
}
}
class ReportReader {
// 这里已经对具体的实现 XmlFormatter 产生了依赖,实际上只需要依赖方法: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 {
// 转换 XML 字符串
}
}
class JsonFormatter implements Formatter {
parse<T>(content: string): T {
// 转换 Json 字符串
}
}
class ReportReader {
// 只依赖了抽象,也就是接口 Formatter,而非它的具体实现 XmlFormatter 或 JsonFormatter
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');
const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');
5. 并发
5.1 用 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 更容易实现超时。
5.2 用 Async/Await 替代 Promises
使用 async/await 语法,可以编写更简洁、更易理解代码。一个函数使用 async 关键字作为前缀,那么就告诉了 JavaScript 运行时暂停 await 关键字上的代码执行。
上一节中的例子可以继续优化为:
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;
}
try {
const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');
console.log(content);
} catch (error) {
console.error(error);
}
6. 错误处理
抛出错误并非是件坏事,至少在运行时可以识别出错的位置。通常,程序会在控制台中打印堆栈信息。
6.1 抛出 Error 或 使用 reject
JavaScript 和 TypeScript 允许 throw 任何对象,Promise 也可以用任何理由对象拒绝。
代码中的 Error 可以在 catch 中被捕获,所以还是建议使用 throw error,而不是简单的字符串。 Promise 也是同样的道理。
反例:
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.'));
}
或者:
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 };
}
详细解释请参考原文。
6.2 有始有终,别忘了捕获 Error
绝对不能忽略 Error,或者捕获到 Error 不处理而是打印到控制台(console.log),这样的话这些异常会丢失在控制台日志的汪洋大海中。如果代码写在 try/catch 中,说明那里可能会发生错误,因此应该考虑在错误发生时做一些对应的处理。
反例:
try {
throwError();
} catch (error) {
// 打印到控制台,或则直接忽略。
// ignore error
}
正例:
import { logger } from './logging'
try {
throwError();
} catch (error) {
logger.log(error);
}
另外,Promises 的 Error 也要正确处理。如下:
import { logger } from './logging'
getUser()
.then((user: User) => {
return sendEmail(user.email, 'Welcome!');
}).catch((error) => {
logger.log(error);
});
或者使用 async/await 时。如下:
try {
const user = await getUser();
await sendEmail(user.email, 'Welcome!');
} catch (error) {
logger.log(error);
}
7. 测试
测试至关重要。如果没有测试或用例数量不足,那么每次提交代码时都无法确保不引入问题。
怎样才算是足够的测试?一般情况下都会按照覆盖率来衡量,拥有 100% 的分支覆盖率当然最好。这一切都要基于好的测试框架以及覆盖率工具,如:istanbul。
没有理由不编写测试。有很多优秀的 JS 测试框架都支持 TypeScript,找个合适的,然后为每个新特性/模块编写测试。如果你习惯于测试驱动开发(TDD),那就太好了,重点是确保在开发任何特性或重构现有特性之前,代码覆盖率已经达到要求。
7.1 TDD(测试驱动开发)
TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD 的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。
TDD 三定律:
- 在编写不能通过的单元测试前,不可编写生产代码。
- 只可编写刚好无法通过的单元测试,不能编译也算不过。
- 只可编写刚好足以通过当前失败测试的生产代码。
7.2 F.I.R.S.T. 准则
为了写出有效的测试代码,应遵循以下准则:
- 快速(Fast),测试应该快(及时反馈出业务代码的问题)。
- 独立(Independent),每个测试流程应该独立。
- 可重复(Repeatable),测试应该在任何环境上都能重复通过。
- 自我验证(Self-Validating),测试结果应该明确通过或者失败。
- 及时(Timely),测试代码应该在产品代码之前编写。
7.3 单独测试每一个逻辑
测试代码和业务代码一样,也要遵循单一职责原则,每个单元测试只有一个断言。
反例:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles date boundaries', () => {
let date: AwesomeDate;
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 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);
});
});
7.4 测试用例名称应该显示它的意图
同样,也要取一个有意义的用例名字。如果用例出错,根据用例名就可以判断代码大概的问题。在命名上,我们曾在 Java 的 UT 中使用中文来描述用例名,效果也是挺好的。如果是英文,也可以使用 should *** when ***
这样的命名格式,总之,表达清楚即可。
反例:
describe('Calendar', () => {
it('throws', () => {
// ...
});
});
正例:
describe('Calendar', () => {
it('should throw error when format is invalid', () => {
// ...
});
});
8. 格式化与注释
格式化是让代码整洁的一个简单却又重要手段(我在项目组见过,有多年工作经验的老司机也未对代码格式化),但是,格式定义却没有什么硬性规定。争论那种格式更好都是徒劳,浪费时间,在格式化上这点上,最重要的就是要统一,项目或公司级的统一格式规范。确实,很多国内外公司都有自己的代码格式规范。
另外,可以使用工具帮助处理格式。例如,静态分析工具 TSLint,项目中使用可以参考以下 TSLint 配置:
- TSLint Config Standard:标准格式规则
- TSLint Config Airbnb:Airbnb 格式规则
- TSLint react:React 相关的 Lint 规则
- TSLint + Prettier:Prettier 代码格式化相关的 lint 规则
- ESLint rules for TSLint:TypeScript 的 ESLint
8.1 大小写一致
这是一个主观性的规则,主要看我们怎么选。关键是无论怎么选,都要保持一致。
反例:
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const users = ['张三', '李四', '王五', '赵六'];
const Books = ['Clean Code', 'TypeScript in Action'];
function addRecord() {}
function remove_record() {}
class user {}
class Book {}
正例:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const USERS = ['张三', '李四', '王五', '赵六'];
const BOOKS = ['Clean Code', 'TypeScript in Action'];
function addRecord() {}
function removeRecord() {}
class User {}
class Book {}
命名规则推荐:
- 类名、接口名、类型名和命名空间名最好使用帕斯卡命名(Pascal)。
- 变量、函数和类成员使用驼峰式命名(Camel)。
8.2 把函数和被调函数放在一起
两个函数如果有调用关系,那么把调用者放在被调用者的上方。我们阅读代码时,就会更自然、更顺畅。
8.3 组织导入
对 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(例如:
一些 IDE 应该是支持格式化导入语句的。
反例:
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';
8.4 路径映射(路径别名)
为了创建简洁的导入语句,可以在 tsconfig.json 中设置编译器选项的 paths 和 baseUrl 属性,这样可以避免导入时使用较长的相对路径。
反例:
import { UserService } from '../../../services/UserService';
正例:
import { UserService } from '@services/UserService';
tsconfig.json 配置:
"compilerOptions": {
...
"baseUrl": "src",
"paths": {
"@services": ["services/*"]
}
...
}
8.5 合理的注释
在好格式化基础之上,我们要考虑合理的运用注释。好的代码并非不需要注释,合理的注释会帮助理解代码。
在这个问题上,应该争议最大。大概一类认为:代码自解释,另一类则是需要详尽的注释。个人认为只是需要找到一个平衡点即可。对他人来说,我们的代码不可能完整做到自解释,甚至对一段时间后的自己都不行。而合理的注释,只需要在一些关键点上做到点睛即可。
不要注释坏代码,重写吧!——Brian W. Kernighan and P. J. Plaugher
当然对一些烂代码还是要及时重构的。
反例:
// 检查订阅是否处于激活状态
if (subscription.endDate > Date.now) { }
正例:
// 不需要注释,通过抽取成变量或者函数,通过变量名或者函数名来说明含义。
const isSubscriptionActive = subscription.endDate > Date.now;
if (isSubscriptionActive) { /* ... */ }
8.6 使用版本控制
使用版本控制,而非注释,这样做的好处:
- 删除注释掉的代码而无需担心。
- 使用提交日志来替代注释。如,使用
git log
来获取历史提交信息,避免日志中出现日记式注释。
8.7 避免使用注释标记位置
这样的注释干扰正常阅读代码。要让代码结构化,函数和变量要有合适的缩进和格式。
反例:
// User class
class User {
id: number;
address: Address;
// public methods
public getInfo(): string {
// ...
}
// private methods
private getAddress(): string {
// ...
}
};
8.8 TODO 注释
在 Code Review 时会常常会留下很多 // TODO
注释,多数 IDE 都对这类注释提供了支持,一般可快速浏览整个 TODO 列表。
但是,TODO 注释并不是坏代码的借口,要尽快处理掉。